diff --git a/.eslint-plugin-local/code-translation-remind.ts b/.eslint-plugin-local/code-translation-remind.ts index 42032321167..ed636ec0cb6 100644 --- a/.eslint-plugin-local/code-translation-remind.ts +++ b/.eslint-plugin-local/code-translation-remind.ts @@ -26,18 +26,19 @@ export default new class TranslationRemind implements eslint.Rule.RuleModule { private _checkImport(context: eslint.Rule.RuleContext, node: TSESTree.Node, path: string) { - if (path !== TranslationRemind.NLS_MODULE) { + if (path !== TranslationRemind.NLS_MODULE && !path.endsWith('/nls.js')) { return; } const currentFile = context.getFilename(); const matchService = currentFile.match(/vs\/workbench\/services\/\w+/); const matchPart = currentFile.match(/vs\/workbench\/contrib\/\w+/); - if (!matchService && !matchPart) { + const matchSessionsPart = currentFile.match(/vs\/sessions\/contrib\/\w+/); + if (!matchService && !matchPart && !matchSessionsPart) { return; } - const resource = matchService ? matchService[0] : matchPart![0]; + const resource = matchService ? matchService[0] : matchPart ? matchPart[0] : matchSessionsPart![0]; let resourceDefined = false; let json; @@ -47,9 +48,10 @@ export default new class TranslationRemind implements eslint.Rule.RuleModule { console.error('[translation-remind rule]: File with resources to pull from Transifex was not found. Aborting translation resource check for newly defined workbench part/service.'); return; } - const workbenchResources = JSON.parse(json).workbench; + const parsed = JSON.parse(json); + const resources = [...parsed.workbench, ...parsed.sessions]; - workbenchResources.forEach((existingResource: any) => { + resources.forEach((existingResource: any) => { if (existingResource.name === resource) { resourceDefined = true; return; diff --git a/.github/commands.json b/.github/commands.json index 978a0960eab..c52e21eeb8d 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -631,7 +631,7 @@ "addLabel": "capi", "removeLabel": "~capi", "assign": [ - "samvantran", + "rheapatel", "sharonlo" ], "comment": "Thank you for creating this issue! Please provide one or more `requestIds` to help the platform team investigate. You can follow instructions [found here](https://github.com/microsoft/vscode/wiki/Copilot-Issues#language-model-requests-and-responses) to locate the `requestId` value.\n\nHappy Coding!" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 07296619597..f7e3481c75b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,27 +8,3 @@ updates: directory: "/" schedule: interval: "weekly" - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "daily" - allow: - - dependency-name: "@vscode/component-explorer" - - dependency-name: "@vscode/component-explorer-cli" - groups: - component-explorer: - patterns: - - "@vscode/component-explorer" - - "@vscode/component-explorer-cli" - - package-ecosystem: "npm" - directory: "/build/vite" - schedule: - interval: "daily" - allow: - - dependency-name: "@vscode/component-explorer" - - dependency-name: "@vscode/component-explorer-vite-plugin" - groups: - component-explorer: - patterns: - - "@vscode/component-explorer" - - "@vscode/component-explorer-vite-plugin" diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json index 5e27e6db893..4457634963e 100644 --- a/.github/hooks/hooks.json +++ b/.github/hooks/hooks.json @@ -4,8 +4,7 @@ "sessionStart": [ { "type": "command", - "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup bash -c 'npm ci && npm run compile' > /tmp/worktree-setup-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi", - "powershell": "if (Test-Path \"$env:USERPROFILE\\.vscode-worktree-setup\") { $log = \"$env:TEMP\\worktree-setup-$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').log\"; $dir = $PWD.Path; Start-Job -ScriptBlock { param($d, $l) Set-Location $d; & { npm ci; if ($LASTEXITCODE -eq 0) { npm run compile } } *> $l } -ArgumentList $dir, $log | Out-Null }" + "bash": "" } ], "sessionEnd": [ diff --git a/.github/skills/update-screenshots/SKILL.md b/.github/skills/update-screenshots/SKILL.md index 46172cfee2d..294125273ef 100644 --- a/.github/skills/update-screenshots/SKILL.md +++ b/.github/skills/update-screenshots/SKILL.md @@ -72,7 +72,16 @@ git add test/componentFixtures/.screenshots/baseline/ git commit -m "update screenshot baselines from CI" ``` -### 7. Verify +### 7. Push LFS objects before pushing + +Screenshot baselines are stored in Git LFS. The `git lfs pre-push` hook is not active in this repo (husky overwrites it), so LFS objects are NOT automatically uploaded on `git push`. You must push them manually before pushing the branch, otherwise the push will fail with `GH008: Your push referenced unknown Git LFS objects`. + +```bash +git lfs push --all origin +git push +``` + +### 8. Verify Confirm the baselines are updated by listing the files: diff --git a/.github/workflows/api-proposal-version-check.yml b/.github/workflows/api-proposal-version-check.yml index 23f6e052f9f..ee082dee49f 100644 --- a/.github/workflows/api-proposal-version-check.yml +++ b/.github/workflows/api-proposal-version-check.yml @@ -23,16 +23,19 @@ jobs: check-version-changes: name: Check API Proposal Version Changes # Run on PR events, or on issue_comment if it's on a PR and contains the override command - if: false # temporarily disabled - # github.event_name == 'pull_request' || - # (github.event_name == 'issue_comment' && - # github.event.issue.pull_request && - # contains(github.event.comment.body, '/api-proposal-change-required')) + if: | + github.event_name == 'pull_request' || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '/api-proposal-change-required') && + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR')) runs-on: ubuntu-latest steps: - name: Get PR info id: pr_info - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | let prNumber, headSha, baseSha; @@ -59,7 +62,7 @@ jobs: - name: Check for override comment id: check_override - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const prNumber = ${{ steps.pr_info.outputs.number }}; @@ -71,25 +74,50 @@ jobs: // Only accept overrides from trusted users (repo members/collaborators) const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; - const overrideComment = comments.find(comment => - comment.body.includes('/api-proposal-change-required') && - trustedAssociations.includes(comment.author_association) - ); + let overrideComment = null; + const untrustedOverrides = []; + + comments.forEach((comment, index) => { + const hasOverrideText = comment.body.includes('/api-proposal-change-required'); + const isTrusted = trustedAssociations.includes(comment.author_association); + console.log(`Comment ${index + 1}:`); + console.log(` Author: ${comment.user.login}`); + console.log(` Author association: ${comment.author_association}`); + console.log(` Created at: ${comment.created_at}`); + console.log(` Contains override command: ${hasOverrideText}`); + console.log(` Author is trusted: ${isTrusted}`); + console.log(` Would be valid override: ${hasOverrideText && isTrusted}`); + + if (hasOverrideText) { + if (isTrusted && !overrideComment) { + overrideComment = comment; + } else if (!isTrusted) { + untrustedOverrides.push(comment); + } + } + }); if (overrideComment) { - console.log(`Override comment found by ${overrideComment.user.login} (${overrideComment.author_association})`); + console.log(`✅ Override comment FOUND`); + console.log(` Comment ID: ${overrideComment.id}`); + console.log(` Author: ${overrideComment.user.login}`); + console.log(` Association: ${overrideComment.author_association}`); + console.log(` Created at: ${overrideComment.created_at}`); core.setOutput('override_found', 'true'); core.setOutput('override_user', overrideComment.user.login); } else { - // Check if there's an override from an untrusted user - const untrustedOverride = comments.find(comment => - comment.body.includes('/api-proposal-change-required') && - !trustedAssociations.includes(comment.author_association) - ); - if (untrustedOverride) { - console.log(`Override comment by ${untrustedOverride.user.login} ignored (${untrustedOverride.author_association} is not trusted)`); + if (untrustedOverrides.length > 0) { + console.log(`⚠️ Found ${untrustedOverrides.length} override comment(s) from UNTRUSTED user(s):`); + untrustedOverrides.forEach((comment, index) => { + console.log(` Untrusted override ${index + 1}:`); + console.log(` Author: ${comment.user.login}`); + console.log(` Association: ${comment.author_association}`); + console.log(` Created at: ${comment.created_at}`); + console.log(` Comment ID: ${comment.id}`); + }); + console.log(` Trusted associations are: ${trustedAssociations.join(', ')}`); } - console.log('No valid override comment found'); + console.log('❌ No valid override comment found'); core.setOutput('override_found', 'false'); } @@ -102,7 +130,7 @@ jobs: (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const headSha = '${{ steps.pr_info.outputs.head_sha }}'; @@ -213,7 +241,7 @@ jobs: - name: Post warning comment if: steps.check_override.outputs.override_found != 'true' && steps.version_check.outputs.version_changed == 'true' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const prNumber = ${{ steps.pr_info.outputs.number }}; diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index c3107065279..01f186a1c81 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -18,7 +18,6 @@ concurrency: jobs: screenshots: - if: false # temporarily disabled name: Checking Component Screenshots runs-on: ubuntu-latest steps: @@ -55,12 +54,12 @@ jobs: run: npx playwright install chromium - name: Capture screenshots - run: npx component-explorer screenshot --project ./test/componentFixtures/component-explorer.json + run: ./node_modules/.bin/component-explorer screenshot --project ./test/componentFixtures/component-explorer.json - name: Compare screenshots id: compare run: | - npx component-explorer screenshot:compare \ + ./node_modules/.bin/component-explorer screenshot:compare \ --project ./test/componentFixtures \ --report ./test/componentFixtures/.screenshots/report continue-on-error: true @@ -93,19 +92,33 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | REPORT="test/componentFixtures/.screenshots/report/report.json" + STATE="success" if [ -f "$REPORT" ]; then CHANGED=$(node -e "const r = require('./$REPORT'); console.log(r.summary.added + r.summary.removed + r.summary.changed)") TITLE="⚠ ${CHANGED} screenshots changed" + BLOCKS_CI=$(node -e " + const r = require('./$REPORT'); + const blocking = Object.entries(r.fixtures).filter(([, f]) => + f.status !== 'unchanged' && (f.labels || []).includes('blocks-ci') + ); + if (blocking.length > 0) { + console.log(blocking.map(([name]) => name).join(', ')); + } + ") + if [ -n "$BLOCKS_CI" ]; then + STATE="failure" + TITLE="❌ ${CHANGED} screenshots changed (blocks CI: ${BLOCKS_CI})" + fi else TITLE="✅ Screenshots match" fi SHA="${{ github.event.pull_request.head.sha || github.sha }}" - DETAILS_URL="https://hediet-ghartifactpreview.azurewebsites.net/${{ github.repository }}/run/${{ github.run_id }}/component-explorer/___explorer.html?report=./screenshot-report/report.json" + DETAILS_URL="https://hediet-ghartifactpreview.azurewebsites.net/${{ github.repository }}/run/${{ github.run_id }}/component-explorer/___explorer.html?report=./screenshot-report/report.json&search=changed" gh api "repos/${{ github.repository }}/statuses/$SHA" \ --input - < { { ignore: ['**/node_modules'] } ); + // Find all webpack configs, excluding those that will be esbuilt + const esbuildExtensionDirs = new Set(esbuildConfigLocations.map(p => path.dirname(p))); + const webpackConfigLocations = (await nodeUtil.promisify(glob)( + path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), + { ignore: ['**/node_modules'] } + )).filter(configPath => !esbuildExtensionDirs.has(path.dirname(configPath))); + const promises: Promise[] = []; // Esbuild for extensions @@ -323,5 +330,10 @@ async function buildWebExtensions(isWatch: boolean): Promise { ); } + // Run webpack for remaining extensions + if (webpackConfigLocations.length > 0) { + promises.push(ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath })))); + } + await Promise.all(promises); } diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index 8e7f6bbbdca..0ad08ab6fbe 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -83,6 +83,7 @@ const serverResourceIncludes = [ 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh', 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh', 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish', + 'out-build/vs/workbench/contrib/terminal/common/scripts/psreadline/**', ]; diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 4fb4c3cc438..e953a898bee 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -91,6 +91,7 @@ const vscodeResourceIncludes = [ 'out-build/vs/workbench/contrib/terminal/common/scripts/*.psm1', 'out-build/vs/workbench/contrib/terminal/common/scripts/*.sh', 'out-build/vs/workbench/contrib/terminal/common/scripts/*.zsh', + 'out-build/vs/workbench/contrib/terminal/common/scripts/psreadline/**', // Accessibility Signals 'out-build/vs/platform/accessibilitySignal/browser/media/*.mp3', diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index aacf25cbbc1..5710f4d6919 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -20,8 +20,10 @@ import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; +import webpack from 'webpack'; import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; +import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; import { createTsgoStream, spawnTsgo } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; @@ -30,8 +32,8 @@ import { createRequire } from 'module'; const require = createRequire(import.meta.url); const root = path.dirname(path.dirname(import.meta.dirname)); -// const commit = getVersion(root); -// const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +const commit = getVersion(root); +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); @@ -63,24 +65,32 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): .pipe(packageJsonFilter.restore); } -function fromLocal(extensionPath: string, forWeb: boolean, _disableMangle: boolean): Stream { +function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { const esbuildConfigFileName = forWeb ? 'esbuild.browser.mts' : 'esbuild.mts'; + const webpackConfigFileName = forWeb + ? `extension-browser.webpack.config.js` + : `extension.webpack.config.js`; + const hasEsbuild = fs.existsSync(path.join(extensionPath, esbuildConfigFileName)); + const hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input: Stream; let isBundled = false; if (hasEsbuild) { - // Esbuild only does bundling so we still want to run a separate type check step + // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step input = es.merge( fromLocalEsbuild(extensionPath, esbuildConfigFileName), ...getBuildRootsForExtension(extensionPath).map(root => typeCheckExtensionStream(root, forWeb)), ); isBundled = true; + } else if (hasWebpack) { + input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle); + isBundled = true; } else { input = fromLocalNormal(extensionPath); } @@ -112,6 +122,132 @@ export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean) return createTsgoStream(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); } +function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { + const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); + const webpack = require('webpack'); + const webpackGulp = require('webpack-stream'); + const result = es.through(); + + const packagedDependencies: string[] = []; + const stripOutSourceMaps: string[] = []; + const packageJsonConfig = require(path.join(extensionPath, 'package.json')); + if (packageJsonConfig.dependencies) { + const webpackConfig = require(path.join(extensionPath, webpackConfigFileName)); + const webpackRootConfig = webpackConfig.default; + for (const key in webpackRootConfig.externals) { + if (key in packageJsonConfig.dependencies) { + packagedDependencies.push(key); + } + } + + if (webpackConfig.StripOutSourceMaps) { + for (const filePath of webpackConfig.StripOutSourceMaps) { + stripOutSourceMaps.push(filePath); + } + } + } + + // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar + // to vsce.PackageManager.Yarn. + // A static analysis showed there are no webpack externals that are dependencies of the current + // local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list + // 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({ + path: filePath, + stat: fs.statSync(filePath), + base: extensionPath, + contents: fs.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'] } + ) as string[]); + const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { + + const webpackDone = (err: Error | undefined, stats: any) => { + fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); + if (err) { + result.emit('error', err); + } + const { compilation } = stats; + if (compilation.errors.length > 0) { + result.emit('error', compilation.errors.join('\n')); + } + if (compilation.warnings.length > 0) { + result.emit('error', compilation.warnings.join('\n')); + } + }; + + const exportedConfig = require(webpackConfigPath).default; + return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { + const webpackConfig = { + ...config, + ...{ mode: 'production' } + }; + if (disableMangle) { + if (Array.isArray(config.module.rules)) { + for (const rule of config.module.rules) { + if (Array.isArray(rule.use)) { + for (const use of rule.use) { + if (String(use.loader).endsWith('mangle-loader.js')) { + use.options.disabled = true; + } + } + } + } + } + } + const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data: File) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + if (path.extname(data.basename) === '.js') { + if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); + } else { + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); + } + } + + this.emit('data', data); + })); + }); + }); + + es.merge(...webpackStreams, es.readArray(files)) + // .pipe(es.through(function (data) { + // // debug + // console.log('out', data.path, data.contents.length); + // this.emit('data', data); + // })) + .pipe(result); + + }).catch(err => { + console.error(extensionPath); + console.error(packagedDependencies); + result.emit('error', err); + }); + + return result.pipe(createStatsStream(path.basename(extensionPath))); +} function fromLocalNormal(extensionPath: string): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); @@ -513,6 +649,70 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); +export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { + const webpack = require('webpack') as typeof import('webpack'); + + const webpackConfigs: webpack.Configuration[] = []; + + for (const { configPath, outputRoot } of webpackConfigLocations) { + const configOrFnOrArray = require(configPath).default; + function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) { + 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!)); + } + webpackConfigs.push(config); + } + } + addConfig(configOrFnOrArray); + } + + function reporter(fullStats: any) { + if (Array.isArray(fullStats.children)) { + for (const stats of fullStats.children) { + const outputPath = stats.outputPath; + if (outputPath) { + const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/'); + const match = relativePath.match(/[^\/]+(\/server|\/client)?/); + fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`); + } + if (Array.isArray(stats.errors)) { + stats.errors.forEach((error: any) => { + fancyLog.error(error); + }); + } + if (Array.isArray(stats.warnings)) { + stats.warnings.forEach((warning: any) => { + fancyLog.warn(warning); + }); + } + } + } + } + return new Promise((resolve, reject) => { + if (isWatch) { + webpack(webpackConfigs).watch({}, (err, stats) => { + if (err) { + reject(); + } else { + reporter(stats?.toJson()); + } + }); + } else { + webpack(webpackConfigs).run((err, stats) => { + if (err) { + fancyLog.error(err); + reject(); + } else { + reporter(stats?.toJson()); + resolve(); + } + }); + } + }); +} + export async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string; outputRoot?: string }[]): Promise { function reporter(stdError: string, script: string) { const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 921137824ee..3d641bae50f 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -561,6 +561,120 @@ { "name": "vs/workbench/contrib/list", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/browserView", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/dropOrPasteInto", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/editTelemetry", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/inlineCompletions", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/mcp", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/meteredConnection", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/processExplorer", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/remoteCodingAgents", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/telemetry", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/welcomeAgentSessions", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/accounts", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/chat", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/request", + "project": "vscode-workbench" + } + ], + "sessions": [ + { + "name": "vs/sessions", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/accountMenu", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/agentFeedback", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/aiCustomizationTreeView", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/applyCommitsToParentRepo", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/changes", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/chat", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/codeReview", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/fileTreeView", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/files", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/git", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/logs", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/sessions", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/terminal", + "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/welcome", + "project": "vscode-sessions" } ] } diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 8ebcb1f177b..e9fac0b8eb5 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -391,7 +391,8 @@ const editorProject: string = 'vscode-editor', workbenchProject: string = 'vscode-workbench', extensionsProject: string = 'vscode-extensions', setupProject: string = 'vscode-setup', - serverProject: string = 'vscode-server'; + serverProject: string = 'vscode-server', + sessionsProject: string = 'vscode-sessions'; export function getResource(sourceFile: string): Resource { let resource: string; @@ -416,6 +417,11 @@ export function getResource(sourceFile: string): Resource { return { name: resource, project: workbenchProject }; } else if (/^vs\/workbench/.test(sourceFile)) { return { name: 'vs/workbench', project: workbenchProject }; + } else if (/^vs\/sessions\/contrib/.test(sourceFile)) { + resource = sourceFile.split('/', 4).join('/'); + return { name: resource, project: sessionsProject }; + } else if (/^vs\/sessions/.test(sourceFile)) { + return { name: 'vs/sessions', project: sessionsProject }; } throw new Error(`Could not identify the XLF bundle for ${sourceFile}`); @@ -737,6 +743,11 @@ export function prepareI18nPackFiles(resultingTranslationPaths: TranslationPath[ if (EXTERNAL_EXTENSIONS.find(e => e === resource)) { project = extensionsProject; } + // TODO(tyleonha): Support localization for the Sessions app (https://github.com/microsoft/vscode-internalbacklog/issues/7045) + // vscode-setup has its own import path via prepareIslFiles + if (project === sessionsProject || project === setupProject) { + return; + } const contents = xlf.contents!.toString(); log(`Found ${project}: ${resource}`); const parsePromise = getL10nFilesFromXlf(contents); diff --git a/build/lib/test/i18n.test.ts b/build/lib/test/i18n.test.ts index 7d5bb0433fe..6c9409bcb4a 100644 --- a/build/lib/test/i18n.test.ts +++ b/build/lib/test/i18n.test.ts @@ -31,7 +31,8 @@ suite('XLF Parser Tests', () => { test('JSON file source path to Transifex resource match', () => { const editorProject: string = 'vscode-editor', - workbenchProject: string = 'vscode-workbench'; + workbenchProject: string = 'vscode-workbench', + sessionsProject: string = 'vscode-sessions'; const platform: i18n.Resource = { name: 'vs/platform', project: editorProject }, editorContrib = { name: 'vs/editor/contrib', project: editorProject }, @@ -40,7 +41,9 @@ suite('XLF Parser Tests', () => { 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 }; + workbench = { name: 'vs/workbench', project: workbenchProject }, + sessionsContrib = { name: 'vs/sessions/contrib/chat', project: sessionsProject }, + sessions = { name: 'vs/sessions', project: sessionsProject }; assert.deepStrictEqual(i18n.getResource('vs/platform/actions/browser/menusExtensionPoint'), platform); assert.deepStrictEqual(i18n.getResource('vs/editor/contrib/clipboard/browser/clipboard'), editorContrib); @@ -50,5 +53,7 @@ suite('XLF Parser Tests', () => { 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.deepStrictEqual(i18n.getResource('vs/sessions/contrib/chat/browser/chatWidget'), sessionsContrib); + assert.deepStrictEqual(i18n.getResource('vs/sessions/browser/layoutActions'), sessions); }); }); diff --git a/build/next/index.ts b/build/next/index.ts index 27f5754e020..786fde3bb6f 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -261,6 +261,12 @@ const desktopResourcePatterns = [ 'vs/workbench/contrib/terminal/common/scripts/*.psm1', 'vs/workbench/contrib/terminal/common/scripts/*.fish', 'vs/workbench/contrib/terminal/common/scripts/*.zsh', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/*.psd1', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/*.psm1', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/*.dll', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/*.ps1xml', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/net6plus/*.dll', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/netstd/*.dll', 'vs/workbench/contrib/externalTerminal/**/*.scpt', // Media - audio @@ -298,6 +304,12 @@ const serverResourcePatterns = [ 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh', 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh', 'vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/*.psd1', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/*.psm1', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/*.dll', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/*.ps1xml', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/net6plus/*.dll', + 'vs/workbench/contrib/terminal/common/scripts/psreadline/netstd/*.dll', ]; // Resources for server-web target (server + web UI) diff --git a/build/npm/gyp/package-lock.json b/build/npm/gyp/package-lock.json index 3142db6e89d..8c499c6740f 100644 --- a/build/npm/gyp/package-lock.json +++ b/build/npm/gyp/package-lock.json @@ -1069,9 +1069,9 @@ } }, "node_modules/tar": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", - "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/build/package-lock.json b/build/package-lock.json index 3dd620ab59e..644e16f901b 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -48,7 +48,7 @@ "@types/workerpool": "^6.4.0", "@types/xml2js": "0.0.33", "@vscode/iconv-lite-umd": "0.7.1", - "@vscode/ripgrep": "^1.15.13", + "@vscode/ripgrep": "^1.17.1", "@vscode/vsce": "3.6.1", "ansi-colors": "^3.2.3", "byline": "^5.0.0", @@ -1927,9 +1927,9 @@ "license": "MIT" }, "node_modules/@vscode/ripgrep": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.17.0.tgz", - "integrity": "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.17.1.tgz", + "integrity": "sha512-xTs7DGyAO3IsJYOCTBP8LnTvPiYVKEuyv8s0xyJDBXfs8rhBfqnZPvb6xDT+RnwWzcXqW27xLS/aGrkjX7lNWw==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/build/package.json b/build/package.json index 785f04f3b22..8a65120c4d6 100644 --- a/build/package.json +++ b/build/package.json @@ -42,7 +42,7 @@ "@types/workerpool": "^6.4.0", "@types/xml2js": "0.0.33", "@vscode/iconv-lite-umd": "0.7.1", - "@vscode/ripgrep": "^1.15.13", + "@vscode/ripgrep": "^1.17.1", "@vscode/vsce": "3.6.1", "ansi-colors": "^3.2.3", "byline": "^5.0.0", diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index b9b854eacf2..72f036bc0df 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,13 +8,166 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/component-explorer": "^0.1.1-20", - "@vscode/component-explorer-vite-plugin": "^0.1.1-19", + "@vscode/component-explorer": "^0.1.1-24", + "@vscode/component-explorer-vite-plugin": "^0.1.1-24", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" } }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.2.0.tgz", + "integrity": "sha512-9UAZqn8ywdR70n3GwVle4N8ALosQs4z50N7XMXrSTUVOmVpaBC5kE3TRTT7qQdi3OaQV24mjGuJZsHUmhD+ZXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/http-client": "^1.0.3", + "@octokit/graphql": "^4.3.1", + "@octokit/rest": "^16.43.1" + } + }, + "node_modules/@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -49,6 +202,65 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@hediet/semver": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@hediet/semver/-/semver-0.2.2.tgz", + "integrity": "sha512-sdH+TwXwaYOgnKij3QQbJERl2HkJ+l8idWINwHBI+8nXl1yuTCMerDLDPC48t1wbr849qBTpJTV1EJXlh7OGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.0.4", + "@actions/github": "^2.2.0", + "@typescript-eslint/eslint-plugin": "^3.0.1", + "@typescript-eslint/parser": "^3.0.1", + "eslint": "^7.1.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -66,6 +278,322 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@octokit/core/node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@octokit/core/node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@octokit/core/node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz", + "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.1" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz", + "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.1", + "deprecation": "^2.3.1" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest": { + "version": "16.43.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz", + "integrity": "sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^2.4.0", + "@octokit/plugin-paginate-rest": "^1.1.1", + "@octokit/plugin-request-log": "^1.0.0", + "@octokit/plugin-rest-endpoint-methods": "2.4.0", + "@octokit/request": "^5.2.0", + "@octokit/request-error": "^1.0.2", + "atob-lite": "^2.0.0", + "before-after-hook": "^2.0.0", + "btoa-lite": "^1.0.0", + "deprecation": "^2.0.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "lodash.uniq": "^4.5.0", + "octokit-pagination-methods": "^1.1.0", + "once": "^1.4.0", + "universal-user-agent": "^4.0.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz", + "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^2.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/types": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", + "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz", + "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "os-name": "^3.1.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, "node_modules/@oxc-project/runtime": { "version": "0.101.0", "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.101.0.tgz", @@ -675,6 +1203,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -682,24 +1217,187 @@ "dev": true, "license": "MIT" }, - "node_modules/@vscode/component-explorer": { - "version": "0.1.1-20", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-20.tgz", - "integrity": "sha512-HvMWH+wK0SWC+eKZ2cL2LSsWnXiQjyQRURUgW2FBd8SM1G99+kKce0ESTYSr4b0tNJ1/FONE0ixADFlSRduzTg==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "dev": true, "license": "MIT", "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", + "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/experimental-utils": "3.10.1", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^3.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vscode/component-explorer": { + "version": "0.1.1-24", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-24.tgz", + "integrity": "sha512-o+uFX1bqD6dvAALx+Y32Gf7FmQehPsjGAI1Bm+5PvaV/++RIqsniM+VXIwqwjtuUvOyAMOz2TEOPYy3Uju//Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hediet/semver": "^0.2.2", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@vscode/component-explorer-vite-plugin": { - "version": "0.1.1-19", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-19.tgz", - "integrity": "sha512-V0wMhLvHMbeUHOzwGrBPMwwvcbGhXXaQTCGc9hNfF4fjUutOtQFu5o+9XKDG1hIcKgk5qyvcRoXjVazBcg19lA==", + "version": "0.1.1-24", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-24.tgz", + "integrity": "sha512-XHccBmg4mnIHahBTmoIBaJwvDZM0QOIbDm/qxZAw8Zr1xSfTCRQNBwBAYNrOZe4/XK52N5DLMBmjpFroEtY2WQ==", "dev": true, "license": "MIT", "dependencies": { + "@hediet/semver": "^0.2.2", "tinyglobby": "^0.2.0" }, "peerDependencies": { @@ -717,6 +1415,242 @@ "rollup": "^3.0.0 || ^4.0.0" } }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/atob-lite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", + "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "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", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -727,6 +1661,415 @@ "node": ">=8" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -745,6 +2088,48 @@ } } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -760,6 +2145,203 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -767,6 +2349,73 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-with-bigint": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -1028,6 +2677,49 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1041,6 +2733,39 @@ "loose-envify": "cli.js" } }, + "node_modules/macos-release": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1060,6 +2785,156 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/octokit-pagination-methods": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", + "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1109,6 +2984,47 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1136,6 +3052,56 @@ "react": "^18.3.1" } }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rolldown": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", @@ -1223,6 +3189,67 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1233,6 +3260,125 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1250,6 +3396,13 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1258,6 +3411,111 @@ "license": "0BSD", "optional": true }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "name": "rolldown-vite", "version": "7.3.1", @@ -1334,6 +3592,73 @@ "optional": true } } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/windows-release": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^1.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" } } } diff --git a/build/vite/package.json b/build/vite/package.json index 67e2f227e40..05a96fa1ffd 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "devDependencies": { - "@vscode/component-explorer": "^0.1.1-20", - "@vscode/component-explorer-vite-plugin": "^0.1.1-19", + "@vscode/component-explorer": "^0.1.1-24", + "@vscode/component-explorer-vite-plugin": "^0.1.1-24", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" diff --git a/eslint.config.js b/eslint.config.js index 9411af56dfe..c9eb58d074b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1975,6 +1975,74 @@ export default tseslint.config( 'vs/sessions/sessions.common.main.js' ] }, + { + 'target': 'src/vs/sessions/sessions.web.main.ts', + 'layer': 'browser', + 'restrictions': [ + 'vs/base/*/~', + 'vs/base/parts/*/~', + 'vs/platform/*/~', + 'vs/editor/~', + 'vs/editor/contrib/*/~', + 'vs/editor/editor.all.js', + 'vs/sessions/~', + 'vs/sessions/contrib/*/~', + 'vs/workbench/~', + 'vs/workbench/api/~', + 'vs/workbench/services/*/~', + 'vs/workbench/contrib/*/~', + 'vs/sessions/sessions.common.main.js' + ] + }, + { + 'target': 'src/vs/sessions/sessions.web.main.internal.ts', + 'layer': 'browser', + 'restrictions': [ + 'vs/base/~', + 'vs/base/parts/*/~', + 'vs/platform/*/~', + 'vs/sessions/~', + 'vs/sessions/contrib/*/~', + 'vs/workbench/~', + 'vs/workbench/browser/**', + 'vs/workbench/services/*/~', + 'vs/workbench/contrib/*/~', + 'vs/sessions/sessions.web.main.js' + ] + }, + { + 'target': 'src/vs/sessions/test/sessions.web.test.internal.ts', + 'layer': 'browser', + 'restrictions': [ + 'vs/base/~', + 'vs/base/parts/*/~', + 'vs/platform/*/~', + 'vs/sessions/~', + 'vs/sessions/test/**', + 'vs/sessions/contrib/*/~', + 'vs/workbench/~', + 'vs/workbench/browser/**', + 'vs/workbench/services/*/~', + 'vs/workbench/contrib/*/~', + 'vs/sessions/sessions.web.main.js' + ] + }, + { + 'target': 'src/vs/sessions/test/{web.test.ts,web.test.factory.ts}', + 'layer': 'browser', + 'restrictions': [ + 'vs/base/~', + 'vs/base/parts/*/~', + 'vs/platform/*/~', + 'vs/sessions/~', + 'vs/sessions/test/**', + 'vs/sessions/contrib/*/~', + 'vs/workbench/~', + 'vs/workbench/browser/**', + 'vs/workbench/services/*/~', + 'vs/workbench/contrib/*/~' + ] + }, { 'target': 'src/vs/sessions/~', 'restrictions': [ @@ -2205,21 +2273,13 @@ export default tseslint.config( '@typescript-eslint': tseslint.plugin, }, rules: { - '@typescript-eslint/naming-convention': [ + 'no-restricted-syntax': [ 'warn', { - 'selector': 'default', - 'modifiers': ['private'], - 'format': null, - 'leadingUnderscore': 'require' + selector: ':matches(PropertyDefinition, TSParameterProperty, MethodDefinition[key.name!="constructor"])[accessibility="private"]', + message: 'Use #private instead', }, - { - 'selector': 'default', - 'modifiers': ['public'], - 'format': null, - 'leadingUnderscore': 'forbid' - } - ] + ], } }, // Additional extension strictness rules @@ -2285,5 +2345,4 @@ export default tseslint.config( }, ], } - }, -); + }); diff --git a/extensions/git/package.json b/extensions/git/package.json index 1fbac49569f..3eac301dfbe 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -38,6 +38,7 @@ "scmTextDocument", "scmValidation", "statusBarItemTooltip", + "taskRunOptions", "tabInputMultiDiff", "tabInputTextMerge", "textEditorDiffInformation", diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index bd6b6a5c7ff..446384bc3db 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -9,7 +9,7 @@ import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; import * as path from 'path'; import picomatch from 'picomatch'; -import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, ExcludeSettingOptions, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, CustomExecution, Disposable, Event, EventEmitter, ExcludeSettingOptions, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProcessExecution, ProgressLocation, ProgressOptions, RelativePattern, scm, ShellExecution, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, Task, TaskRunOn, tasks, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; import type { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, LogOptions, Ref, Remote, RepositoryKind } from './api/git'; @@ -1892,15 +1892,41 @@ export class Repository implements Disposable { this.globalState.update(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${this.root}`, newWorktreeRoot); } - // Copy worktree include files. We explicitly do not await this - // since we don't want to block the worktree creation on the - // copy operation. - this._copyWorktreeIncludeFiles(worktreePath!); + this._setupWorktree(worktreePath!); return worktreePath!; }); } + private async _setupWorktree(worktreePath: string): Promise { + // Copy worktree include files and wait for the copy to complete + // before running any worktree-created tasks. + await this._copyWorktreeIncludeFiles(worktreePath); + + await this._runWorktreeCreatedTasks(worktreePath); + } + + private async _runWorktreeCreatedTasks(worktreePath: string): Promise { + try { + const allTasks = await tasks.fetchTasks(); + const worktreeTasks = allTasks.filter(task => task.runOptions.runOn === TaskRunOn.WorktreeCreated); + + for (const task of worktreeTasks) { + const worktreeTask = retargetTaskToWorktree(task, worktreePath); + if (!worktreeTask) { + this.logger.warn(`[Repository][_runWorktreeCreatedTasks] Skipped task '${task.name}' because it could not be retargeted to worktree '${worktreePath}'.`); + continue; + } + + tasks.executeTask(worktreeTask).then(undefined, err => { + this.logger.warn(`[Repository][_runWorktreeCreatedTasks] Failed to execute worktree-created task '${task.name}' for '${worktreePath}': ${err}`); + }); + } + } catch (err) { + this.logger.warn(`[Repository][_runWorktreeCreatedTasks] Failed to execute worktree-created tasks for '${worktreePath}': ${err}`); + } + } + private async _getWorktreeIncludePaths(): Promise> { const config = workspace.getConfiguration('git', Uri.file(this.root)); const worktreeIncludeFiles = config.get('worktreeIncludeFiles', []); @@ -3349,3 +3375,56 @@ export class Repository implements Disposable { this.disposables = dispose(this.disposables); } } + +function retargetTaskToWorktree(task: Task, worktreePath: string): Task | undefined { + const execution = retargetTaskExecution(task.execution, worktreePath); + if (!execution) { + return undefined; + } + + const worktreeFolder: WorkspaceFolder = { + uri: Uri.file(worktreePath), + name: path.basename(worktreePath), + index: workspace.workspaceFolders?.length ?? 0 + }; + + const worktreeTask = new Task({ ...task.definition }, worktreeFolder, task.name, task.source, execution, task.problemMatchers); + worktreeTask.detail = task.detail; + worktreeTask.group = task.group; + worktreeTask.isBackground = task.isBackground; + worktreeTask.presentationOptions = { ...task.presentationOptions }; + worktreeTask.runOptions = { ...task.runOptions }; + + return worktreeTask; +} + +function retargetTaskExecution(execution: ProcessExecution | ShellExecution | CustomExecution | undefined, worktreePath: string): ProcessExecution | ShellExecution | CustomExecution | undefined { + if (!execution) { + return undefined; + } + + if (execution instanceof ProcessExecution) { + return new ProcessExecution(execution.process, execution.args, { + ...execution.options, + cwd: worktreePath + }); + } + + if (execution instanceof ShellExecution) { + if (execution.commandLine !== undefined) { + return new ShellExecution(execution.commandLine, { + ...execution.options, + cwd: worktreePath + }); + } + + if (execution.command !== undefined) { + return new ShellExecution(execution.command, execution.args ?? [], { + ...execution.options, + cwd: worktreePath + }); + } + } + + return execution; +} diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 2a7ad5259ac..a34d12aaa48 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -27,6 +27,7 @@ "../../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.taskRunOptions.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", diff --git a/extensions/github/esbuild.mts b/extensions/github/esbuild.mts index f91916e622d..51494c329a5 100644 --- a/extensions/github/esbuild.mts +++ b/extensions/github/esbuild.mts @@ -16,4 +16,10 @@ run({ }, srcDir, outdir: outDir, + additionalOptions: { + banner: { + // The tunnel package uses `require` and needs this shim to work in an ESM environment + js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`, + }, + }, }, process.argv); diff --git a/extensions/mangle-loader.js b/extensions/mangle-loader.js new file mode 100644 index 00000000000..ed32a85e633 --- /dev/null +++ b/extensions/mangle-loader.js @@ -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. + *--------------------------------------------------------------------------------------------*/ +// @ts-check + +const fs = require('fs'); +const webpack = require('webpack'); +const fancyLog = require('fancy-log'); +const ansiColors = require('ansi-colors'); +const { Mangler } = require('../build/lib/mangle/index.js'); + +/** + * Map of project paths to mangled file contents + * + * @type {Map>>} + */ +const mangleMap = new Map(); + +/** + * @param {string} projectPath + */ +function getMangledFileContents(projectPath) { + let entry = mangleMap.get(projectPath); + if (!entry) { + const log = (...data) => fancyLog(ansiColors.blue('[mangler]'), ...data); + log(`Mangling ${projectPath}`); + const ts2tsMangler = new Mangler(projectPath, log, { mangleExports: true, manglePrivateFields: true }); + entry = ts2tsMangler.computeNewFileContents(); + mangleMap.set(projectPath, entry); + } + + return entry; +} + +/** + * @type {webpack.LoaderDefinitionFunction} + */ +module.exports = async function (source, sourceMap, meta) { + if (this.mode !== 'production') { + // Only enable mangling in production builds + return source; + } + if (true) { + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + return source; + } + const options = this.getOptions(); + if (options.disabled) { + // Dynamically disabled + return source; + } + + if (source !== fs.readFileSync(this.resourcePath).toString()) { + // File content has changed by previous webpack steps. + // Skip mangling. + return source; + } + + const callback = this.async(); + + const fileContentsMap = await getMangledFileContents(options.configFile); + + const newContents = fileContentsMap.get(this.resourcePath); + callback(null, newContents?.out ?? source, sourceMap, meta); +}; diff --git a/extensions/markdown-language-features/preview-src/activeLineMarker.ts b/extensions/markdown-language-features/preview-src/activeLineMarker.ts index 75c1ed7cbc9..0e63dad4599 100644 --- a/extensions/markdown-language-features/preview-src/activeLineMarker.ts +++ b/extensions/markdown-language-features/preview-src/activeLineMarker.ts @@ -5,27 +5,27 @@ import { getElementsForSourceLine } from './scroll-sync'; export class ActiveLineMarker { - private _current: any; + #current: HTMLElement | undefined; onDidChangeTextEditorSelection(line: number, documentVersion: number) { const { previous } = getElementsForSourceLine(line, documentVersion); - this._update(previous && (previous.codeElement || previous.element)); + this.#update(previous && (previous.codeElement || previous.element)); } - private _update(before: HTMLElement | undefined) { - this._unmarkActiveElement(this._current); - this._markActiveElement(before); - this._current = before; + #update(before: HTMLElement | undefined) { + this.#unmarkActiveElement(this.#current); + this.#markActiveElement(before); + this.#current = before; } - private _unmarkActiveElement(element: HTMLElement | undefined) { + #unmarkActiveElement(element: HTMLElement | undefined) { if (!element) { return; } element.classList.toggle('code-active-line', false); } - private _markActiveElement(element: HTMLElement | undefined) { + #markActiveElement(element: HTMLElement | undefined) { if (!element) { return; } diff --git a/extensions/markdown-language-features/preview-src/csp.ts b/extensions/markdown-language-features/preview-src/csp.ts index fcc38352da8..4db9d7b116f 100644 --- a/extensions/markdown-language-features/preview-src/csp.ts +++ b/extensions/markdown-language-features/preview-src/csp.ts @@ -11,45 +11,49 @@ import { getStrings } from './strings'; * Shows an alert when there is a content security policy violation. */ export class CspAlerter { - private _didShow = false; - private _didHaveCspWarning = false; + #didShow = false; + #didHaveCspWarning = false; - private _messaging?: MessagePoster; + #messaging?: MessagePoster; + + readonly #settingsManager: SettingsManager; constructor( - private readonly _settingsManager: SettingsManager, + settingsManager: SettingsManager, ) { + this.#settingsManager = settingsManager; + document.addEventListener('securitypolicyviolation', () => { - this._onCspWarning(); + this.#onCspWarning(); }); window.addEventListener('message', (event) => { if (event?.data && event.data.name === 'vscode-did-block-svg') { - this._onCspWarning(); + this.#onCspWarning(); } }); } public setPoster(poster: MessagePoster) { - this._messaging = poster; - if (this._didHaveCspWarning) { - this._showCspWarning(); + this.#messaging = poster; + if (this.#didHaveCspWarning) { + this.#showCspWarning(); } } - private _onCspWarning() { - this._didHaveCspWarning = true; - this._showCspWarning(); + #onCspWarning() { + this.#didHaveCspWarning = true; + this.#showCspWarning(); } - private _showCspWarning() { + #showCspWarning() { const strings = getStrings(); - const settings = this._settingsManager.settings; + const settings = this.#settingsManager.settings; - if (this._didShow || settings.disableSecurityWarnings || !this._messaging) { + if (this.#didShow || settings.disableSecurityWarnings || !this.#messaging) { return; } - this._didShow = true; + this.#didShow = true; const notification = document.createElement('a'); notification.innerText = strings.cspAlertMessageText; @@ -59,7 +63,7 @@ export class CspAlerter { notification.setAttribute('role', 'button'); notification.setAttribute('aria-label', strings.cspAlertMessageLabel); notification.onclick = () => { - this._messaging!.postMessage('showPreviewSecuritySelector', { source: settings.source }); + this.#messaging!.postMessage('showPreviewSecuritySelector', { source: settings.source }); }; document.body.appendChild(notification); } diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index 08475ad7fa4..95ec3211b58 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -26,12 +26,14 @@ const vscode = acquireVsCodeApi(); interface State { scrollProgress?: number; resource?: string; + line?: number; + fragment?: string; } const originalState: State = vscode.getState() ?? {}; -const state = { +const state: State = { ...originalState, - ...getData('data-state') + ...getData>('data-state') }; if (typeof originalState.scrollProgress !== 'undefined' && originalState?.resource !== state.resource) { @@ -335,10 +337,10 @@ document.addEventListener('click', event => { return; } - let node: any = event.target; + let node = event.target as Element | null; while (node) { - if (node.tagName && node.tagName === 'A' && node.href) { - if (node.getAttribute('href').startsWith('#')) { + if (node.tagName && node.tagName === 'A' && (node as HTMLAnchorElement).href) { + if (node.getAttribute('href')?.startsWith('#')) { return; } @@ -346,13 +348,13 @@ document.addEventListener('click', event => { if (!hrefText) { hrefText = node.getAttribute('href'); // Pass through known schemes - if (passThroughLinkSchemes.some(scheme => hrefText.startsWith(scheme))) { + if (hrefText && passThroughLinkSchemes.some(scheme => hrefText!.startsWith(scheme))) { return; } } // If original link doesn't look like a url, delegate back to VS Code to resolve - if (!/^[a-z\-]+:/i.test(hrefText)) { + if (hrefText && !/^[a-z\-]+:/i.test(hrefText)) { messaging.postMessage('openLink', { href: hrefText }); event.preventDefault(); event.stopPropagation(); @@ -361,7 +363,7 @@ document.addEventListener('click', event => { return; } - node = node.parentNode; + node = node.parentElement; } }, true); diff --git a/extensions/markdown-language-features/preview-src/loading.ts b/extensions/markdown-language-features/preview-src/loading.ts index c6e6d27acd9..f3270b22214 100644 --- a/extensions/markdown-language-features/preview-src/loading.ts +++ b/extensions/markdown-language-features/preview-src/loading.ts @@ -5,15 +5,20 @@ import { MessagePoster } from './messaging'; export class StyleLoadingMonitor { - private readonly _unloadedStyles: string[] = []; - private _finishedLoading: boolean = false; + readonly #unloadedStyles: string[] = []; + #finishedLoading: boolean = false; - private _poster?: MessagePoster; + #poster?: MessagePoster; constructor() { - const onStyleLoadError = (event: any) => { - const source = event.target.dataset.source; - this._unloadedStyles.push(source); + const onStyleLoadError = (event: Event | string) => { + if (!(event instanceof Event)) { + return; + } + const source = (event.target as HTMLElement | null)?.dataset.source; + if (source) { + this.#unloadedStyles.push(source); + } }; window.addEventListener('DOMContentLoaded', () => { @@ -25,18 +30,18 @@ export class StyleLoadingMonitor { }); window.addEventListener('load', () => { - if (!this._unloadedStyles.length) { + if (!this.#unloadedStyles.length) { return; } - this._finishedLoading = true; - this._poster?.postMessage('previewStyleLoadError', { unloadedStyles: this._unloadedStyles }); + this.#finishedLoading = true; + this.#poster?.postMessage('previewStyleLoadError', { unloadedStyles: this.#unloadedStyles }); }); } public setPoster(poster: MessagePoster): void { - this._poster = poster; - if (this._finishedLoading) { - poster.postMessage('previewStyleLoadError', { unloadedStyles: this._unloadedStyles }); + this.#poster = poster; + if (this.#finishedLoading) { + poster.postMessage('previewStyleLoadError', { unloadedStyles: this.#unloadedStyles }); } } } diff --git a/extensions/markdown-language-features/preview-src/messaging.ts b/extensions/markdown-language-features/preview-src/messaging.ts index 1fb29f0b55b..0458cac997c 100644 --- a/extensions/markdown-language-features/preview-src/messaging.ts +++ b/extensions/markdown-language-features/preview-src/messaging.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SettingsManager } from './settings'; +import type { WebviewApi } from 'vscode-webview'; import type { FromWebviewMessage } from '../types/previewMessaging'; +import { SettingsManager } from './settings'; export interface MessagePoster { /** @@ -16,7 +17,7 @@ export interface MessagePoster { ): void; } -export const createPosterForVsCode = (vscode: any, settingsManager: SettingsManager): MessagePoster => { +export const createPosterForVsCode = (vscode: WebviewApi, settingsManager: SettingsManager): MessagePoster => { return { postMessage( type: T['type'], diff --git a/extensions/markdown-language-features/preview-src/scroll-sync.ts b/extensions/markdown-language-features/preview-src/scroll-sync.ts index 33d81094cb5..14aa18d5490 100644 --- a/extensions/markdown-language-features/preview-src/scroll-sync.ts +++ b/extensions/markdown-language-features/preview-src/scroll-sync.ts @@ -9,18 +9,18 @@ const codeLineClass = 'code-line'; export class CodeLineElement { - private readonly _detailParentElements: readonly HTMLDetailsElement[]; + readonly #detailParentElements: readonly HTMLDetailsElement[]; constructor( readonly element: HTMLElement, readonly line: number, readonly codeElement?: HTMLElement, ) { - this._detailParentElements = Array.from(getParentsWithTagName(element, 'DETAILS')); + this.#detailParentElements = Array.from(getParentsWithTagName(element, 'DETAILS')); } get isVisible(): boolean { - if (this._detailParentElements.some(x => !x.open)) { + if (this.#detailParentElements.some(x => !x.open)) { return false; } diff --git a/extensions/markdown-language-features/preview-src/settings.ts b/extensions/markdown-language-features/preview-src/settings.ts index 0fb5d0c2686..6d642b58c64 100644 --- a/extensions/markdown-language-features/preview-src/settings.ts +++ b/extensions/markdown-language-features/preview-src/settings.ts @@ -33,13 +33,13 @@ export function getData(key: string): T { } export class SettingsManager { - private _settings: PreviewSettings = getData('data-settings'); + #settings: PreviewSettings = getData('data-settings'); public get settings(): PreviewSettings { - return this._settings; + return this.#settings; } public updateSettings(newSettings: PreviewSettings) { - this._settings = newSettings; + this.#settings = newSettings; } } diff --git a/extensions/markdown-language-features/src/client/client.ts b/extensions/markdown-language-features/src/client/client.ts index bf7be3f3206..dc279f02d84 100644 --- a/extensions/markdown-language-features/src/client/client.ts +++ b/extensions/markdown-language-features/src/client/client.ts @@ -17,37 +17,43 @@ export type LanguageClientConstructor = (name: string, description: string, clie export class MdLanguageClient implements IDisposable { + readonly #client: BaseLanguageClient; + readonly #workspace: VsCodeMdWorkspace; + constructor( - private readonly _client: BaseLanguageClient, - private readonly _workspace: VsCodeMdWorkspace, - ) { } + client: BaseLanguageClient, + workspace: VsCodeMdWorkspace, + ) { + this.#client = client; + this.#workspace = workspace; + } dispose(): void { - this._client.stop(); - this._workspace.dispose(); + this.#client.stop(); + this.#workspace.dispose(); } resolveLinkTarget(linkText: string, uri: vscode.Uri): Promise { - return this._client.sendRequest(proto.resolveLinkTarget, { linkText, uri: uri.toString() }); + return this.#client.sendRequest(proto.resolveLinkTarget, { linkText, uri: uri.toString() }); } getEditForFileRenames(files: ReadonlyArray<{ oldUri: string; newUri: string }>, token: vscode.CancellationToken) { - return this._client.sendRequest(proto.getEditForFileRenames, files, token); + return this.#client.sendRequest(proto.getEditForFileRenames, files, token); } getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken) { - return this._client.sendRequest(proto.getReferencesToFileInWorkspace, { uri: resource.toString() }, token); + return this.#client.sendRequest(proto.getReferencesToFileInWorkspace, { uri: resource.toString() }, token); } prepareUpdatePastedLinks(doc: vscode.Uri, ranges: readonly vscode.Range[], token: vscode.CancellationToken) { - return this._client.sendRequest(proto.prepareUpdatePastedLinks, { + return this.#client.sendRequest(proto.prepareUpdatePastedLinks, { uri: doc.toString(), ranges: ranges.map(range => Range.create(range.start.line, range.start.character, range.end.line, range.end.character)), }, token); } getUpdatePastedLinksEdit(pastingIntoDoc: vscode.Uri, edits: readonly vscode.TextEdit[], metadata: string, token: vscode.CancellationToken) { - return this._client.sendRequest(proto.getUpdatePastedLinksEdit, { + return this.#client.sendRequest(proto.getUpdatePastedLinksEdit, { metadata, pasteIntoDoc: pastingIntoDoc.toString(), edits: edits.map(edit => TextEdit.replace(edit.range, edit.newText)), diff --git a/extensions/markdown-language-features/src/client/fileWatchingManager.ts b/extensions/markdown-language-features/src/client/fileWatchingManager.ts index e2010edda8a..c617a73634d 100644 --- a/extensions/markdown-language-features/src/client/fileWatchingManager.ts +++ b/extensions/markdown-language-features/src/client/fileWatchingManager.ts @@ -17,12 +17,12 @@ type DirWatcherEntry = { export class FileWatcherManager { - private readonly _fileWatchers = new Map(); - private readonly _dirWatchers = new ResourceMap<{ + readonly #dirWatchers = new ResourceMap<{ readonly watcher: vscode.FileSystemWatcher; refCount: number; }>(); @@ -35,7 +35,7 @@ export class FileWatcherManager { const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, '*'), !listeners.create, !listeners.change, !listeners.delete); const parentDirWatchers: DirWatcherEntry[] = []; - this._fileWatchers.set(id, { watcher, dirWatchers: parentDirWatchers }); + this.#fileWatchers.set(id, { watcher, dirWatchers: parentDirWatchers }); if (listeners.create) { watcher.onDidCreate(listeners.create); } if (listeners.change) { watcher.onDidChange(listeners.change); } @@ -46,12 +46,12 @@ export class FileWatcherManager { for (let dirUri = Utils.dirname(uri); dirUri.path.length > 1; dirUri = Utils.dirname(dirUri)) { const disposables: IDisposable[] = []; - let parentDirWatcher = this._dirWatchers.get(dirUri); + let parentDirWatcher = this.#dirWatchers.get(dirUri); if (!parentDirWatcher) { const glob = new vscode.RelativePattern(Utils.dirname(dirUri), Utils.basename(dirUri)); const parentWatcher = vscode.workspace.createFileSystemWatcher(glob, !listeners.create, true, !listeners.delete); parentDirWatcher = { refCount: 0, watcher: parentWatcher }; - this._dirWatchers.set(dirUri, parentDirWatcher); + this.#dirWatchers.set(dirUri, parentDirWatcher); } parentDirWatcher.refCount++; @@ -81,16 +81,16 @@ export class FileWatcherManager { } delete(id: number): void { - const entry = this._fileWatchers.get(id); + const entry = this.#fileWatchers.get(id); if (entry) { for (const dirWatcher of entry.dirWatchers) { disposeAll(dirWatcher.disposables); - const dirWatcherEntry = this._dirWatchers.get(dirWatcher.uri); + const dirWatcherEntry = this.#dirWatchers.get(dirWatcher.uri); if (dirWatcherEntry) { if (--dirWatcherEntry.refCount <= 0) { dirWatcherEntry.watcher.dispose(); - this._dirWatchers.delete(dirWatcher.uri); + this.#dirWatchers.delete(dirWatcher.uri); } } } @@ -98,6 +98,6 @@ export class FileWatcherManager { entry.watcher.dispose(); } - this._fileWatchers.delete(id); + this.#fileWatchers.delete(id); } } diff --git a/extensions/markdown-language-features/src/client/inMemoryDocument.ts b/extensions/markdown-language-features/src/client/inMemoryDocument.ts index 953f0da7c89..2726adb6de1 100644 --- a/extensions/markdown-language-features/src/client/inMemoryDocument.ts +++ b/extensions/markdown-language-features/src/client/inMemoryDocument.ts @@ -9,7 +9,7 @@ import { ITextDocument } from '../types/textDocument'; export class InMemoryDocument implements ITextDocument { - private readonly _doc: TextDocument; + readonly #doc: TextDocument; public readonly uri: vscode.Uri; public readonly version: number; @@ -21,15 +21,15 @@ export class InMemoryDocument implements ITextDocument { ) { this.uri = uri; this.version = version; - this._doc = TextDocument.create(this.uri.toString(), 'markdown', 0, contents); + this.#doc = TextDocument.create(this.uri.toString(), 'markdown', 0, contents); } getText(range?: vscode.Range): string { - return this._doc.getText(range); + return this.#doc.getText(range); } positionAt(offset: number): vscode.Position { - const pos = this._doc.positionAt(offset); + const pos = this.#doc.positionAt(offset); return new vscode.Position(pos.line, pos.character); } } diff --git a/extensions/markdown-language-features/src/client/workspace.ts b/extensions/markdown-language-features/src/client/workspace.ts index 9ea3173c9cc..b07a93ade78 100644 --- a/extensions/markdown-language-features/src/client/workspace.ts +++ b/extensions/markdown-language-features/src/client/workspace.ts @@ -17,47 +17,47 @@ import { ResourceMap } from '../util/resourceMap'; */ export class VsCodeMdWorkspace extends Disposable { - private readonly _watcher: vscode.FileSystemWatcher | undefined; + readonly #watcher: vscode.FileSystemWatcher | undefined; - private readonly _documentCache = new ResourceMap(); + readonly #documentCache = new ResourceMap(); - private readonly _utf8Decoder = new TextDecoder('utf-8'); + readonly #utf8Decoder = new TextDecoder('utf-8'); constructor() { super(); - this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md')); + this.#watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md')); - this._register(this._watcher.onDidChange(async resource => { - this._documentCache.delete(resource); + this._register(this.#watcher.onDidChange(async resource => { + this.#documentCache.delete(resource); })); - this._register(this._watcher.onDidDelete(resource => { - this._documentCache.delete(resource); + this._register(this.#watcher.onDidDelete(resource => { + this.#documentCache.delete(resource); })); this._register(vscode.workspace.onDidOpenTextDocument(e => { - this._documentCache.delete(e.uri); + this.#documentCache.delete(e.uri); })); this._register(vscode.workspace.onDidCloseTextDocument(e => { - this._documentCache.delete(e.uri); + this.#documentCache.delete(e.uri); })); } - private _isRelevantMarkdownDocument(doc: vscode.TextDocument) { + #isRelevantMarkdownDocument(doc: vscode.TextDocument) { return isMarkdownFile(doc) && doc.uri.scheme !== 'vscode-bulkeditpreview'; } public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise { - const existing = this._documentCache.get(resource); + const existing = this.#documentCache.get(resource); if (existing) { return existing; } - const matchingDocument = vscode.workspace.textDocuments.find((doc) => this._isRelevantMarkdownDocument(doc) && doc.uri.toString() === resource.toString()); + const matchingDocument = vscode.workspace.textDocuments.find((doc) => this.#isRelevantMarkdownDocument(doc) && doc.uri.toString() === resource.toString()); if (matchingDocument) { - this._documentCache.set(resource, matchingDocument); + this.#documentCache.set(resource, matchingDocument); return matchingDocument; } @@ -69,9 +69,9 @@ export class VsCodeMdWorkspace extends Disposable { const bytes = await vscode.workspace.fs.readFile(resource); // We assume that markdown is in UTF-8 - const text = this._utf8Decoder.decode(bytes); + const text = this.#utf8Decoder.decode(bytes); const doc = new InMemoryDocument(resource, text, 0); - this._documentCache.set(resource, doc); + this.#documentCache.set(resource, doc); return doc; } catch { return undefined; diff --git a/extensions/markdown-language-features/src/commandManager.ts b/extensions/markdown-language-features/src/commandManager.ts index ae2c4985066..0563db020c7 100644 --- a/extensions/markdown-language-features/src/commandManager.ts +++ b/extensions/markdown-language-features/src/commandManager.ts @@ -12,27 +12,27 @@ export interface Command { } export class CommandManager { - private readonly _commands = new Map(); + readonly #commands = new Map(); public dispose() { - for (const registration of this._commands.values()) { + for (const registration of this.#commands.values()) { registration.dispose(); } - this._commands.clear(); + this.#commands.clear(); } public register(command: T): vscode.Disposable { - this._registerCommand(command.id, command.execute, command); + this.#registerCommand(command.id, command.execute, command); return new vscode.Disposable(() => { - this._commands.delete(command.id); + this.#commands.delete(command.id); }); } - private _registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { - if (this._commands.has(id)) { + #registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { + if (this.#commands.has(id)) { return; } - this._commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); + this.#commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); } } diff --git a/extensions/markdown-language-features/src/commands/copyImage.ts b/extensions/markdown-language-features/src/commands/copyImage.ts index 86fd349c730..09a683ecfb2 100644 --- a/extensions/markdown-language-features/src/commands/copyImage.ts +++ b/extensions/markdown-language-features/src/commands/copyImage.ts @@ -10,12 +10,16 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class CopyImageCommand implements Command { public readonly id = '_markdown.copyImage'; + readonly #webviewManager: MarkdownPreviewManager; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - ) { } + webviewManager: MarkdownPreviewManager, + ) { + this.#webviewManager = webviewManager; + } public execute(args: { id: string; resource: string }) { const source = vscode.Uri.parse(args.resource); - this._webviewManager.findPreview(source)?.copyImage(args.id); + this.#webviewManager.findPreview(source)?.copyImage(args.id); } } diff --git a/extensions/markdown-language-features/src/commands/openImage.ts b/extensions/markdown-language-features/src/commands/openImage.ts index 64b1831df0d..4a121fd6989 100644 --- a/extensions/markdown-language-features/src/commands/openImage.ts +++ b/extensions/markdown-language-features/src/commands/openImage.ts @@ -10,12 +10,16 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class OpenImageCommand implements Command { public readonly id = '_markdown.openImage'; + readonly #webviewManager: MarkdownPreviewManager; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - ) { } + webviewManager: MarkdownPreviewManager, + ) { + this.#webviewManager = webviewManager; + } public execute(args: { resource: string; imageSource: string }) { const source = vscode.Uri.parse(args.resource); - this._webviewManager.openDocumentLink(args.imageSource, source); + this.#webviewManager.openDocumentLink(args.imageSource, source); } } diff --git a/extensions/markdown-language-features/src/commands/refreshPreview.ts b/extensions/markdown-language-features/src/commands/refreshPreview.ts index a94fa797462..52f320098b7 100644 --- a/extensions/markdown-language-features/src/commands/refreshPreview.ts +++ b/extensions/markdown-language-features/src/commands/refreshPreview.ts @@ -10,13 +10,19 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class RefreshPreviewCommand implements Command { public readonly id = 'markdown.preview.refresh'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #engine: MarkdownItEngine; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _engine: MarkdownItEngine - ) { } + webviewManager: MarkdownPreviewManager, + engine: MarkdownItEngine + ) { + this.#webviewManager = webviewManager; + this.#engine = engine; + } public execute() { - this._engine.cleanCache(); - this._webviewManager.refresh(); + this.#engine.cleanCache(); + this.#webviewManager.refresh(); } } diff --git a/extensions/markdown-language-features/src/commands/reloadPlugins.ts b/extensions/markdown-language-features/src/commands/reloadPlugins.ts index 16be408bbef..5d410780a2b 100644 --- a/extensions/markdown-language-features/src/commands/reloadPlugins.ts +++ b/extensions/markdown-language-features/src/commands/reloadPlugins.ts @@ -10,14 +10,20 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class ReloadPlugins implements Command { public readonly id = 'markdown.api.reloadPlugins'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #engine: MarkdownItEngine; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _engine: MarkdownItEngine, - ) { } + webviewManager: MarkdownPreviewManager, + engine: MarkdownItEngine, + ) { + this.#webviewManager = webviewManager; + this.#engine = engine; + } public execute(): void { - this._engine.reloadPlugins(); - this._engine.cleanCache(); - this._webviewManager.refresh(); + this.#engine.reloadPlugins(); + this.#engine.cleanCache(); + this.#webviewManager.refresh(); } } diff --git a/extensions/markdown-language-features/src/commands/renderDocument.ts b/extensions/markdown-language-features/src/commands/renderDocument.ts index ccefddedbd2..ffcbab6d41a 100644 --- a/extensions/markdown-language-features/src/commands/renderDocument.ts +++ b/extensions/markdown-language-features/src/commands/renderDocument.ts @@ -10,11 +10,15 @@ import { ITextDocument } from '../types/textDocument'; export class RenderDocument implements Command { public readonly id = 'markdown.api.render'; + readonly #engine: MarkdownItEngine; + public constructor( - private readonly _engine: MarkdownItEngine - ) { } + engine: MarkdownItEngine + ) { + this.#engine = engine; + } public async execute(document: ITextDocument | string): Promise { - return (await (this._engine.render(document))).html; + return (await (this.#engine.render(document))).html; } } diff --git a/extensions/markdown-language-features/src/commands/showPreview.ts b/extensions/markdown-language-features/src/commands/showPreview.ts index d5d430ade0e..4d59062c1a1 100644 --- a/extensions/markdown-language-features/src/commands/showPreview.ts +++ b/extensions/markdown-language-features/src/commands/showPreview.ts @@ -53,14 +53,20 @@ async function showPreview( export class ShowPreviewCommand implements Command { public readonly id = 'markdown.showPreview'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #telemetryReporter: TelemetryReporter; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _telemetryReporter: TelemetryReporter - ) { } + webviewManager: MarkdownPreviewManager, + telemetryReporter: TelemetryReporter + ) { + this.#webviewManager = webviewManager; + this.#telemetryReporter = telemetryReporter; + } public execute(mainUri?: vscode.Uri, allUris?: vscode.Uri[], previewSettings?: DynamicPreviewSettings) { for (const uri of Array.isArray(allUris) ? allUris : [mainUri]) { - showPreview(this._webviewManager, this._telemetryReporter, uri, { + showPreview(this.#webviewManager, this.#telemetryReporter, uri, { sideBySide: false, locked: previewSettings?.locked }); @@ -71,13 +77,19 @@ export class ShowPreviewCommand implements Command { export class ShowPreviewToSideCommand implements Command { public readonly id = 'markdown.showPreviewToSide'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #telemetryReporter: TelemetryReporter; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _telemetryReporter: TelemetryReporter - ) { } + webviewManager: MarkdownPreviewManager, + telemetryReporter: TelemetryReporter + ) { + this.#webviewManager = webviewManager; + this.#telemetryReporter = telemetryReporter; + } public execute(uri?: vscode.Uri, previewSettings?: DynamicPreviewSettings) { - showPreview(this._webviewManager, this._telemetryReporter, uri, { + showPreview(this.#webviewManager, this.#telemetryReporter, uri, { sideBySide: true, locked: previewSettings?.locked }); @@ -88,13 +100,19 @@ export class ShowPreviewToSideCommand implements Command { export class ShowLockedPreviewToSideCommand implements Command { public readonly id = 'markdown.showLockedPreviewToSide'; + readonly #webviewManager: MarkdownPreviewManager; + readonly #telemetryReporter: TelemetryReporter; + public constructor( - private readonly _webviewManager: MarkdownPreviewManager, - private readonly _telemetryReporter: TelemetryReporter - ) { } + webviewManager: MarkdownPreviewManager, + telemetryReporter: TelemetryReporter + ) { + this.#webviewManager = webviewManager; + this.#telemetryReporter = telemetryReporter; + } public execute(uri?: vscode.Uri) { - showPreview(this._webviewManager, this._telemetryReporter, uri, { + showPreview(this.#webviewManager, this.#telemetryReporter, uri, { sideBySide: true, locked: true }); diff --git a/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts b/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts index 7ea5a4079ed..fb6cd9ce9e7 100644 --- a/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts +++ b/extensions/markdown-language-features/src/commands/showPreviewSecuritySelector.ts @@ -12,19 +12,25 @@ import { isMarkdownFile } from '../util/file'; export class ShowPreviewSecuritySelectorCommand implements Command { public readonly id = 'markdown.showPreviewSecuritySelector'; + readonly #previewSecuritySelector: PreviewSecuritySelector; + readonly #previewManager: MarkdownPreviewManager; + public constructor( - private readonly _previewSecuritySelector: PreviewSecuritySelector, - private readonly _previewManager: MarkdownPreviewManager - ) { } + previewSecuritySelector: PreviewSecuritySelector, + previewManager: MarkdownPreviewManager + ) { + this.#previewSecuritySelector = previewSecuritySelector; + this.#previewManager = previewManager; + } public execute(resource: string | undefined) { - if (this._previewManager.activePreviewResource) { - this._previewSecuritySelector.showSecuritySelectorForResource(this._previewManager.activePreviewResource); + if (this.#previewManager.activePreviewResource) { + this.#previewSecuritySelector.showSecuritySelectorForResource(this.#previewManager.activePreviewResource); } else if (resource) { const source = vscode.Uri.parse(resource); - this._previewSecuritySelector.showSecuritySelectorForResource(source.query ? vscode.Uri.parse(source.query) : source); + this.#previewSecuritySelector.showSecuritySelectorForResource(source.query ? vscode.Uri.parse(source.query) : source); } else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) { - this._previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri); + this.#previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri); } } } diff --git a/extensions/markdown-language-features/src/commands/showSource.ts b/extensions/markdown-language-features/src/commands/showSource.ts index 87d6b21ec68..3a6bb3e0e20 100644 --- a/extensions/markdown-language-features/src/commands/showSource.ts +++ b/extensions/markdown-language-features/src/commands/showSource.ts @@ -10,12 +10,16 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class ShowSourceCommand implements Command { public readonly id = 'markdown.showSource'; + readonly #previewManager: MarkdownPreviewManager; + public constructor( - private readonly _previewManager: MarkdownPreviewManager - ) { } + previewManager: MarkdownPreviewManager + ) { + this.#previewManager = previewManager; + } public execute() { - const { activePreviewResource, activePreviewResourceColumn } = this._previewManager; + const { activePreviewResource, activePreviewResourceColumn } = this.#previewManager; if (activePreviewResource && activePreviewResourceColumn) { return vscode.workspace.openTextDocument(activePreviewResource).then(document => { return vscode.window.showTextDocument(document, activePreviewResourceColumn); diff --git a/extensions/markdown-language-features/src/commands/toggleLock.ts b/extensions/markdown-language-features/src/commands/toggleLock.ts index 9975d4872bb..0bc4656d08f 100644 --- a/extensions/markdown-language-features/src/commands/toggleLock.ts +++ b/extensions/markdown-language-features/src/commands/toggleLock.ts @@ -9,11 +9,15 @@ import { MarkdownPreviewManager } from '../preview/previewManager'; export class ToggleLockCommand implements Command { public readonly id = 'markdown.preview.toggleLock'; + readonly #previewManager: MarkdownPreviewManager; + public constructor( - private readonly _previewManager: MarkdownPreviewManager - ) { } + previewManager: MarkdownPreviewManager + ) { + this.#previewManager = previewManager; + } public execute() { - this._previewManager.toggleLock(); + this.#previewManager.toggleLock(); } } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts index 7791d6b19e4..e59a207487d 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts @@ -36,14 +36,18 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v ...Object.values(rootMediaMimesTypes).map(type => `${type}/*`), ]; - private readonly _yieldTo = [ + readonly #yieldTo = [ vscode.DocumentDropOrPasteEditKind.Text, vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link', 'image', 'attachment'), // Prefer notebook attachments ]; + readonly #parser: IMdParser; + constructor( - private readonly _parser: IMdParser, - ) { } + parser: IMdParser, + ) { + this.#parser = parser; + } public async provideDocumentDropEdits( document: vscode.TextDocument, @@ -51,8 +55,8 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken, ): Promise { - const edit = await this._createEdit(document, [new vscode.Range(position, position)], dataTransfer, { - insert: this._getEnabled(document, 'editor.drop.enabled'), + const edit = await this.#createEdit(document, [new vscode.Range(position, position)], dataTransfer, { + insert: this.#getEnabled(document, 'editor.drop.enabled'), copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get('editor.drop.copyIntoWorkspace', CopyFilesSettings.MediaFiles) }, undefined, token); @@ -64,7 +68,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v dropEdit.title = edit.label; dropEdit.kind = edit.kind; dropEdit.additionalEdit = edit.additionalEdits; - dropEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo]; + dropEdit.yieldTo = [...this.#yieldTo, ...edit.yieldTo]; return dropEdit; } @@ -75,8 +79,8 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, ): Promise { - const edit = await this._createEdit(document, ranges, dataTransfer, { - insert: this._getEnabled(document, 'editor.paste.enabled'), + const edit = await this.#createEdit(document, ranges, dataTransfer, { + insert: this.#getEnabled(document, 'editor.paste.enabled'), copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get('editor.paste.copyIntoWorkspace', CopyFilesSettings.MediaFiles) }, context, token); @@ -86,11 +90,11 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, edit.kind); pasteEdit.additionalEdit = edit.additionalEdits; - pasteEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo]; + pasteEdit.yieldTo = [...this.#yieldTo, ...edit.yieldTo]; return [pasteEdit]; } - private _getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink { + #getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink { const setting = vscode.workspace.getConfiguration('markdown', document).get(settingName, true); // Convert old boolean values to new enum setting if (setting === false) { @@ -102,7 +106,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v } } - private async _createEdit( + async #createEdit( document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, @@ -117,27 +121,27 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v return; } - let edit = await this._createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token); + let edit = await this.#createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token); if (token.isCancellationRequested) { return; } if (!edit) { - edit = await this._createEditFromUriListData(document, ranges, dataTransfer, context, token); + edit = await this.#createEditFromUriListData(document, ranges, dataTransfer, context, token); } if (!edit || token.isCancellationRequested) { return; } - if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, settings.insert, ranges, token))) { + if (!(await shouldInsertMarkdownLinkByDefault(this.#parser, document, settings.insert, ranges, token))) { edit.yieldTo.push(vscode.DocumentDropOrPasteEditKind.Empty.append('uri')); } return edit; } - private async _createEditFromUriListData( + async #createEditFromUriListData( document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, @@ -194,7 +198,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v * * This tries copying files outside of the workspace into the workspace. */ - private async _createEditForMediaFiles( + async #createEditForMediaFiles( document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, copyIntoWorkspace: CopyFilesSettings, diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts index 1625977a72c..fe9030704c1 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/newFilePathGenerator.ts @@ -12,7 +12,7 @@ import { CopyFileConfiguration, getCopyFileConfiguration, parseGlob, resolveCopy export class NewFilePathGenerator { - private readonly _usedPaths = new Set(); + readonly #usedPaths = new Set(); async getNewFilePath( document: vscode.TextDocument, @@ -33,13 +33,13 @@ export class NewFilePathGenerator { const name = i === 0 ? baseName : `${baseName}-${i}`; const uri = vscode.Uri.joinPath(root, name + ext); - if (this._wasPathAlreadyUsed(uri)) { + if (this.#wasPathAlreadyUsed(uri)) { continue; } // Try overwriting if it already exists if (config.overwriteBehavior === 'overwrite') { - this._usedPaths.add(uri.toString()); + this.#usedPaths.add(uri.toString()); return { uri, overwrite: true }; } @@ -47,17 +47,17 @@ export class NewFilePathGenerator { try { await vscode.workspace.fs.stat(uri); } catch { - if (!this._wasPathAlreadyUsed(uri)) { + if (!this.#wasPathAlreadyUsed(uri)) { // Does not exist - this._usedPaths.add(uri.toString()); + this.#usedPaths.add(uri.toString()); return { uri, overwrite: false }; } } } } - private _wasPathAlreadyUsed(uri: vscode.Uri) { - return this._usedPaths.has(uri.toString()); + #wasPathAlreadyUsed(uri: vscode.Uri) { + return this.#usedPaths.has(uri.toString()); } } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts index a947216fe32..faad54e10c3 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts @@ -21,9 +21,13 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { public static readonly pasteMimeTypes = [Mime.textPlain]; + readonly #parser: IMdParser; + constructor( - private readonly _parser: IMdParser, - ) { } + parser: IMdParser, + ) { + this.#parser = parser; + } async provideDocumentPasteEdits( document: vscode.TextDocument, @@ -64,7 +68,7 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { workspaceEdit.set(document.uri, edit.edits); pasteEdit.additionalEdit = workspaceEdit; - if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, pasteUrlSetting, ranges, token))) { + if (!(await shouldInsertMarkdownLinkByDefault(this.#parser, document, pasteUrlSetting, ranges, token))) { pasteEdit.yieldTo = [ vscode.DocumentDropOrPasteEditKind.Text, vscode.DocumentDropOrPasteEditKind.Empty.append('uri') diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index 8df16f4dcc6..871333cf4a4 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -19,18 +19,18 @@ export enum DiagnosticCode { class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { - private static readonly _addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks'; + static readonly #addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks'; - private static readonly _metadata: vscode.CodeActionProviderMetadata = { + static readonly #metadata: vscode.CodeActionProviderMetadata = { providedCodeActionKinds: [ vscode.CodeActionKind.QuickFix ], }; public static register(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable { - const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider._metadata); + const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider.#metadata); const commandReg = commandManager.register({ - id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, + id: AddToIgnoreLinksQuickFixProvider.#addToIgnoreLinksCommandId, execute(resource: vscode.Uri, path: string) { const settingId = 'validate.ignoredLinks'; const config = vscode.workspace.getConfiguration('markdown', resource); @@ -58,7 +58,7 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { vscode.CodeActionKind.QuickFix); fix.command = { - command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, + command: AddToIgnoreLinksQuickFixProvider.#addToIgnoreLinksCommandId, title: '', arguments: [document.uri, hrefText], }; diff --git a/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts b/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts index bda8b721e8b..23a2ed9e770 100644 --- a/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts +++ b/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts @@ -13,9 +13,13 @@ export class FindFileReferencesCommand implements Command { public readonly id = 'markdown.findAllFileReferences'; + readonly #client: MdLanguageClient; + constructor( - private readonly _client: MdLanguageClient, - ) { } + client: MdLanguageClient, + ) { + this.#client = client; + } public async execute(resource?: vscode.Uri) { resource ??= vscode.window.activeTextEditor?.document.uri; @@ -28,7 +32,7 @@ export class FindFileReferencesCommand implements Command { location: vscode.ProgressLocation.Window, title: vscode.l10n.t("Finding file references") }, async (_progress, token) => { - const locations = (await this._client.getReferencesToFileInWorkspace(resource, token)).map(loc => { + const locations = (await this.#client.getReferencesToFileInWorkspace(resource, token)).map(loc => { return new vscode.Location(vscode.Uri.parse(loc.uri), convertRange(loc.range)); }); diff --git a/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts b/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts index 5d42a033842..d912caa9060 100644 --- a/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts +++ b/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts @@ -33,46 +33,48 @@ interface RenameAction { class UpdateLinksOnFileRenameHandler extends Disposable { - private readonly _delayer = new Delayer(50); - private readonly _pendingRenames = new Set(); + readonly #delayer = new Delayer(50); + readonly #pendingRenames = new Set(); + readonly #client: MdLanguageClient; public constructor( - private readonly _client: MdLanguageClient, + client: MdLanguageClient, ) { super(); + this.#client = client; this._register(vscode.workspace.onDidRenameFiles(async (e) => { await Promise.all(e.files.map(async (rename) => { - if (await this._shouldParticipateInLinkUpdate(rename.newUri)) { - this._pendingRenames.add(rename); + if (await this.#shouldParticipateInLinkUpdate(rename.newUri)) { + this.#pendingRenames.add(rename); } })); - if (this._pendingRenames.size) { - this._delayer.trigger(() => { + if (this.#pendingRenames.size) { + this.#delayer.trigger(() => { vscode.window.withProgress({ location: vscode.ProgressLocation.Window, title: vscode.l10n.t("Checking for Markdown links to update") - }, () => this._flushRenames()); + }, () => this.#flushRenames()); }); } })); } - private async _flushRenames(): Promise { - const renames = Array.from(this._pendingRenames); - this._pendingRenames.clear(); + async #flushRenames(): Promise { + const renames = Array.from(this.#pendingRenames); + this.#pendingRenames.clear(); - const result = await this._getEditsForFileRename(renames, noopToken); + const result = await this.#getEditsForFileRename(renames, noopToken); if (result?.edit.size) { - if (await this._confirmActionWithUser(result.resourcesBeingRenamed)) { + if (await this.#confirmActionWithUser(result.resourcesBeingRenamed)) { await vscode.workspace.applyEdit(result.edit); } } } - private async _confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise { + async #confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise { if (!newResources.length) { return false; } @@ -81,7 +83,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { const setting = config.get(settingNames.enabled); switch (setting) { case UpdateLinksOnFileMoveSetting.Prompt: - return this._promptUser(newResources); + return this.#promptUser(newResources); case UpdateLinksOnFileMoveSetting.Always: return true; case UpdateLinksOnFileMoveSetting.Never: @@ -89,7 +91,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { return false; } } - private async _shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise { + async #shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise { const config = vscode.workspace.getConfiguration('markdown', newUri); const setting = config.get(settingNames.enabled); if (setting === UpdateLinksOnFileMoveSetting.Never) { @@ -113,7 +115,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { return false; } - private async _promptUser(newResources: readonly vscode.Uri[]): Promise { + async #promptUser(newResources: readonly vscode.Uri[]): Promise { if (!newResources.length) { return false; } @@ -138,7 +140,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { const choice = await vscode.window.showInformationMessage( newResources.length === 1 ? vscode.l10n.t("Update Markdown links for '{0}'?", Utils.basename(newResources[0])) - : this._getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), { + : this.#getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), { modal: true, }, rejectItem, acceptItem, alwaysItem, neverItem); @@ -154,7 +156,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { config.update( settingNames.enabled, UpdateLinksOnFileMoveSetting.Always, - this._getConfigTargetScope(config, settingNames.enabled)); + this.#getConfigTargetScope(config, settingNames.enabled)); return true; } case neverItem: { @@ -162,7 +164,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { config.update( settingNames.enabled, UpdateLinksOnFileMoveSetting.Never, - this._getConfigTargetScope(config, settingNames.enabled)); + this.#getConfigTargetScope(config, settingNames.enabled)); return false; } default: { @@ -171,8 +173,8 @@ class UpdateLinksOnFileRenameHandler extends Disposable { } } - private async _getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> { - const result = await this._client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token); + async #getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> { + const result = await this.#client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token); if (!result?.edit.documentChanges?.length) { return undefined; } @@ -192,7 +194,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { }; } - private _getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string { + #getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string { const MAX_CONFIRM_FILES = 10; const paths = [start]; @@ -211,7 +213,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable { return paths.join('\n'); } - private _getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget { + #getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget { const inspected = config.inspect(settingsName); if (inspected?.workspaceFolderValue) { return vscode.ConfigurationTarget.WorkspaceFolder; diff --git a/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts b/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts index f8a7128eb05..b27a869db0e 100644 --- a/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts +++ b/extensions/markdown-language-features/src/languageFeatures/updateLinksOnPaste.ts @@ -13,16 +13,20 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider public static readonly metadataMime = 'application/vnd.vscode.markdown.updatelinks.metadata'; + readonly #client: MdLanguageClient; + constructor( - private readonly _client: MdLanguageClient, - ) { } + client: MdLanguageClient, + ) { + this.#client = client; + } async prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { - if (!this._isEnabled(document)) { + if (!this.#isEnabled(document)) { return; } - const metadata = await this._client.prepareUpdatePastedLinks(document.uri, ranges, token); + const metadata = await this.#client.prepareUpdatePastedLinks(document.uri, ranges, token); if (token.isCancellationRequested) { return; } @@ -37,7 +41,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, ): Promise { - if (!this._isEnabled(document)) { + if (!this.#isEnabled(document)) { return; } @@ -56,7 +60,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider // - copy empty line // - Copy with multiple cursors and paste into multiple locations // - ... - const edits = await this._client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token); + const edits = await this.#client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token); if (!edits?.length || token.isCancellationRequested) { return; } @@ -73,7 +77,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider return [pasteEdit]; } - private _isEnabled(document: vscode.TextDocument): boolean { + #isEnabled(document: vscode.TextDocument): boolean { return vscode.workspace.getConfiguration('markdown', document.uri).get('editor.updateLinksOnPaste.enabled', true); } } diff --git a/extensions/markdown-language-features/src/logging.ts b/extensions/markdown-language-features/src/logging.ts index b5ea76f3608..30839d8c756 100644 --- a/extensions/markdown-language-features/src/logging.ts +++ b/extensions/markdown-language-features/src/logging.ts @@ -12,11 +12,11 @@ export interface ILogger { } export class VsCodeOutputLogger extends Disposable implements ILogger { - private _outputChannelValue?: vscode.LogOutputChannel; + #outputChannelValue?: vscode.LogOutputChannel; - private get _outputChannel() { - this._outputChannelValue ??= this._register(vscode.window.createOutputChannel('Markdown', { log: true })); - return this._outputChannelValue; + get #outputChannel() { + this.#outputChannelValue ??= this._register(vscode.window.createOutputChannel('Markdown', { log: true })); + return this.#outputChannelValue; } constructor() { @@ -24,6 +24,6 @@ export class VsCodeOutputLogger extends Disposable implements ILogger { } public trace(title: string, message: string, data?: any): void { - this._outputChannel.trace(`${title}: ${message}`, ...(data ? [JSON.stringify(data, null, 4)] : [])); + this.#outputChannel.trace(`${title}: ${message}`, ...(data ? [JSON.stringify(data, null, 4)] : [])); } } diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 0f4c7eb6717..4ed3186c380 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -44,37 +44,37 @@ const pluginSourceMap: MarkdownIt.PluginSimple = (md): void => { type MarkdownItConfig = Readonly>>; class TokenCache { - private _cachedDocument?: { + #cachedDocument?: { readonly uri: vscode.Uri; readonly version: number; readonly config: MarkdownItConfig; }; - private _tokens?: MarkdownIt.Token[]; + #tokens?: MarkdownIt.Token[]; public tryGetCached(document: ITextDocument, config: MarkdownItConfig): MarkdownIt.Token[] | undefined { - if (this._cachedDocument - && this._cachedDocument.uri.toString() === document.uri.toString() - && document.version >= 0 && this._cachedDocument.version === document.version - && this._cachedDocument.config.breaks === config.breaks - && this._cachedDocument.config.linkify === config.linkify + if (this.#cachedDocument + && this.#cachedDocument.uri.toString() === document.uri.toString() + && document.version >= 0 && this.#cachedDocument.version === document.version + && this.#cachedDocument.config.breaks === config.breaks + && this.#cachedDocument.config.linkify === config.linkify ) { - return this._tokens; + return this.#tokens; } return undefined; } public update(document: ITextDocument, config: MarkdownItConfig, tokens: MarkdownIt.Token[]) { - this._cachedDocument = { + this.#cachedDocument = { uri: document.uri, version: document.version, config, }; - this._tokens = tokens; + this.#tokens = tokens; } public clean(): void { - this._cachedDocument = undefined; - this._tokens = undefined; + this.#cachedDocument = undefined; + this.#tokens = undefined; } } @@ -98,40 +98,45 @@ export interface IMdParser { export class MarkdownItEngine implements IMdParser { - private _md?: Promise; + #md?: Promise; - private readonly _tokenCache = new TokenCache(); + readonly #tokenCache = new TokenCache(); public readonly slugifier: ISlugifier; - public constructor( - private readonly _contributionProvider: MarkdownContributionProvider, - slugifier: ISlugifier, - private readonly _logger: ILogger, - ) { - this.slugifier = slugifier; + readonly #contributionProvider: MarkdownContributionProvider; + readonly #logger: ILogger; - _contributionProvider.onContributionsChanged(() => { + public constructor( + contributionProvider: MarkdownContributionProvider, + slugifier: ISlugifier, + logger: ILogger, + ) { + this.#contributionProvider = contributionProvider; + this.slugifier = slugifier; + this.#logger = logger; + + contributionProvider.onContributionsChanged(() => { // Markdown plugin contributions may have changed - this._md = undefined; - this._tokenCache.clean(); + this.#md = undefined; + this.#tokenCache.clean(); }); } public async getEngine(resource: vscode.Uri | undefined): Promise { - const config = this._getConfig(resource); - return this._getEngine(config); + const config = this.#getConfig(resource); + return this.#getEngine(config); } - private async _getEngine(config: MarkdownItConfig): Promise { - if (!this._md) { - this._md = (async () => { + async #getEngine(config: MarkdownItConfig): Promise { + if (!this.#md) { + this.#md = (async () => { const markdownIt = await import('markdown-it'); let md: MarkdownIt = markdownIt.default(await getMarkdownOptions(() => md)); md.linkify.set({ fuzzyLink: false }); - for (const plugin of this._contributionProvider.contributions.markdownItPlugins.values()) { + for (const plugin of this.#contributionProvider.contributions.markdownItPlugins.values()) { try { md = (await plugin)(md); } catch (e) { @@ -154,43 +159,43 @@ export class MarkdownItEngine implements IMdParser { alt: ['paragraph', 'reference', 'blockquote', 'list'] }); - this._addImageRenderer(md); - this._addFencedRenderer(md); - this._addLinkNormalizer(md); - this._addLinkValidator(md); - this._addNamedHeaders(md); - this._addLinkRenderer(md); + this.#addImageRenderer(md); + this.#addFencedRenderer(md); + this.#addLinkNormalizer(md); + this.#addLinkValidator(md); + this.#addNamedHeaders(md); + this.#addLinkRenderer(md); md.use(pluginSourceMap); return md; })(); } - const md = await this._md!; + const md = await this.#md!; md.set(config); return md; } public reloadPlugins() { - this._md = undefined; + this.#md = undefined; } - private _tokenizeDocument( + #tokenizeDocument( document: ITextDocument, config: MarkdownItConfig, engine: MarkdownIt ): MarkdownIt.Token[] { - const cached = this._tokenCache.tryGetCached(document, config); + const cached = this.#tokenCache.tryGetCached(document, config); if (cached) { return cached; } - this._logger.trace('MarkdownItEngine', `tokenizeDocument - ${document.uri}`); - const tokens = this._tokenizeString(document.getText(), engine); - this._tokenCache.update(document, config, tokens); + this.#logger.trace('MarkdownItEngine', `tokenizeDocument - ${document.uri}`); + const tokens = this.#tokenizeString(document.getText(), engine); + this.#tokenCache.update(document, config, tokens); return tokens; } - private _tokenizeString(text: string, engine: MarkdownIt) { + #tokenizeString(text: string, engine: MarkdownIt) { const env: RenderEnv = { currentDocument: undefined, containingImages: new Set(), @@ -201,12 +206,12 @@ export class MarkdownItEngine implements IMdParser { } public async render(input: ITextDocument | string, resourceProvider?: WebviewResourceProvider): Promise { - const config = this._getConfig(typeof input === 'string' ? undefined : input.uri); - const engine = await this._getEngine(config); + const config = this.#getConfig(typeof input === 'string' ? undefined : input.uri); + const engine = await this.#getEngine(config); const tokens = typeof input === 'string' - ? this._tokenizeString(input, engine) - : this._tokenizeDocument(input, config, engine); + ? this.#tokenizeString(input, engine) + : this.#tokenizeDocument(input, config, engine); const env: RenderEnv = { containingImages: new Set(), @@ -227,16 +232,16 @@ export class MarkdownItEngine implements IMdParser { } public async tokenize(document: ITextDocument): Promise { - const config = this._getConfig(document.uri); - const engine = await this._getEngine(config); - return this._tokenizeDocument(document, config, engine); + const config = this.#getConfig(document.uri); + const engine = await this.#getEngine(config); + return this.#tokenizeDocument(document, config, engine); } public cleanCache(): void { - this._tokenCache.clean(); + this.#tokenCache.clean(); } - private _getConfig(resource?: vscode.Uri): MarkdownItConfig { + #getConfig(resource?: vscode.Uri): MarkdownItConfig { const config = MarkdownPreviewConfiguration.getForResource(resource ?? null); return { breaks: config.previewLineBreaks, @@ -245,7 +250,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addImageRenderer(md: MarkdownIt): void { + #addImageRenderer(md: MarkdownIt): void { const original = md.renderer.rules.image; md.renderer.rules.image = (tokens: MarkdownIt.Token[], idx: number, options, env: RenderEnv, self) => { const token = tokens[idx]; @@ -254,7 +259,7 @@ export class MarkdownItEngine implements IMdParser { env.containingImages?.add(src); if (!token.attrGet('data-src')) { - token.attrSet('src', this._toResourceUri(src, env.currentDocument, env.resourceProvider)); + token.attrSet('src', this.#toResourceUri(src, env.currentDocument, env.resourceProvider)); token.attrSet('data-src', src); } } @@ -267,7 +272,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addFencedRenderer(md: MarkdownIt): void { + #addFencedRenderer(md: MarkdownIt): void { const original = md.renderer.rules['fenced']; md.renderer.rules['fenced'] = (tokens: MarkdownIt.Token[], idx: number, options, env, self) => { const token = tokens[idx]; @@ -283,7 +288,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addLinkNormalizer(md: MarkdownIt): void { + #addLinkNormalizer(md: MarkdownIt): void { const normalizeLink = md.normalizeLink; md.normalizeLink = (link: string) => { try { @@ -299,7 +304,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addLinkValidator(md: MarkdownIt): void { + #addLinkValidator(md: MarkdownIt): void { const validateLink = md.validateLink; md.validateLink = (link: string) => { return validateLink(link) @@ -309,10 +314,10 @@ export class MarkdownItEngine implements IMdParser { }; } - private _addNamedHeaders(md: MarkdownIt): void { + #addNamedHeaders(md: MarkdownIt): void { const original = md.renderer.rules.heading_open; md.renderer.rules.heading_open = (tokens: MarkdownIt.Token[], idx: number, options, env: unknown, self) => { - const title = this._tokenToPlainText(tokens[idx + 1]); + const title = this.#tokenToPlainText(tokens[idx + 1]); const slug = (env as RenderEnv).slugifier ? (env as RenderEnv).slugifier.add(title) : this.slugifier.fromHeading(title); tokens[idx].attrSet('id', slug.value); @@ -324,9 +329,9 @@ export class MarkdownItEngine implements IMdParser { }; } - private _tokenToPlainText(token: MarkdownIt.Token): string { + #tokenToPlainText(token: MarkdownIt.Token): string { if (token.children) { - return token.children.map(x => this._tokenToPlainText(x)).join(''); + return token.children.map(x => this.#tokenToPlainText(x)).join(''); } switch (token.type) { @@ -339,7 +344,7 @@ export class MarkdownItEngine implements IMdParser { } } - private _addLinkRenderer(md: MarkdownIt): void { + #addLinkRenderer(md: MarkdownIt): void { const original = md.renderer.rules.link_open; md.renderer.rules.link_open = (tokens: MarkdownIt.Token[], idx: number, options, env, self) => { @@ -357,7 +362,7 @@ export class MarkdownItEngine implements IMdParser { }; } - private _toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string { + #toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string { try { // Support file:// links if (isOfScheme(Schemes.file, href)) { diff --git a/extensions/markdown-language-features/src/markdownExtensions.ts b/extensions/markdown-language-features/src/markdownExtensions.ts index 1357b03d1de..568a0f0b638 100644 --- a/extensions/markdown-language-features/src/markdownExtensions.ts +++ b/extensions/markdown-language-features/src/markdownExtensions.ts @@ -119,36 +119,38 @@ export interface MarkdownContributionProvider { class VSCodeExtensionMarkdownContributionProvider extends Disposable implements MarkdownContributionProvider { - private _contributions?: MarkdownContributions; + #contributions?: MarkdownContributions; + readonly #extensionContext: vscode.ExtensionContext; public constructor( - private readonly _extensionContext: vscode.ExtensionContext, + extensionContext: vscode.ExtensionContext, ) { super(); + this.#extensionContext = extensionContext; this._register(vscode.extensions.onDidChange(() => { - const currentContributions = this._getCurrentContributions(); - const existingContributions = this._contributions || MarkdownContributions.Empty; + const currentContributions = this.#getCurrentContributions(); + const existingContributions = this.#contributions || MarkdownContributions.Empty; if (!MarkdownContributions.equal(existingContributions, currentContributions)) { - this._contributions = currentContributions; - this._onContributionsChanged.fire(this); + this.#contributions = currentContributions; + this.#onContributionsChanged.fire(this); } })); } public get extensionUri() { - return this._extensionContext.extensionUri; + return this.#extensionContext.extensionUri; } - private readonly _onContributionsChanged = this._register(new vscode.EventEmitter()); - public readonly onContributionsChanged = this._onContributionsChanged.event; + readonly #onContributionsChanged = this._register(new vscode.EventEmitter()); + public readonly onContributionsChanged = this.#onContributionsChanged.event; public get contributions(): MarkdownContributions { - this._contributions ??= this._getCurrentContributions(); - return this._contributions; + this.#contributions ??= this.#getCurrentContributions(); + return this.#contributions; } - private _getCurrentContributions(): MarkdownContributions { + #getCurrentContributions(): MarkdownContributions { return vscode.extensions.all .map(MarkdownContributions.fromExtension) .reduce(MarkdownContributions.merge, MarkdownContributions.Empty); diff --git a/extensions/markdown-language-features/src/preview/documentRenderer.ts b/extensions/markdown-language-features/src/preview/documentRenderer.ts index 61182a24436..f96fce9b745 100644 --- a/extensions/markdown-language-features/src/preview/documentRenderer.ts +++ b/extensions/markdown-language-features/src/preview/documentRenderer.ts @@ -41,16 +41,28 @@ export interface ImageInfo { } export class MdDocumentRenderer { + + readonly #engine: MarkdownItEngine; + readonly #context: vscode.ExtensionContext; + readonly #cspArbiter: ContentSecurityPolicyArbiter; + readonly #contributionProvider: MarkdownContributionProvider; + readonly #logger: ILogger; + constructor( - private readonly _engine: MarkdownItEngine, - private readonly _context: vscode.ExtensionContext, - private readonly _cspArbiter: ContentSecurityPolicyArbiter, - private readonly _contributionProvider: MarkdownContributionProvider, - private readonly _logger: ILogger + engine: MarkdownItEngine, + context: vscode.ExtensionContext, + cspArbiter: ContentSecurityPolicyArbiter, + contributionProvider: MarkdownContributionProvider, + logger: ILogger ) { + this.#engine = engine; + this.#context = context; + this.#cspArbiter = cspArbiter; + this.#contributionProvider = contributionProvider; + this.#logger = logger; this.iconPath = { - dark: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-dark.svg'), - light: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-light.svg'), + dark: vscode.Uri.joinPath(this.#context.extensionUri, 'media', 'preview-dark.svg'), + light: vscode.Uri.joinPath(this.#context.extensionUri, 'media', 'preview-light.svg'), }; } @@ -76,15 +88,15 @@ export class MdDocumentRenderer { scrollPreviewWithEditor: config.scrollPreviewWithEditor, scrollEditorWithPreview: config.scrollEditorWithPreview, doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor, - disableSecurityWarnings: this._cspArbiter.shouldDisableSecurityWarnings(), + disableSecurityWarnings: this.#cspArbiter.shouldDisableSecurityWarnings(), webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(), }; - this._logger.trace('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData); + this.#logger.trace('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData); // Content Security Policy const nonce = generateUuid(); - const csp = this._getCsp(resourceProvider, sourceUri, nonce); + const csp = this.#getCsp(resourceProvider, sourceUri, nonce); const body = await this.renderBody(markdownDocument, resourceProvider); if (token.isCancellationRequested) { @@ -92,7 +104,7 @@ export class MdDocumentRenderer { } const html = ` - + @@ -101,12 +113,12 @@ export class MdDocumentRenderer { data-strings="${escapeAttribute(JSON.stringify(previewStrings))}" data-state="${escapeAttribute(JSON.stringify(state || {}))}" data-initial-md-content="${escapeAttribute(body.html)}"> - - ${this._getStyles(resourceProvider, sourceUri, config, imageInfo)} + + ${this.#getStyles(resourceProvider, sourceUri, config, imageInfo)} - ${this._getScripts(resourceProvider, nonce)} + ${this.#getScripts(resourceProvider, nonce)} `; return { @@ -119,7 +131,7 @@ export class MdDocumentRenderer { markdownDocument: vscode.TextDocument, resourceProvider: WebviewResourceProvider, ): Promise { - const rendered = await this._engine.render(markdownDocument, resourceProvider); + const rendered = await this.#engine.render(markdownDocument, resourceProvider); const html = `
${rendered.html}
`; return { html, @@ -138,13 +150,13 @@ export class MdDocumentRenderer { `; } - private _extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string { + #extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string { const webviewResource = resourceProvider.asWebviewUri( - vscode.Uri.joinPath(this._context.extensionUri, 'media', mediaFile)); + vscode.Uri.joinPath(this.#context.extensionUri, 'media', mediaFile)); return webviewResource.toString(); } - private _fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string { + #fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string { if (!href) { return href; } @@ -168,18 +180,18 @@ export class MdDocumentRenderer { return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString(); } - private _computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { + #computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { if (!Array.isArray(config.styles)) { return ''; } const out: string[] = []; for (const style of config.styles) { - out.push(``); + out.push(``); } return out.join('\n'); } - private _getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string { + #getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string { return [ config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '', isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`, @@ -187,7 +199,7 @@ export class MdDocumentRenderer { ].join(' '); } - private _getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string { + #getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string { if (!imageInfo.length) { return ''; } @@ -204,20 +216,20 @@ export class MdDocumentRenderer { return ret; } - private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string { + #getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string { const baseStyles: string[] = []; - for (const resource of this._contributionProvider.contributions.previewStyles) { + for (const resource of this.#contributionProvider.contributions.previewStyles) { baseStyles.push(``); } return `${baseStyles.join('\n')} - ${this._computeCustomStyleSheetIncludes(resourceProvider, resource, config)} - ${this._getImageStabilizerStyles(imageInfo)}`; + ${this.#computeCustomStyleSheetIncludes(resourceProvider, resource, config)} + ${this.#getImageStabilizerStyles(imageInfo)}`; } - private _getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string { + #getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string { const out: string[] = []; - for (const resource of this._contributionProvider.contributions.previewScripts) { + for (const resource of this.#contributionProvider.contributions.previewScripts) { out.push(` + + + + + +`; +} + +/** Recursively collect *.css paths relative to `dir`. */ +function collectCssFiles(dir, prefix) { + let results = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const rel = prefix ? prefix + '/' + entry.name : entry.name; + if (entry.isDirectory()) { + results = results.concat(collectCssFiles(path.join(dir, entry.name), rel)); + } else if (entry.name.endsWith('.css')) { + results.push(rel); + } + } + return results; +} + +main(); + diff --git a/scripts/code-sessions-web.sh b/scripts/code-sessions-web.sh new file mode 100755 index 00000000000..be62921a05f --- /dev/null +++ b/scripts/code-sessions-web.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(realpath "$0"))) +else + ROOT=$(dirname $(dirname $(readlink -f $0))) +fi + +function code() { + cd $ROOT + + # Sync built-in extensions + npm run download-builtin-extensions + + NODE=$(node build/lib/node.ts) + if [ ! -e $NODE ];then + # Load remote node + npm run gulp node + fi + + NODE=$(node build/lib/node.ts) + + $NODE ./scripts/code-sessions-web.js "$@" +} + +code "$@" diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 881275f34af..cdd1923cbe6 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -8,11 +8,10 @@ import { CharCode } from '../../../base/common/charCode.js'; import * as strings from '../../../base/common/strings.js'; import { assertReturnsDefined } from '../../../base/common/types.js'; import { applyFontInfo } from '../config/domFontInfo.js'; -import { WrappingIndent } from '../../common/config/editorOptions.js'; -import { FontInfo } from '../../common/config/fontInfo.js'; +import { EditorOption, IComputedEditorOptions, WrappingIndent } from '../../common/config/editorOptions.js'; import { StringBuilder } from '../../common/core/stringBuilder.js'; import { InjectedTextOptions } from '../../common/model.js'; -import { ILineBreaksComputer, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js'; +import { ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js'; import { LineInjectedText } from '../../common/textModelEvents.js'; const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value }); @@ -26,26 +25,25 @@ export class DOMLineBreaksComputerFactory implements ILineBreaksComputerFactory constructor(private targetWindow: WeakRef) { } - public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer { - const requests: string[] = []; - const injectedTexts: (LineInjectedText[] | null)[] = []; + public createLineBreaksComputer(context: ILineBreaksComputerContext, options: IComputedEditorOptions, tabSize: number): ILineBreaksComputer { + const lineNumbers: number[] = []; return { - addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => { - requests.push(lineText); - injectedTexts.push(injectedText); + addRequest: (lineNumber: number, previousLineBreakData: ModelLineProjectionData | null) => { + lineNumbers.push(lineNumber); }, finalize: () => { - return createLineBreaks(assertReturnsDefined(this.targetWindow.deref()), requests, fontInfo, tabSize, wrappingColumn, wrappingIndent, wordBreak, injectedTexts); + return createLineBreaks(assertReturnsDefined(this.targetWindow.deref()), context, lineNumbers, options, tabSize); } }; } } -function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: FontInfo, tabSize: number, firstLineBreakColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', injectedTextsPerLine: (LineInjectedText[] | null)[]): (ModelLineProjectionData | null)[] { - function createEmptyLineBreakWithPossiblyInjectedText(requestIdx: number): ModelLineProjectionData | null { - const injectedTexts = injectedTextsPerLine[requestIdx]; +function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerContext, lineNumbers: number[], options: IComputedEditorOptions, tabSize: number): (ModelLineProjectionData | null)[] { + function createEmptyLineBreakWithPossiblyInjectedText(lineNumber: number): ModelLineProjectionData | null { + const injectedTexts = context.getLineInjectedText(lineNumber); if (injectedTexts) { - const lineText = LineInjectedText.applyInjectedText(requests[requestIdx], injectedTexts); + const lineContent = context.getLineContent(lineNumber); + const lineText = LineInjectedText.applyInjectedText(lineContent, injectedTexts); const injectionOptions = injectedTexts.map(t => t.options); const injectionOffsets = injectedTexts.map(text => text.column - 1); @@ -57,11 +55,14 @@ function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: Fo return null; } } - + const wrappingIndent = options.get(EditorOption.wrappingIndent); + const fontInfo = options.get(EditorOption.fontInfo); + const wordBreak = options.get(EditorOption.wordBreak); + const firstLineBreakColumn = options.get(EditorOption.wrappingInfo).wrappingColumn; if (firstLineBreakColumn === -1) { const result: (ModelLineProjectionData | null)[] = []; - for (let i = 0, len = requests.length; i < len; i++) { - result[i] = createEmptyLineBreakWithPossiblyInjectedText(i); + for (let i = 0, len = lineNumbers.length; i < len; i++) { + result[i] = createEmptyLineBreakWithPossiblyInjectedText(lineNumbers[i]); } return result; } @@ -80,8 +81,9 @@ function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: Fo const renderLineContents: string[] = []; const allCharOffsets: number[][] = []; const allVisibleColumns: number[][] = []; - for (let i = 0; i < requests.length; i++) { - const lineContent = LineInjectedText.applyInjectedText(requests[i], injectedTextsPerLine[i]); + for (let i = 0; i < lineNumbers.length; i++) { + const lineNumber = lineNumbers[i]; + const lineContent = LineInjectedText.applyInjectedText(context.getLineContent(lineNumber), context.getLineInjectedText(lineNumber)); let firstNonWhitespaceIndex = 0; let wrappedTextIndentLength = 0; @@ -146,11 +148,12 @@ function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: Fo const lineDomNodes = Array.prototype.slice.call(containerDomNode.children, 0); const result: (ModelLineProjectionData | null)[] = []; - for (let i = 0; i < requests.length; i++) { + for (let i = 0; i < lineNumbers.length; i++) { + const lineNumber = lineNumbers[i]; const lineDomNode = lineDomNodes[i]; const breakOffsets: number[] | null = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i]); if (breakOffsets === null) { - result[i] = createEmptyLineBreakWithPossiblyInjectedText(i); + result[i] = createEmptyLineBreakWithPossiblyInjectedText(lineNumber); continue; } @@ -172,7 +175,7 @@ function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: Fo let injectionOptions: InjectedTextOptions[] | null; let injectionOffsets: number[] | null; - const curInjectedTexts = injectedTextsPerLine[i]; + const curInjectedTexts = context.getLineInjectedText(lineNumber); if (curInjectedTexts) { injectionOptions = curInjectedTexts.map(t => t.options); injectionOffsets = curInjectedTexts.map(text => text.column - 1); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index 62bf7ece01a..56d3850c945 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -31,6 +31,7 @@ import { IContextMenuService } from '../../../../../../platform/contextview/brow import { DiffEditorOptions } from '../../diffEditorOptions.js'; import { Range } from '../../../../../common/core/range.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../common/viewModel/inlineDecorations.js'; +import { ILineBreaksComputerContext } from '../../../../../common/modelLineProjectionData.js'; /** * Ensures both editors have the same height by aligning unchanged lines. @@ -163,8 +164,15 @@ export class DiffEditorViewZones extends Disposable { } const renderSideBySide = this._options.renderSideBySide.read(reader); - - const deletedCodeLineBreaksComputer = !renderSideBySide ? this._editors.modified._getViewModel()?.createLineBreaksComputer() : undefined; + const context: ILineBreaksComputerContext = { + getLineContent: (lineNumber: number): string => { + return this._editors.original.getModel()!.getLineContent(lineNumber); + }, + getLineInjectedText: (lineNumber: number) => { + return null; + } + }; + const deletedCodeLineBreaksComputer = !renderSideBySide ? this._editors.modified._getViewModel()?.createLineBreaksComputer(context) : undefined; if (deletedCodeLineBreaksComputer) { const originalModel = this._editors.original.getModel()!; for (const a of alignmentsVal) { @@ -176,7 +184,7 @@ export class DiffEditorViewZones extends Disposable { if (i > originalModel.getLineCount()) { return { orig: origViewZones, mod: modViewZones }; } - deletedCodeLineBreaksComputer?.addRequest(originalModel.getLineContent(i), null, null); + deletedCodeLineBreaksComputer?.addRequest(i, null); } } } diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 5195e0b6353..2fc027b34a2 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -19,7 +19,7 @@ import { IWordAtPosition } from './core/wordHelper.js'; import { FormattingOptions } from './languages.js'; import { ILanguageSelection } from './languages/language.js'; import { IBracketPairsTextModelPart } from './textModelBracketPairs.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelFontChangedEvent, ModelLineHeightChangedEvent } from './textModelEvents.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, LineInjectedText, ModelFontChangedEvent, ModelLineHeightChangedEvent } from './textModelEvents.js'; import { IModelContentChange } from './model/mirrorTextModel.js'; import { IGuidesTextModelPart } from './textModelGuides.js'; import { ITokenizationTextModelPart } from './tokenizationTextModelPart.js'; @@ -856,6 +856,12 @@ export interface ITextModel { */ getLineContent(lineNumber: number): string; + /** + * Get the line injected text for a certain line. + * @internal + */ + getLineInjectedText(lineNumber: number, ownerId?: number): LineInjectedText[]; + /** * Get the text length for a certain line. */ diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 8a276a599e4..6d7e5a6be55 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ArrayQueue, pushMany } from '../../../base/common/arrays.js'; +import { pushMany } from '../../../base/common/arrays.js'; import { VSBuffer, VSBufferReadableStream } from '../../../base/common/buffer.js'; import { CharCode } from '../../../base/common/charCode.js'; import { SetWithKey } from '../../../base/common/collections.js'; @@ -1539,33 +1539,15 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const changeLineCountDelta = (insertingLinesCnt - deletingLinesCnt); const currentEditStartLineNumber = newLineCount - lineCount - changeLineCountDelta + startLineNumber; - const firstEditLineNumber = currentEditStartLineNumber; - const lastInsertedLineNumber = currentEditStartLineNumber + insertingLinesCnt; - - const decorationsWithInjectedTextInEditedRange = this._decorationsTree.getInjectedTextInInterval( - this, - this.getOffsetAt(new Position(firstEditLineNumber, 1)), - this.getOffsetAt(new Position(lastInsertedLineNumber, this.getLineMaxColumn(lastInsertedLineNumber))), - 0 - ); - - - const injectedTextInEditedRange = LineInjectedText.fromDecorations(decorationsWithInjectedTextInEditedRange); - const injectedTextInEditedRangeQueue = new ArrayQueue(injectedTextInEditedRange); for (let j = editingLinesCnt; j >= 0; j--) { const editLineNumber = startLineNumber + j; const currentEditLineNumber = currentEditStartLineNumber + j; - injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber > currentEditLineNumber); - const decorationsInCurrentLine = injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber === currentEditLineNumber); - rawContentChanges.push( new ModelRawLineChanged( editLineNumber, - currentEditLineNumber, - this.getLineContent(currentEditLineNumber), - decorationsInCurrentLine + currentEditLineNumber )); } @@ -1578,28 +1560,15 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } if (editingLinesCnt < insertingLinesCnt) { - const injectedTextInEditedRangeQueue = new ArrayQueue(injectedTextInEditedRange); // Must insert some lines const spliceLineNumber = startLineNumber + editingLinesCnt; const cnt = insertingLinesCnt - editingLinesCnt; const fromLineNumber = newLineCount - lineCount - cnt + spliceLineNumber + 1; - const injectedTexts: (LineInjectedText[] | null)[] = []; - const newLines: string[] = []; - for (let i = 0; i < cnt; i++) { - const lineNumber = fromLineNumber + i; - newLines[i] = this.getLineContent(lineNumber); - - injectedTextInEditedRangeQueue.takeWhile(r => r.lineNumber < lineNumber); - injectedTexts[i] = injectedTextInEditedRangeQueue.takeWhile(r => r.lineNumber === lineNumber); - } - rawContentChanges.push( new ModelRawLinesInserted( spliceLineNumber + 1, fromLineNumber, - cnt, - newLines, - injectedTexts + cnt ) ); } @@ -1657,7 +1626,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (affectedInjectedTextLines && affectedInjectedTextLines.size > 0) { const affectedLines = Array.from(affectedInjectedTextLines); - const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); + const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, lineNumber)); this._onDidChangeContentOrInjectedText(new ModelInjectedTextChangedEvent(lineChangeEvents)); } this._fireOnDidChangeLineHeight(affectedLineHeights); @@ -1883,11 +1852,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return decs; } - private _getInjectedTextInLine(lineNumber: number): LineInjectedText[] { + public getLineInjectedText(lineNumber: number, ownerId: number = 0): LineInjectedText[] { const startOffset = this._buffer.getOffsetAt(lineNumber, 1); const endOffset = startOffset + this._buffer.getLineLength(lineNumber); - const result = this._decorationsTree.getInjectedTextInInterval(this, startOffset, endOffset, 0); + const result = this._decorationsTree.getInjectedTextInInterval(this, startOffset, endOffset, ownerId); return LineInjectedText.fromDecorations(result).filter(t => t.lineNumber === lineNumber); } diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index aac6ae4642d..dc661c4b25a 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { assertNever } from '../../base/common/assert.js'; -import { WrappingIndent } from './config/editorOptions.js'; -import { FontInfo } from './config/fontInfo.js'; +import { IComputedEditorOptions } from './config/editorOptions.js'; import { Position } from './core/position.js'; import { InjectedTextCursorStops, InjectedTextOptions, PositionAffinity } from './model.js'; import { LineInjectedText } from './textModelEvents.js'; @@ -328,14 +327,19 @@ export class OutputPosition { } } +export interface ILineBreaksComputerContext { + getLineContent(lineNumber: number): string; + getLineInjectedText(lineNumber: number): LineInjectedText[] | null; +} + export interface ILineBreaksComputerFactory { - createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer; + createLineBreaksComputer(context: ILineBreaksComputerContext, options: IComputedEditorOptions, tabSize: number): ILineBreaksComputer; } export interface ILineBreaksComputer { /** * Pass in `previousLineBreakData` if the only difference is in breaking columns!!! */ - addRequest(lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null): void; + addRequest(lineNumber: number, previousLineBreakData: ModelLineProjectionData | null): void; finalize(): (ModelLineProjectionData | null)[]; } diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index ebbba093183..1f504db5852 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -315,20 +315,10 @@ export class ModelRawLineChanged { * The new line number the old one is mapped to (after the change was applied). */ public readonly lineNumberPostEdit: number; - /** - * The new value of the line. - */ - public readonly detail: string; - /** - * The injected text on the line. - */ - public readonly injectedText: LineInjectedText[] | null; - constructor(lineNumber: number, lineNumberPostEdit: number, detail: string, injectedText: LineInjectedText[] | null) { + constructor(lineNumber: number, lineNumberPostEdit: number) { this.lineNumber = lineNumber; this.lineNumberPostEdit = lineNumberPostEdit; - this.detail = detail; - this.injectedText = injectedText; } } @@ -439,21 +429,11 @@ export class ModelRawLinesInserted { public get toLineNumberPostEdit(): number { return this.fromLineNumberPostEdit + this.count - 1; } - /** - * The text that was inserted - */ - public readonly detail: string[]; - /** - * The injected texts for every inserted line. - */ - public readonly injectedTexts: (LineInjectedText[] | null)[]; - constructor(fromLineNumber: number, fromLineNumberPostEdit: number, count: number, detail: string[], injectedTexts: (LineInjectedText[] | null)[]) { - this.injectedTexts = injectedTexts; + constructor(fromLineNumber: number, fromLineNumberPostEdit: number, count: number) { this.fromLineNumber = fromLineNumber; this.fromLineNumberPostEdit = fromLineNumberPostEdit; this.count = count; - this.detail = detail; } } diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 37ccca993c4..94d4488ec9b 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -15,7 +15,7 @@ import { CursorChangeReason } from './cursorEvents.js'; import { INewScrollPosition, ScrollType } from './editorCommon.js'; import { EditorTheme } from './editorTheme.js'; import { EndOfLinePreference, IGlyphMarginLanesModel, IModelDecorationOptions, ITextModel, TextDirection } from './model.js'; -import { ILineBreaksComputer, InjectedText } from './modelLineProjectionData.js'; +import { ILineBreaksComputer, ILineBreaksComputerContext, InjectedText } from './modelLineProjectionData.js'; import { InternalModelContentChangeEvent, ModelInjectedTextChangedEvent } from './textModelEvents.js'; import { BracketGuideOptions, IActiveIndentGuideInfo, IndentGuide } from './textModelGuides.js'; import { IViewLineTokens } from './tokens/lineTokens.js'; @@ -89,7 +89,7 @@ export interface IViewModel extends ICursorSimpleModel, ISimpleModel { onDidChangeContentOrInjectedText(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void; emitContentChangeEvent(e: InternalModelContentChangeEvent | ModelInjectedTextChangedEvent): void; - createLineBreaksComputer(): ILineBreaksComputer; + createLineBreaksComputer(context?: ILineBreaksComputerContext): ILineBreaksComputer; //#region cursor getPrimaryCursorState(): CursorState; diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 37fba226625..58d52910d2e 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -7,10 +7,9 @@ import { CharCode } from '../../../base/common/charCode.js'; import * as strings from '../../../base/common/strings.js'; import { WrappingIndent, IComputedEditorOptions, EditorOption } from '../config/editorOptions.js'; import { CharacterClassifier } from '../core/characterClassifier.js'; -import { FontInfo } from '../config/fontInfo.js'; import { LineInjectedText } from '../textModelEvents.js'; import { InjectedTextOptions } from '../model.js'; -import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData } from '../modelLineProjectionData.js'; +import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory { public static create(options: IComputedEditorOptions): MonospaceLineBreaksComputerFactory { @@ -26,23 +25,27 @@ export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFa this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars); } - public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer { - const requests: string[] = []; - const injectedTexts: (LineInjectedText[] | null)[] = []; + public createLineBreaksComputer(context: ILineBreaksComputerContext, options: IComputedEditorOptions, tabSize: number): ILineBreaksComputer { + const lineNumbers: number[] = []; const previousBreakingData: (ModelLineProjectionData | null)[] = []; return { - addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => { - requests.push(lineText); - injectedTexts.push(injectedText); + addRequest: (lineNumber: number, previousLineBreakData: ModelLineProjectionData | null) => { + lineNumbers.push(lineNumber); previousBreakingData.push(previousLineBreakData); }, finalize: () => { + const fontInfo = options.get(EditorOption.fontInfo); + const wrappingColumn = options.get(EditorOption.wrappingInfo).wrappingColumn; + const wrappingIndent = options.get(EditorOption.wrappingIndent); + const wordBreak = options.get(EditorOption.wordBreak); + const wrapOnEscapedLineFeeds = options.get(EditorOption.wrapOnEscapedLineFeeds); const columnsForFullWidthChar = fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth; const result: (ModelLineProjectionData | null)[] = []; - for (let i = 0, len = requests.length; i < len; i++) { - const injectedText = injectedTexts[i]; + for (let i = 0, len = lineNumbers.length; i < len; i++) { + const lineNumber = lineNumbers[i]; + const injectedText = context.getLineInjectedText(lineNumber); + const lineText = context.getLineContent(lineNumber); const previousLineBreakData = previousBreakingData[i]; - const lineText = requests[i]; const isLineFeedWrappingEnabled = wrapOnEscapedLineFeeds && lineText.includes('"') && lineText.includes('\\n'); if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText && !isLineFeedWrappingEnabled) { result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, lineText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index f632d695033..b84682c5441 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -33,7 +33,7 @@ import { EditorTheme } from '../editorTheme.js'; import * as viewEvents from '../viewEvents.js'; import { ViewLayout } from '../viewLayout/viewLayout.js'; import { MinimapTokensColorTracker } from './minimapTokensColorTracker.js'; -import { ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText } from '../modelLineProjectionData.js'; +import { ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, InjectedText } from '../modelLineProjectionData.js'; import { ViewEventHandler } from '../viewEventHandler.js'; import { ILineHeightChangeAccessor, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; import { ViewModelDecorations } from './viewModelDecorations.js'; @@ -97,25 +97,13 @@ export class ViewModel extends Disposable implements IViewModel { } else { const options = this._configuration.options; - const fontInfo = options.get(EditorOption.fontInfo); - const wrappingStrategy = options.get(EditorOption.wrappingStrategy); - const wrappingInfo = options.get(EditorOption.wrappingInfo); - const wrappingIndent = options.get(EditorOption.wrappingIndent); - const wordBreak = options.get(EditorOption.wordBreak); - const wrapOnEscapedLineFeeds = options.get(EditorOption.wrapOnEscapedLineFeeds); - this._lines = new ViewModelLinesFromProjectedModel( this._editorId, this.model, domLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, - fontInfo, - this.model.getOptions().tabSize, - wrappingStrategy, - wrappingInfo.wrappingColumn, - wrappingIndent, - wordBreak, - wrapOnEscapedLineFeeds + options, + this.model.getOptions().tabSize ); } @@ -184,8 +172,8 @@ export class ViewModel extends Disposable implements IViewModel { return this._configuration.options.get(id); } - public createLineBreaksComputer(): ILineBreaksComputer { - return this._lines.createLineBreaksComputer(); + public createLineBreaksComputer(context?: ILineBreaksComputerContext): ILineBreaksComputer { + return this._lines.createLineBreaksComputer(context); } public addViewEventHandler(eventHandler: ViewEventHandler): void { @@ -274,13 +262,8 @@ export class ViewModel extends Disposable implements IViewModel { private _onConfigurationChanged(eventsCollector: ViewModelEventsCollector, e: ConfigurationChangedEvent): void { const stableViewport = this._captureStableViewport(); const options = this._configuration.options; - const fontInfo = options.get(EditorOption.fontInfo); - const wrappingStrategy = options.get(EditorOption.wrappingStrategy); - const wrappingInfo = options.get(EditorOption.wrappingInfo); - const wrappingIndent = options.get(EditorOption.wrappingIndent); - const wordBreak = options.get(EditorOption.wordBreak); - if (this._lines.setWrappingSettings(fontInfo, wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent, wordBreak)) { + if (this._lines.setWrappingSettings(e, options)) { eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); @@ -332,22 +315,13 @@ export class ViewModel extends Disposable implements IViewModel { for (const change of changes) { switch (change.changeType) { case textModelEvents.RawContentChangedType.LinesInserted: { - for (let lineIdx = 0; lineIdx < change.detail.length; lineIdx++) { - const line = change.detail[lineIdx]; - let injectedText = change.injectedTexts[lineIdx]; - if (injectedText) { - injectedText = injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId)); - } - lineBreaksComputer.addRequest(line, injectedText, null); + for (let i = 0; i < change.count; i++) { + lineBreaksComputer.addRequest(change.fromLineNumberPostEdit + i, null); } break; } case textModelEvents.RawContentChangedType.LineChanged: { - let injectedText: textModelEvents.LineInjectedText[] | null = null; - if (change.injectedText) { - injectedText = change.injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId)); - } - lineBreaksComputer.addRequest(change.detail, injectedText, null); + lineBreaksComputer.addRequest(change.lineNumberPostEdit, null); break; } } @@ -381,7 +355,7 @@ export class ViewModel extends Disposable implements IViewModel { break; } case textModelEvents.RawContentChangedType.LinesInserted: { - const insertedLineBreaks = lineBreakQueue.takeCount(change.detail.length); + const insertedLineBreaks = lineBreakQueue.takeCount(change.count); const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); if (linesInsertedEvent !== null) { eventsCollector.emitViewEvent(linesInsertedEvent); diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index b3721760c9d..d57ca33a6f6 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -3,32 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as arrays from '../../../base/common/arrays.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; -import { WrappingIndent } from '../config/editorOptions.js'; -import { FontInfo } from '../config/fontInfo.js'; +import { ConfigurationChangedEvent, EditorOption, IComputedEditorOptions } from '../config/editorOptions.js'; import { IPosition, Position } from '../core/position.js'; import { Range } from '../core/range.js'; import { IModelDecoration, IModelDeltaDecoration, ITextModel, PositionAffinity } from '../model.js'; import { IActiveIndentGuideInfo, BracketGuideOptions, IndentGuide, IndentGuideHorizontalLine } from '../textModelGuides.js'; import { ModelDecorationOptions } from '../model/textModel.js'; -import { LineInjectedText } from '../textModelEvents.js'; import * as viewEvents from '../viewEvents.js'; import { createModelLineProjection, IModelLineProjection } from './modelLineProjection.js'; -import { ILineBreaksComputer, ModelLineProjectionData, InjectedText, ILineBreaksComputerFactory } from '../modelLineProjectionData.js'; +import { ILineBreaksComputer, ModelLineProjectionData, InjectedText, ILineBreaksComputerFactory, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; import { ConstantTimePrefixSumComputer } from '../model/prefixSumComputer.js'; import { ViewLineData } from '../viewModel.js'; import { ICoordinatesConverter, IdentityCoordinatesConverter } from '../coordinatesConverter.js'; +import { LineInjectedText } from '../textModelEvents.js'; export interface IViewModelLines extends IDisposable { createCoordinatesConverter(): ICoordinatesConverter; - setWrappingSettings(fontInfo: FontInfo, wrappingStrategy: 'simple' | 'advanced', wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): boolean; + setWrappingSettings(e: ConfigurationChangedEvent, options: IComputedEditorOptions): boolean; setTabSize(newTabSize: number): boolean; getHiddenAreas(): Range[]; setHiddenAreas(_ranges: readonly Range[]): boolean; - createLineBreaksComputer(): ILineBreaksComputer; + createLineBreaksComputer(context?: ILineBreaksComputerContext): ILineBreaksComputer; onModelFlushed(): void; onModelLinesDeleted(versionId: number | null, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null; onModelLinesInserted(versionId: number | null, fromLineNumber: number, toLineNumber: number, lineBreaks: (ModelLineProjectionData | null)[]): viewEvents.ViewLinesInsertedEvent | null; @@ -66,13 +64,8 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { private readonly _domLineBreaksComputerFactory: ILineBreaksComputerFactory; private readonly _monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory; - private fontInfo: FontInfo; + private options: IComputedEditorOptions; private tabSize: number; - private wrappingColumn: number; - private wrappingIndent: WrappingIndent; - private wordBreak: 'normal' | 'keepAll'; - private wrappingStrategy: 'simple' | 'advanced'; - private wrapOnEscapedLineFeeds: boolean; private modelLineProjections!: IModelLineProjection[]; @@ -88,26 +81,16 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { model: ITextModel, domLineBreaksComputerFactory: ILineBreaksComputerFactory, monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory, - fontInfo: FontInfo, + options: IComputedEditorOptions, tabSize: number, - wrappingStrategy: 'simple' | 'advanced', - wrappingColumn: number, - wrappingIndent: WrappingIndent, - wordBreak: 'normal' | 'keepAll', - wrapOnEscapedLineFeeds: boolean ) { this._editorId = editorId; this.model = model; this._validModelVersionId = -1; this._domLineBreaksComputerFactory = domLineBreaksComputerFactory; this._monospaceLineBreaksComputerFactory = monospaceLineBreaksComputerFactory; - this.fontInfo = fontInfo; + this.options = options; this.tabSize = tabSize; - this.wrappingStrategy = wrappingStrategy; - this.wrappingColumn = wrappingColumn; - this.wrappingIndent = wrappingIndent; - this.wordBreak = wordBreak; - this.wrapOnEscapedLineFeeds = wrapOnEscapedLineFeeds; this._constructLines(/*resetHiddenAreas*/true, null); } @@ -128,14 +111,11 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { } const linesContent = this.model.getLinesContent(); - const injectedTextDecorations = this.model.getInjectedTextDecorations(this._editorId); const lineCount = linesContent.length; const lineBreaksComputer = this.createLineBreaksComputer(); - const injectedTextQueue = new arrays.ArrayQueue(LineInjectedText.fromDecorations(injectedTextDecorations)); for (let i = 0; i < lineCount; i++) { - const lineInjectedText = injectedTextQueue.takeWhile(t => t.lineNumber === i + 1); - lineBreaksComputer.addRequest(linesContent[i], lineInjectedText, previousLineBreaks ? previousLineBreaks[i] : null); + lineBreaksComputer.addRequest(i + 1, previousLineBreaks ? previousLineBreaks[i] : null); } const linesBreaks = lineBreaksComputer.finalize(); @@ -278,23 +258,19 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { return true; } - public setWrappingSettings(fontInfo: FontInfo, wrappingStrategy: 'simple' | 'advanced', wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): boolean { - const equalFontInfo = this.fontInfo.equals(fontInfo); - const equalWrappingStrategy = (this.wrappingStrategy === wrappingStrategy); - const equalWrappingColumn = (this.wrappingColumn === wrappingColumn); - const equalWrappingIndent = (this.wrappingIndent === wrappingIndent); - const equalWordBreak = (this.wordBreak === wordBreak); + public setWrappingSettings(e: ConfigurationChangedEvent, options: IComputedEditorOptions): boolean { + const equalFontInfo = !e.hasChanged(EditorOption.fontInfo); + const equalWrappingStrategy = !e.hasChanged(EditorOption.wrappingStrategy); + const equalWrappingColumn = !e.hasChanged(EditorOption.wordWrapColumn); + const equalWrappingIndent = !e.hasChanged(EditorOption.wrappingIndent); + const equalWordBreak = !e.hasChanged(EditorOption.wordBreak); if (equalFontInfo && equalWrappingStrategy && equalWrappingColumn && equalWrappingIndent && equalWordBreak) { return false; } const onlyWrappingColumnChanged = (equalFontInfo && equalWrappingStrategy && !equalWrappingColumn && equalWrappingIndent && equalWordBreak); - this.fontInfo = fontInfo; - this.wrappingStrategy = wrappingStrategy; - this.wrappingColumn = wrappingColumn; - this.wrappingIndent = wrappingIndent; - this.wordBreak = wordBreak; + this.options = options; let previousLineBreaks: ((ModelLineProjectionData | null)[]) | null = null; if (onlyWrappingColumnChanged) { @@ -309,13 +285,21 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { return true; } - public createLineBreaksComputer(): ILineBreaksComputer { + public createLineBreaksComputer(_context?: ILineBreaksComputerContext): ILineBreaksComputer { const lineBreaksComputerFactory = ( - this.wrappingStrategy === 'advanced' + this.options.get(EditorOption.wrappingStrategy) === 'advanced' ? this._domLineBreaksComputerFactory : this._monospaceLineBreaksComputerFactory ); - return lineBreaksComputerFactory.createLineBreaksComputer(this.fontInfo, this.tabSize, this.wrappingColumn, this.wrappingIndent, this.wordBreak, this.wrapOnEscapedLineFeeds); + const context: ILineBreaksComputerContext = _context ?? { + getLineContent: (lineNumber: number): string => { + return this.model.getLineContent(lineNumber); + }, + getLineInjectedText: (lineNumber: number): LineInjectedText[] => { + return this.model.getLineInjectedText(lineNumber, this._editorId); + } + }; + return lineBreaksComputerFactory.createLineBreaksComputer(context, this.options, this.tabSize); } public onModelFlushed(): void { @@ -1146,14 +1130,14 @@ export class ViewModelLinesFromModelAsIs implements IViewModelLines { return false; } - public setWrappingSettings(_fontInfo: FontInfo, _wrappingStrategy: 'simple' | 'advanced', _wrappingColumn: number, _wrappingIndent: WrappingIndent): boolean { + public setWrappingSettings(e: ConfigurationChangedEvent, options: IComputedEditorOptions): boolean { return false; } public createLineBreaksComputer(): ILineBreaksComputer { const result: null[] = []; return { - addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => { + addRequest: (lineNumber: number, previousLineBreakData: ModelLineProjectionData | null) => { result.push(null); }, finalize: () => { diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index a1b8b00bd48..6bcb8223428 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -348,6 +348,19 @@ export class ContextMenuController implements IEditorContribution { value: 'always' }] )); + actions.push(createEnumAction<'right' | 'left'>( + nls.localize('context.minimap.side', "Side"), + minimapOptions.enabled, + 'editor.minimap.side', + minimapOptions.side, + [{ + label: nls.localize('context.minimap.side.right', "Right"), + value: 'right' + }, { + label: nls.localize('context.minimap.side.left', "Left"), + value: 'left' + }] + )); const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035 this._contextMenuIsBeingShownCount++; diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css index d613456e162..31dcb1d38a9 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css @@ -78,6 +78,9 @@ .monaco-editor .parameter-hints-widget .docs { padding: 0 10px 0 5px; white-space: pre-wrap; + overflow-wrap: break-word; + word-break: break-word; + min-width: 0; } .monaco-editor .parameter-hints-widget .docs.empty { @@ -95,6 +98,9 @@ .monaco-editor .parameter-hints-widget .docs .markdown-docs { white-space: initial; + overflow-wrap: break-word; + word-break: break-word; + max-width: 100%; } .monaco-editor .parameter-hints-widget .docs code { diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 05f1deb2aa0..dfe7d943135 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -94,14 +94,9 @@ suite('Editor ViewModel - SplitLinesCollection', () => { }); function withSplitLinesCollection(text: string, callback: (model: TextModel, linesCollection: ViewModelLinesFromProjectedModel) => void): void { - const config = new TestConfiguration({}); - const wrappingInfo = config.options.get(EditorOption.wrappingInfo); - const fontInfo = config.options.get(EditorOption.fontInfo); + const config = new TestConfiguration({ wrappingStrategy: 'simple' }); const wordWrapBreakAfterCharacters = config.options.get(EditorOption.wordWrapBreakAfterCharacters); const wordWrapBreakBeforeCharacters = config.options.get(EditorOption.wordWrapBreakBeforeCharacters); - const wrappingIndent = config.options.get(EditorOption.wrappingIndent); - const wordBreak = config.options.get(EditorOption.wordBreak); - const wrapOnEscapedLineFeeds = config.options.get(EditorOption.wrapOnEscapedLineFeeds); const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory(wordWrapBreakBeforeCharacters, wordWrapBreakAfterCharacters); const model = createTextModel(text); @@ -111,13 +106,8 @@ suite('Editor ViewModel - SplitLinesCollection', () => { model, lineBreaksComputerFactory, lineBreaksComputerFactory, - fontInfo, - model.getOptions().tabSize, - 'simple', - wrappingInfo.wrappingColumn, - wrappingIndent, - wordBreak, - wrapOnEscapedLineFeeds + config.options, + model.getOptions().tabSize ); callback(model, linesCollection); @@ -943,14 +933,11 @@ suite('SplitLinesCollection', () => { const configuration = new TestConfiguration({ wordWrap: wordWrap, wordWrapColumn: wordWrapColumn, - wrappingIndent: 'indent' + wrappingIndent: 'indent', + wrappingStrategy: 'simple' }); - const wrappingInfo = configuration.options.get(EditorOption.wrappingInfo); - const fontInfo = configuration.options.get(EditorOption.fontInfo); const wordWrapBreakAfterCharacters = configuration.options.get(EditorOption.wordWrapBreakAfterCharacters); const wordWrapBreakBeforeCharacters = configuration.options.get(EditorOption.wordWrapBreakBeforeCharacters); - const wrappingIndent = configuration.options.get(EditorOption.wrappingIndent); - const wordBreak = configuration.options.get(EditorOption.wordBreak); const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory(wordWrapBreakBeforeCharacters, wordWrapBreakAfterCharacters); @@ -959,13 +946,8 @@ suite('SplitLinesCollection', () => { model, lineBreaksComputerFactory, lineBreaksComputerFactory, - fontInfo, - model.getOptions().tabSize, - 'simple', - wrappingInfo.wrappingColumn, - wrappingIndent, - wordBreak, - wrapOnEscapedLineFeeds + configuration.options, + model.getOptions().tabSize ); callback(linesCollection); diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/browser/viewModel/monospaceLineBreaksComputer.test.ts similarity index 94% rename from src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts rename to src/vs/editor/test/browser/viewModel/monospaceLineBreaksComputer.test.ts index bed861e44a1..7934cc68674 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/browser/viewModel/monospaceLineBreaksComputer.test.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { EditorOptions, WrappingIndent } from '../../../common/config/editorOptions.js'; +import { EditorOption, EditorOptions, WrappingIndent } from '../../../common/config/editorOptions.js'; import { FontInfo } from '../../../common/config/fontInfo.js'; -import { ILineBreaksComputerFactory, ModelLineProjectionData } from '../../../common/modelLineProjectionData.js'; +import { ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../../common/modelLineProjectionData.js'; import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js'; +import { ComputedEditorOptions } from '../../../browser/config/editorConfiguration.js'; function parseAnnotatedText(annotatedText: string): { text: string; indices: number[] } { let text = ''; @@ -63,9 +64,29 @@ function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, wsmiddotWidth: 7, maxDigitWidth: 7 }, false); - const lineBreaksComputer = factory.createLineBreaksComputer(fontInfo, tabSize, breakAfter, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds); + const context: ILineBreaksComputerContext = { + getLineContent(lineNumber: number) { + return text; + }, + getLineInjectedText(lineNumber) { + return null; + } + }; + const options = new ComputedEditorOptions(); + options._write(EditorOption.fontInfo, fontInfo); + options._write(EditorOption.wrappingIndent, wrappingIndent); + options._write(EditorOption.wordWrapColumn, breakAfter); + options._write(EditorOption.wordBreak, wordBreak); + options._write(EditorOption.wrapOnEscapedLineFeeds, wrapOnEscapedLineFeeds); + options._write(EditorOption.wrappingInfo, { + isDominatedByLongLines: false, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: breakAfter, + }); + const lineBreaksComputer = factory.createLineBreaksComputer(context, options, tabSize); const previousLineBreakDataClone = previousLineBreakData ? new ModelLineProjectionData(null, null, previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), previousLineBreakData.wrappedTextIndentLength) : null; - lineBreaksComputer.addRequest(text, null, previousLineBreakDataClone); + lineBreaksComputer.addRequest(1, previousLineBreakDataClone); return lineBreaksComputer.finalize()[0]; } diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index c518ee86152..953e761c408 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -130,7 +130,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 1, 'foo My First Line', null) + new ModelRawLineChanged(1, 1) ], 2, false, @@ -144,8 +144,8 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 1, 'My new line', null), - new ModelRawLinesInserted(2, 2, 1, ['No longer First Line'], [null]), + new ModelRawLineChanged(1, 1), + new ModelRawLinesInserted(2, 2, 1), ], 2, false, @@ -216,7 +216,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 1, 'y First Line', null), + new ModelRawLineChanged(1, 1), ], 2, false, @@ -230,7 +230,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 1, '', null), + new ModelRawLineChanged(1, 1), ], 2, false, @@ -244,7 +244,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 1, 'My Second Line', null), + new ModelRawLineChanged(1, 1), new ModelRawLinesDeleted(2, 2, 1), ], 2, @@ -259,7 +259,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 1, 'My Third Line', null), + new ModelRawLineChanged(1, 1), new ModelRawLinesDeleted(2, 3, 1), ], 2, diff --git a/src/vs/editor/test/common/model/modelInjectedText.test.ts b/src/vs/editor/test/common/model/modelInjectedText.test.ts index d01509f6421..f67efe5677f 100644 --- a/src/vs/editor/test/common/model/modelInjectedText.test.ts +++ b/src/vs/editor/test/common/model/modelInjectedText.test.ts @@ -8,7 +8,7 @@ import { mock } from '../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { EditOperation } from '../../../common/core/editOperation.js'; import { Range } from '../../../common/core/range.js'; -import { InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, RawContentChangedType } from '../../../common/textModelEvents.js'; +import { InternalModelContentChangeEvent, ModelInjectedTextChangedEvent, ModelRawChange, RawContentChangedType } from '../../../common/textModelEvents.js'; import { IViewModel } from '../../../common/viewModel.js'; import { createTextModel } from '../testTextModel.js'; @@ -43,8 +43,8 @@ suite('Editor Model - Injected Text Events', () => { assert.deepStrictEqual(recordedChanges.splice(0), [ { kind: 'lineChanged', - line: '[injected1]First Line', lineNumber: 1, + lineNumberPostEdit: 1, } ]); @@ -67,13 +67,13 @@ suite('Editor Model - Injected Text Events', () => { assert.deepStrictEqual(recordedChanges.splice(0), [ { kind: 'lineChanged', - line: 'First Line', lineNumber: 1, + lineNumberPostEdit: 1, }, { kind: 'lineChanged', - line: '[injected1]S[injected2]econd Line', lineNumber: 2, + lineNumberPostEdit: 2, } ]); @@ -82,8 +82,8 @@ suite('Editor Model - Injected Text Events', () => { assert.deepStrictEqual(recordedChanges.splice(0), [ { kind: 'lineChanged', - line: '[injected1]SHello[injected2]econd Line', lineNumber: 2, + lineNumberPostEdit: 2, } ]); @@ -100,17 +100,13 @@ suite('Editor Model - Injected Text Events', () => { assert.deepStrictEqual(recordedChanges.splice(0), [ { kind: 'lineChanged', - line: '[injected1]S', lineNumber: 2, + lineNumberPostEdit: 2, }, { - fromLineNumber: 3, kind: 'linesInserted', - lines: [ - '', - '', - 'Hello[injected2]econd Line', - ] + fromLineNumber: 3, + count: 3, } ]); @@ -119,36 +115,24 @@ suite('Editor Model - Injected Text Events', () => { thisModel.pushEditOperations(null, [EditOperation.replace(new Range(3, 1, 5, 1), '\n\n\n\n\n\n\n\n\n\n\n\n\n')], null); assert.deepStrictEqual(recordedChanges.splice(0), [ { - 'kind': 'lineChanged', - 'line': '', - 'lineNumber': 5, + kind: 'lineChanged', + lineNumber: 5, + lineNumberPostEdit: 5, }, { - 'kind': 'lineChanged', - 'line': '', - 'lineNumber': 4, + kind: 'lineChanged', + lineNumber: 4, + lineNumberPostEdit: 4, }, { - 'kind': 'lineChanged', - 'line': '', - 'lineNumber': 3, + kind: 'lineChanged', + lineNumber: 3, + lineNumberPostEdit: 3, }, { - 'fromLineNumber': 6, - 'kind': 'linesInserted', - 'lines': [ - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - 'Hello[injected2]econd Line', - ] + kind: 'linesInserted', + fromLineNumber: 6, + count: 11, } ]); @@ -157,8 +141,8 @@ suite('Editor Model - Injected Text Events', () => { assert.deepStrictEqual(recordedChanges.splice(0), [ { kind: 'lineChanged', - line: '[injected1]SHello[injected2]econd Line', lineNumber: 2, + lineNumberPostEdit: 2, }, { kind: 'linesDeleted', @@ -171,20 +155,16 @@ suite('Editor Model - Injected Text Events', () => { function mapChange(change: ModelRawChange): unknown { if (change.changeType === RawContentChangedType.LineChanged) { - (change.injectedText || []).every(e => { - assert.deepStrictEqual(e.lineNumber, change.lineNumber); - }); - return { kind: 'lineChanged', - line: getDetail(change.detail, change.injectedText), lineNumber: change.lineNumber, + lineNumberPostEdit: change.lineNumberPostEdit, }; } else if (change.changeType === RawContentChangedType.LinesInserted) { return { kind: 'linesInserted', - lines: change.detail.map((e, idx) => getDetail(e, change.injectedTexts[idx])), - fromLineNumber: change.fromLineNumber + fromLineNumber: change.fromLineNumber, + count: change.count, }; } else if (change.changeType === RawContentChangedType.LinesDeleted) { return { @@ -201,7 +181,3 @@ function mapChange(change: ModelRawChange): unknown { } return { kind: 'unknown' }; } - -function getDetail(line: string, injectedTexts: LineInjectedText[] | null): string { - return LineInjectedText.applyInjectedText(line, (injectedTexts || []).map(t => t.withText(`[${t.options.content}]`))); -} diff --git a/src/vs/platform/browserElements/common/browserElements.ts b/src/vs/platform/browserElements/common/browserElements.ts index 2973d9db939..29735127134 100644 --- a/src/vs/platform/browserElements/common/browserElements.ts +++ b/src/vs/platform/browserElements/common/browserElements.ts @@ -56,6 +56,8 @@ export interface INativeBrowserElementsService { getElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator, cancellationId?: number): Promise; + getFocusedElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator, cancellationId?: number): Promise; + startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise; startConsoleSession(token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise; diff --git a/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts b/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts index 5e018a6d718..84a64f6d43b 100644 --- a/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts +++ b/src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts @@ -431,6 +431,121 @@ export class NativeBrowserElementsMainService extends Disposable implements INat }; } + async getFocusedElementData(windowId: number | undefined, rect: IRectangle, _token: CancellationToken, locator: IBrowserTargetLocator, _cancellationId?: number): Promise { + const window = this.windowById(windowId); + if (!window?.win) { + return undefined; + } + + const allWebContents = webContents.getAllWebContents(); + const simpleBrowserWebview = allWebContents.find(webContent => webContent.id === window.id); + if (!simpleBrowserWebview) { + return undefined; + } + + const debuggers = simpleBrowserWebview.debugger; + if (!debuggers.isAttached()) { + debuggers.attach(); + } + + let sessionId: string | undefined; + try { + const targetId = await this.findWebviewTarget(debuggers, locator); + if (!targetId) { + return undefined; + } + + const attach = await debuggers.sendCommand('Target.attachToTarget', { targetId, flatten: true }); + sessionId = attach.sessionId; + await debuggers.sendCommand('Runtime.enable', {}, sessionId); + + const { result } = await debuggers.sendCommand('Runtime.evaluate', { + expression: `(() => { + const el = document.activeElement; + if (!el || el.nodeType !== 1) { + return undefined; + } + const r = el.getBoundingClientRect(); + const attrs = {}; + for (let i = 0; i < el.attributes.length; i++) { + attrs[el.attributes[i].name] = el.attributes[i].value; + } + const ancestors = []; + let n = el; + while (n && n.nodeType === 1) { + const entry = { tagName: n.tagName.toLowerCase() }; + if (n.id) { + entry.id = n.id; + } + if (typeof n.className === 'string' && n.className.trim().length > 0) { + entry.classNames = n.className.trim().split(/\\s+/).filter(Boolean); + } + ancestors.unshift(entry); + n = n.parentElement; + } + const css = getComputedStyle(el); + const computedStyles = {}; + for (let i = 0; i < css.length; i++) { + const name = css[i]; + computedStyles[name] = css.getPropertyValue(name); + } + const text = (el.innerText || '').trim(); + return { + outerHTML: el.outerHTML, + computedStyle: '', + bounds: { x: r.x, y: r.y, width: r.width, height: r.height }, + ancestors, + attributes: attrs, + computedStyles, + dimensions: { top: r.top, left: r.left, width: r.width, height: r.height }, + innerText: text.length > 100 ? text.slice(0, 100) + '\\u2026' : text + }; + })();`, + returnByValue: true + }, sessionId); + + const focusedData = result?.value as NodeDataResponse | undefined; + if (!focusedData) { + return undefined; + } + + const zoomFactor = simpleBrowserWebview.getZoomFactor(); + const absoluteBounds = { + x: rect.x + focusedData.bounds.x, + y: rect.y + focusedData.bounds.y, + width: focusedData.bounds.width, + height: focusedData.bounds.height + }; + + const clippedBounds = { + x: Math.max(absoluteBounds.x, rect.x), + y: Math.max(absoluteBounds.y, rect.y), + width: Math.max(0, Math.min(absoluteBounds.x + absoluteBounds.width, rect.x + rect.width) - Math.max(absoluteBounds.x, rect.x)), + height: Math.max(0, Math.min(absoluteBounds.y + absoluteBounds.height, rect.y + rect.height) - Math.max(absoluteBounds.y, rect.y)) + }; + + return { + outerHTML: focusedData.outerHTML, + computedStyle: focusedData.computedStyle, + bounds: { + x: clippedBounds.x * zoomFactor, + y: clippedBounds.y * zoomFactor, + width: clippedBounds.width * zoomFactor, + height: clippedBounds.height * zoomFactor + }, + ancestors: focusedData.ancestors, + attributes: focusedData.attributes, + computedStyles: focusedData.computedStyles, + dimensions: focusedData.dimensions, + innerText: focusedData.innerText, + }; + } finally { + if (debuggers.isAttached()) { + debuggers.detach(); + } + } + } + async getNodeData(sessionId: string, debuggers: Electron.Debugger, window: BrowserWindow, cancellationId?: number): Promise { return new Promise((resolve, reject) => { const onMessage = async (event: Electron.Event, method: string, params: { backendNodeId: number }) => { diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index fe4f22d727d..098bb41f8f4 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -6,6 +6,7 @@ import { Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { URI } from '../../../base/common/uri.js'; +import { localize } from '../../../nls.js'; const commandPrefix = 'workbench.action.browser'; export enum BrowserViewCommandId { @@ -58,7 +59,7 @@ export interface IBrowserViewState { lastFavicon: string | undefined; lastError: IBrowserViewLoadError | undefined; storageScope: BrowserViewStorageScope; - zoomFactor: number; + browserZoomIndex: number; } export interface IBrowserViewNavigationEvent { @@ -143,6 +144,16 @@ export enum BrowserViewStorageScope { export const ipcBrowserViewChannelName = 'browserView'; +/** + * Discrete zoom levels matching Edge/Chrome. + * Note: When those browsers say "33%" and "67%" zoom, they really mean 33.33...% and 66.66...% + */ +export const browserZoomFactors = [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5] as const; +export const browserZoomDefaultIndex = browserZoomFactors.indexOf(1); +export function browserZoomLabel(zoomFactor: number): string { + return localize('browserZoomPercent', "{0}%", Math.round(zoomFactor * 100)); +} + /** * This should match the isolated world ID defined in `preload-browserView.ts`. */ @@ -311,6 +322,9 @@ export interface IBrowserViewService { */ clearStorage(id: string): Promise; + /** Set the browser zoom index (independent from VS Code zoom). */ + setBrowserZoomIndex(id: string, zoomIndex: number): Promise; + /** * Update the keybinding accelerators used in browser view context menus. * @param keybindings A map of command ID to accelerator label diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index e08d161db21..445951e0c29 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -8,7 +8,7 @@ import { FileAccess } from '../../../base/common/network.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; -import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation, browserViewIsolatedWorldId } from '../common/browserView.js'; +import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, IBrowserViewFindInPageResult, IBrowserViewVisibilityEvent, BrowserNewPageLocation, browserViewIsolatedWorldId, browserZoomFactors, browserZoomDefaultIndex } from '../common/browserView.js'; import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { ICodeWindow } from '../../window/electron-main/window.js'; @@ -46,6 +46,7 @@ export class BrowserView extends Disposable implements ICDPTarget { private _lastFavicon: string | undefined = undefined; private _lastError: IBrowserViewLoadError | undefined = undefined; private _lastUserGestureTimestamp: number = -Infinity; + private _browserZoomIndex: number = browserZoomDefaultIndex; private _debugger: BrowserViewDebugger; private _window: ICodeWindow | IAuxiliaryWindow | undefined; @@ -278,6 +279,12 @@ export class BrowserView extends Disposable implements ICDPTarget { webContents.on('did-navigate', fireNavigationEvent); webContents.on('did-navigate-in-page', fireNavigationEvent); + // Chromium resets the zoom factor to its per-origin default (100%) when + // navigating to a new document. Re-apply our stored zoom to override it. + webContents.on('did-navigate', () => { + this._view.webContents.setZoomFactor(browserZoomFactors[this._browserZoomIndex]); + }); + // Focus events webContents.on('focus', () => { this._onDidChangeFocus.fire({ focused: true }); @@ -366,7 +373,7 @@ export class BrowserView extends Disposable implements ICDPTarget { lastFavicon: this._lastFavicon, lastError: this._lastError, storageScope: this.session.storageScope, - zoomFactor: webContents.getZoomFactor() + browserZoomIndex: this._browserZoomIndex }; } @@ -390,7 +397,6 @@ export class BrowserView extends Disposable implements ICDPTarget { } } - this._view.webContents.setZoomFactor(bounds.zoomFactor); this._view.setBorderRadius(Math.round(bounds.cornerRadius * bounds.zoomFactor)); this._view.setBounds({ x: Math.round(bounds.x * bounds.zoomFactor), @@ -400,6 +406,12 @@ export class BrowserView extends Disposable implements ICDPTarget { }); } + setBrowserZoomIndex(zoomIndex: number): void { + this._browserZoomIndex = Math.max(0, Math.min(zoomIndex, browserZoomFactors.length - 1)); + const browserZoomFactor = browserZoomFactors[this._browserZoomIndex]; + this._view.webContents.setZoomFactor(browserZoomFactor); + } + /** * Set the visibility of this view */ @@ -644,6 +656,7 @@ export class BrowserView extends Disposable implements ICDPTarget { const isArrowKey = keyCode >= KeyCode.LeftArrow && keyCode <= KeyCode.DownArrow; const isNonEditingKey = + keyCode === KeyCode.Enter || keyCode === KeyCode.Escape || keyCode >= KeyCode.F1 && keyCode <= KeyCode.F24 || keyCode >= KeyCode.AudioVolumeMute; diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index 313b3f416de..83901046a8e 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -328,6 +328,10 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa return this._getBrowserView(id).clearStorage(); } + async setBrowserZoomIndex(id: string, zoomIndex: number): Promise { + return this._getBrowserView(id).setBrowserZoomIndex(zoomIndex); + } + async clearGlobalStorage(): Promise { const browserSession = BrowserSession.getOrCreateGlobal(); await browserSession.electronSession.clearData(); diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index a5756b645fc..7af7bce71bc 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -123,6 +123,7 @@ export interface NativeParsedArgs { 'file-write'?: boolean; 'file-chmod'?: boolean; 'enable-smoke-test-driver'?: boolean; + 'skip-sessions-welcome'?: boolean; 'remote'?: string; 'force'?: boolean; 'do-not-sync'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 83a77caee4e..9a2575a40f4 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -168,6 +168,7 @@ export const OPTIONS: OptionDescriptions> = { 'export-policy-data': { type: 'string', allowEmptyValue: true }, 'install-source': { type: 'string' }, 'enable-smoke-test-driver': { type: 'boolean' }, + 'skip-sessions-welcome': { type: 'boolean' }, 'logExtensionHostCommunication': { type: 'boolean' }, 'skip-release-notes': { type: 'boolean' }, 'skip-welcome': { type: 'boolean' }, diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 00e09a016ac..65fd42574b0 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -426,6 +426,9 @@ const _allApiProposals = { taskProblemMatcherStatus: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskProblemMatcherStatus.d.ts', }, + taskRunOptions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskRunOptions.d.ts', + }, telemetry: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', }, diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index df18c523dd7..70e8cd57e47 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -284,6 +284,12 @@ function registerProxyConfigurations(useHostProxy = true, useHostProxyDefault = 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 }, + 'http.webSocketAdditionalSupport': { + type: 'boolean', + default: true, + markdownDescription: localize('webSocketAdditionalSupport', "Controls whether the built-in WebSocket 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 + }, 'http.experimental.networkInterfaceCheckInterval': { type: 'number', default: 300, diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 5aa0a0dd13a..178bed35e83 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -118,6 +118,7 @@ export async function getShellIntegrationInjection( if (!newArgs) { return { type: 'failure', reason: ShellIntegrationInjectionFailureReason.UnsupportedArgs }; } + newArgs = [...newArgs]; newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, ''); envMixin['VSCODE_STABLE'] = productService.quality === 'stable' ? '1' : '0'; return { type, newArgs, envMixin }; diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index e684180e35d..60563ff6487 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -110,7 +110,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _isPtyPaused: boolean = false; private _unacknowledgedCharCount: number = 0; - private _writeQueue: Promise = Promise.resolve(); get exitMessage(): string | undefined { return this._exitMessage; } get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; } @@ -469,32 +468,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._logService.trace('node-pty.IPty#write', data, isBinary); if (isBinary) { this._ptyProcess!.write(Buffer.from(data, 'binary')); - } else if (isMacintosh && data.length > 512 && data.includes('\r')) { - // macOS PTY has a ~1024-byte canonical-mode input buffer. Multiline - // input exceeding this causes writes to block or corrupt due to - // backpressure from the shell's line editor echoing characters. - // https://github.com/microsoft/vscode/issues/296955 - this._writeChunked(data); } else { this._ptyProcess!.write(data); } this._childProcessMonitor?.handleInput(); } - private _writeChunked(data: string): void { - this._writeQueue = this._writeQueue.then(async () => { - for (let i = 0; i < data.length; i += 512) { - if (this._store.isDisposed) { - return; - } - this._ptyProcess!.write(data.slice(i, i + 512)); - if (i + 512 < data.length) { - await timeout(5); - } - } - }); - } - sendSignal(signal: string): void { if (this._store.isDisposed || !this._ptyProcess) { return; diff --git a/src/vs/platform/terminal/test/node/terminalProcess.test.ts b/src/vs/platform/terminal/test/node/terminalProcess.test.ts deleted file mode 100644 index b5aed3fed0a..00000000000 --- a/src/vs/platform/terminal/test/node/terminalProcess.test.ts +++ /dev/null @@ -1,139 +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 { deepStrictEqual } from 'assert'; -import { tmpdir } from 'os'; -import * as path from '../../../../base/common/path.js'; -import * as fs from 'fs'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { NullLogService } from '../../../log/common/log.js'; -import { IProductService } from '../../../product/common/productService.js'; -import { ITerminalProcessOptions, ITerminalLaunchError } from '../../common/terminal.js'; -import { TerminalProcess } from '../../node/terminalProcess.js'; -import { isWindows } from '../../../../base/common/platform.js'; - -const processOptions: ITerminalProcessOptions = { - shellIntegration: { enabled: false, suggestEnabled: false, nonce: '' }, - windowsUseConptyDll: false, - environmentVariableCollections: undefined, - workspaceFolder: undefined, - isScreenReaderOptimized: false -}; - -/** - * Build a multiline shell command that writes its content to a file. - * The command writes numbered lines to a temp file so we can verify - * the entire payload was received intact by the shell. - */ -function buildMultilineCommand(lineCount: number, outputFile: string): { command: string; expectedLines: string[] } { - const lines: string[] = []; - for (let i = 1; i <= lineCount; i++) { - // Pad line number, add filler to make each line ~55 chars - const line = `L${String(i).padStart(2, '0')} ${'a'.repeat(51)}`; - lines.push(line); - } - // Use cat heredoc to write content to a file — this exercises multiline PTY input - const command = `cat > ${outputFile} << 'TESTEOF'\n${lines.join('\n')}\nTESTEOF\n`; - return { command, expectedLines: lines }; -} - -// These tests spawn real PTY processes and are macOS/Linux only -(isWindows ? suite.skip : suite)('TerminalProcess - multiline write', () => { - const store = ensureNoDisposablesAreLeakedInTestSuite(); - let outputDir: string; - - setup(() => { - outputDir = fs.mkdtempSync(path.join(tmpdir(), 'vscode-pty-test-')); - }); - - teardown(() => { - fs.rmSync(outputDir, { recursive: true, force: true }); - }); - - async function runMultilineTest(lineCount: number): Promise { - const outputFile = path.join(outputDir, `output-${lineCount}.txt`); - const { command, expectedLines } = buildMultilineCommand(lineCount, outputFile); - - const terminalProcess = store.add(new TerminalProcess( - { executable: '/bin/bash', args: ['--norc', '--noprofile', '-i'] }, - outputDir, - 80, - 24, - { ...process.env } as Record, - { ...process.env } as Record, - processOptions, - new NullLogService(), - { applicationName: 'vscode' } as IProductService - )); - - const result = await terminalProcess.start(); - const error = result as ITerminalLaunchError | undefined; - if (error?.message) { - throw new Error(`Failed to start terminal: ${error.message}`); - } - - // Wait for shell to produce output (prompt), indicating it's ready for input - await new Promise(resolve => { - const timeout = setTimeout(() => { - listener.dispose(); - resolve(); - }, 10000); - const listener = terminalProcess.onProcessData(() => { - clearTimeout(timeout); - listener.dispose(); - resolve(); - }); - }); - - // Send the multiline command — newlines are converted to \r for PTY - const ptyData = command.replace(/\n/g, '\r'); - terminalProcess.input(ptyData); - - // Wait for the command to execute and write the file - const maxWait = 10000; - const start = Date.now(); - while (Date.now() - start < maxWait) { - await new Promise(resolve => setTimeout(resolve, 200)); - if (fs.existsSync(outputFile)) { - // Give a moment for the write to flush - await new Promise(resolve => setTimeout(resolve, 200)); - break; - } - } - - // Shut down and wait for the process to exit - const exitPromise = new Promise(resolve => { - const listener = terminalProcess.onProcessExit(() => { - listener.dispose(); - resolve(); - }); - }); - terminalProcess.shutdown(true); - await exitPromise; - - if (!fs.existsSync(outputFile)) { - throw new Error(`Output file was not created — terminal likely got stuck (command was ${command.length} bytes)`); - } - - const actualContent = fs.readFileSync(outputFile, 'utf-8'); - const actualLines = actualContent.trimEnd().split('\n'); - deepStrictEqual(actualLines, expectedLines); - } - - test('small multiline command (10 lines, ~700 bytes)', async function () { - this.timeout(15000); - await runMultilineTest(10); - }); - - test('medium multiline command (20 lines, ~1300 bytes)', async function () { - this.timeout(15000); - await runMultilineTest(20); - }); - - test.skip('large multiline command (500 lines, ~32KB)', async function () { - this.timeout(30000); - await runMultilineTest(500); - }); -}); diff --git a/src/vs/sessions/browser/parts/auxiliaryBarPart.ts b/src/vs/sessions/browser/parts/auxiliaryBarPart.ts index f97118b5419..5142ee114f6 100644 --- a/src/vs/sessions/browser/parts/auxiliaryBarPart.ts +++ b/src/vs/sessions/browser/parts/auxiliaryBarPart.ts @@ -82,7 +82,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart { return undefined; } - return Math.max(width, 300); + return Math.max(width, 320); } readonly priority = LayoutPriority.Low; diff --git a/src/vs/sessions/browser/web.factory.ts b/src/vs/sessions/browser/web.factory.ts new file mode 100644 index 00000000000..e33129acea8 --- /dev/null +++ b/src/vs/sessions/browser/web.factory.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWorkbench, IWorkbenchConstructionOptions } from '../../workbench/browser/web.api.js'; +import { SessionsBrowserMain } from './web.main.js'; +import { IDisposable, toDisposable } from '../../base/common/lifecycle.js'; +import { mark } from '../../base/common/performance.js'; +import { DeferredPromise } from '../../base/common/async.js'; + +const workbenchPromise = new DeferredPromise(); + +/** + * Creates the Sessions workbench with the provided options in the provided container. + */ +export function create(domElement: HTMLElement, options: IWorkbenchConstructionOptions): IDisposable { + + mark('code/didLoadWorkbenchMain'); + + let instantiatedWorkbench: IWorkbench | undefined = undefined; + new SessionsBrowserMain(domElement, options).open().then(workbench => { + instantiatedWorkbench = workbench; + workbenchPromise.complete(workbench); + }); + + return toDisposable(() => { + if (instantiatedWorkbench) { + instantiatedWorkbench.shutdown(); + } else { + workbenchPromise.p.then(w => w.shutdown()); + } + }); +} diff --git a/src/vs/sessions/browser/web.main.ts b/src/vs/sessions/browser/web.main.ts new file mode 100644 index 00000000000..0c57fb902f6 --- /dev/null +++ b/src/vs/sessions/browser/web.main.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. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceCollection } from '../../platform/instantiation/common/serviceCollection.js'; +import { ILogService } from '../../platform/log/common/log.js'; +import { BrowserMain, IBrowserMainWorkbench } from '../../workbench/browser/web.main.js'; +import { Workbench as SessionsWorkbench } from './workbench.js'; + +export class SessionsBrowserMain extends BrowserMain { + + protected override createWorkbench(domElement: HTMLElement, serviceCollection: ServiceCollection, logService: ILogService): IBrowserMainWorkbench { + console.log('[Sessions Web] Creating Sessions workbench (not standard workbench)'); + return new SessionsWorkbench(domElement, undefined, serviceCollection, logService); + } +} diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index ffb41b25c95..7a0963566d9 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -13,7 +13,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, observableFromPromise, observableValue } from '../../../../base/common/observable.js'; +import { autorun, constObservable, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, ObservablePromise, observableValue } from '../../../../base/common/observable.js'; import { basename, dirname } from '../../../../base/common/path.js'; import { extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -91,6 +91,8 @@ const enum ChangesVersionMode { } const changesVersionModeContextKey = new RawContextKey('sessions.changesVersionMode', ChangesVersionMode.AllChanges); +const isMergeBaseBranchProtectedContextKey = new RawContextKey('sessions.isMergeBaseBranchProtected', false); +const hasOpenPullRequestContextKey = new RawContextKey('sessions.hasOpenPullRequest', false); const hasUncommittedChangesContextKey = new RawContextKey('sessions.hasUncommittedChanges', false); // --- List Item @@ -263,7 +265,7 @@ export class ChangesViewPane extends ViewPane { private readonly activeSessionFileCountObs: IObservableWithChange; private readonly activeSessionHasChangesObs: IObservableWithChange; private readonly activeSessionRepositoryChangesObs: IObservableWithChange; - private readonly activeSessionRepositoryObs: IObservableWithChange | undefined>; + private readonly activeSessionRepositoryObs: IObservableWithChange; get activeSessionHasChanges(): IObservable { return this.activeSessionHasChangesObs; @@ -325,22 +327,31 @@ export class ChangesViewPane extends ViewPane { }).recomputeInitiallyAndOnChange(this._store); // Track active session repository changes - this.activeSessionRepositoryObs = derived(reader => { + const activeSessionRepositoryPromiseObs = derived(reader => { const activeSessionWorktree = this.activeSession.read(reader)?.worktree; if (!activeSessionWorktree) { + return constObservable(undefined); + } + + return new ObservablePromise(this.gitService.openRepository(activeSessionWorktree)).resolvedValue; + }); + + this.activeSessionRepositoryObs = derived(reader => { + const activeSessionRepositoryPromise = activeSessionRepositoryPromiseObs.read(reader); + if (activeSessionRepositoryPromise === undefined) { return undefined; } - return observableFromPromise(this.gitService.openRepository(activeSessionWorktree)); + return activeSessionRepositoryPromise.read(reader); }); this.activeSessionRepositoryChangesObs = derived(reader => { - const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader); + const repository = this.activeSessionRepositoryObs.read(reader); if (!repository) { return undefined; } - const state = repository.value?.state.read(reader); + const state = repository.state.read(reader); const headCommit = state?.HEAD?.commit; return (state?.workingTreeChanges ?? []).map(change => { const isDeletion = change.modifiedUri === undefined; @@ -615,34 +626,34 @@ export class ChangesViewPane extends ViewPane { // Reactively computes the diff between HEAD^ and HEAD. Memoize the diff observable so // that we only recompute it when the HEAD commit id actually changes. const headCommitObs = derived(reader => { - const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader)?.value; + const repository = this.activeSessionRepositoryObs.read(reader); return repository?.state.read(reader)?.HEAD?.commit; }); const lastTurnChangesObs = derived(reader => { - const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader)?.value; + const repository = this.activeSessionRepositoryObs.read(reader); const headCommit = headCommitObs.read(reader); if (!repository || !headCommit) { - return undefined; + return constObservable(undefined); } - return observableFromPromise(repository.diffBetweenWithStats(`${headCommit}^`, headCommit)); + return new ObservablePromise(repository.diffBetweenWithStats(`${headCommit}^`, headCommit)).resolvedValue; }); // Combine both entry sources for display const combinedEntriesObs = derived(reader => { const headCommit = headCommitObs.read(reader); + const versionMode = this.versionModeObs.read(reader); const editEntries = editSessionEntriesObs.read(reader); const sessionFiles = sessionFilesObs.read(reader); const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader) ?? []; - const versionMode = this.versionModeObs.read(reader); + const lastTurnDiffChanges = lastTurnChangesObs.read(reader).read(reader); let sourceEntries: IChangesFileItem[]; if (versionMode === ChangesVersionMode.Uncommitted) { sourceEntries = repositoryFiles; } else if (versionMode === ChangesVersionMode.LastTurn) { - const lastTurn = lastTurnChangesObs.read(reader); - const diffChanges = lastTurn?.read(reader).value ?? []; + const diffChanges = lastTurnDiffChanges ?? []; const parentRef = headCommit ? `${headCommit}^` : ''; sourceEntries = diffChanges.map(change => { const isDeletion = change.modifiedUri === undefined; @@ -702,19 +713,18 @@ export class ChangesViewPane extends ViewPane { if (this.actionsContainer) { dom.clearNode(this.actionsContainer); - const scopedContextKeyService = this.renderDisposables.add(this.contextKeyService.createScoped(this.actionsContainer)); - const scopedInstantiationService = this.renderDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))); + const scopedInstantiationService = this.renderDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); // Set the chat session type context key reactively so that menu items with // `chatSessionType == copilotcli` (e.g. Create Pull Request) are shown - const chatSessionTypeKey = scopedContextKeyService.createKey(ChatContextKeys.agentSessionType.key, ''); + const chatSessionTypeKey = this.scopedContextKeyService.createKey(ChatContextKeys.agentSessionType.key, ''); this.renderDisposables.add(autorun(reader => { const activeSession = this.activeSession.read(reader); chatSessionTypeKey.set(activeSession?.sessionType ?? ''); })); // Bind required context keys for the menu buttons - this.renderDisposables.add(bindContextKey(hasUndecidedChatEditingResourceContextKey, scopedContextKeyService, r => { + this.renderDisposables.add(bindContextKey(hasUndecidedChatEditingResourceContextKey, this.scopedContextKeyService, r => { const session = activeEditingSessionObs.read(r); if (!session) { return false; @@ -723,7 +733,7 @@ export class ChangesViewPane extends ViewPane { return entries.some(entry => entry.state.read(r) === ModifiedFileEntryState.Modified); })); - this.renderDisposables.add(bindContextKey(hasAppliedChatEditsContextKey, scopedContextKeyService, r => { + this.renderDisposables.add(bindContextKey(hasAppliedChatEditsContextKey, this.scopedContextKeyService, r => { const session = activeEditingSessionObs.read(r); if (!session) { return false; @@ -732,43 +742,40 @@ export class ChangesViewPane extends ViewPane { return entries.length > 0; })); - this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, scopedContextKeyService, r => { - const { files } = topLevelStats.read(r); + const hasAgentSessionChangesObs = derived(reader => { + const { files } = topLevelStats.read(reader); return files > 0; - })); + }); - // Also bind to the ViewPane's scoped context key service so the ViewTitle menu can evaluate it - this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, this.scopedContextKeyService, r => { - const { files } = topLevelStats.read(r); - return files > 0; - })); + this.renderDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, this.scopedContextKeyService, r => hasAgentSessionChangesObs.read(r))); - // Track whether there are uncommitted (working tree) changes - this.renderDisposables.add(bindContextKey(hasUncommittedChangesContextKey, this.scopedContextKeyService, r => { - const repositoryFiles = this.activeSessionRepositoryChangesObs.read(r); + const hasUncommittedChangesObs = derived(reader => { + const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader); return (repositoryFiles?.length ?? 0) > 0; - })); + }); - // Set context key for merge base branch protection - const isMergeBaseBranchProtectedContextKey = scopedContextKeyService.createKey('sessions.isMergeBaseBranchProtected', false); - this.renderDisposables.add(autorun(reader => { - const repository = this.activeSessionRepositoryObs.read(reader)?.read(reader).value; - const state = repository?.state.read(reader); - isMergeBaseBranchProtectedContextKey.set(state?.HEAD?.base?.isProtected === true); - })); + this.renderDisposables.add(bindContextKey(hasUncommittedChangesContextKey, this.scopedContextKeyService, r => hasUncommittedChangesObs.read(r))); - // Set context key for PR state from session metadata - const hasOpenPullRequestKey = scopedContextKeyService.createKey('sessions.hasOpenPullRequest', false); - this.renderDisposables.add(autorun(reader => { + const isMergeBaseBranchProtectedObs = derived(reader => { + const state = this.activeSessionRepositoryObs.read(reader)?.state.read(reader); + return state?.HEAD?.base?.isProtected === true; + }); + + this.renderDisposables.add(bindContextKey(isMergeBaseBranchProtectedContextKey, this.scopedContextKeyService, r => isMergeBaseBranchProtectedObs.read(r))); + + const hasOpenPullRequestObs = derived(reader => { const sessionResource = activeSessionResource.read(reader); - sessionsChangedSignal.read(reader); - if (sessionResource) { - const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata; - hasOpenPullRequestKey.set(!!metadata?.pullRequestUrl); - } else { - hasOpenPullRequestKey.set(false); + if (!sessionResource) { + return false; } - })); + + sessionsChangedSignal.read(reader); + + const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata; + return !!metadata?.pullRequestUrl; + }); + + this.renderDisposables.add(bindContextKey(hasOpenPullRequestContextKey, this.scopedContextKeyService, r => hasOpenPullRequestObs.read(r))); this.renderDisposables.add(autorun(reader => { const { isSessionMenu, added, removed } = topLevelStats.read(reader); @@ -829,7 +836,7 @@ export class ChangesViewPane extends ViewPane { return { showIcon: true, showLabel: false, isSecondary: true }; } if (action.id === 'chatEditing.synchronizeChanges') { - return { showIcon: true, showLabel: true, isSecondary: true }; + return { showIcon: true, showLabel: true, isSecondary: false }; } if (action.id === 'github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR') { return { showIcon: true, showLabel: true, isSecondary: false }; diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css b/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css index 57410a89fc0..16e1ab73d47 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css @@ -12,7 +12,7 @@ height: 100%; box-sizing: border-box; overflow: hidden; - padding: 0 12px 10% 12px; + padding: 0 12px 48px 12px; container-type: size; } @@ -109,6 +109,7 @@ display: none; flex-direction: row; align-items: center; + gap: 4px; min-height: 28px; } diff --git a/src/vs/sessions/contrib/chat/browser/media/runScriptAction.css b/src/vs/sessions/contrib/chat/browser/media/runScriptAction.css new file mode 100644 index 00000000000..0837bc7b8c0 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/media/runScriptAction.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.run-script-action-widget { + display: flex; + flex-direction: column; + gap: 12px; + background-color: var(--vscode-quickInput-background); + padding: 8px 8px 12px; +} + +.run-script-action-section { + display: flex; + flex-direction: column; + gap: 6px; +} + +.run-script-action-label { + font-size: 12px; + font-weight: 600; +} + +.run-script-action-input .monaco-inputbox { + width: 100%; +} + +.run-script-action-option-row { + display: flex; + align-items: center; + min-height: 22px; + gap: 8px; +} + +.run-script-action-option-text { + cursor: pointer; + -webkit-user-select: none; + user-select: none; +} + +.run-script-action-section .monaco-custom-radio { + width: fit-content; + max-width: 100%; +} + +.run-script-action-hint { + font-size: 12px; + opacity: 0.8; +} + +.run-script-action-buttons { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 4px; +} diff --git a/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts b/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts index b5c8ee1730a..83c775b304b 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatPermissionPicker.ts @@ -30,7 +30,7 @@ interface IPermissionItem { /** * A permission picker for the new-session welcome view. - * Shows Default Approvals and Bypass Approvals options (no Autopilot for CLI sessions). + * Shows Default Approvals, Bypass Approvals, and Autopilot options. */ export class NewChatPermissionPicker extends Disposable { @@ -95,6 +95,7 @@ export class NewChatPermissionPicker extends Disposable { } const policyRestricted = this.configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; + const isAutopilotEnabled = this.configurationService.getValue(ChatConfiguration.AutopilotEnabled) !== false; const items: IActionListItem[] = [ { @@ -125,6 +126,22 @@ export class NewChatPermissionPicker extends Disposable { }, ]; + if (isAutopilotEnabled) { + items.push({ + kind: ActionListItemKind.Action, + group: { kind: ActionListItemKind.Header, title: '', icon: Codicon.rocket }, + item: { + level: ChatPermissionLevel.Autopilot, + label: localize('permissions.autopilot', "Autopilot (Preview)"), + icon: Codicon.rocket, + checked: this._currentLevel === ChatPermissionLevel.Autopilot, + }, + label: localize('permissions.autopilot', "Autopilot (Preview)"), + description: localize('permissions.autopilot.subtext', "Autonomously iterates from start to finish"), + disabled: policyRestricted, + }); + } + const triggerElement = this._triggerElement; const delegate: IActionListDelegate = { onSelect: async (item) => { @@ -177,6 +194,33 @@ export class NewChatPermissionPicker extends Disposable { shownWarnings.add(ChatPermissionLevel.AutoApprove); } + if (level === ChatPermissionLevel.Autopilot && !shownWarnings.has(ChatPermissionLevel.Autopilot)) { + const result = await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('permissions.autopilot.warning.title', "Enable Autopilot?"), + buttons: [ + { + label: localize('permissions.autopilot.warning.confirm', "Enable"), + run: () => true + }, + { + label: localize('permissions.autopilot.warning.cancel', "Cancel"), + run: () => false + }, + ], + custom: { + icon: Codicon.rocket, + markdownDetails: [{ + markdown: new MarkdownString(localize('permissions.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.")), + }], + }, + }); + if (result.result !== true) { + return; + } + shownWarnings.add(ChatPermissionLevel.Autopilot); + } + this._currentLevel = level; this._updateTriggerLabel(this._triggerElement); this._onDidChangeLevel.fire(level); @@ -188,10 +232,22 @@ export class NewChatPermissionPicker extends Disposable { } dom.clearNode(trigger); - const icon = this._currentLevel === ChatPermissionLevel.AutoApprove ? Codicon.warning : Codicon.shield; - const label = this._currentLevel === ChatPermissionLevel.AutoApprove - ? localize('permissions.autoApprove.label', "Bypass Approvals") - : localize('permissions.default.label', "Default Approvals"); + let icon: ThemeIcon; + let label: string; + switch (this._currentLevel) { + case ChatPermissionLevel.Autopilot: + icon = Codicon.rocket; + label = localize('permissions.autopilot.label', "Autopilot (Preview)"); + break; + case ChatPermissionLevel.AutoApprove: + icon = Codicon.warning; + label = localize('permissions.autoApprove.label', "Bypass Approvals"); + break; + default: + icon = Codicon.shield; + label = localize('permissions.default.label', "Default Approvals"); + break; + } dom.append(trigger, renderIcon(icon)); const labelSpan = dom.append(trigger, dom.$('span.sessions-chat-dropdown-label')); diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index da219eb37c0..24419a32fd3 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -132,7 +132,6 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // Send button private _sendButton: Button | undefined; private _sending = false; - private _altKeyDown = false; // Repository loading private readonly _openRepositoryCts = this._register(new MutableDisposable()); @@ -212,7 +211,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._register(this._targetPicker.onDidChangeTarget((target) => { this._createNewSession(); const isLocal = target === AgentSessionProviders.Background; - this._isolationModePicker.setVisible(isLocal); + this._updateIsolationPickerVisibility(); this._permissionPicker.setVisible(isLocal); this._branchPicker.setVisible(isLocal); this._syncIndicator.setVisible(isLocal); @@ -276,6 +275,27 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._currentLanguageModel.read(reader); this._updateDraftState(); })); + + // When isolation option config changes, update picker visibility + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('github.copilot.chat.cli.isolationOption.enabled')) { + this._updateIsolationPickerVisibility(); + } + })); + } + + private get _isIsolationOptionEnabled(): boolean { + return this.configurationService.getValue('github.copilot.chat.cli.isolationOption.enabled') !== false; + } + + private _updateIsolationPickerVisibility(): void { + const isLocal = this._targetPicker.selectedTarget === AgentSessionProviders.Background; + const enabled = this._isIsolationOptionEnabled; + if (!enabled) { + this._isolationModePicker.setPreferredIsolationMode('worktree'); + } + this._isolationModePicker.setVisible(isLocal); + this._isolationModePicker.setEnabled(enabled); } // --- Rendering --- @@ -317,17 +337,17 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // Isolation mode and branch pickers (below the input, shown when Local target is selected) const isolationContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-local-mode')); this._isolationModePicker.render(isolationContainer); - dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-spacer')); this._permissionPicker.render(isolationContainer); + dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-spacer')); const branchContainer = dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-right')); this._branchPicker.render(branchContainer); this._syncIndicator.render(branchContainer); // Set initial visibility based on default target and isolation mode const isLocal = this._targetPicker.selectedTarget === AgentSessionProviders.Background; - const isWorktree = this._isolationModePicker.isolationMode === 'worktree'; - this._isolationModePicker.setVisible(isLocal); + this._updateIsolationPickerVisibility(); this._permissionPicker.setVisible(isLocal); + const isWorktree = this._isolationModePicker.isolationMode === 'worktree'; this._branchPicker.setVisible(isLocal && isWorktree); this._syncIndicator.setVisible(isLocal && isWorktree); @@ -444,6 +464,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._updateInputLoadingState(); this._branchPicker.setRepository(undefined); this._isolationModePicker.setRepository(undefined); + this._updateIsolationPickerVisibility(); this._syncIndicator.setRepository(undefined); this._modePicker.setRepository(undefined); @@ -454,6 +475,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._repositoryLoading = false; this._updateInputLoadingState(); this._isolationModePicker.setRepository(repository); + this._updateIsolationPickerVisibility(); this._branchPicker.setRepository(repository); this._syncIndicator.setRepository(repository); this._modePicker.setRepository(repository); @@ -465,6 +487,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._repositoryLoading = false; this._updateInputLoadingState(); this._isolationModePicker.setRepository(undefined); + this._updateIsolationPickerVisibility(); this._branchPicker.setRepository(undefined); this._syncIndicator.setRepository(undefined); this._modePicker.setRepository(undefined); @@ -515,6 +538,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { fontFamily: 'system-ui, -apple-system, sans-serif', fontSize: 13, lineHeight: 20, + cursorWidth: 1, padding: { top: 8, bottom: 2 }, wrappingStrategy: 'advanced', stickyScroll: { enabled: false }, @@ -562,7 +586,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { if (e.keyCode === KeyCode.Enter && !e.shiftKey && !e.ctrlKey && e.altKey) { e.preventDefault(); e.stopPropagation(); - this._send({ openNewAfterSend: true }); + this._send(); } })); @@ -657,14 +681,14 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._createAttachButton(toolbar); + // Mode picker (before model pickers) + this._modePicker.render(toolbar); + this._modePicker.setVisible(false); + // Local model picker (EnhancedModelPickerActionItem) this._localModelPickerContainer = dom.append(toolbar, dom.$('.sessions-chat-model-picker')); this._createLocalModelPicker(this._localModelPickerContainer); - // Local mode picker - this._modePicker.render(toolbar); - this._modePicker.setVisible(false); - // Remote model picker (action list dropdown) this._cloudModelPicker.render(toolbar); this._cloudModelPicker.setVisible(false); @@ -683,19 +707,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { ariaLabel: localize('send', "Send"), })); sendButton.icon = Codicon.send; - this._register(sendButton.onDidClick(() => this._send({ openNewAfterSend: this._altKeyDown }))); - this._register(dom.addDisposableListener(dom.getWindow(container), dom.EventType.KEY_DOWN, e => { - if (e.key === 'Alt') { - this._altKeyDown = true; - sendButton.icon = Codicon.runAbove; - } - })); - this._register(dom.addDisposableListener(dom.getWindow(container), dom.EventType.KEY_UP, e => { - if (e.key === 'Alt') { - this._altKeyDown = false; - sendButton.icon = Codicon.send; - } - })); + this._register(sendButton.onDidClick(() => this._send())); this._updateSendButtonState(); } @@ -712,6 +724,8 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { getModels: () => this._getAvailableModels(), useGroupedModelPicker: () => true, showManageModelsAction: () => false, + showUnavailableFeatured: () => false, + showFeatured: () => true, }; const pickerOptions: IChatInputPickerOptions = { @@ -1008,7 +1022,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._sendButton.enabled = !this._sending && hasText && !(this._newSession.value?.disabled ?? true); } - private async _send(options?: { openNewAfterSend?: boolean }): Promise { + private async _send(): Promise { let query = this._editor.getModel()?.getValue().trim(); const session = this._newSession.value; if (!query || !session || this._sending) { @@ -1055,7 +1069,6 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { await this.sessionsManagementService.sendRequestForNewSession( session.resource, { - ...options?.openNewAfterSend ? { openNewSessionView: true } : {}, permissionLevel: this._permissionPicker.permissionLevel, } ); @@ -1133,8 +1146,13 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { } } if (draft.isolationMode) { - this._isolationModePicker.setPreferredIsolationMode(draft.isolationMode); - this._isolationModePicker.setIsolationMode(draft.isolationMode); + if (this._isIsolationOptionEnabled) { + this._isolationModePicker.setPreferredIsolationMode(draft.isolationMode); + this._isolationModePicker.setIsolationMode(draft.isolationMode); + } else { + this._isolationModePicker.setPreferredIsolationMode('worktree'); + this._isolationModePicker.setIsolationMode('worktree'); + } } if (draft.branch) { this._branchPicker.setPreferredBranch(draft.branch); diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index a1cd803058a..72f9d4c771a 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -6,7 +6,7 @@ import { equals } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { autorun, derivedOpts, IObservable } from '../../../../base/common/observable.js'; import { localize, localize2 } from '../../../../nls.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; @@ -18,9 +18,10 @@ import { IWorkbenchContribution } from '../../../../workbench/common/contributio import { SessionsCategories } from '../../../common/categories.js'; import { IActiveSessionItem, IsActiveSessionBackgroundProviderContext, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { Menus } from '../../../browser/menus.js'; -import { ISessionsConfigurationService, ITaskEntry, TaskStorageTarget } from './sessionsConfigurationService.js'; +import { INonSessionTaskEntry, ISessionsConfigurationService, ITaskEntry, TaskStorageTarget } from './sessionsConfigurationService.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; +import { IRunScriptCustomTaskWidgetResult, RunScriptCustomTaskWidget } from './runScriptCustomTaskWidget.js'; @@ -31,7 +32,6 @@ export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdow const RUN_SCRIPT_ACTION_ID = 'workbench.action.agentSessions.runScript'; const RUN_SCRIPT_ACTION_PRIMARY_ID = 'workbench.action.agentSessions.runScriptPrimary'; const CONFIGURE_DEFAULT_RUN_ACTION_ID = 'workbench.action.agentSessions.configureDefaultRunAction'; - function getTaskDisplayLabel(task: ITaskEntry): string { if (task.label && task.label.length > 0) { return task.label; @@ -48,6 +48,19 @@ function getTaskDisplayLabel(task: ITaskEntry): string { return ''; } +function getTaskCommandPreview(task: ITaskEntry): string { + if (task.command && task.command.length > 0) { + return task.command; + } + if (task.script && task.script.length > 0) { + return localize('npmTaskCommandPreview', "npm run {0}", task.script); + } + if (task.task && task.task.toString().length > 0) { + return task.task.toString(); + } + return getTaskDisplayLabel(task); +} + interface IRunScriptActionContext { readonly session: IActiveSessionItem; readonly tasks: readonly ITaskEntry[]; @@ -103,7 +116,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr constructor() { super({ id: RUN_SCRIPT_ACTION_PRIMARY_ID, - title: { value: localize('runPrimaryScript', 'Run Primary Script'), original: 'Run Primary Script' }, + title: { value: localize('runPrimaryTask', 'Run Primary Task'), original: 'Run Primary Task' }, icon: Codicon.play, category: SessionsCategories.Sessions, f1: true, @@ -182,7 +195,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr constructor() { super({ id: CONFIGURE_DEFAULT_RUN_ACTION_ID, - title: localize2('configureDefaultRunAction', "Add Run Action..."), + title: localize2('configureDefaultRunAction', "Add Action..."), category: SessionsCategories.Sessions, icon: Codicon.play, precondition: configureScriptPrecondition, @@ -226,12 +239,12 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr if (nonSessionTasks.length > 0) { items.push({ type: 'separator', label: localize('existingTasks', "Existing Tasks") }); - for (const task of nonSessionTasks) { + for (const { task, target } of nonSessionTasks) { items.push({ label: getTaskDisplayLabel(task), description: task.command, task, - source: 'workspace', + source: target, }); } } @@ -246,81 +259,86 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr const pickedItem = picked as ITaskPickItem; if (pickedItem.task) { - // Existing task — set inSessions: true - await this._sessionsConfigService.addTaskToSessions(pickedItem.task, session, pickedItem.source ?? 'workspace'); - return pickedItem.task; + return this._showCustomCommandInput(session, { task: pickedItem.task, target: pickedItem.source ?? 'workspace' }); } else { // Custom command path return this._showCustomCommandInput(session); } } - private async _showCustomCommandInput(session: IActiveSessionItem): Promise { - const command = await this._quickInputService.input({ - placeHolder: localize('enterCommandPlaceholder', "Enter command (e.g., npm run dev)"), - prompt: localize('enterCommandPrompt', "This command will be run as a task in the integrated terminal") - }); - - if (!command) { + private async _showCustomCommandInput(session: IActiveSessionItem, existingTask?: INonSessionTaskEntry): Promise { + const taskConfiguration = await this._showCustomCommandWidget(session, existingTask); + if (!taskConfiguration) { return undefined; } - const target = await this._pickStorageTarget(session); - if (!target) { - return undefined; + if (existingTask) { + await this._sessionsConfigService.addTaskToSessions(existingTask.task, session, existingTask.target, { runOn: taskConfiguration.runOn ?? 'default' }); + return { + ...existingTask.task, + inSessions: true, + ...(taskConfiguration.runOn ? { runOptions: { runOn: taskConfiguration.runOn } } : {}), + }; } - return this._sessionsConfigService.createAndAddTask(command, session, target); + return this._sessionsConfigService.createAndAddTask( + taskConfiguration.label, + taskConfiguration.command, + session, + taskConfiguration.target, + taskConfiguration.runOn ? { runOn: taskConfiguration.runOn } : undefined + ); } - private async _pickStorageTarget(session: IActiveSessionItem): Promise { - const hasWorktree = !!session.worktree; - const hasRepository = !!session.repository; + private _showCustomCommandWidget(session: IActiveSessionItem, existingTask?: INonSessionTaskEntry): Promise { + const workspaceTargetDisabledReason = !(session.worktree ?? session.repository) + ? localize('workspaceStorageUnavailableTooltip', "Workspace storage is unavailable for this session") + : undefined; - interface IStorageTargetItem extends IQuickPickItem { - target: TaskStorageTarget; - } + return new Promise(resolve => { + const disposables = new DisposableStore(); + let settled = false; - const items: IStorageTargetItem[] = [ - { - target: 'user', - label: localize('storeInUserSettings', "User Settings"), - description: localize('storeInUserSettingsDesc', "Available in all sessions"), - }, - hasWorktree ? { - target: 'workspace', - label: localize('storeInWorkspaceWorktreeSettings', "Workspace (Worktree)"), - description: localize('storeInWorkspaceWorktreeSettingsDesc', "Stored in session worktree"), - } : hasRepository ? { - target: 'workspace', - label: localize('storeInWorkspaceSettings', "Workspace"), - description: localize('storeInWorkspace', "Stored in the workspace"), - } : { - target: 'workspace', - label: localize('storeInWorkspaceSettingsDisable', "Workspace Unavailable"), - description: localize('storeInWorkspaceDisabled', "Stored in the workspace Unavailable"), - disabled: true, - italic: true, - } - ]; + const quickWidget = disposables.add(this._quickInputService.createQuickWidget()); + quickWidget.title = existingTask + ? localize('addExistingActionWidgetTitle', "Add Existing Action...") + : localize('addActionWidgetTitle', "Add Action..."); + quickWidget.description = existingTask + ? localize('addExistingActionWidgetDescription', "Enable an existing task for sessions and configure when it should run") + : localize('addActionWidgetDescription', "Create a shell task and configure how it should be saved and run"); + quickWidget.ignoreFocusOut = true; + const widget = disposables.add(new RunScriptCustomTaskWidget({ + label: existingTask?.task.label, + labelDisabledReason: existingTask ? localize('existingTaskLabelLocked', "This name comes from an existing task and cannot be changed here.") : undefined, + command: existingTask ? getTaskCommandPreview(existingTask.task) : undefined, + commandDisabledReason: existingTask ? localize('existingTaskCommandLocked', "This command comes from an existing task and cannot be changed here.") : undefined, + target: existingTask?.target, + targetDisabledReason: existingTask ? localize('existingTaskTargetLocked', "This existing task cannot be moved between workspace and user storage.") : workspaceTargetDisabledReason, + runOn: existingTask?.task.runOptions?.runOn === 'worktreeCreated' ? 'worktreeCreated' : undefined, + })); + quickWidget.widget = widget.domNode; - return new Promise(resolve => { - const picker = this._quickInputService.createQuickPick({ useSeparators: true }); - picker.placeholder = localize('pickStorageTarget', "Where should this action be saved?"); - picker.items = items; - - picker.onDidAccept(() => { - const selected = picker.activeItems[0]; - if (selected && (selected.target !== 'workspace' || hasWorktree)) { - resolve(selected.target); - picker.dispose(); + const complete = (result: IRunScriptCustomTaskWidgetResult | undefined) => { + if (settled) { + return; } - }); - picker.onDidHide(() => { - resolve(undefined); - picker.dispose(); - }); - picker.show(); + settled = true; + resolve(result); + quickWidget.hide(); + }; + + disposables.add(widget.onDidSubmit(result => complete(result))); + disposables.add(widget.onDidCancel(() => complete(undefined))); + disposables.add(quickWidget.onDidHide(() => { + if (!settled) { + settled = true; + resolve(undefined); + } + disposables.dispose(); + })); + + quickWidget.show(); + widget.focus(); }); } } diff --git a/src/vs/sessions/contrib/chat/browser/runScriptCustomTaskWidget.ts b/src/vs/sessions/contrib/chat/browser/runScriptCustomTaskWidget.ts new file mode 100644 index 00000000000..32259cd6446 --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/runScriptCustomTaskWidget.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/runScriptAction.css'; + +import * as dom from '../../../../base/browser/dom.js'; +import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; +import { Radio } from '../../../../base/browser/ui/radio/radio.js'; +import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { localize } from '../../../../nls.js'; +import { defaultButtonStyles, defaultCheckboxStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { TaskStorageTarget } from './sessionsConfigurationService.js'; + +export const WORKTREE_CREATED_RUN_ON = 'worktreeCreated' as const; + +export interface IRunScriptCustomTaskWidgetState { + readonly label?: string; + readonly labelDisabledReason?: string; + readonly command?: string; + readonly commandDisabledReason?: string; + readonly target?: TaskStorageTarget; + readonly targetDisabledReason?: string; + readonly runOn?: typeof WORKTREE_CREATED_RUN_ON; +} + +export interface IRunScriptCustomTaskWidgetResult { + readonly label?: string; + readonly command: string; + readonly target: TaskStorageTarget; + readonly runOn?: typeof WORKTREE_CREATED_RUN_ON; +} + +export class RunScriptCustomTaskWidget extends Disposable { + + readonly domNode: HTMLElement; + + private readonly _labelInput: InputBox; + private readonly _commandInput: InputBox; + private readonly _runOnCheckbox: Checkbox; + private readonly _storageOptions: Radio; + private readonly _submitButton: Button; + private readonly _cancelButton: Button; + private readonly _labelLocked: boolean; + private readonly _commandLocked: boolean; + private readonly _targetLocked: boolean; + private _selectedTarget: TaskStorageTarget; + + private readonly _onDidSubmit = this._register(new Emitter()); + readonly onDidSubmit: Event = this._onDidSubmit.event; + + private readonly _onDidCancel = this._register(new Emitter()); + readonly onDidCancel: Event = this._onDidCancel.event; + + constructor(state: IRunScriptCustomTaskWidgetState) { + super(); + + this._labelLocked = !!state.labelDisabledReason; + this._commandLocked = !!state.commandDisabledReason; + this._targetLocked = !!state.targetDisabledReason && state.target !== undefined; + this._selectedTarget = state.target ?? (state.targetDisabledReason ? 'user' : 'workspace'); + + this.domNode = dom.$('.run-script-action-widget'); + + const labelSection = dom.append(this.domNode, dom.$('.run-script-action-section')); + dom.append(labelSection, dom.$('label.run-script-action-label', undefined, localize('labelFieldLabel', "Name"))); + const labelInputContainer = dom.append(labelSection, dom.$('.run-script-action-input')); + this._labelInput = this._register(new InputBox(labelInputContainer, undefined, { + placeholder: localize('enterLabelPlaceholder', "Enter a name for this action (optional)"), + tooltip: state.labelDisabledReason, + ariaLabel: localize('enterLabelAriaLabel', "Task name"), + inputBoxStyles: defaultInputBoxStyles, + })); + this._labelInput.value = state.label ?? ''; + if (state.labelDisabledReason) { + this._labelInput.disable(); + } + + const commandSection = dom.append(this.domNode, dom.$('.run-script-action-section')); + dom.append(commandSection, dom.$('label.run-script-action-label', undefined, localize('commandFieldLabel', "Command"))); + const commandInputContainer = dom.append(commandSection, dom.$('.run-script-action-input')); + this._commandInput = this._register(new InputBox(commandInputContainer, undefined, { + placeholder: localize('enterCommandPlaceholder', "Enter command (for example, npm run dev)"), + tooltip: state.commandDisabledReason, + ariaLabel: localize('enterCommandAriaLabel', "Task command"), + inputBoxStyles: defaultInputBoxStyles, + })); + this._commandInput.value = state.command ?? ''; + if (state.commandDisabledReason) { + this._commandInput.disable(); + } + + const runOnSection = dom.append(this.domNode, dom.$('.run-script-action-section')); + dom.append(runOnSection, dom.$('div.run-script-action-label', undefined, localize('runOptionsLabel', "Run Options"))); + const runOnRow = dom.append(runOnSection, dom.$('.run-script-action-option-row')); + this._runOnCheckbox = this._register(new Checkbox(localize('runOnWorktreeCreated', "Run When Worktree Is Created"), state.runOn === WORKTREE_CREATED_RUN_ON, defaultCheckboxStyles)); + runOnRow.appendChild(this._runOnCheckbox.domNode); + const runOnText = dom.append(runOnRow, dom.$('span.run-script-action-option-text', undefined, localize('runOnWorktreeCreatedDescription', "Automatically run this action when the session worktree is created"))); + this._register(dom.addDisposableListener(runOnText, dom.EventType.CLICK, () => this._runOnCheckbox.checked = !this._runOnCheckbox.checked)); + + const storageSection = dom.append(this.domNode, dom.$('.run-script-action-section')); + dom.append(storageSection, dom.$('div.run-script-action-label', undefined, localize('storageLabel', "Save In"))); + const storageDisabledReason = state.targetDisabledReason; + const workspaceTargetDisabled = !!storageDisabledReason; + this._storageOptions = this._register(new Radio({ + items: [ + { + text: localize('workspaceStorageLabel', "Workspace"), + tooltip: storageDisabledReason ?? localize('workspaceStorageTooltip', "Save this action in the current workspace"), + isActive: this._selectedTarget === 'workspace', + disabled: workspaceTargetDisabled, + }, + { + text: localize('userStorageLabel', "User"), + tooltip: this._targetLocked ? storageDisabledReason : localize('userStorageTooltip', "Save this action in your user tasks and make it available in all sessions"), + isActive: this._selectedTarget === 'user', + disabled: this._targetLocked, + } + ] + })); + this._storageOptions.domNode.setAttribute('aria-label', localize('storageAriaLabel', "Task storage target")); + storageSection.appendChild(this._storageOptions.domNode); + if (storageDisabledReason && !this._targetLocked) { + dom.append(storageSection, dom.$('div.run-script-action-hint', undefined, storageDisabledReason)); + } + + const buttonRow = dom.append(this.domNode, dom.$('.run-script-action-buttons')); + this._cancelButton = this._register(new Button(buttonRow, { ...defaultButtonStyles, secondary: true })); + this._cancelButton.label = localize('cancelAddAction', "Cancel"); + this._submitButton = this._register(new Button(buttonRow, defaultButtonStyles)); + this._submitButton.label = localize('confirmAddAction', "Add Action"); + + this._register(this._labelInput.onDidChange(() => this._updateButtonEnablement())); + this._register(this._commandInput.onDidChange(() => this._updateButtonEnablement())); + this._register(this._storageOptions.onDidSelect(index => { + this._selectedTarget = index === 0 ? 'workspace' : 'user'; + })); + this._register(this._submitButton.onDidClick(() => this._submit())); + this._register(this._cancelButton.onDidClick(() => this._onDidCancel.fire())); + this._register(dom.addDisposableListener(this._labelInput.inputElement, dom.EventType.KEY_DOWN, event => { + const keyboardEvent = new StandardKeyboardEvent(event); + if (keyboardEvent.equals(KeyCode.Enter)) { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + this._submit(); + } + })); + this._register(dom.addDisposableListener(this._commandInput.inputElement, dom.EventType.KEY_DOWN, event => { + const keyboardEvent = new StandardKeyboardEvent(event); + if (keyboardEvent.equals(KeyCode.Enter)) { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + this._submit(); + } + })); + this._register(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, event => { + const keyboardEvent = new StandardKeyboardEvent(event); + if (keyboardEvent.equals(KeyCode.Escape)) { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + this._onDidCancel.fire(); + } + })); + + this._updateButtonEnablement(); + } + + focus(): void { + if (!this._labelLocked) { + this._labelInput.focus(); + return; + } + if (this._commandLocked) { + this._runOnCheckbox.focus(); + return; + } + this._commandInput.focus(); + } + + private _submit(): void { + const label = this._labelInput.value.trim(); + const command = this._commandInput.value.trim(); + if (!command) { + return; + } + + this._onDidSubmit.fire({ + label: label.length > 0 ? label : undefined, + command, + target: this._selectedTarget, + runOn: this._runOnCheckbox.checked ? WORKTREE_CREATED_RUN_ON : undefined, + }); + } + + private _updateButtonEnablement(): void { + this._submitButton.enabled = this._commandInput.value.trim().length > 0; + } +} diff --git a/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts b/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts index 56fdf228c42..8d022018d2d 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts @@ -140,6 +140,7 @@ export class IsolationModePicker extends Disposable { private _isolationMode: IsolationMode = 'worktree'; private _preferredIsolationMode: IsolationMode | undefined; private _repository: IGitRepository | undefined; + private _enabled: boolean = true; private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; @@ -227,24 +228,33 @@ export class IsolationModePicker extends Disposable { } } + /** + * Enables or disables the picker. When disabled, the picker is shown + * but cannot be interacted with. + */ + setEnabled(enabled: boolean): void { + this._enabled = enabled; + this._updateTriggerLabel(); + } + private _showPicker(): void { - if (!this._triggerElement || this.actionWidgetService.isVisible || !this._repository) { + if (!this._triggerElement || this.actionWidgetService.isVisible || !this._repository || !this._enabled) { return; } const items: IActionListItem[] = [ - { - kind: ActionListItemKind.Action, - label: localize('isolationMode.folder', "Folder"), - group: { title: '', icon: Codicon.folder }, - item: 'workspace', - }, { kind: ActionListItemKind.Action, label: localize('isolationMode.worktree', "Worktree"), group: { title: '', icon: Codicon.worktree }, item: 'worktree', }, + { + kind: ActionListItemKind.Action, + label: localize('isolationMode.folder', "Folder"), + group: { title: '', icon: Codicon.folder }, + item: 'workspace', + }, ]; const triggerElement = this._triggerElement; @@ -296,7 +306,11 @@ export class IsolationModePicker extends Disposable { labelSpan.textContent = modeLabel; dom.append(this._triggerElement, renderIcon(Codicon.chevronDown)); - this._slotElement?.classList.toggle('disabled', isDisabled); + this._slotElement?.classList.toggle('disabled', isDisabled || !this._enabled); + if (this._triggerElement) { + this._triggerElement.tabIndex = (!isDisabled && this._enabled) ? 0 : -1; + this._triggerElement.setAttribute('aria-disabled', String(isDisabled || !this._enabled)); + } } } diff --git a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts index a36bed73da5..5ba62f2c0a0 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; +import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; import { joinPath, dirname, isEqual } from '../../../../base/common/resources.js'; import { parse } from '../../../../base/common/jsonc.js'; import { isMacintosh, isWindows } from '../../../../base/common/platform.js'; @@ -20,6 +20,11 @@ import { ITerminalInstance, ITerminalService } from '../../../../workbench/contr import { CommandString } from '../../../../workbench/contrib/tasks/common/taskConfiguration.js'; export type TaskStorageTarget = 'user' | 'workspace'; +type TaskRunOnOption = 'default' | 'folderOpen' | 'worktreeCreated'; + +interface ITaskRunOptions { + readonly runOn?: TaskRunOnOption; +} /** * Shape of a single task entry inside tasks.json. @@ -32,12 +37,18 @@ export interface ITaskEntry { readonly command?: string; readonly args?: CommandString[]; readonly inSessions?: boolean; + readonly runOptions?: ITaskRunOptions; readonly windows?: { command?: string; args?: CommandString[] }; readonly osx?: { command?: string; args?: CommandString[] }; readonly linux?: { command?: string; args?: CommandString[] }; readonly [key: string]: unknown; } +export interface INonSessionTaskEntry { + readonly task: ITaskEntry; + readonly target: TaskStorageTarget; +} + interface ITasksJson { version?: string; tasks?: ITaskEntry[]; @@ -56,19 +67,19 @@ export interface ISessionsConfigurationService { * Returns tasks that do NOT have `inSessions: true` — used as * suggestions in the "Add Run Action" picker. */ - getNonSessionTasks(session: IActiveSessionItem): Promise; + getNonSessionTasks(session: IActiveSessionItem): Promise; /** * Sets `inSessions: true` on an existing task (identified by label), * updating it in place in its tasks.json. */ - addTaskToSessions(task: ITaskEntry, session: IActiveSessionItem, target: TaskStorageTarget): Promise; + addTaskToSessions(task: ITaskEntry, session: IActiveSessionItem, target: TaskStorageTarget, options?: ITaskRunOptions): Promise; /** * Creates a new shell task with `inSessions: true` and writes it to * the appropriate tasks.json (user or workspace). */ - createAndAddTask(command: string, session: IActiveSessionItem, target: TaskStorageTarget): Promise; + createAndAddTask(label: string | undefined, command: string, session: IActiveSessionItem, target: TaskStorageTarget, options?: ITaskRunOptions): Promise; /** * Runs a task entry in a terminal, resolving the correct platform @@ -95,6 +106,7 @@ export class SessionsConfigurationService extends Disposable implements ISession private readonly _fileWatcher = this._register(new MutableDisposable()); /** Maps `cwd.toString() + command` to the terminal `instanceId`. */ private readonly _taskTerminals = new Map(); + private readonly _knownSessionWorktrees = new Map(); private readonly _lastRunTaskLabels: Map; private readonly _lastRunTaskObservables = new Map>>(); @@ -111,6 +123,11 @@ export class SessionsConfigurationService extends Disposable implements ISession ) { super(); this._lastRunTaskLabels = this._loadLastRunTaskLabels(); + + this._register(autorun(reader => { + const activeSession = this._sessionsManagementService.activeSession.read(reader); + this._handleActiveSessionChange(activeSession); + })); } getSessionTasks(session: IActiveSessionItem): IObservable { @@ -126,12 +143,33 @@ export class SessionsConfigurationService extends Disposable implements ISession return this._sessionTasks; } - async getNonSessionTasks(session: IActiveSessionItem): Promise { - const allTasks = await this._readAllTasks(session); - return allTasks.filter(t => !t.inSessions); + async getNonSessionTasks(session: IActiveSessionItem): Promise { + const result: INonSessionTaskEntry[] = []; + + const workspaceUri = this._getTasksJsonUri(session, 'workspace'); + if (workspaceUri) { + const workspaceJson = await this._readTasksJson(workspaceUri); + for (const task of workspaceJson.tasks ?? []) { + if (!task.inSessions && this._isSupportedTask(task)) { + result.push({ task, target: 'workspace' }); + } + } + } + + const userUri = this._getTasksJsonUri(session, 'user'); + if (userUri) { + const userJson = await this._readTasksJson(userUri); + for (const task of userJson.tasks ?? []) { + if (!task.inSessions && this._isSupportedTask(task)) { + result.push({ task, target: 'user' }); + } + } + } + + return result; } - async addTaskToSessions(task: ITaskEntry, session: IActiveSessionItem, target: TaskStorageTarget): Promise { + async addTaskToSessions(task: ITaskEntry, session: IActiveSessionItem, target: TaskStorageTarget, options?: ITaskRunOptions): Promise { const tasksJsonUri = this._getTasksJsonUri(session, target); if (!tasksJsonUri) { return; @@ -144,16 +182,25 @@ export class SessionsConfigurationService extends Disposable implements ISession return; } - await this._jsonEditingService.write(tasksJsonUri, [ - { path: ['tasks', index, 'inSessions'], value: true } - ], true); + const edits: { path: (string | number)[]; value: unknown }[] = [ + { path: ['tasks', index, 'inSessions'], value: true }, + ]; + + if (options) { + edits.push({ + path: ['tasks', index, 'runOptions'], + value: options.runOn && options.runOn !== 'default' ? { runOn: options.runOn } : undefined, + }); + } + + await this._jsonEditingService.write(tasksJsonUri, edits, true); if (target === 'workspace') { await this._commitTasksFile(session); } } - async createAndAddTask(command: string, session: IActiveSessionItem, target: TaskStorageTarget): Promise { + async createAndAddTask(label: string | undefined, command: string, session: IActiveSessionItem, target: TaskStorageTarget, options?: ITaskRunOptions): Promise { const tasksJsonUri = this._getTasksJsonUri(session, target); if (!tasksJsonUri) { return undefined; @@ -161,11 +208,13 @@ export class SessionsConfigurationService extends Disposable implements ISession const tasksJson = await this._readTasksJson(tasksJsonUri); const tasks = tasksJson.tasks ?? []; + const resolvedLabel = label?.trim() || command; const newTask: ITaskEntry = { - label: command, + label: resolvedLabel, type: 'shell', command, inSessions: true, + ...(options?.runOn && options.runOn !== 'default' ? { runOptions: { runOn: options.runOn } } : {}), }; await this._jsonEditingService.write(tasksJsonUri, [ @@ -332,6 +381,37 @@ export class SessionsConfigurationService extends Disposable implements ISession return `${command} ${resolvedArgs}`; } + private _handleActiveSessionChange(session: IActiveSessionItem | undefined): void { + if (!session) { + return; + } + + const sessionKey = session.resource.toString(); + const currentWorktree = session.worktree?.toString(); + if (!this._knownSessionWorktrees.has(sessionKey)) { + this._knownSessionWorktrees.set(sessionKey, currentWorktree); + return; + } + + const previousWorktree = this._knownSessionWorktrees.get(sessionKey); + this._knownSessionWorktrees.set(sessionKey, currentWorktree); + if (!currentWorktree || previousWorktree === currentWorktree) { + return; + } + + void this._runWorktreeCreatedTasks(session); + } + + private async _runWorktreeCreatedTasks(session: IActiveSessionItem): Promise { + const tasks = await this._readAllTasks(session); + for (const task of tasks) { + if (!task.inSessions || task.runOptions?.runOn !== 'worktreeCreated') { + continue; + } + await this.runTask(task, session); + } + } + private _ensureFileWatch(folder: URI): void { const tasksUri = joinPath(folder, '.vscode', 'tasks.json'); if (this._watchedResource && this._watchedResource.toString() === tasksUri.toString()) { diff --git a/src/vs/sessions/contrib/chat/test/browser/runScriptCustomTaskWidget.fixture.ts b/src/vs/sessions/contrib/chat/test/browser/runScriptCustomTaskWidget.fixture.ts new file mode 100644 index 00000000000..e20913a9a98 --- /dev/null +++ b/src/vs/sessions/contrib/chat/test/browser/runScriptCustomTaskWidget.fixture.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { IRunScriptCustomTaskWidgetState, RunScriptCustomTaskWidget, WORKTREE_CREATED_RUN_ON } from '../../browser/runScriptCustomTaskWidget.js'; + +// Ensure color registrations are loaded +import '../../../../common/theme.js'; +import '../../../../../platform/theme/common/colors/inputColors.js'; + +const filledLabel = 'Start Dev Server'; +const filledCommand = 'npm run dev'; +const workspaceUnavailableReason = 'Workspace storage is unavailable for this session'; + +function renderWidget(ctx: ComponentFixtureContext, state: IRunScriptCustomTaskWidgetState): void { + ctx.container.style.width = '600px'; + ctx.container.style.padding = '0'; + ctx.container.style.borderRadius = 'var(--vscode-cornerRadius-xLarge)'; + ctx.container.style.backgroundColor = 'var(--vscode-quickInput-background)'; + ctx.container.style.overflow = 'hidden'; + + const widget = ctx.disposableStore.add(new RunScriptCustomTaskWidget(state)); + ctx.container.appendChild(widget.domNode); +} + +function defineFixture(state: IRunScriptCustomTaskWidgetState) { + return defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderWidget(ctx, state), + }); +} + +export default defineThemedFixtureGroup({ path: 'sessions/' }, { + WorkspaceSelectedEmpty: defineFixture({ + target: 'workspace', + }), + + WorkspaceSelectedCheckedEmpty: defineFixture({ + target: 'workspace', + runOn: WORKTREE_CREATED_RUN_ON, + }), + + WorkspaceSelectedFilled: defineFixture({ + label: filledLabel, + target: 'workspace', + command: filledCommand, + }), + + WorkspaceSelectedCheckedFilled: defineFixture({ + label: filledLabel, + target: 'workspace', + command: filledCommand, + runOn: WORKTREE_CREATED_RUN_ON, + }), + + UserSelectedEmpty: defineFixture({ + target: 'user', + }), + + UserSelectedCheckedEmpty: defineFixture({ + target: 'user', + runOn: WORKTREE_CREATED_RUN_ON, + }), + + UserSelectedFilled: defineFixture({ + label: filledLabel, + target: 'user', + command: filledCommand, + }), + + UserSelectedCheckedFilled: defineFixture({ + label: filledLabel, + target: 'user', + command: filledCommand, + runOn: WORKTREE_CREATED_RUN_ON, + }), + + WorkspaceUnavailableEmpty: defineFixture({ + target: 'user', + targetDisabledReason: workspaceUnavailableReason, + }), + + WorkspaceUnavailableCheckedEmpty: defineFixture({ + target: 'user', + targetDisabledReason: workspaceUnavailableReason, + runOn: WORKTREE_CREATED_RUN_ON, + }), + + WorkspaceUnavailableFilled: defineFixture({ + label: filledLabel, + target: 'user', + command: filledCommand, + targetDisabledReason: workspaceUnavailableReason, + }), + + WorkspaceUnavailableCheckedFilled: defineFixture({ + label: filledLabel, + target: 'user', + command: filledCommand, + targetDisabledReason: workspaceUnavailableReason, + runOn: WORKTREE_CREATED_RUN_ON, + }), + + ExistingWorkspaceTaskLocked: defineFixture({ + label: filledLabel, + labelDisabledReason: 'This name comes from an existing task and cannot be changed here.', + command: filledCommand, + commandDisabledReason: 'This command comes from an existing task and cannot be changed here.', + target: 'workspace', + targetDisabledReason: 'This existing task cannot be moved between workspace and user storage.', + }), + + ExistingUserTaskLockedChecked: defineFixture({ + label: filledLabel, + labelDisabledReason: 'This name comes from an existing task and cannot be changed here.', + command: filledCommand, + commandDisabledReason: 'This command comes from an existing task and cannot be changed here.', + target: 'user', + targetDisabledReason: 'This existing task cannot be moved between workspace and user storage.', + runOn: WORKTREE_CREATED_RUN_ON, + }), +}); diff --git a/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts b/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts index d127cff33e9..073e6c42d02 100644 --- a/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts @@ -15,14 +15,19 @@ import { IJSONEditingService, IJSONValue } from '../../../../../workbench/servic import { IPreferencesService } from '../../../../../workbench/services/preferences/common/preferences.js'; import { ITerminalInstance, ITerminalService } from '../../../../../workbench/contrib/terminal/browser/terminal.js'; import { IActiveSessionItem, ISessionsManagementService } from '../../../sessions/browser/sessionsManagementService.js'; -import { ISessionsConfigurationService, SessionsConfigurationService, ITaskEntry } from '../../browser/sessionsConfigurationService.js'; +import { INonSessionTaskEntry, ISessionsConfigurationService, SessionsConfigurationService, ITaskEntry } from '../../browser/sessionsConfigurationService.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; import { observableValue } from '../../../../../base/common/observable.js'; function makeSession(opts: { repository?: URI; worktree?: URI } = {}): IActiveSessionItem { return { + resource: URI.parse('file:///session'), + isUntitled: false, + label: 'session', repository: opts.repository, worktree: opts.worktree, + worktreeBranchName: undefined, + providerType: 'background', } as IActiveSessionItem; } @@ -53,6 +58,7 @@ suite('SessionsConfigurationService', () => { let committedFiles: { session: IActiveSessionItem; fileUris: URI[] }[]; let storageService: InMemoryStorageService; let readFileCalls: URI[]; + let activeSessionObs: ReturnType>; const userSettingsUri = URI.parse('file:///user/settings.json'); const repoUri = URI.parse('file:///repo'); @@ -67,6 +73,7 @@ suite('SessionsConfigurationService', () => { readFileCalls = []; const instantiationService = store.add(new TestInstantiationService()); + activeSessionObs = observableValue('activeSession', undefined); instantiationService.stub(IFileService, new class extends mock() { override async readFile(resource: URI) { @@ -115,7 +122,7 @@ suite('SessionsConfigurationService', () => { instantiationService.stub(ITerminalService, terminalServiceMock); instantiationService.stub(ISessionsManagementService, new class extends mock() { - override activeSession = observableValue('activeSession', undefined); + override activeSession = activeSessionObs; override async commitWorktreeFiles(session: IActiveSessionItem, fileUris: URI[]) { committedFiles.push({ session, fileUris }); } }); @@ -219,7 +226,7 @@ suite('SessionsConfigurationService', () => { const session = makeSession({ worktree: worktreeUri, repository: repoUri }); const nonSessionTasks = await service.getNonSessionTasks(session); - assert.deepStrictEqual(nonSessionTasks.map(t => t.label), ['lint', 'test', 'watch']); + assert.deepStrictEqual(nonSessionTasks.map(t => t.task.label), ['lint', 'test', 'watch']); }); test('getNonSessionTasks reads from repository when no worktree', async () => { @@ -234,7 +241,26 @@ suite('SessionsConfigurationService', () => { const session = makeSession({ repository: repoUri }); const nonSessionTasks = await service.getNonSessionTasks(session); - assert.deepStrictEqual(nonSessionTasks.map(t => t.label), ['lint']); + assert.deepStrictEqual(nonSessionTasks.map(t => t.task.label), ['lint']); + }); + + test('getNonSessionTasks preserves the source target for workspace and user tasks', async () => { + const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json'); + const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' }); + fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([ + makeTask('workspaceTask', 'npm run workspace'), + ])); + fileContents.set(userTasksUri.toString(), tasksJsonContent([ + makeTask('userTask', 'npm run user'), + ])); + + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const nonSessionTasks = await service.getNonSessionTasks(session); + + assert.deepStrictEqual(nonSessionTasks, [ + { task: { label: 'workspaceTask', type: 'shell', command: 'npm run workspace' }, target: 'workspace' }, + { task: { label: 'userTask', type: 'shell', command: 'npm run user' }, target: 'user' }, + ] satisfies INonSessionTaskEntry[]); }); // --- addTaskToSessions --- @@ -284,6 +310,36 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(committedFiles.length, 0, 'should not commit when there is no worktree'); }); + test('addTaskToSessions updates runOptions when provided', async () => { + const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json'); + fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([ + makeTask('build', 'npm run build'), + ])); + + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + await service.addTaskToSessions(makeTask('build', 'npm run build'), session, 'workspace', { runOn: 'worktreeCreated' }); + + assert.deepStrictEqual(jsonEdits[0].values, [ + { path: ['tasks', 0, 'inSessions'], value: true }, + { path: ['tasks', 0, 'runOptions'], value: { runOn: 'worktreeCreated' } }, + ]); + }); + + test('addTaskToSessions clears runOptions when default is requested', async () => { + const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json'); + fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([ + { ...makeTask('build', 'npm run build'), runOptions: { runOn: 'worktreeCreated' } }, + ])); + + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + await service.addTaskToSessions(makeTask('build', 'npm run build'), session, 'workspace', { runOn: 'default' }); + + assert.deepStrictEqual(jsonEdits[0].values, [ + { path: ['tasks', 0, 'inSessions'], value: true }, + { path: ['tasks', 0, 'runOptions'], value: undefined }, + ]); + }); + // --- createAndAddTask --- test('createAndAddTask writes new task with inSessions: true', async () => { @@ -293,7 +349,7 @@ suite('SessionsConfigurationService', () => { ])); const session = makeSession({ worktree: worktreeUri, repository: repoUri }); - await service.createAndAddTask('npm run dev', session, 'workspace'); + await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace'); assert.strictEqual(jsonEdits.length, 1); const edit = jsonEdits[0]; @@ -315,7 +371,7 @@ suite('SessionsConfigurationService', () => { ])); const session = makeSession({ repository: repoUri }); - await service.createAndAddTask('npm run dev', session, 'workspace'); + await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace'); assert.strictEqual(jsonEdits.length, 1); assert.strictEqual(jsonEdits[0].uri.toString(), repoTasksUri.toString()); @@ -328,6 +384,35 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(committedFiles.length, 0, 'should not commit when there is no worktree'); }); + test('createAndAddTask writes worktreeCreated run option when requested', async () => { + const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json'); + fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([])); + + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + await service.createAndAddTask(undefined, 'npm run dev', session, 'workspace', { runOn: 'worktreeCreated' }); + + assert.strictEqual(jsonEdits.length, 1); + const tasksValue = jsonEdits[0].values.find(v => v.path[0] === 'tasks'); + assert.ok(tasksValue); + const tasks = tasksValue!.value as ITaskEntry[]; + assert.deepStrictEqual(tasks[0].runOptions, { runOn: 'worktreeCreated' }); + }); + + test('createAndAddTask writes a custom label when provided', async () => { + const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json'); + fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([])); + + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + await service.createAndAddTask('Start Dev Server', 'npm run dev', session, 'workspace'); + + assert.strictEqual(jsonEdits.length, 1); + const tasksValue = jsonEdits[0].values.find(v => v.path[0] === 'tasks'); + assert.ok(tasksValue); + const tasks = tasksValue!.value as ITaskEntry[]; + assert.strictEqual(tasks[0].label, 'Start Dev Server'); + assert.strictEqual(tasks[0].command, 'npm run dev'); + }); + // --- runTask --- test('runTask creates terminal and sends command', async () => { @@ -456,6 +541,26 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(createdTerminals.length, 2, 'should create two terminals for different worktrees'); }); + test('runs worktreeCreated session tasks when a session gains a worktree', async () => { + const sessionResource = URI.parse('file:///session-worktree-created'); + const worktreeTasksUri = URI.parse('file:///worktree/.vscode/tasks.json'); + const userTasksUri = URI.from({ scheme: userSettingsUri.scheme, path: '/user/tasks.json' }); + fileContents.set(worktreeTasksUri.toString(), tasksJsonContent([ + { label: 'build', type: 'shell', command: 'npm run build', inSessions: true, runOptions: { runOn: 'worktreeCreated' } }, + makeTask('manual', 'npm test', true), + ])); + fileContents.set(userTasksUri.toString(), tasksJsonContent([])); + + activeSessionObs.set({ ...makeSession({ repository: repoUri }), resource: sessionResource }, undefined); + await new Promise(r => setTimeout(r, 10)); + + activeSessionObs.set({ ...makeSession({ repository: repoUri, worktree: worktreeUri }), resource: sessionResource }, undefined); + await new Promise(r => setTimeout(r, 10)); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'npm run build'); + }); + // --- getLastRunTaskLabel (MRU) --- test('getLastRunTaskLabel returns undefined when no task has been run', () => { diff --git a/src/vs/sessions/contrib/git/browser/git.contribution.ts b/src/vs/sessions/contrib/git/browser/git.contribution.ts index 9aaaac7acb3..ad28526fdb6 100644 --- a/src/vs/sessions/contrib/git/browser/git.contribution.ts +++ b/src/vs/sessions/contrib/git/browser/git.contribution.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, observableValue } from '../../../../base/common/observable.js'; +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, constObservable, derived, ObservablePromise, observableValue } from '../../../../base/common/observable.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; @@ -14,21 +14,19 @@ import { IContextKeyService, RawContextKey } from '../../../../platform/contextk import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; -import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; +import { GitBranch, GitRepositoryState, IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; -const hasGitSyncChangesContextKey = new RawContextKey('agentSessionHasGitSyncChanges', false, { +const hasUpstreamBranchContextKey = new RawContextKey('agentSessionGitHasUpstreamBranch', false, { type: 'boolean', - description: localize('agentSessionHasGitSyncChanges', "True when the active agent session worktree has ahead or behind commits relative to its upstream.") + description: localize('agentSessionGitHasUpstreamBranch', "True when the active agent session worktree has an upstream branch."), }); class GitSyncContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'sessions.contrib.gitSync'; - private readonly _syncActionDisposable = this._register(new MutableDisposable()); - private readonly _gitRepoDisposables = this._register(new DisposableStore()); - private readonly _isSyncing = observableValue(this, false); + private readonly _isSyncingObs = observableValue(this, false); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -37,65 +35,68 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { ) { super(); - const contextKey = hasGitSyncChangesContextKey.bindTo(this.contextKeyService); + const hasUpstreamBranch = hasUpstreamBranchContextKey.bindTo(this.contextKeyService); + + const activeSessionWorktreeObs = derived(reader => { + const activeSession = this.sessionManagementService.activeSession.read(reader); + return activeSession?.worktree; + }); + + const activeSessionRepositoryPromiseObs = derived(reader => { + const worktreeUri = activeSessionWorktreeObs.read(reader); + if (!worktreeUri) { + return constObservable(undefined); + } + + return new ObservablePromise(this.gitService.openRepository(worktreeUri)).resolvedValue; + }); + + const activeSessionRepositoryStateObs = derived(reader => { + const activeSessionRepository = activeSessionRepositoryPromiseObs.read(reader).read(reader); + if (activeSessionRepository === undefined) { + return undefined; + } + + return activeSessionRepository.state.read(reader); + }); this._register(autorun(reader => { - const activeSession = this.sessionManagementService.activeSession.read(reader); - this._gitRepoDisposables.clear(); - - const worktreeUri = activeSession ? this.sessionManagementService.getActiveSession()?.worktree : undefined; - if (!worktreeUri) { - this._syncActionDisposable.clear(); - contextKey.set(false); + const isSyncing = this._isSyncingObs.read(reader); + const activeSessionRepositoryState = activeSessionRepositoryStateObs.read(reader); + if (!activeSessionRepositoryState) { + hasUpstreamBranch.set(false); return; } - const repoDisposables = this._gitRepoDisposables.add(new DisposableStore()); - this.gitService.openRepository(worktreeUri).then(repository => { - if (repoDisposables.isDisposed) { - return; - } - if (!repository) { - this._syncActionDisposable.clear(); - contextKey.set(false); - return; - } - repoDisposables.add(autorun(innerReader => { - const state = repository.state.read(innerReader); - const isSyncing = this._isSyncing.read(innerReader); - const head = state.HEAD; - if (!head?.upstream) { - this._syncActionDisposable.clear(); - contextKey.set(false); - return; - } - const ahead = head.ahead ?? 0; - const behind = head.behind ?? 0; - const hasSyncChanges = ahead > 0 || behind > 0; - contextKey.set(hasSyncChanges); - this._syncActionDisposable.clear(); - this._syncActionDisposable.value = registerSyncAction(behind, ahead, isSyncing, (syncing) => { - this._isSyncing.set(syncing, undefined); - }); - })); - }); + const head = activeSessionRepositoryState.HEAD; + hasUpstreamBranch.set(head?.upstream !== undefined); + + if (!head?.upstream) { + return; + } + + reader.store.add(registerSyncAction(head, isSyncing, (syncing) => { + this._isSyncingObs.set(syncing, undefined); + })); })); } } -function registerSyncAction(behind: number, ahead: number, isSyncing: boolean, setSyncing: (syncing: boolean) => void): IDisposable { - if (behind === 0 && ahead === 0) { - return Disposable.None; - } - let title = ''; +function registerSyncAction(branch: GitBranch, isSyncing: boolean, setSyncing: (syncing: boolean) => void): IDisposable { + const ahead = branch.ahead ?? 0; + const behind = branch.behind ?? 0; + + const titleSegments = [localize('synchronizeChangesTitle', "Sync Changes")]; if (behind > 0) { - title += `${behind}↓ `; + titleSegments.push(`${behind}↓`); } if (ahead > 0) { - title += `${ahead}↑`; + titleSegments.push(`${ahead}↑`); } - const icon = isSyncing ? ThemeIcon.modify(Codicon.sync, 'spin') : Codicon.sync; + const icon = isSyncing + ? ThemeIcon.modify(Codicon.sync, 'spin') + : Codicon.sync; class SynchronizeChangesAction extends Action2 { static readonly ID = 'chatEditing.synchronizeChanges'; @@ -103,16 +104,16 @@ function registerSyncAction(behind: number, ahead: number, isSyncing: boolean, s constructor() { super({ id: SynchronizeChangesAction.ID, - title, + title: titleSegments.join(' '), tooltip: localize('synchronizeChanges', "Synchronize Changes with Git (Behind {0}, Ahead {1})", behind, ahead), icon, category: CHAT_CATEGORY, menu: [ { - id: MenuId.ChatEditingSessionChangesToolbar, + id: MenuId.ChatEditingSessionApplySubmenu, group: 'navigation', - order: 5, - when: hasGitSyncChangesContextKey, + order: 0, + when: hasUpstreamBranchContextKey, }, ], }); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index cfa0f99049f..63ff340cf77 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -13,7 +13,7 @@ import { IContextKey, IContextKeyService, RawContextKey } from '../../../../plat import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ISessionOpenOptions, openSession as openSessionDefault } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.js'; -import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; +import { ChatViewPaneTarget, IChatWidget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IChatService, IChatSendRequestOptions } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../../../../workbench/contrib/chat/common/constants.js'; @@ -27,8 +27,8 @@ import { isBuiltinChatMode } from '../../../../workbench/contrib/chat/common/cha import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; import { ILanguageModelToolsService } from '../../../../workbench/contrib/chat/common/tools/languageModelToolsService.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; -import { isUntitledChatSession } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; import { IGitHubSessionContext } from '../../github/common/types.js'; +import { ResourceSet } from '../../../../base/common/map.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -41,7 +41,6 @@ export const IsActiveSessionBackgroundProviderContext = new RawContextKey; + sendRequestForNewSession(sessionResource: URI, options?: { permissionLevel?: ChatPermissionLevel }): Promise; /** * Commit files in a worktree and refresh the agent sessions model @@ -183,27 +180,16 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return; } - const agentSession = this.agentSessionsService.model.getSession(currentActive.resource); - if (!agentSession) { - if (currentActive.isUntitled) { - // The untitled session was committed by the extension via - // onDidCommitChatSessionItem, which replaces the untitled - // resource with a new committed resource. The commit handler - // already swapped the ChatViewPane widget to the new resource, - // so find it by checking the widget's current session resource. - const chatViewWidgets = this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat); - const committedResource = chatViewWidgets[0]?.viewModel?.sessionResource; - const committedSession = committedResource ? this.agentSessionsService.model.getSession(committedResource) : undefined; - if (committedSession) { - this.setActiveSession(committedSession); - } - } else { - this.showNextSession(); - } + if (currentActive.isUntitled) { return; } - this.setActiveSession(agentSession); + const agentSession = this.agentSessionsService.model.getSession(currentActive.resource); + if (agentSession) { + this.setActiveSession(agentSession); + } else { + this.showNextSession(); + } } private showNextSession(): void { @@ -255,37 +241,18 @@ export class SessionsManagementService extends Disposable implements ISessionsMa worktreeBranchName]; } - private getRepositoryFromSessionOption(sessionResource: URI): URI | undefined { - const optionValue = this.chatSessionsService.getSessionOption(sessionResource, repositoryOptionId); - if (!optionValue) { - return undefined; - } - - // Option value can be a string or IChatSessionProviderOptionItem - const optionId = typeof optionValue === 'string' ? optionValue : (optionValue as IChatSessionProviderOptionItem).id; - if (!optionId) { - return undefined; - } - - try { - return URI.parse(optionId); - } catch { - return undefined; - } - } - getActiveSession(): IActiveSessionItem | undefined { return this._activeSession.get(); } async openSession(sessionResource: URI, openOptions?: ISessionOpenOptions): Promise { - this.isNewChatSessionContext.set(false); const existingSession = this.agentSessionsService.model.getSession(sessionResource); - if (existingSession) { - await this.openExistingSession(existingSession, openOptions); - } else if (this._newSession.value && this.uriIdentityService.extUri.isEqual(sessionResource, this._newSession.value.resource)) { - await this.openNewSession(this._newSession.value); + if (!existingSession) { + throw new Error(`Session with resource ${sessionResource.toString()} not found`); } + this.isNewChatSessionContext.set(false); + this.setActiveSession(existingSession); + await this.instantiationService.invokeFunction(openSessionDefault, existingSession, openOptions); } async createNewSessionForTarget(target: AgentSessionProviders, sessionResource: URI, defaultRepoUri?: URI): Promise { @@ -304,30 +271,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return newSession; } - /** - * Open an existing agent session - set it as active and reveal it. - */ - private async openExistingSession(session: IAgentSession, openOptions?: ISessionOpenOptions): Promise { - this.setActiveSession(session); - await this.instantiationService.invokeFunction(openSessionDefault, session, openOptions); - } - - /** - * Open a new remote session - load the model first, then show it in the ChatViewPane. - */ - private async openNewSession(newSession: INewSession): Promise { - this.setActiveSession(newSession); - const sessionResource = newSession.resource; - const chatWidget = await this.chatWidgetService.openSession(sessionResource, ChatViewPaneTarget); - if (!chatWidget?.viewModel) { - this.logService.warn(`[ActiveSessionService] Failed to open session: ${sessionResource.toString()}`); - return; - } - const repository = this.getRepositoryFromSessionOption(sessionResource); - this.logService.info(`[ActiveSessionService] Active session changed (new): ${sessionResource.toString()}, repository: ${repository?.toString() ?? 'none'}`); - } - - async sendRequestForNewSession(sessionResource: URI, options?: { openNewSessionView?: boolean; permissionLevel?: ChatPermissionLevel }): Promise { + async sendRequestForNewSession(sessionResource: URI, options?: { permissionLevel?: ChatPermissionLevel }): Promise { const session = this._newSession.value; if (!session) { this.logService.error(`[SessionsManagementService] No new session found for resource: ${sessionResource.toString()}`); @@ -376,28 +320,53 @@ export class SessionsManagementService extends Disposable implements ISessionsMa }; await this.chatSessionsService.getOrCreateChatSession(session.resource, CancellationToken.None); - await this.doSendRequestForNewSession(session, query, sendOptions, session.selectedOptions, options?.openNewSessionView); + await this.doSendRequestForNewSession(session, query, sendOptions, session.selectedOptions); // Clean up the session after sending (setter disposes the previous value) this._newSession.value = undefined; } - private async doSendRequestForNewSession(session: INewSession, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap, openNewSessionView?: boolean): Promise { + private async doSendRequestForNewSession(session: INewSession, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap): Promise { // 1. Open the session - loads the model and shows the ChatViewPane - await this.openSession(session.resource); - if (openNewSessionView) { - this.openNewSessionView(); - } - - // Sync the permission level from the welcome picker to the ChatWidget's input part + const chatWidget = await this.openNewSession(session); const permissionLevel = sendOptions.modeInfo?.permissionLevel; if (permissionLevel) { - const chatWidget = this.chatWidgetService.getWidgetBySessionResource(session.resource); - chatWidget?.input.setPermissionLevel(permissionLevel); + chatWidget.input.setPermissionLevel(permissionLevel); } - // 2. Apply selected model and options to the session - const modelRef = this.chatService.acquireExistingSession(session.resource); + // 2. Load the session to apply selected options and have it ready when the view opens + await this.loadNewSession(session, selectedOptions); + + //3. Send the initial request to kick off the session creation on the extension side + const existingResources = new ResourceSet(this.agentSessionsService.model.sessions.map(s => s.resource)); + const result = await this.chatService.sendRequest(session.resource, query, sendOptions); + if (result.kind === 'rejected') { + this.logService.error(`[ActiveSessionService] sendRequest rejected: ${result.reason}`); + return; + } + + // 4. This is just a heuristic to wait for the extension to create the session before trying to find the session associated with the chat widget, which is what we want to set as active. + // This allows to set the active session to the new session immediately instead of waiting for the chat widget to open, which results in a smoother user experience + const probableNewSession = await this.loadProbableNewAgentSession(session, existingResources); + this.setActiveSession(probableNewSession); + + // 5. Wait for the real new session to appear in the chat widget + const newSession = await this.loadNewAgentSession(chatWidget, session); + this.setActiveSession(newSession); + } + + private async openNewSession(session: INewSession): Promise { + this.isNewChatSessionContext.set(false); + const sessionResource = session.resource; + const chatWidget = await this.chatWidgetService.openSession(sessionResource, ChatViewPaneTarget); + if (!chatWidget) { + throw new Error(`Failed to open chat session for resource ${sessionResource.toString()}`); + } + return chatWidget; + } + + private async loadNewSession(session: INewSession, selectedOptions?: ReadonlyMap): Promise { + const modelRef = await this.chatService.acquireOrLoadSession(session.resource, ChatAgentLocation.Chat, CancellationToken.None); if (modelRef) { const model = modelRef.object; @@ -433,42 +402,47 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } modelRef.dispose(); } + } - // 3. Send the request - const existingResources = new Set( - this.agentSessionsService.model.sessions.map(s => s.resource.toString()) - ); - const result = await this.chatService.sendRequest(session.resource, query, sendOptions); - if (result.kind === 'rejected') { - this.logService.error(`[ActiveSessionService] sendRequest rejected: ${result.reason}`); - return; + private async loadProbableNewAgentSession(session: INewSession, existingSessions: ResourceSet): Promise { + const probableNewSession = this.agentSessionsService.model.sessions.find(s => s.providerType === session.target && !existingSessions.has(s.resource)); + if (probableNewSession) { + return probableNewSession; } - - // 4. Wait for the extension to create an agent session, then set it as active - let newSession = this.agentSessionsService.model.sessions.find( - s => !existingResources.has(s.resource.toString()) - ); - - if (!newSession) { - let listener: IDisposable | undefined; - newSession = await Promise.race([ - new Promise(resolve => { - listener = this.agentSessionsService.model.onDidChangeSessions(() => { - const session = this.agentSessionsService.model.sessions.find( - s => !existingResources.has(s.resource.toString()) - ); - if (session) { - resolve(session); - } - }); - }), - new Promise(resolve => setTimeout(() => resolve(undefined), 30_000)), - ]); + let listener: IDisposable | undefined; + try { + return await new Promise(resolve => { + listener = this.agentSessionsService.model.onDidChangeSessions(() => { + const s = this.agentSessionsService.model.sessions.find(s => s.providerType === session.target && !existingSessions.has(s.resource)); + if (s) { + listener?.dispose(); + resolve(s); + } + }); + }); + } finally { listener?.dispose(); } + } - if (newSession && !openNewSessionView) { - this.setActiveSession(newSession, session); + private async loadNewAgentSession(chatWidget: IChatWidget, session: INewSession): Promise { + const newSession = this.agentSessionsService.model.sessions.find(s => s.providerType === session.target && this.uriIdentityService.extUri.isEqual(s.resource, chatWidget.viewModel?.sessionResource)); + if (newSession) { + return newSession; + } + let listener: IDisposable | undefined; + try { + return await new Promise(resolve => { + listener = chatWidget.onDidChangeViewModel(() => { + const s = this.agentSessionsService.model.sessions.find(s => s.providerType === session.target && this.uriIdentityService.extUri.isEqual(s.resource, chatWidget.viewModel?.sessionResource)); + if (s) { + listener?.dispose(); + resolve(s); + } + }); + }); + } finally { + listener?.dispose(); } } @@ -481,19 +455,19 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this.isNewChatSessionContext.set(true); } - private setActiveSession(session: IAgentSession | INewSession | undefined, pendingSession?: INewSession): void { + private setActiveSession(session: IAgentSession | INewSession | undefined): void { let activeSessionItem: IActiveSessionItem | undefined; if (session) { if (isAgentSession(session)) { this.lastSelectedSession = session.resource; const [repository, worktree, worktreeBranchName] = this.getRepositoryFromMetadata(session); activeSessionItem = { - isUntitled: isUntitledChatSession(session.resource), + isUntitled: false, label: session.label, resource: session.resource, - repository: repository ?? pendingSession?.repoUri, + repository: repository, worktree, - worktreeBranchName: worktreeBranchName ?? pendingSession?.branch, + worktreeBranchName: worktreeBranchName, providerType: session.providerType, }; } else { diff --git a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts index 209161e31c7..f22ddd122c5 100644 --- a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts +++ b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts @@ -26,6 +26,7 @@ import { Action2, registerAction2 } from '../../../../platform/actions/common/ac import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IWorkbenchEnvironmentService } from '../../../../workbench/services/environment/common/environmentService.js'; const WELCOME_COMPLETE_KEY = 'workbench.agentsession.welcomeComplete'; @@ -138,6 +139,7 @@ class SessionsWelcomeContribution extends Disposable implements IWorkbenchContri @IProductService private readonly productService: IProductService, @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, ) { super(); @@ -145,6 +147,17 @@ class SessionsWelcomeContribution extends Disposable implements IWorkbenchContri return; } + // Allow automated tests to skip the welcome overlay entirely. + // Desktop: --skip-sessions-welcome CLI flag + // Web: ?skip-sessions-welcome query parameter + const envArgs = (this.environmentService as IWorkbenchEnvironmentService & { args?: Record }).args; + if (envArgs?.['skip-sessions-welcome']) { + return; + } + if (typeof globalThis.location !== 'undefined' && new URLSearchParams(globalThis.location.search).has('skip-sessions-welcome')) { + return; + } + const isFirstLaunch = !this.storageService.getBoolean(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION, false); if (isFirstLaunch) { this.showOverlay(); diff --git a/src/vs/sessions/prompts/create-draft-pr.prompt.md b/src/vs/sessions/prompts/create-draft-pr.prompt.md index b64c60e1ea7..4def295b9fc 100644 --- a/src/vs/sessions/prompts/create-draft-pr.prompt.md +++ b/src/vs/sessions/prompts/create-draft-pr.prompt.md @@ -1,7 +1,7 @@ --- description: Create a draft pull request for the current session --- - + Use the GitHub MCP server to create a draft pull request — do NOT use the `gh` CLI. diff --git a/src/vs/sessions/prompts/create-pr.prompt.md b/src/vs/sessions/prompts/create-pr.prompt.md index 0a742161762..02208021e3a 100644 --- a/src/vs/sessions/prompts/create-pr.prompt.md +++ b/src/vs/sessions/prompts/create-pr.prompt.md @@ -1,7 +1,7 @@ --- description: Create a pull request for the current session --- - + Use the GitHub MCP server to create a pull request — do NOT use the `gh` CLI. diff --git a/src/vs/sessions/sessions.web.main.internal.ts b/src/vs/sessions/sessions.web.main.internal.ts new file mode 100644 index 00000000000..4e7ae75ffc2 --- /dev/null +++ b/src/vs/sessions/sessions.web.main.internal.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This file is the web embedder entry point for the Sessions workbench. +// It mirrors workbench.web.main.internal.ts but loads the sessions entry +// point and factory instead of the standard workbench ones. + +import './sessions.web.main.js'; +import { create } from './browser/web.factory.js'; +import { URI } from '../base/common/uri.js'; +import { Event, Emitter } from '../base/common/event.js'; +import { Disposable } from '../base/common/lifecycle.js'; +import { LogLevel } from '../platform/log/common/log.js'; + +export { + create, + URI, + Event, + Emitter, + Disposable, + LogLevel, +}; diff --git a/src/vs/sessions/sessions.web.main.ts b/src/vs/sessions/sessions.web.main.ts new file mode 100644 index 00000000000..9a335e542f8 --- /dev/null +++ b/src/vs/sessions/sessions.web.main.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +// ####################################################################### +// ### ### +// ### !!! PLEASE ADD COMMON IMPORTS INTO SESSIONS.COMMON.MAIN.TS !!! ### +// ### ### +// ####################################################################### + +//#region --- sessions common + +import './sessions.common.main.js'; + +//#endregion + + +//#region --- workbench parts + +import '../workbench/browser/parts/dialogs/dialog.web.contribution.js'; + +//#endregion + + +//#region --- sessions (web main) — sessions-specific web bootstrap + +import './browser/web.main.js'; + +//#endregion + + +//#region --- workbench services (browser equivalents of the electron services) + +import '../workbench/services/integrity/browser/integrityService.js'; +import '../workbench/services/search/browser/searchService.js'; +import '../workbench/services/textfile/browser/browserTextFileService.js'; +import '../workbench/services/keybinding/browser/keyboardLayoutService.js'; +import '../workbench/services/extensions/browser/extensionService.js'; +import '../workbench/services/extensionManagement/browser/extensionsProfileScannerService.js'; +import '../workbench/services/extensions/browser/extensionsScannerService.js'; +import '../workbench/services/extensionManagement/browser/webExtensionsScannerService.js'; +import '../workbench/services/extensionManagement/common/extensionManagementServerService.js'; +import '../workbench/services/mcp/browser/mcpWorkbenchManagementService.js'; +import '../workbench/services/extensionManagement/browser/extensionGalleryManifestService.js'; +import '../workbench/services/telemetry/browser/telemetryService.js'; +import '../workbench/services/url/browser/urlService.js'; +import '../workbench/services/update/browser/updateService.js'; +import '../workbench/services/workspaces/browser/workspacesService.js'; +import '../workbench/services/workspaces/browser/workspaceEditingService.js'; +import '../workbench/services/dialogs/browser/fileDialogService.js'; +import '../workbench/services/host/browser/browserHostService.js'; +import '../platform/meteredConnection/browser/meteredConnectionService.js'; +import '../workbench/services/lifecycle/browser/lifecycleService.js'; +import '../workbench/services/clipboard/browser/clipboardService.js'; +import '../workbench/services/localization/browser/localeService.js'; +import '../workbench/services/path/browser/pathService.js'; +import '../workbench/services/themes/browser/browserHostColorSchemeService.js'; +import '../workbench/services/encryption/browser/encryptionService.js'; +import '../workbench/services/imageResize/browser/imageResizeService.js'; +import '../workbench/services/secrets/browser/secretStorageService.js'; +import '../workbench/services/workingCopy/browser/workingCopyBackupService.js'; +import '../workbench/services/tunnel/browser/tunnelService.js'; +import '../workbench/services/files/browser/elevatedFileService.js'; +import '../workbench/services/workingCopy/browser/workingCopyHistoryService.js'; +import '../workbench/services/userDataSync/browser/webUserDataSyncEnablementService.js'; +import '../workbench/services/userDataProfile/browser/userDataProfileStorageService.js'; +import '../workbench/services/configurationResolver/browser/configurationResolverService.js'; +import '../platform/extensionResourceLoader/browser/extensionResourceLoaderService.js'; +import '../workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.js'; +import '../workbench/services/browserElements/browser/webBrowserElementsService.js'; +import '../workbench/services/power/browser/powerService.js'; + +import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; +import { IAccessibilityService } from '../platform/accessibility/common/accessibility.js'; +import { IContextMenuService } from '../platform/contextview/browser/contextView.js'; +import { ContextMenuService } from '../platform/contextview/browser/contextMenuService.js'; +import { IExtensionTipsService } from '../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionTipsService } from '../platform/extensionManagement/common/extensionTipsService.js'; +import { IWorkbenchExtensionManagementService } from '../workbench/services/extensionManagement/common/extensionManagement.js'; +import { ExtensionManagementService } from '../workbench/services/extensionManagement/common/extensionManagementService.js'; +import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from '../platform/userDataSync/common/userDataSyncMachines.js'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataAutoSyncService, IUserDataSyncLocalStoreService, IUserDataSyncResourceProviderService } from '../platform/userDataSync/common/userDataSync.js'; +import { UserDataSyncStoreService } from '../platform/userDataSync/common/userDataSyncStoreService.js'; +import { UserDataSyncLocalStoreService } from '../platform/userDataSync/common/userDataSyncLocalStoreService.js'; +import { UserDataSyncService } from '../platform/userDataSync/common/userDataSyncService.js'; +import { IUserDataSyncAccountService, UserDataSyncAccountService } from '../platform/userDataSync/common/userDataSyncAccount.js'; +import { UserDataAutoSyncService } from '../platform/userDataSync/common/userDataAutoSyncService.js'; +import { AccessibilityService } from '../platform/accessibility/browser/accessibilityService.js'; +import { ICustomEndpointTelemetryService } from '../platform/telemetry/common/telemetry.js'; +import { NullEndpointTelemetryService } from '../platform/telemetry/common/telemetryUtils.js'; +import { ITitleService } from '../workbench/services/title/browser/titleService.js'; +import { BrowserTitleService } from '../workbench/browser/parts/titlebar/titlebarPart.js'; +import { ITimerService, TimerService } from '../workbench/services/timer/browser/timerService.js'; +import { IDiagnosticsService, NullDiagnosticsService } from '../platform/diagnostics/common/diagnostics.js'; +import { ILanguagePackService } from '../platform/languagePacks/common/languagePacks.js'; +import { WebLanguagePacksService } from '../platform/languagePacks/browser/languagePacks.js'; +import { IWebContentExtractorService, NullWebContentExtractorService, ISharedWebContentExtractorService, NullSharedWebContentExtractorService } from '../platform/webContentExtractor/common/webContentExtractor.js'; +import { IMcpGalleryManifestService } from '../platform/mcp/common/mcpGalleryManifest.js'; +import { WorkbenchMcpGalleryManifestService } from '../workbench/services/mcp/browser/mcpGalleryManifestService.js'; +import { UserDataSyncResourceProviderService } from '../platform/userDataSync/common/userDataSyncResourceProvider.js'; + +registerSingleton(IWorkbenchExtensionManagementService, ExtensionManagementService, InstantiationType.Delayed); +registerSingleton(IAccessibilityService, AccessibilityService, InstantiationType.Delayed); +registerSingleton(IContextMenuService, ContextMenuService, InstantiationType.Delayed); +registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService, InstantiationType.Delayed); +registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService, InstantiationType.Delayed); +registerSingleton(IUserDataSyncLocalStoreService, UserDataSyncLocalStoreService, InstantiationType.Delayed); +registerSingleton(IUserDataSyncAccountService, UserDataSyncAccountService, InstantiationType.Delayed); +registerSingleton(IUserDataSyncService, UserDataSyncService, InstantiationType.Delayed); +registerSingleton(IUserDataSyncResourceProviderService, UserDataSyncResourceProviderService, InstantiationType.Delayed); +registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService, InstantiationType.Eager); +registerSingleton(ITitleService, BrowserTitleService, InstantiationType.Eager); +registerSingleton(IExtensionTipsService, ExtensionTipsService, InstantiationType.Delayed); +registerSingleton(ITimerService, TimerService, InstantiationType.Delayed); +registerSingleton(ICustomEndpointTelemetryService, NullEndpointTelemetryService, InstantiationType.Delayed); +registerSingleton(IDiagnosticsService, NullDiagnosticsService, InstantiationType.Delayed); +registerSingleton(ILanguagePackService, WebLanguagePacksService, InstantiationType.Delayed); +registerSingleton(IWebContentExtractorService, NullWebContentExtractorService, InstantiationType.Delayed); +registerSingleton(ISharedWebContentExtractorService, NullSharedWebContentExtractorService, InstantiationType.Delayed); +registerSingleton(IMcpGalleryManifestService, WorkbenchMcpGalleryManifestService, InstantiationType.Delayed); + +//#endregion + + +//#region --- workbench contributions (browser versions) + +import '../workbench/contrib/logs/browser/logs.contribution.js'; +import '../workbench/contrib/localization/browser/localization.contribution.js'; +import '../workbench/contrib/performance/browser/performance.web.contribution.js'; +import '../workbench/contrib/preferences/browser/keyboardLayoutPicker.js'; +import '../workbench/contrib/debug/browser/extensionHostDebugService.js'; +import '../workbench/contrib/welcomeBanner/browser/welcomeBanner.contribution.js'; +import '../workbench/contrib/webview/browser/webview.web.contribution.js'; +import '../workbench/contrib/extensions/browser/extensions.web.contribution.js'; +import '../workbench/contrib/terminal/browser/terminal.web.contribution.js'; +import '../workbench/contrib/externalTerminal/browser/externalTerminal.contribution.js'; +import '../workbench/contrib/terminal/browser/terminalInstanceService.js'; +import '../workbench/contrib/tasks/browser/taskService.js'; +import '../workbench/contrib/tags/browser/workspaceTagsService.js'; +import '../workbench/contrib/issue/browser/issue.contribution.js'; +import '../workbench/contrib/splash/browser/splash.contribution.js'; +import '../workbench/contrib/remote/browser/remoteStartEntry.contribution.js'; +import '../workbench/contrib/processExplorer/browser/processExplorer.web.contribution.js'; + +//#endregion + + +//#region --- sessions contributions (same as desktop — these are all browser-safe) + +import './browser/paneCompositePartService.js'; +import './browser/layoutActions.js'; + +import './contrib/accountMenu/browser/account.contribution.js'; +import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.js'; +import './contrib/chat/browser/chat.contribution.js'; +import './contrib/sessions/browser/sessions.contribution.js'; +import './contrib/sessions/browser/customizationsToolbar.contribution.js'; +import './contrib/changes/browser/changesView.contribution.js'; +import './contrib/codeReview/browser/codeReview.contributions.js'; +import './contrib/github/browser/github.contribution.js'; +import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; +import './contrib/configuration/browser/configuration.contribution.js'; +import './contrib/welcome/browser/welcome.contribution.js'; + +//#endregion diff --git a/src/vs/sessions/test/e2e/.gitignore b/src/vs/sessions/test/e2e/.gitignore new file mode 100644 index 00000000000..141d99daed5 --- /dev/null +++ b/src/vs/sessions/test/e2e/.gitignore @@ -0,0 +1,3 @@ +out/ +*.png +node_modules/ diff --git a/src/vs/sessions/test/e2e/README.md b/src/vs/sessions/test/e2e/README.md new file mode 100644 index 00000000000..a2b6cce5d3f --- /dev/null +++ b/src/vs/sessions/test/e2e/README.md @@ -0,0 +1,366 @@ +# Agent Sessions — E2E Tests + +Automated dogfooding tests for the Agent Sessions window using a +**compile-and-replay** architecture powered by +[`playwright-cli`](https://github.com/microsoft/playwright-cli) and Copilot CLI. + +## Mocking Architecture + +These tests run the **real** Sessions workbench with only the minimal set of +services mocked — specifically the services that require external backends +(auth, LLM, git). Everything downstream from the mock agent's canned response +runs through the real code paths. + +### What's Mocked (Minimal) + +| Service | Mock | Why | +|---------|------|-----| +| `IChatEntitlementService` | Returns `ChatEntitlement.Free` | No real Copilot account in CI | +| `IDefaultAccountService` | Returns a fake signed-in account | Hides the "Sign In" button | +| `IGitService` | Resolves immediately (no 10s barrier) | No real git extension in web tests | +| Chat agents (`copilotcli`, etc.) | Canned keyword-matched responses with `textEdit` progress items | No real LLM backend | +| `mock-fs://` FileSystemProvider | `InMemoryFileSystemProvider` registered directly in the workbench (not extension host) | Must be available before any service tries to resolve workspace files | +| GitHub authentication | Always-signed-in mock provider (extension) | No real OAuth flow | + +### What's Real (Everything Else) + +The following services run with their **real** implementations, ensuring tests +exercise the actual code paths: + +- **`ChatEditingService`** — Processes `textEdit` progress items from the mock + agent, creates `IModifiedFileEntry` objects with real before/after diffs, and + computes actual `linesAdded`/`linesRemoved` from content changes +- **`ChatModel`** — Routes agent progress through `acceptResponseProgress()` +- **`ChangesViewPane`** — Reads file modification state from `IChatEditingService` + observables and renders the tree with real diff stats +- **Diff editor** — Opens a real diff view when clicking files in the changes list +- **Context keys** — `hasUndecidedChatEditingResourceContextKey`, + `hasAppliedChatEditsContextKey` are set by real `ModifiedFileEntryState` + observations +- **Menu actions** — "Create PR", "Accept", "Reject" buttons appear based on + real context key state + +### Data Flow + +``` +User types message → Chat Widget → ChatService + → Mock Agent invoke() → progress([{ kind: 'textEdit', uri, edits }]) + → ChatModel.acceptResponseProgress() + → ChatEditingService observes textEditGroup parts + → Creates IModifiedFileEntry per file + → Reads original content from mock-fs:// FileSystemProvider + → Computes real diff (linesAdded, linesRemoved) + → ChangesViewPane renders via observable chain + → Click file → Opens real diff editor +``` + +The mock agent is the **only** point where canned data enters the system. +Everything downstream uses real service implementations. + +### Why the FileSystem Provider Is Registered in the Workbench + +The `mock-fs://` `InMemoryFileSystemProvider` is registered directly on +`IFileService` inside `TestSessionsBrowserMain.createWorkbench()` — **not** in +the mock extension. This is critical because several workbench services +(SnippetsService, AgenticPromptFilesLocator, MCP, etc.) try to resolve files +in the workspace folder **before** the extension host activates. If the +provider were only registered via `vscode.workspace.registerFileSystemProvider()` +in the extension, these services would see `ENOPRO: No file system provider` +errors and fail silently. + +The mock extension still registers a `mock-fs` provider via the extension API +(needed for extension host operations), but the workbench-level registration +is the source of truth. + +### File Edit Strategy + +Mock edits target files that exist in the `mock-fs://` file store so the +`ChatEditingService` can compute real before/after diffs: + +- **Existing files** (e.g. `/mock-repo/src/index.ts`, `/mock-repo/package.json`) — edits use a + full-file replacement range (`line 1 → line 99999`) so the editing service + diffs the old content against the new content +- **New files** (e.g. `/mock-repo/src/build.ts`) — edits use an insert-at-beginning + range, producing a "file created" entry in the changes view + +### Mock Workspace Folder + +The workspace folder URI is `mock-fs://mock-repo/mock-repo`. The path +`/mock-repo` (not root `/`) is used so that `basename(folderUri)` returns +`"mock-repo"` — this is what the folder picker displays. All mock files are +stored under this path in the in-memory file store. + +## How It Works + +There are two phases: + +### Phase 1: Generate (uses LLM — slow, run once) + +```bash +npm run generate +``` + +For each `.scenario.md` file, the generate script: +1. Starts the Sessions web server and opens the page in `playwright-cli` +2. Takes an accessibility tree snapshot of the current page +3. Sends each natural-language step + snapshot to **Copilot CLI**, which returns + the exact `playwright-cli` commands (e.g. `click e43`, `type "hello"`) +4. Executes the commands to advance the UI state for the next step +5. Writes the compiled commands to a `.commands.json` file in the `scenarios/generated/` folder + +``` +scenarios/ +├── 01-repo-picker-on-submit.scenario.md ← human-written +├── 02-cloud-disables-add-run-action.scenario.md +└── generated/ + ├── 01-repo-picker-on-submit.commands.json ← agent-generated + └── 02-cloud-disables-add-run-action.commands.json +``` + +The `.commands.json` files are **committed to git** — they're the deterministic +test plan that everyone runs. + +### Phase 2: Test (no LLM — fast, deterministic) + +```bash +npm test +``` + +The test runner reads each `.commands.json` and replays the `playwright-cli` +commands mechanically. No LLM calls, no regex matching, no icon stripping. +Just sequential commands and assertions. + +### When to Re-generate + +Run `npm run generate` when: +- You add a new `.scenario.md` file +- The UI changes and refs are stale (tests start failing) +- You modify an existing scenario's steps + +## File Structure + +``` +e2e/ +├── common.cjs # Shared helpers (server, playwright-cli, parser) +├── generate.cjs # Compiles scenarios → .commands.json via Copilot CLI +├── test.cjs # Replays .commands.json deterministically +├── package.json # npm scripts: generate, test +├── extensions/ +│ └── sessions-e2e-mock/ # Mock extension (auth + mock-fs:// file system) +├── scenarios/ +│ ├── 01-chat-response.scenario.md +│ ├── 02-chat-with-changes.scenario.md +│ └── generated/ +│ ├── 01-chat-response.commands.json +│ └── 02-chat-with-changes.commands.json +├── .gitignore +└── README.md +``` + +Supporting files outside `e2e/`: + +``` +src/vs/sessions/test/ +├── web.test.ts # TestSessionsBrowserMain + MockChatAgentContribution +├── web.test.factory.ts # Factory for test workbench (replaces web.factory.ts) +└── sessions.web.test.internal.ts # Test entry point + +scripts/ +├── code-sessions-web.js # HTTP server that serves Sessions as a web app +└── code-sessions-web.sh # Shell wrapper +``` + +## Prerequisites + +- VS Code compiled (`out/` at the repo root): + ```bash + npm install && npm run compile + ``` +- Dependencies installed: + ```bash + cd src/vs/sessions/test/e2e && npm install + ``` +- Copilot CLI available (for `npm run generate` only): + ```bash + copilot --version + ``` + +## Running + +```bash +cd src/vs/sessions/test/e2e + +# First time or after UI changes: +npm run generate + +# Run tests (fast, deterministic): +npm test +``` + +Example test output: + +``` +Found 2 compiled scenario(s) + +Starting sessions web server on port 9542… +Server ready. + +▶ Scenario: Repository picker opens when submitting without a repo + ✅ step 1: Click button "Cloud" + ✅ step 2: Type "build the project" in the chat input + ✅ step 3: Press Enter to submit + ✅ step 4: Verify the repository picker dropdown is visible + +▶ Scenario: Switching to Cloud target disables the Add Run Action button + ✅ step 1: Click button "Cloud" + ✅ step 2: Click button "Local" + +Results: 6 passed, 0 failed +``` + +## Writing a New Scenario + +1. Create a new `NN-description.scenario.md` file in `scenarios/`. + Files are sorted by name and run in order. + +2. Use this format: + +```markdown +# Scenario: Short description of what this tests + +## Steps +1. Click button "Cloud" +2. Type "build the project" in the chat input +3. Press Enter to submit +4. Verify the repository picker dropdown is visible +``` + +3. Run `npm run generate` to compile it into a `.commands.json` file. + +4. Run `npm test` to verify it works. + +5. Commit both the `.scenario.md` and `.commands.json` files. + +### Step Language + +Write steps in plain English. The Copilot agent interprets them against the +page's accessibility tree. Common patterns: + +| Pattern | Example | +|---------|---------| +| Click a button | `Click button "Cloud"` | +| Type in an input | `Type "hello" in the chat input` | +| Press a key | `Press Enter` | +| Verify visibility | `Verify the repository picker dropdown is visible` | +| Verify button state | `Verify the "Send" button is disabled` | + +You're not limited to these patterns — the agent understands natural language. + +### The .commands.json Format + +Each compiled step looks like: + +```json +{ + "description": "Click button \"Cloud\"", + "commands": [ + "click e143" + ] +} +``` + +For assertions, the agent outputs a `snapshot` command followed by an assertion comment: + +```json +{ + "description": "Verify the repository picker dropdown is visible", + "commands": [ + "snapshot", + "# ASSERT_VISIBLE: Repository Picker" + ] +} +``` + +The test runner understands these comment-based assertions: +- `# ASSERT_VISIBLE: ` — checks snapshot contains the text +- `# ASSERT_DISABLED: