diff --git a/.github/workflows/bad-tag.yml b/.github/workflows/bad-tag.yml new file mode 100644 index 00000000000..e996639dfbd --- /dev/null +++ b/.github/workflows/bad-tag.yml @@ -0,0 +1,22 @@ +name: Bad Tag +on: + create + +jobs: + main: + runs-on: ubuntu-latest + if: github.event.ref == '1.999.0' + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: "microsoft/vscode-github-triage-actions" + ref: stable + path: ./actions + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run Bad Tag + uses: ./actions/tag-alert + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + tag-name: '1.999.0' diff --git a/.vscode/settings.json b/.vscode/settings.json index 923b5737fa2..12ebf38576f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -100,4 +100,5 @@ "comments": "inline", "strings": "inline" }, + "githubPullRequests.assignCreated": "${user}" } diff --git a/build/azure-pipelines/linux/product-build-alpine.yml b/build/azure-pipelines/linux/product-build-alpine.yml index 74577b52a68..3aef7279243 100644 --- a/build/azure-pipelines/linux/product-build-alpine.yml +++ b/build/azure-pipelines/linux/product-build-alpine.yml @@ -118,7 +118,9 @@ steps: - script: | set -e - docker run -e VSCODE_QUALITY -v $(pwd):/root/vscode -v ~/.netrc:/root/.netrc vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) /root/vscode/build/azure-pipelines/linux/scripts/install-remote-dependencies.sh + docker run -e VSCODE_QUALITY -e GITHUB_TOKEN -v $(pwd):/root/vscode -v ~/.netrc:/root/.netrc vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) /root/vscode/build/azure-pipelines/linux/scripts/install-remote-dependencies.sh + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Prebuild - script: | diff --git a/build/azure-pipelines/linux/product-build-linux-client.yml b/build/azure-pipelines/linux/product-build-linux-client.yml index 9d0a3567159..817d34d88ab 100644 --- a/build/azure-pipelines/linux/product-build-linux-client.yml +++ b/build/azure-pipelines/linux/product-build-linux-client.yml @@ -104,6 +104,16 @@ steps: - script: | set -e export npm_config_arch=$(NPM_ARCH) + # node-gyp@9.0.0 shipped with node@16.15.0 starts using config.gypi + # from the custom headers path if dist-url option was set instead of + # using the config value from the process. Electron builds with pointer compression + # enabled for x64 and arm64, but incorrectly ships a single copy of config.gypi + # with v8_enable_pointer_compression option always set for all target architectures. + # We use the force_process_config option to use the config.gypi from the + # nodejs process executing npm for 32-bit architectures. + if [ "$NPM_ARCH" = "armv7l" ]; then + export npm_config_force_process_config="true" + fi if [ -z "$CC" ] || [ -z "$CXX" ]; then # Download clang based on chromium revision used by vscode diff --git a/build/azure-pipelines/upload-configuration.js b/build/azure-pipelines/upload-configuration.js index 40440108773..8be7f4492cc 100644 --- a/build/azure-pipelines/upload-configuration.js +++ b/build/azure-pipelines/upload-configuration.js @@ -52,7 +52,7 @@ function generateVSCodeConfigurationTask() { const timer = setTimeout(() => { codeProc.kill(); reject(new Error('export-default-configuration process timed out')); - }, 12 * 1000); + }, 30 * 1000); codeProc.on('error', err => { clearTimeout(timer); reject(err); diff --git a/build/azure-pipelines/upload-configuration.ts b/build/azure-pipelines/upload-configuration.ts index f39175ae91d..49bc00fc46f 100644 --- a/build/azure-pipelines/upload-configuration.ts +++ b/build/azure-pipelines/upload-configuration.ts @@ -63,7 +63,7 @@ function generateVSCodeConfigurationTask(): Promise { const timer = setTimeout(() => { codeProc.kill(); reject(new Error('export-default-configuration process timed out')); - }, 12 * 1000); + }, 30 * 1000); codeProc.on('error', err => { clearTimeout(timer); diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 7a578fb4c7b..78367983406 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -86,6 +86,14 @@ steps: . build/azure-pipelines/win32/retry.ps1 $ErrorActionPreference = "Stop" $env:npm_config_arch="$(VSCODE_ARCH)" + # node-gyp@9.0.0 shipped with node@16.15.0 starts using config.gypi + # from the custom headers path if dist-url option was set instead of + # using the config value from the process. Electron builds with pointer compression + # enabled for x64 and arm64, but incorrectly ships a single copy of config.gypi + # with v8_enable_pointer_compression option always set for all target architectures. + # We use the force_process_config option to use the config.gypi from the + # nodejs process executing npm for 32-bit architectures. + if ('$(VSCODE_ARCH)' -eq 'ia32') { $env:npm_config_force_process_config="true" } $env:CHILD_CONCURRENCY="1" retry { exec { yarn --frozen-lockfile --check-files } } env: diff --git a/extensions/git/package.json b/extensions/git/package.json index ced39d466d1..ca58c729680 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1790,6 +1790,11 @@ "markdownDescription": "%config.autofetchPeriod%", "default": 180 }, + "git.branchPrefix": { + "type": "string", + "description": "%config.branchPrefix%", + "default": "" + }, "git.branchValidationRegex": { "type": "string", "description": "%config.branchValidationRegex%", @@ -1800,6 +1805,22 @@ "description": "%config.branchWhitespaceChar%", "default": "-" }, + "git.branchRandomName.enable": { + "type": "boolean", + "description": "%config.branchRandomNameEnable%", + "default": false + }, + "git.branchRandomName.dictionary": { + "type": "array", + "markdownDescription": "%config.branchRandomNameDictionary%", + "items": { + "type": "string" + }, + "default": [ + "adjectives", + "animals" + ] + }, "git.confirmSync": { "type": "boolean", "description": "%config.confirmSync%", @@ -2516,6 +2537,7 @@ ] }, "dependencies": { + "@joaomoreno/unique-names-generator": "5.0.0", "@vscode/extension-telemetry": "0.4.10", "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 2a1ec856252..8c6c2564fef 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -117,8 +117,11 @@ "config.checkoutType.local": "Local branches", "config.checkoutType.tags": "Tags", "config.checkoutType.remote": "Remote branches", + "config.branchPrefix": "Prefix used when creating a new branch.", + "config.branchRandomNameDictionary": "List of dictionaries used when the branch name that is randomly generated. Supported values: `adjectives`, `animals`, `colors`, `numbers`.", + "config.branchRandomNameEnable": "Controls whether a random name is generated when creating a new branch.", "config.branchValidationRegex": "A regular expression to validate new branch names.", - "config.branchWhitespaceChar": "The character to replace whitespace in new branch names.", + "config.branchWhitespaceChar": "The character to replace whitespace in new branch names, and to separate segments of a randomly generated branch name.", "config.ignoreLegacyWarning": "Ignores the legacy Git warning.", "config.ignoreMissingGitWarning": "Ignores the warning when Git is missing.", "config.ignoreWindowsGit27Warning": "Ignores the warning when Git 2.25 - 2.26 is installed on Windows.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 93cf4df895a..4d1cef177df 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,9 +5,10 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; +import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher } from './api/git'; import { Git, Stash } from './git'; import { Model } from './model'; @@ -1785,34 +1786,100 @@ export class CommandCenter { await this._branch(repository, undefined, true); } - private async promptForBranchName(defaultName?: string, initialValue?: string): Promise { + private generateRandomBranchName(repository: Repository, separator: string): string { const config = workspace.getConfiguration('git'); + const branchRandomNameDictionary = config.get('branchRandomName.dictionary')!; + + const dictionaries: string[][] = []; + for (const dictionary of branchRandomNameDictionary) { + if (dictionary.toLowerCase() === 'adjectives') { + dictionaries.push(adjectives); + } + if (dictionary.toLowerCase() === 'animals') { + dictionaries.push(animals); + } + if (dictionary.toLowerCase() === 'colors') { + dictionaries.push(colors); + } + if (dictionary.toLowerCase() === 'numbers') { + dictionaries.push(NumberDictionary.generate({ length: 3 })); + } + } + + if (dictionaries.length === 0) { + return ''; + } + + // 5 attempts to generate a random branch name + for (let index = 0; index < 5; index++) { + const randomName = uniqueNamesGenerator({ + dictionaries, + length: dictionaries.length, + separator + }); + + // Check for local ref conflict + if (!repository.refs.find(r => r.type === RefType.Head && r.name === randomName)) { + return randomName; + } + } + + return ''; + } + + private async promptForBranchName(repository: Repository, defaultName?: string, initialValue?: string): Promise { + const config = workspace.getConfiguration('git'); + const branchPrefix = config.get('branchPrefix')!; const branchWhitespaceChar = config.get('branchWhitespaceChar')!; const branchValidationRegex = config.get('branchValidationRegex')!; const sanitize = (name: string) => name ? name.trim().replace(/^-+/, '').replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar) : name; - const rawBranchName = defaultName || await window.showInputBox({ - placeHolder: localize('branch name', "Branch name"), - prompt: localize('provide branch name', "Please provide a new branch name"), - value: initialValue, - ignoreFocusOut: true, - validateInput: (name: string) => { - const validateName = new RegExp(branchValidationRegex); - if (validateName.test(sanitize(name))) { - return null; - } + let rawBranchName = defaultName; - return localize('branch name format invalid', "Branch name needs to match regex: {0}", branchValidationRegex); + if (!rawBranchName) { + // Branch name + if (!initialValue) { + const branchRandomNameEnabled = config.get('branchRandomName.enable', false); + initialValue = `${branchPrefix}${branchRandomNameEnabled ? this.generateRandomBranchName(repository, branchWhitespaceChar) : ''}`; } - }); + + // Branch name selection + const initialValueSelection: [number, number] | undefined = + initialValue.startsWith(branchPrefix) ? [branchPrefix.length, initialValue.length] : undefined; + + rawBranchName = await window.showInputBox({ + placeHolder: localize('branch name', "Branch name"), + prompt: localize('provide branch name', "Please provide a new branch name"), + value: initialValue, + valueSelection: initialValueSelection, + ignoreFocusOut: true, + validateInput: (name: string) => { + const validateName = new RegExp(branchValidationRegex); + const sanitizedName = sanitize(name); + if (validateName.test(sanitizedName)) { + // If the sanitized name that we will use is different than what is + // in the input box, show an info message to the user informing them + // the branch name that will be used. + return name === sanitizedName + ? null + : { + message: localize('branch name does not match sanitized', "The new branch will be '{0}'", sanitizedName), + severity: InputBoxValidationSeverity.Info + }; + } + + return localize('branch name format invalid', "Branch name needs to match regex: {0}", branchValidationRegex); + } + }); + } return sanitize(rawBranchName || ''); } private async _branch(repository: Repository, defaultName?: string, from = false): Promise { - const branchName = await this.promptForBranchName(defaultName); + const branchName = await this.promptForBranchName(repository, defaultName); if (!branchName) { return; @@ -1875,7 +1942,7 @@ export class CommandCenter { @command('git.renameBranch', { repository: true }) async renameBranch(repository: Repository): Promise { const currentBranchName = repository.HEAD && repository.HEAD.name; - const branchName = await this.promptForBranchName(undefined, currentBranchName); + const branchName = await this.promptForBranchName(repository, undefined, currentBranchName); if (!branchName) { return; diff --git a/extensions/git/src/protocolHandler.ts b/extensions/git/src/protocolHandler.ts index 2b1a204c603..c0c3f2a6527 100644 --- a/extensions/git/src/protocolHandler.ts +++ b/extensions/git/src/protocolHandler.ts @@ -7,6 +7,8 @@ import { UriHandler, Uri, window, Disposable, commands } from 'vscode'; import { dispose } from './util'; import * as querystring from 'querystring'; +const schemes = new Set(['file', 'git', 'http', 'https', 'ssh']); + export class GitProtocolHandler implements UriHandler { private disposables: Disposable[] = []; @@ -26,9 +28,27 @@ export class GitProtocolHandler implements UriHandler { if (!data.url) { console.warn('Failed to open URI:', uri); + return; } - commands.executeCommand('git.clone', data.url); + if (Array.isArray(data.url) && data.url.length === 0) { + console.warn('Failed to open URI:', uri); + return; + } + + let cloneUri: Uri; + try { + cloneUri = Uri.parse(Array.isArray(data.url) ? data.url[0] : data.url, true); + if (!schemes.has(cloneUri.scheme.toLowerCase())) { + throw new Error('Unsupported scheme.'); + } + } + catch (ex) { + console.warn('Invalid URI:', uri); + return; + } + + commands.executeCommand('git.clone', cloneUri.toString(true)); } dispose(): void { diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 023d16df83a..cfeee5c745e 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@joaomoreno/unique-names-generator@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@joaomoreno/unique-names-generator/-/unique-names-generator-5.0.0.tgz#a67fe66e3d825c929fc97abfdf17fd80a72beab0" + integrity sha512-3kP6z7aoGEoM3tvhTBZioYa1QFkovOU8uxAlVclnZlXivwF/WTE5EcOzvoDdM+jtjJyWvCMDR1Q4RBjDqupD3A== + "@types/byline@4.2.31": version "4.2.31" resolved "https://registry.yarnpkg.com/@types/byline/-/byline-4.2.31.tgz#0e61fcb9c03e047d21c4496554c7116297ab60cd" diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 9e7c97c613f..1af496d0fce 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -423,7 +423,7 @@ "markdown.experimental.validate.referenceLinks": { "type": "string", "scope": "resource", - "description": "%configuration.markdown.experimental.validate.referenceLinks.description%", + "markdownDescription": "%configuration.markdown.experimental.validate.referenceLinks.description%", "default": "warning", "enum": [ "ignore", @@ -434,7 +434,7 @@ "markdown.experimental.validate.headerLinks": { "type": "string", "scope": "resource", - "description": "%configuration.markdown.experimental.validate.headerLinks.description%", + "markdownDescription": "%configuration.markdown.experimental.validate.headerLinks.description%", "default": "warning", "enum": [ "ignore", @@ -445,7 +445,7 @@ "markdown.experimental.validate.fileLinks": { "type": "string", "scope": "resource", - "description": "%configuration.markdown.experimental.validate.fileLinks.description%", + "markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.description%", "default": "warning", "enum": [ "ignore", diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index 5ca5a22d063..fd11cce6531 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -8,11 +8,11 @@ import * as nls from 'vscode-nls'; import { MarkdownEngine } from '../markdownEngine'; import { TableOfContents } from '../tableOfContents'; import { Delayer } from '../util/async'; -import { noopToken } from '../util/cancellation'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; +import { Limiter } from '../util/limiter'; import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents'; -import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider } from './documentLinkProvider'; +import { LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinkProvider'; import { tryFindMdDocumentForLink } from './references'; const localize = nls.loadMessageBundle(); @@ -73,12 +73,58 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf } } +class InflightDiagnosticRequests { + + private readonly inFlightRequests = new Map(); + + public trigger(resource: vscode.Uri, compute: (token: vscode.CancellationToken) => Promise) { + this.cancel(resource); + + const key = this.getResourceKey(resource); + const cts = new vscode.CancellationTokenSource(); + const entry = { cts }; + this.inFlightRequests.set(key, entry); + + compute(cts.token).finally(() => { + if (this.inFlightRequests.get(key) === entry) { + this.inFlightRequests.delete(key); + } + cts.dispose(); + }); + } + + public cancel(resource: vscode.Uri) { + const key = this.getResourceKey(resource); + const existing = this.inFlightRequests.get(key); + if (existing) { + existing.cts.cancel(); + this.inFlightRequests.delete(key); + } + } + + public dispose() { + this.clear(); + } + + public clear() { + for (const { cts } of this.inFlightRequests.values()) { + cts.dispose(); + } + this.inFlightRequests.clear(); + } + + private getResourceKey(resource: vscode.Uri): string { + return resource.toString(); + } +} + export class DiagnosticManager extends Disposable { private readonly collection: vscode.DiagnosticCollection; - private readonly pendingDiagnostics = new Set(); private readonly diagnosticDelayer: Delayer; + private readonly pendingDiagnostics = new Set(); + private readonly inFlightDiagnostics = this._register(new InflightDiagnosticRequests()); constructor( private readonly computer: DiagnosticComputer, @@ -86,7 +132,7 @@ export class DiagnosticManager extends Disposable { ) { super(); - this.diagnosticDelayer = new Delayer(300); + this.diagnosticDelayer = this._register(new Delayer(300)); this.collection = this._register(vscode.languages.createDiagnosticCollection('markdown')); @@ -94,49 +140,61 @@ export class DiagnosticManager extends Disposable { this.rebuild(); })); - const onDocUpdated = (doc: vscode.TextDocument) => { - if (isMarkdownFile(doc)) { - this.pendingDiagnostics.add(doc.uri); - this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics()); - } - }; - this._register(vscode.workspace.onDidOpenTextDocument(doc => { - onDocUpdated(doc); + this.triggerDiagnostics(doc); })); this._register(vscode.workspace.onDidChangeTextDocument(e => { - onDocUpdated(e.document); + this.triggerDiagnostics(e.document); })); this._register(vscode.workspace.onDidCloseTextDocument(doc => { this.pendingDiagnostics.delete(doc.uri); + this.inFlightDiagnostics.cancel(doc.uri); this.collection.delete(doc.uri); })); this.rebuild(); } - private recomputePendingDiagnostics(): void { + public override dispose() { + super.dispose(); + this.pendingDiagnostics.clear(); + } + + public async getDiagnostics(doc: SkinnyTextDocument, token: vscode.CancellationToken): Promise { + const config = this.configuration.getOptions(doc.uri); + if (!config.enabled) { + return []; + } + return this.computer.getDiagnostics(doc, config, token); + } + + private async recomputePendingDiagnostics(): Promise { const pending = [...this.pendingDiagnostics]; this.pendingDiagnostics.clear(); for (const resource of pending) { const doc = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === resource.fsPath); if (doc) { - this.update(doc); + this.inFlightDiagnostics.trigger(doc.uri, async (token) => { + const diagnostics = await this.getDiagnostics(doc, token); + this.collection.set(doc.uri, diagnostics); + }); } } } private async rebuild() { this.collection.clear(); + this.pendingDiagnostics.clear(); + this.inFlightDiagnostics.clear(); const allOpenedTabResources = this.getAllTabResources(); await Promise.all( vscode.workspace.textDocuments .filter(doc => allOpenedTabResources.has(doc.uri.toString()) && isMarkdownFile(doc)) - .map(doc => this.update(doc))); + .map(doc => this.triggerDiagnostics(doc))); } private getAllTabResources() { @@ -151,17 +209,55 @@ export class DiagnosticManager extends Disposable { return openedTabDocs; } - private async update(doc: vscode.TextDocument): Promise { - const diagnostics = await this.getDiagnostics(doc, noopToken); - this.collection.set(doc.uri, diagnostics); + private triggerDiagnostics(doc: vscode.TextDocument) { + this.inFlightDiagnostics.cancel(doc.uri); + + if (isMarkdownFile(doc)) { + this.pendingDiagnostics.add(doc.uri); + this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics()); + } + } +} + +interface FileLinksData { + readonly path: vscode.Uri; + + readonly links: Array<{ + readonly source: MdLinkSource; + readonly fragment: string; + }>; +} + +/** + * Map of file paths to markdown links to that file. + */ +class FileLinkMap { + + private readonly _filesToLinksMap = new Map(); + + constructor(links: Iterable) { + for (const link of links) { + if (link.href.kind !== 'internal') { + continue; + } + + const fileKey = link.href.path.toString(); + const existingFileEntry = this._filesToLinksMap.get(fileKey); + const linkData = { source: link.source, fragment: link.href.fragment }; + if (existingFileEntry) { + existingFileEntry.links.push(linkData); + } else { + this._filesToLinksMap.set(fileKey, { path: link.href.path, links: [linkData] }); + } + } } - public async getDiagnostics(doc: SkinnyTextDocument, token: vscode.CancellationToken): Promise { - const config = this.configuration.getOptions(doc.uri); - if (!config.enabled) { - return []; - } - return this.computer.getDiagnostics(doc, config, token); + public get size(): number { + return this._filesToLinksMap.size; + } + + public entries(): Iterable { + return this._filesToLinksMap.values(); } } @@ -237,52 +333,47 @@ export class DiagnosticComputer { return []; } - const tocs = new Map(); - - // TODO: cache links so we don't recompute duplicate hrefs - // TODO: parallelize - - const diagnostics: vscode.Diagnostic[] = []; - for (const link of links) { - if (token.isCancellationRequested) { - return []; - } - - if (link.href.kind !== 'internal') { - continue; - } - - const hrefDoc = await tryFindMdDocumentForLink(link.href, this.workspaceContents); - if (hrefDoc && hrefDoc.uri.toString() === doc.uri.toString()) { - continue; - } - - if (!hrefDoc && !await this.workspaceContents.pathExists(link.href.path)) { - diagnostics.push( - new vscode.Diagnostic( - link.source.hrefRange, - localize('invalidPathLink', 'File does not exist at path: {0}', (link.href as InternalHref).path.toString(true)), - severity)); - } else if (hrefDoc) { - if (link.href.fragment) { - // validate fragment looks valid - let hrefDocToc = tocs.get(link.href.path.toString()); - if (!hrefDocToc) { - hrefDocToc = await TableOfContents.create(this.engine, hrefDoc); - tocs.set(link.href.path.toString(), hrefDocToc); - } - - if (!hrefDocToc.lookup(link.href.fragment)) { - diagnostics.push( - new vscode.Diagnostic( - link.source.hrefRange, - localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', (link.href as InternalHref).path.fragment), - severity)); - } - } - } + const linkSet = new FileLinkMap(links); + if (linkSet.size === 0) { + return []; } + const limiter = new Limiter(10); + + const diagnostics: vscode.Diagnostic[] = []; + await Promise.all( + Array.from(linkSet.entries()).map(({ path, links }) => { + return limiter.queue(async () => { + if (token.isCancellationRequested) { + return; + } + + const hrefDoc = await tryFindMdDocumentForLink({ kind: 'internal', path: path, fragment: '' }, this.workspaceContents); + if (hrefDoc && hrefDoc.uri.toString() === doc.uri.toString()) { + // We've already validated our own links in `validateOwnHeaderLinks` + return; + } + + if (!hrefDoc && !await this.workspaceContents.pathExists(path)) { + const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.toString(true)); + for (const link of links) { + diagnostics.push(new vscode.Diagnostic(link.source.hrefRange, msg, severity)); + } + } else if (hrefDoc) { + // Validate each of the links to headers in the file + const fragmentLinks = links.filter(x => x.fragment); + if (fragmentLinks.length) { + const toc = await TableOfContents.create(this.engine, hrefDoc); + for (const link of fragmentLinks) { + if (!toc.lookup(link.fragment)) { + const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment); + diagnostics.push(new vscode.Diagnostic(link.source.hrefRange, msg, severity)); + } + } + } + } + }); + })); return diagnostics; } } diff --git a/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts b/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts index 5ee7d1d4b60..b43f564c510 100644 --- a/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts @@ -87,7 +87,7 @@ function getWorkspaceFolder(document: SkinnyTextDocument) { || vscode.workspace.workspaceFolders?.[0]?.uri; } -interface MdLinkSource { +export interface MdLinkSource { readonly text: string; readonly resource: vscode.Uri; readonly hrefRange: vscode.Range; diff --git a/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts index 0fda6175da2..c3fb1f55631 100644 --- a/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts +++ b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts @@ -57,7 +57,7 @@ export function registerDropIntoEditor(selector: vscode.DocumentSelector) { const snippet = new vscode.SnippetString(); uris.forEach((uri, i) => { const mdPath = document.uri.scheme === uri.scheme - ? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath)) + ? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath).replace(/\\/g, '/')) : uri.toString(false); const ext = URI.Utils.extname(uri).toLowerCase(); diff --git a/extensions/markdown-language-features/src/util/async.ts b/extensions/markdown-language-features/src/util/async.ts index 75ccf258f28..a3bca46d63c 100644 --- a/extensions/markdown-language-features/src/util/async.ts +++ b/extensions/markdown-language-features/src/util/async.ts @@ -25,6 +25,10 @@ export class Delayer { this.task = null; } + dispose() { + this.cancelTimeout(); + } + public trigger(task: ITask, delay: number = this.defaultDelay): Promise { this.task = task; if (delay >= 0) { diff --git a/extensions/typescript-basics/package.json b/extensions/typescript-basics/package.json index 2695c089993..cb6c20d8eed 100644 --- a/extensions/typescript-basics/package.json +++ b/extensions/typescript-basics/package.json @@ -51,6 +51,12 @@ "tsconfig-*.json", "jsconfig-*.json" ] + }, + { + "id": "json", + "filenames": [ + "tsconfig.tsbuildinfo" + ] } ], "grammars": [ diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 9df101d76dd..306dd8570eb 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -95,6 +95,7 @@ namespace ServerState { export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient { private readonly pathSeparator: string; + private readonly emptyAuthority = 'ts-nul-authority'; private readonly inMemoryResourcePrefix = '^'; private readonly workspaceState: vscode.Memento; @@ -676,7 +677,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType { return this.inMemoryResourcePrefix + '/' + resource.scheme - + '/' + resource.authority + + '/' + (resource.authority || this.emptyAuthority) + (resource.path.startsWith('/') ? resource.path : '/' + resource.path) + (resource.fragment ? '#' + resource.fragment : ''); } @@ -724,9 +725,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType } if (filepath.startsWith(this.inMemoryResourcePrefix)) { - const parts = filepath.match(/^\^\/([^\/]+)\/(.+)$/); + const parts = filepath.match(/^\^\/([^\/]+)\/([^\/]*)\/(.+)$/); if (parts) { - const resource = vscode.Uri.parse(parts[1] + '://' + parts[2]); + const resource = vscode.Uri.parse(parts[1] + '://' + (parts[2] === this.emptyAuthority ? '' : parts[2]) + '/' + parts[3]); return this.bufferSyncSupport.toVsCodeResource(resource); } } diff --git a/package.json b/package.json index 0c15ad1366b..98d7a68bf3c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.68.0", - "distro": "0cfb470e9eff5c2a4bc6b5a37be833abcb72ed84", + "distro": "ab6870a18e848ce53c0a4b6303132c8fe7d129f2", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/browser/indexedDB.ts b/src/vs/base/browser/indexedDB.ts index 1ca9458eb92..5fba9c0ab4c 100644 --- a/src/vs/base/browser/indexedDB.ts +++ b/src/vs/base/browser/indexedDB.ts @@ -14,6 +14,13 @@ class MissingStoresError extends Error { } } +export class DBClosedError extends Error { + readonly code = 'DBClosed'; + constructor(dbName: string) { + super(`IndexedDB database '${dbName}' is closed.`); + } +} + export class IndexedDB { static async create(name: string, version: number | undefined, stores: string[]): Promise { @@ -109,7 +116,7 @@ export class IndexedDB { runInTransaction(store: string, transactionMode: IDBTransactionMode, dbRequestFn: (store: IDBObjectStore) => IDBRequest): Promise; async runInTransaction(store: string, transactionMode: IDBTransactionMode, dbRequestFn: (store: IDBObjectStore) => IDBRequest | IDBRequest[]): Promise { if (!this.database) { - throw new Error(`IndexedDB database '${this.name}' is not opened.`); + throw new DBClosedError(this.name); } const transaction = this.database.transaction(store, transactionMode); this.pendingTransactions.push(transaction); @@ -128,7 +135,7 @@ export class IndexedDB { async getKeyValues(store: string, isValid: (value: unknown) => value is V): Promise> { if (!this.database) { - throw new Error(`IndexedDB database '${this.name}' is not opened.`); + throw new DBClosedError(this.name); } const transaction = this.database.transaction(store, 'readonly'); this.pendingTransactions.push(transaction); diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 7dbb5c1c71a..4d210d5c16a 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -54,7 +54,7 @@ .monaco-action-bar .action-item.disabled .action-label, .monaco-action-bar .action-item.disabled .action-label::before, .monaco-action-bar .action-item.disabled .action-label:hover { - color: var(--vscode-disabledForeground); + opacity: 0.6; } /* Vertical actions */ diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 2f5dbfcc76d..2c1c167368c 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 6a8982c175b..e4cbed18667 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -748,6 +748,16 @@ export class SerializableGrid extends Grid { return result; } + /** + * Construct a new {@link SerializableGrid} from a grid descriptor. + * + * @param gridDescriptor A grid descriptor in which leaf nodes point to actual views. + * @returns A new {@link SerializableGrid} instance. + */ + static from(gridDescriptor: GridDescriptor, options: IGridOptions = {}): SerializableGrid { + return SerializableGrid.deserialize(createSerializedGrid(gridDescriptor), { fromJSON: view => view }, options); + } + /** * Useful information in order to proportionally restore view sizes * upon the very first layout call. @@ -776,15 +786,21 @@ export class SerializableGrid extends Grid { } } -export type GridNodeDescriptor = { size?: number; groups?: GridNodeDescriptor[] }; -export type GridDescriptor = { orientation: Orientation; groups?: GridNodeDescriptor[] }; +export type GridLeafNodeDescriptor = { size?: number; data?: any }; +export type GridBranchNodeDescriptor = { size?: number; groups: GridNodeDescriptor[] }; +export type GridNodeDescriptor = GridBranchNodeDescriptor | GridLeafNodeDescriptor; +export type GridDescriptor = { orientation: Orientation } & GridBranchNodeDescriptor; -export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor, rootNode: boolean): void { - if (!rootNode && nodeDescriptor.groups && nodeDescriptor.groups.length <= 1) { - nodeDescriptor.groups = undefined; +function isGridBranchNodeDescriptor(nodeDescriptor: GridNodeDescriptor): nodeDescriptor is GridBranchNodeDescriptor { + return !!(nodeDescriptor as GridBranchNodeDescriptor).groups; +} + +export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor, rootNode: boolean): void { + if (!rootNode && (nodeDescriptor as any).groups && (nodeDescriptor as any).groups.length <= 1) { + (nodeDescriptor as any).groups = undefined; } - if (!nodeDescriptor.groups) { + if (!isGridBranchNodeDescriptor(nodeDescriptor)) { return; } @@ -811,11 +827,11 @@ export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor, r } } -function createSerializedNode(nodeDescriptor: GridNodeDescriptor): ISerializedNode { - if (nodeDescriptor.groups) { +function createSerializedNode(nodeDescriptor: GridNodeDescriptor): ISerializedNode { + if (isGridBranchNodeDescriptor(nodeDescriptor)) { return { type: 'branch', data: nodeDescriptor.groups.map(c => createSerializedNode(c)), size: nodeDescriptor.size! }; } else { - return { type: 'leaf', data: null, size: nodeDescriptor.size! }; + return { type: 'leaf', data: nodeDescriptor.data, size: nodeDescriptor.size! }; } } @@ -843,7 +859,7 @@ function getDimensions(node: ISerializedNode, orientation: Orientation): { width * Creates a new JSON object from a {@link GridDescriptor}, which can * be deserialized by {@link SerializableGrid.deserialize}. */ -export function createSerializedGrid(gridDescriptor: GridDescriptor): ISerializedGrid { +export function createSerializedGrid(gridDescriptor: GridDescriptor): ISerializedGrid { sanitizeGridNodeDescriptor(gridDescriptor, true); const root = createSerializedNode(gridDescriptor); diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index fd1116ee4e1..9c782da6f2b 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -541,6 +541,9 @@ export class Codicon implements CSSIcon { public static readonly layoutStatusbar = new Codicon('layout-statusbar', { fontCharacter: '\\ebf5' }); public static readonly layoutMenubar = new Codicon('layout-menubar', { fontCharacter: '\\ebf6' }); public static readonly layoutCentered = new Codicon('layout-centered', { fontCharacter: '\\ebf7' }); + public static readonly layoutSidebarRightOff = new Codicon('layout-sidebar-right-off', { fontCharacter: '\\ec00' }); + public static readonly layoutPanelOff = new Codicon('layout-panel-off', { fontCharacter: '\\ec01' }); + public static readonly layoutSidebarLeftOff = new Codicon('layout-sidebar-left-off', { fontCharacter: '\\ec02' }); public static readonly target = new Codicon('target', { fontCharacter: '\\ebf8' }); public static readonly indent = new Codicon('indent', { fontCharacter: '\\ebf9' }); public static readonly recordSmall = new Codicon('record-small', { fontCharacter: '\\ebfa' }); diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index bd351acce0d..8dcbb829d7b 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -785,26 +785,26 @@ suite('SerializableGrid', function () { }); test('sanitizeGridNodeDescriptor', () => { - const nodeDescriptor = { groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{}, {}] }] }; - const nodeDescriptorCopy = deepClone(nodeDescriptor); + const nodeDescriptor: GridNodeDescriptor = { groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{}, {}] }] }; + const nodeDescriptorCopy = deepClone(nodeDescriptor); sanitizeGridNodeDescriptor(nodeDescriptorCopy, true); assert.deepStrictEqual(nodeDescriptorCopy, { groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{ size: 0.5 }, { size: 0.5 }] }] }); }); test('createSerializedGrid', () => { - const gridDescriptor = { orientation: Orientation.VERTICAL, groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{}, {}] }] }; + const gridDescriptor = { orientation: Orientation.VERTICAL, groups: [{ size: 0.2, data: 'a' }, { size: 0.2, data: 'b' }, { size: 0.6, groups: [{ data: 'c' }, { data: 'd' }] }] }; const serializedGrid = createSerializedGrid(gridDescriptor); assert.deepStrictEqual(serializedGrid, { root: { type: 'branch', size: undefined, data: [ - { type: 'leaf', size: 0.2, data: null }, - { type: 'leaf', size: 0.2, data: null }, + { type: 'leaf', size: 0.2, data: 'a' }, + { type: 'leaf', size: 0.2, data: 'b' }, { type: 'branch', size: 0.6, data: [ - { type: 'leaf', size: 0.5, data: null }, - { type: 'leaf', size: 0.5, data: null } + { type: 'leaf', size: 0.5, data: 'c' }, + { type: 'leaf', size: 0.5, data: 'd' } ] } ] @@ -842,6 +842,30 @@ suite('SerializableGrid', function () { grid.removeView(views[2]); }); + test('from', () => { + const createView = (): ISerializableView => ({ + element: document.createElement('div'), + layout: () => null, + minimumWidth: 0, + maximumWidth: Number.POSITIVE_INFINITY, + minimumHeight: 0, + maximumHeight: Number.POSITIVE_INFINITY, + onDidChange: Event.None, + toJSON: () => ({}) + }); + + const a = createView(); + const b = createView(); + const c = createView(); + const d = createView(); + + const gridDescriptor = { orientation: Orientation.VERTICAL, groups: [{ size: 0.2, data: a }, { size: 0.2, data: b }, { size: 0.6, groups: [{ data: c }, { data: d }] }] }; + const grid = SerializableGrid.from(gridDescriptor); + + assert.deepStrictEqual(nodesToArrays(grid.getViews()), [a, b, [c, d]]); + grid.dispose(); + }); + test('serialize should store visibility and previous size', function () { const view1 = new TestSerializableView('view1', 50, Number.MAX_VALUE, 50, Number.MAX_VALUE); const grid = new SerializableGrid(view1); diff --git a/src/vs/editor/common/core/indentation.ts b/src/vs/editor/common/core/indentation.ts index 0134b97c376..d4cd6d0e71a 100644 --- a/src/vs/editor/common/core/indentation.ts +++ b/src/vs/editor/common/core/indentation.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as strings from 'vs/base/common/strings'; +import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; function _normalizeIndentationFromWhitespace(str: string, indentSize: number, insertSpaces: boolean): string { let spacesCnt = 0; for (let i = 0; i < str.length; i++) { if (str.charAt(i) === '\t') { - spacesCnt += indentSize; + spacesCnt = CursorColumns.nextIndentTabStop(spacesCnt, indentSize); } else { spacesCnt++; } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 8aa71a280d5..09011aebe61 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -273,6 +273,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati * It is not globally unique in order to limit it to one character. */ private readonly _instanceId: string; + private _deltaDecorationCallCnt: number = 0; private _lastDecorationId: number; private _decorations: { [decorationId: string]: IntervalNode }; private _decorationsTree: DecorationsTrees; @@ -1613,10 +1614,16 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } try { + this._deltaDecorationCallCnt++; + if (this._deltaDecorationCallCnt > 1) { + console.warn(`Invoking deltaDecorations recursively could lead to leaking decorations.`); + onUnexpectedError(new Error(`Invoking deltaDecorations recursively could lead to leaking decorations.`)); + } this._onDidChangeDecorations.beginDeferredEmit(); return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } finally { this._onDidChangeDecorations.endDeferredEmit(); + this._deltaDecorationCallCnt--; } } diff --git a/src/vs/editor/contrib/multicursor/browser/multicursor.ts b/src/vs/editor/contrib/multicursor/browser/multicursor.ts index 7c658f0fa9b..27c7f171adf 100644 --- a/src/vs/editor/contrib/multicursor/browser/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/browser/multicursor.ts @@ -986,7 +986,9 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut this.state = newState; if (!this.state) { - this.decorations = this.editor.deltaDecorations(this.decorations, []); + this.editor.changeDecorations((accessor) => { + this.decorations = accessor.deltaDecorations(this.decorations, []); + }); return; } @@ -1042,7 +1044,9 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut }; }); - this.decorations = this.editor.deltaDecorations(this.decorations, decorations); + this.editor.changeDecorations((accessor) => { + this.decorations = accessor.deltaDecorations(this.decorations, decorations); + }); } private static readonly _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts index 0c2f31c225b..7ce812e34ee 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts @@ -63,7 +63,7 @@ suite('SnippetSession', function () { assertNormalized(new Position(1, 1), 'foo\rbar', 'foo\nbar'); assertNormalized(new Position(1, 1), 'foo\rbar', 'foo\nbar'); assertNormalized(new Position(2, 5), 'foo\r\tbar', 'foo\n bar'); - assertNormalized(new Position(2, 3), 'foo\r\tbar', 'foo\n bar'); + assertNormalized(new Position(2, 3), 'foo\r\tbar', 'foo\n bar'); assertNormalized(new Position(2, 5), 'foo\r\tbar\nfoo', 'foo\n bar\n foo'); //Indentation issue with choice elements that span multiple lines #46266 diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 04e0f6fd2ca..0cad477dd6e 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -13,7 +13,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { DiffNavigator, IDiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; import { ApplyUpdateResult, ConfigurationChangedEvent, EditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; -import { EditorType } from 'vs/editor/common/editorCommon'; +import { EditorType, IDiffEditor } from 'vs/editor/common/editorCommon'; import { FindMatch, ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; import * as languages from 'vs/editor/common/languages'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; @@ -52,6 +52,33 @@ export function onDidCreateEditor(listener: (codeEditor: ICodeEditor) => void): }); } +/** + * Emitted when an diff editor is created. + * @event + */ +export function onDidCreateDiffEditor(listener: (diffEditor: IDiffEditor) => void): IDisposable { + const codeEditorService = StandaloneServices.get(ICodeEditorService); + return codeEditorService.onDiffEditorAdd((editor) => { + listener(editor); + }); +} + +/** + * Get all the created editors. + */ +export function getEditors(): readonly ICodeEditor[] { + const codeEditorService = StandaloneServices.get(ICodeEditorService); + return codeEditorService.listCodeEditors(); +} + +/** + * Get all the created diff editors. + */ +export function getDiffEditors(): readonly IDiffEditor[] { + const codeEditorService = StandaloneServices.get(ICodeEditorService); + return codeEditorService.listDiffEditors(); +} + /** * Create a new diff editor under `domElement`. * `domElement` should be empty (not contain other dom nodes). @@ -283,7 +310,10 @@ export function createMonacoEditorAPI(): typeof monaco.editor { return { // methods create: create, + getEditors: getEditors, + getDiffEditors: getDiffEditors, onDidCreateEditor: onDidCreateEditor, + onDidCreateDiffEditor: onDidCreateDiffEditor, createDiffEditor: createDiffEditor, createDiffNavigator: createDiffNavigator, diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 81a9950b5e4..ef4d4407aa9 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -834,6 +834,10 @@ class StandaloneUriLabelService implements ILabelService { throw new Error('Not implemented'); } + public registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable { + return this.registerFormatter(formatter); + } + public getHostLabel(): string { return ''; } diff --git a/src/vs/editor/standalone/browser/standaloneThemeService.ts b/src/vs/editor/standalone/browser/standaloneThemeService.ts index e5230a0a757..cafca9f55a7 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeService.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeService.ts @@ -16,7 +16,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { asCssVariableName, ColorIdentifier, Extensions, IColorRegistry } from 'vs/platform/theme/common/colorRegistry'; import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IProductIconTheme, IThemingRegistry, ITokenStyle } from 'vs/platform/theme/common/themeService'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { ColorScheme, isDark } from 'vs/platform/theme/common/theme'; import { getIconsStyleSheet, UnthemedProductIconTheme } from 'vs/platform/theme/browser/iconsStyleSheet'; const VS_THEME_NAME = 'vs'; @@ -242,6 +242,7 @@ export class StandaloneThemeService extends Disposable implements IStandaloneThe this._knownThemes.set(VS_THEME_NAME, newBuiltInTheme(VS_THEME_NAME)); this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME)); this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME)); + this._knownThemes.set(HC_LIGHT_THEME_NAME, newBuiltInTheme(HC_LIGHT_THEME_NAME)); const iconsStyleSheet = getIconsStyleSheet(this); @@ -339,10 +340,18 @@ export class StandaloneThemeService extends Disposable implements IStandaloneThe this._updateActualTheme(); } + private getHighContrastTheme() { + if (isDark(this._desiredTheme.type)) { + return HC_BLACK_THEME_NAME; + } else { + return HC_LIGHT_THEME_NAME; + } + } + private _updateActualTheme(): void { const theme = ( this._autoDetectHighContrast && window.matchMedia(`(forced-colors: active)`).matches - ? this._knownThemes.get(HC_BLACK_THEME_NAME)! + ? this._knownThemes.get(this.getHighContrastTheme())! : this._desiredTheme ); if (this._theme === theme) { diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index a2d9d0f3cf9..745e7388b60 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -3225,6 +3225,33 @@ suite('Editor Controller', () => { }); }); + test('issue #148256: Pressing Enter creates line with bad indent with insertSpaces: true', () => { + usingCursor({ + text: [ + ' \t' + ], + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 4, false); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), ' \t\n '); + }); + }); + + test('issue #148256: Pressing Enter creates line with bad indent with insertSpaces: false', () => { + usingCursor({ + text: [ + ' \t' + ] + }, (editor, model, viewModel) => { + model.updateOptions({ + insertSpaces: false + }); + moveTo(editor, viewModel, 1, 4, false); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), ' \t\n\t'); + }); + }); + test('removeAutoWhitespace off', () => { usingCursor({ text: [ @@ -4636,8 +4663,8 @@ suite('Editor Controller', () => { ['(', ')'] ], indentationRules: { - increaseIndentPattern: new RegExp('^.*\\{[^}\"\\\']*$|^.*\\([^\\)\"\\\']*$|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$|^\\s*\\{\\}$'), - decreaseIndentPattern: new RegExp('^\\s*(\\s*/[*].*[*]/\\s*)*\\}|^\\s*(\\s*/[*].*[*]/\\s*)*\\)|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$'), + increaseIndentPattern: new RegExp("({+(?=([^\"]*\"[^\"]*\")*[^\"}]*$))|(\\[+(?=([^\"]*\"[^\"]*\")*[^\"\\]]*$))"), + decreaseIndentPattern: new RegExp("^\\s*[}\\]],?\\s*$") } })); diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index b1ebbf29c20..3cf9ae5c028 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -915,10 +915,11 @@ suite('Editor Model - TextModel', () => { assert.strictEqual(model.normalizeIndentation(' '), ' '); assert.strictEqual(model.normalizeIndentation(' '), ' '); assert.strictEqual(model.normalizeIndentation(''), ''); - assert.strictEqual(model.normalizeIndentation(' \t '), '\t\t'); - assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); - assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); - assert.strictEqual(model.normalizeIndentation(' \t'), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t\t'); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t '), '\t '); + assert.strictEqual(model.normalizeIndentation(' \t'), '\t'); assert.strictEqual(model.normalizeIndentation('\ta'), '\ta'); assert.strictEqual(model.normalizeIndentation(' a'), '\ta'); @@ -926,10 +927,11 @@ suite('Editor Model - TextModel', () => { assert.strictEqual(model.normalizeIndentation(' a'), ' a'); assert.strictEqual(model.normalizeIndentation(' a'), ' a'); assert.strictEqual(model.normalizeIndentation('a'), 'a'); - assert.strictEqual(model.normalizeIndentation(' \t a'), '\t\ta'); - assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); - assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); - assert.strictEqual(model.normalizeIndentation(' \ta'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t\ta'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), '\t a'); + assert.strictEqual(model.normalizeIndentation(' \ta'), '\ta'); model.dispose(); }); @@ -943,10 +945,11 @@ suite('Editor Model - TextModel', () => { assert.strictEqual(model.normalizeIndentation(' a'), ' a'); assert.strictEqual(model.normalizeIndentation(' a'), ' a'); assert.strictEqual(model.normalizeIndentation('a'), 'a'); - assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); - assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); - assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); - assert.strictEqual(model.normalizeIndentation(' \ta'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \t a'), ' a'); + assert.strictEqual(model.normalizeIndentation(' \ta'), ' a'); model.dispose(); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 86ea824eef8..83086200c6f 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -890,6 +890,22 @@ declare namespace monaco.editor { */ export function onDidCreateEditor(listener: (codeEditor: ICodeEditor) => void): IDisposable; + /** + * Emitted when an diff editor is created. + * @event + */ + export function onDidCreateDiffEditor(listener: (diffEditor: IDiffEditor) => void): IDisposable; + + /** + * Get all the created editors. + */ + export function getEditors(): readonly ICodeEditor[]; + + /** + * Get all the created diff editors. + */ + export function getDiffEditors(): readonly IDiffEditor[]; + /** * Create a new diff editor under `domElement`. * `domElement` should be empty (not contain other dom nodes). diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index d6fecacc967..e911ed45923 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -202,11 +202,15 @@ export interface IConfigurationDefaults { export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & { defaultDefaultValue?: any; - source?: IExtensionInfo; - defaultValueSource?: IExtensionInfo | string; + source?: IExtensionInfo; // Source of the Property + defaultValueSource?: IExtensionInfo | string; // Source of the Default Value }; -export type IConfigurationDefaultOverride = { value: any; source?: IExtensionInfo | string }; +export type IConfigurationDefaultOverride = { + readonly value: any; + readonly source?: IExtensionInfo | string; // Source of the default override + readonly valuesSources?: Map; // Source of each value in default language overrides +}; export const allSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; export const applicationSettings: { properties: IStringDictionary; patternProperties: IStringDictionary } = { properties: {}, patternProperties: {} }; @@ -291,8 +295,15 @@ class ConfigurationRegistry implements IConfigurationRegistry { properties.push(key); if (OVERRIDE_PROPERTY_REGEX.test(key)) { - const defaultValue = { ...(this.configurationDefaultsOverrides.get(key)?.value || {}), ...overrides[key] }; - this.configurationDefaultsOverrides.set(key, { source, value: defaultValue }); + const configurationDefaultOverride = this.configurationDefaultsOverrides.get(key); + const valuesSources = configurationDefaultOverride?.valuesSources ?? new Map(); + if (source) { + for (const configuration of Object.keys(overrides[key])) { + valuesSources.set(configuration, source); + } + } + const defaultValue = { ...(configurationDefaultOverride?.value || {}), ...overrides[key] }; + this.configurationDefaultsOverrides.set(key, { source, value: defaultValue, valuesSources }); const plainKey = getLanguageTagSettingPlainKey(key); const property: IRegisteredConfigurationPropertySchema = { type: 'object', @@ -301,6 +312,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { $ref: resourceLanguageSettingsSchemaId, defaultDefaultValue: defaultValue, source: types.isString(source) ? undefined : source, + defaultValueSource: source }; overrideIdentifiers.push(...overrideIdentifiersFromKey(key)); this.configurationProperties[key] = property; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index a3a7c9b84cd..eba50f040e4 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -248,6 +248,7 @@ type GalleryServiceQueryClassification = { readonly errorCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; readonly count?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; readonly source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + readonly searchTextLength?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; type QueryTelemetryData = { @@ -257,6 +258,7 @@ type QueryTelemetryData = { readonly sortOrder: string; readonly pageNumber: string; readonly source?: string; + readonly searchTextLength?: number; }; type GalleryServiceQueryEvent = QueryTelemetryData & { @@ -347,7 +349,8 @@ class Query { sortBy: String(this.sortBy), sortOrder: String(this.sortOrder), pageNumber: String(this.pageNumber), - source: this.state.source + source: this.state.source, + searchTextLength: this.searchText.length }; } } diff --git a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts index c86fa38dea8..ee053cda77a 100644 --- a/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts +++ b/src/vs/platform/files/browser/indexedDBFileSystemProvider.ts @@ -13,7 +13,19 @@ import { isString } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { createFileSystemProviderError, FileChangeType, IFileDeleteOptions, IFileOverwriteOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; -import { IndexedDB } from 'vs/base/browser/indexedDB'; +import { DBClosedError, IndexedDB } from 'vs/base/browser/indexedDB'; + +export type IndexedDBFileSystemProviderErrorDataClassification = { + readonly scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + readonly operation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + readonly code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; +}; + +export type IndexedDBFileSystemProviderErrorData = { + readonly scheme: string; + readonly operation: string; + readonly code: string; +}; // Standard FS Errors (expected to be thrown in production when invalid FS operations are requested) const ERR_FILE_NOT_FOUND = createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); @@ -21,7 +33,7 @@ const ERR_FILE_IS_DIR = createFileSystemProviderError(localize('fileIsDirectory' const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty', "Directory is not empty"), FileSystemProviderErrorCode.Unknown); -// Arbitrary Internal Errors (should never be thrown in production) +// Arbitrary Internal Errors const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occurred in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown); type DirEntry = [string, FileType]; @@ -45,7 +57,6 @@ class IndexedDBFileSystemNode { this.type = entry.type; } - read(path: string): IndexedDBFileSystemEntry | undefined { return this.doRead(path.split('/').filter(p => p.length)); } @@ -237,12 +248,15 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst private readonly _onDidChangeFile = this._register(new Emitter()); readonly onDidChangeFile: Event = this._onDidChangeFile.event; + private readonly _onReportError = this._register(new Emitter()); + readonly onReportError = this._onReportError.event; + private readonly versions = new Map(); private cachedFiletree: Promise | undefined; private writeManyThrottler: Throttler; - constructor(scheme: string, private indexedDB: IndexedDB, private readonly store: string, watchCrossWindowChanges: boolean) { + constructor(readonly scheme: string, private indexedDB: IndexedDB, private readonly store: string, watchCrossWindowChanges: boolean) { super(); this.writeManyThrottler = new Throttler(); @@ -291,46 +305,61 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst } async readdir(resource: URI): Promise { - const entry = (await this.getFiletree()).read(resource.path); - if (!entry) { - // Dirs aren't saved to disk, so empty dirs will be lost on reload. - // Thus we have two options for what happens when you try to read a dir and nothing is found: - // - Throw FileSystemProviderErrorCode.FileNotFound - // - Return [] - // We choose to return [] as creating a dir then reading it (even after reload) should not throw an error. - return []; - } - if (entry.type !== FileType.Directory) { - throw ERR_FILE_NOT_DIR; - } - else { - return [...entry.children.entries()].map(([name, node]) => [name, node.type]); + try { + const entry = (await this.getFiletree()).read(resource.path); + if (!entry) { + // Dirs aren't saved to disk, so empty dirs will be lost on reload. + // Thus we have two options for what happens when you try to read a dir and nothing is found: + // - Throw FileSystemProviderErrorCode.FileNotFound + // - Return [] + // We choose to return [] as creating a dir then reading it (even after reload) should not throw an error. + return []; + } + if (entry.type !== FileType.Directory) { + throw ERR_FILE_NOT_DIR; + } + else { + return [...entry.children.entries()].map(([name, node]) => [name, node.type]); + } + } catch (error) { + this.reportError('readDir', error); + throw error; } } async readFile(resource: URI): Promise { - const result = await this.indexedDB.runInTransaction(this.store, 'readonly', objectStore => objectStore.get(resource.path)); - if (result === undefined) { - throw ERR_FILE_NOT_FOUND; - } - const buffer = result instanceof Uint8Array ? result : isString(result) ? VSBuffer.fromString(result).buffer : undefined; - if (buffer === undefined) { - throw ERR_UNKNOWN_INTERNAL(`IndexedDB entry at "${resource.path}" in unexpected format`); - } + try { + const result = await this.indexedDB.runInTransaction(this.store, 'readonly', objectStore => objectStore.get(resource.path)); + if (result === undefined) { + throw ERR_FILE_NOT_FOUND; + } + const buffer = result instanceof Uint8Array ? result : isString(result) ? VSBuffer.fromString(result).buffer : undefined; + if (buffer === undefined) { + throw ERR_UNKNOWN_INTERNAL(`IndexedDB entry at "${resource.path}" in unexpected format`); + } - // update cache - const fileTree = await this.getFiletree(); - fileTree.add(resource.path, { type: 'file', size: buffer.byteLength }); + // update cache + const fileTree = await this.getFiletree(); + fileTree.add(resource.path, { type: 'file', size: buffer.byteLength }); - return buffer; + return buffer; + } catch (error) { + this.reportError('readFile', error); + throw error; + } } async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { - const existing = await this.stat(resource).catch(() => undefined); - if (existing?.type === FileType.Directory) { - throw ERR_FILE_IS_DIR; + try { + const existing = await this.stat(resource).catch(() => undefined); + if (existing?.type === FileType.Directory) { + throw ERR_FILE_IS_DIR; + } + await this.bulkWrite([[resource, content]]); + } catch (error) { + this.reportError('writeFile', error); + throw error; } - await this.bulkWrite([[resource, content]]); } async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { @@ -482,4 +511,8 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst await this.indexedDB.runInTransaction(this.store, 'readwrite', objectStore => objectStore.clear()); } + private reportError(operation: string, error: Error): void { + this._onReportError.fire({ scheme: this.scheme, operation, code: error instanceof FileSystemProviderError || error instanceof DBClosedError ? error.code : 'unknown' }); + } + } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index a89fb9d0c48..e5b6b8a1045 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -30,6 +30,13 @@ export interface ILabelService { registerFormatter(formatter: ResourceLabelFormatter): IDisposable; onDidChangeFormatters: Event; + + /** + * Registers a formatter that's cached for the machine beyond the lifecycle + * of the current window. Disposing the formatter _will not_ remove it from + * the cache. + */ + registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable; } export interface IFormatterChangeEvent { diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 221d81e7ba6..020de098700 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -93,6 +93,7 @@ export interface ICommandDetectionCapability { readonly onCommandStarted: Event; readonly onCommandFinished: Event; readonly onCommandInvalidated: Event; + readonly onCurrentCommandInvalidated: Event; setCwd(value: string): void; setIsWindowsPty(value: boolean): void; /** diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 01f37b2386e..f9a14291b1f 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { timeout } from 'vs/base/common/async'; +import { debounce } from 'vs/base/common/decorators'; import { Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; @@ -35,6 +36,12 @@ export interface ICurrentPartialCommand { continuations?: { marker: IMarker; end: number }[]; command?: string; + + /** + * Something invalidated the command before it finished, this will prevent the onCommandFinished + * event from firing. + */ + isInvalid?: boolean; } interface ITerminalDimensions { @@ -42,11 +49,6 @@ interface ITerminalDimensions { rows: number; } -interface IBeforeCommandFinishedEvent { - command: ITerminalCommand; - veto?: boolean; -} - export class CommandDetectionCapability implements ICommandDetectionCapability { readonly type = TerminalCapability.CommandDetection; @@ -72,12 +74,14 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { private readonly _onCommandStarted = new Emitter(); readonly onCommandStarted = this._onCommandStarted.event; - private readonly _onBeforeCommandFinished = new Emitter(); + private readonly _onBeforeCommandFinished = new Emitter(); readonly onBeforeCommandFinished = this._onBeforeCommandFinished.event; private readonly _onCommandFinished = new Emitter(); readonly onCommandFinished = this._onCommandFinished.event; private readonly _onCommandInvalidated = new Emitter(); readonly onCommandInvalidated = this._onCommandInvalidated.event; + private readonly _onCurrentCommandInvalidated = new Emitter(); + readonly onCurrentCommandInvalidated = this._onCurrentCommandInvalidated.event; constructor( private readonly _terminal: Terminal, @@ -88,6 +92,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { rows: this._terminal.rows }; this._terminal.onResize(e => this._handleResize(e)); + this._terminal.onCursorMove(() => this._handleCursorMove()); this._setupClearListeners(); } @@ -99,6 +104,27 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { this._dimensions.rows = e.rows; } + @debounce(500) + private _handleCursorMove() { + // Early versions of conpty do not have real support for an alt buffer, in addition certain + // commands such as tsc watch will write to the top of the normal buffer. The following + // checks when the cursor has moved while the normal buffer is empty and if it is above the + // current command, all decorations within the viewport will be invalidated. + // + // This function is debounced so that the cursor is only checked when it is stable so + // conpty's screen reprinting will not trigger decoration clearing. + // + // This is mostly a workaround for Windows but applies to all OS' because of the tsc watch + // case. + if (this._terminal.buffer.active === this._terminal.buffer.normal && this._currentCommand.commandStartMarker) { + if (this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY < this._currentCommand.commandStartMarker.line) { + this._clearCommandsInViewport(); + this._currentCommand.isInvalid = true; + this._onCurrentCommandInvalidated.fire(); + } + } + } + private _setupClearListeners() { // Setup listeners for when clear is run in the shell. Since we don't know immediately if // this is a Windows pty, listen to both routes and do the Windows check inside them @@ -106,12 +132,12 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { // For a Windows backend we cannot listen to CSI J, instead we assume running clear or // cls will clear all commands in the viewport. This is not perfect but it's right most // of the time. - this.onBeforeCommandFinished(event => { + this.onBeforeCommandFinished(command => { if (this._isWindowsPty) { - if (event.command.command.trim().toLowerCase() === 'clear' || event.command.command.trim().toLowerCase() === 'cls') { + if (command.command.trim().toLowerCase() === 'clear' || command.command.trim().toLowerCase() === 'cls') { this._clearCommandsInViewport(); - // Prevent current command to get to command finished listeners - event.veto = true; + this._currentCommand.isInvalid = true; + this._onCurrentCommandInvalidated.fire(); } } }); @@ -403,10 +429,8 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { this._commands.push(newCommand); this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); - // Fire the command finished event provided there is no veto - const beforeEvent: IBeforeCommandFinishedEvent = { command: newCommand }; - this._onBeforeCommandFinished.fire(beforeEvent); - if (!beforeEvent.veto) { + this._onBeforeCommandFinished.fire(newCommand); + if (!this._currentCommand.isInvalid) { this._onCommandFinished.fire(newCommand); } } diff --git a/src/vs/platform/theme/common/theme.ts b/src/vs/platform/theme/common/theme.ts index eabe6ecf497..856b2f12eb5 100644 --- a/src/vs/platform/theme/common/theme.ts +++ b/src/vs/platform/theme/common/theme.ts @@ -16,3 +16,7 @@ export enum ColorScheme { export function isHighContrast(scheme: ColorScheme): boolean { return scheme === ColorScheme.HIGH_CONTRAST_DARK || scheme === ColorScheme.HIGH_CONTRAST_LIGHT; } + +export function isDark(scheme: ColorScheme): boolean { + return scheme === ColorScheme.DARK || scheme === ColorScheme.HIGH_CONTRAST_DARK; +} diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index c50a1195d3e..d80ee843b05 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -253,7 +253,7 @@ export abstract class AbstractTunnelService implements ITunnelService { this._tunnels.clear(); } - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, privacy: string = TunnelPrivacyId.Private, protocol?: string): Promise | undefined { + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, privacy?: string, protocol?: string): Promise | undefined { this.logService.trace(`ForwardedPorts: (TunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`); if (!addressProvider) { return undefined; @@ -380,14 +380,14 @@ export abstract class AbstractTunnelService implements ITunnelService { return !!extractLocalHostUriMetaDataForPortMapping(uri); } - protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise | undefined; + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined; - protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise | undefined { + protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`); const preferredLocalPort = localPort === undefined ? remotePort : localPort; const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; - const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, privacy, public: privacy !== TunnelPrivacyId.Private, protocol }; + const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, privacy, public: privacy ? (privacy !== TunnelPrivacyId.Private) : undefined, protocol }; const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo); this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created by provider.'); if (tunnel) { diff --git a/src/vs/platform/tunnel/node/tunnelService.ts b/src/vs/platform/tunnel/node/tunnelService.ts index 49f6eec7ff7..178247f60c5 100644 --- a/src/vs/platform/tunnel/node/tunnelService.ts +++ b/src/vs/platform/tunnel/node/tunnelService.ts @@ -167,7 +167,7 @@ export class BaseTunnelService extends AbstractTunnelService { return (!settingValue || settingValue === 'localhost') ? '127.0.0.1' : '0.0.0.0'; } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise | undefined { + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index ff692c644dc..3ae68711aa1 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -755,6 +755,8 @@ export async function createServer(address: string | net.AddressInfo | null, arg const telemetryService = accessor.get(ITelemetryService); type ServerStartClassification = { + owner: 'alexdima'; + comment: 'The server has started up'; startTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; startedTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; codeLoadedTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; diff --git a/src/vs/workbench/api/browser/mainThreadLabelService.ts b/src/vs/workbench/api/browser/mainThreadLabelService.ts index 4c3f813a56e..b284ef951c4 100644 --- a/src/vs/workbench/api/browser/mainThreadLabelService.ts +++ b/src/vs/workbench/api/browser/mainThreadLabelService.ts @@ -21,7 +21,7 @@ export class MainThreadLabelService implements MainThreadLabelServiceShape { $registerResourceLabelFormatter(handle: number, formatter: ResourceLabelFormatter): void { // Dynamicily registered formatters should have priority over those contributed via package.json formatter.priority = true; - const disposable = this._labelService.registerFormatter(formatter); + const disposable = this._labelService.registerCachedFormatter(formatter); this._resourceLabelFormatters.set(handle, disposable); } diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 8440c0f8e12..8ace4020a72 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -34,7 +34,9 @@ const menubarIcon = registerIcon('menuBar', Codicon.layoutMenubar, localize('men const activityBarLeftIcon = registerIcon('activity-bar-left', Codicon.layoutActivitybarLeft, localize('activityBarLeft', "Represents the activity bar in the left position")); const activityBarRightIcon = registerIcon('activity-bar-right', Codicon.layoutActivitybarRight, localize('activityBarRight', "Represents the activity bar in the right position")); const panelLeftIcon = registerIcon('panel-left', Codicon.layoutSidebarLeft, localize('panelLeft', "Represents a side bar in the left position")); +const panelLeftOffIcon = registerIcon('panel-left-off', Codicon.layoutSidebarLeftOff, localize('panelLeftOff', "Represents a side bar in the left position toggled off")); const panelRightIcon = registerIcon('panel-right', Codicon.layoutSidebarRight, localize('panelRight', "Represents side bar in the right position")); +const panelRightOffIcon = registerIcon('panel-right-off', Codicon.layoutSidebarRightOff, localize('panelRightOff', "Represents side bar in the right position toggled off")); const panelIcon = registerIcon('panel-bottom', Codicon.layoutPanel, localize('panelBottom', "Represents the bottom panel")); const statusBarIcon = registerIcon('statusBar', Codicon.layoutStatusbar, localize('statusBarIcon', "Represents the status bar")); @@ -427,8 +429,8 @@ MenuRegistry.appendMenuItems([ command: { id: ToggleSidebarVisibilityAction.ID, title: localize('toggleSideBar', "Toggle Primary Side Bar"), - icon: panelLeftIcon, - toggled: SideBarVisibleContext + icon: panelLeftOffIcon, + toggled: { condition: SideBarVisibleContext, icon: panelLeftIcon } }, when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), ContextKeyExpr.equals('config.workbench.sideBar.location', 'left')), order: 0 @@ -440,8 +442,8 @@ MenuRegistry.appendMenuItems([ command: { id: ToggleSidebarVisibilityAction.ID, title: localize('toggleSideBar', "Toggle Primary Side Bar"), - icon: panelRightIcon, - toggled: SideBarVisibleContext + icon: panelRightOffIcon, + toggled: { condition: SideBarVisibleContext, icon: panelRightIcon } }, when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), ContextKeyExpr.equals('config.workbench.sideBar.location', 'right')), order: 2 diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts index d51d77237fb..65fa0d523c4 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts @@ -17,8 +17,10 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -const auxiliaryBarRightIcon = registerIcon('auxiliarybar-right-layout-icon', Codicon.layoutSidebarRight, localize('toggleAuxiliaryIconRight', 'Icon to toggle the auxiliary bar in its right position.')); +const auxiliaryBarRightIcon = registerIcon('auxiliarybar-right-layout-icon', Codicon.layoutSidebarRight, localize('toggleAuxiliaryIconRight', 'Icon to toggle the auxiliary bar off in its right position.')); +const auxiliaryBarRightOffIcon = registerIcon('auxiliarybar-right-off-layout-icon', Codicon.layoutSidebarRightOff, localize('toggleAuxiliaryIconRightOn', 'Icon to toggle the auxiliary bar on in its right position.')); const auxiliaryBarLeftIcon = registerIcon('auxiliarybar-left-layout-icon', Codicon.layoutSidebarLeft, localize('toggleAuxiliaryIconLeft', 'Icon to toggle the auxiliary bar in its left position.')); +const auxiliaryBarLeftOffIcon = registerIcon('auxiliarybar-left-off-layout-icon', Codicon.layoutSidebarLeftOff, localize('toggleAuxiliaryIconLeftOn', 'Icon to toggle the auxiliary bar on in its left position.')); export class ToggleAuxiliaryBarAction extends Action { @@ -87,8 +89,8 @@ MenuRegistry.appendMenuItems([ command: { id: ToggleAuxiliaryBarAction.ID, title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"), - toggled: AuxiliaryBarVisibleContext, - icon: auxiliaryBarLeftIcon, + toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarLeftIcon }, + icon: auxiliaryBarLeftOffIcon, }, when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), ContextKeyExpr.equals('config.workbench.sideBar.location', 'right')), order: 0 @@ -101,8 +103,8 @@ MenuRegistry.appendMenuItems([ command: { id: ToggleAuxiliaryBarAction.ID, title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"), - toggled: AuxiliaryBarVisibleContext, - icon: auxiliaryBarRightIcon, + toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarRightIcon }, + icon: auxiliaryBarRightOffIcon, }, when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), ContextKeyExpr.equals('config.workbench.sideBar.location', 'left')), order: 2 diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 2ed2341a1b8..8663a928675 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -26,7 +26,8 @@ import { ICommandActionTitle } from 'vs/platform/action/common/action'; const maximizeIcon = registerIcon('panel-maximize', Codicon.chevronUp, localize('maximizeIcon', 'Icon to maximize a panel.')); const restoreIcon = registerIcon('panel-restore', Codicon.chevronDown, localize('restoreIcon', 'Icon to restore a panel.')); const closeIcon = registerIcon('panel-close', Codicon.close, localize('closeIcon', 'Icon to close a panel.')); -const panelIcon = registerIcon('panel-layout-icon', Codicon.layoutPanel, localize('togglePanelIcon', 'Icon to toggle the panel.')); +const panelIcon = registerIcon('panel-layout-icon', Codicon.layoutPanel, localize('togglePanelOffIcon', 'Icon to toggle the panel off when it is on.')); +const panelOffIcon = registerIcon('panel-layout-icon-off', Codicon.layoutPanelOff, localize('togglePanelOnIcon', 'Icon to toggle the panel on when it is off.')); export class TogglePanelAction extends Action { @@ -444,8 +445,8 @@ MenuRegistry.appendMenuItems([ command: { id: TogglePanelAction.ID, title: localize('togglePanel', "Toggle Panel"), - icon: panelIcon, - toggled: PanelVisibleContext + icon: panelOffIcon, + toggled: { condition: PanelVisibleContext, icon: panelIcon } }, when: ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.layoutControl.type', 'toggles'), ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both')), order: 1 diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index ec28dd8769c..5ffa3a07bbc 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -378,9 +378,11 @@ export abstract class BasePanelPart extends CompositePart impleme contextKey.set(true); this.compositeBar.addComposite({ id: viewContainer.id, name: viewContainer.title, order: viewContainer.order, requestedIndex: viewContainer.requestedIndex }); - const activeComposite = this.getActiveComposite(); - if (activeComposite === undefined || activeComposite.getId() === viewContainer.id) { - this.compositeBar.activateComposite(viewContainer.id); + if (this.layoutService.isVisible(this.partId)) { + const activeComposite = this.getActiveComposite(); + if (activeComposite === undefined || activeComposite.getId() === viewContainer.id) { + this.compositeBar.activateComposite(viewContainer.id); + } } this.layoutCompositeBar(); diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 063481db5f9..34a5e181dd9 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -42,7 +42,7 @@ import { getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/w import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IndexedDBFileSystemProvider } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; +import { IndexedDBFileSystemProviderErrorDataClassification, IndexedDBFileSystemProvider, IndexedDBFileSystemProviderErrorData } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; import { BrowserRequestService } from 'vs/workbench/services/request/browser/requestService'; import { IRequestService } from 'vs/platform/request/common/request'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; @@ -75,6 +75,7 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; export class BrowserMain extends Disposable { private readonly onWillShutdownDisposables = this._register(new DisposableStore()); + private readonly indexedDBFileSystemProviders: IndexedDBFileSystemProvider[] = []; constructor( private readonly domElement: HTMLElement, @@ -111,6 +112,13 @@ export class BrowserMain extends Disposable { // Logging services.logService.trace('workbench#open with configuration', safeStringify(this.configuration)); + instantiationService.invokeFunction(accessor => { + const telemetryService = accessor.get(ITelemetryService); + for (const indexedDbFileSystemProvider of this.indexedDBFileSystemProviders) { + this._register(indexedDbFileSystemProvider.onReportError(e => telemetryService.publicLog2('indexedDBFileSystemProviderError', e))); + } + }); + // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); @@ -320,7 +328,9 @@ export class BrowserMain extends Disposable { // Logger if (indexedDB) { - fileService.registerProvider(logsPath.scheme, new IndexedDBFileSystemProvider(logsPath.scheme, indexedDB, logsStore, false)); + const logFileSystemProvider = new IndexedDBFileSystemProvider(logsPath.scheme, indexedDB, logsStore, false); + this.indexedDBFileSystemProviders.push(logFileSystemProvider); + fileService.registerProvider(logsPath.scheme, logFileSystemProvider); } else { fileService.registerProvider(logsPath.scheme, new InMemoryFileSystemProvider()); } @@ -335,6 +345,7 @@ export class BrowserMain extends Disposable { let userDataProvider; if (indexedDB) { userDataProvider = new IndexedDBFileSystemProvider(Schemas.vscodeUserData, indexedDB, userDataStore, true); + this.indexedDBFileSystemProviders.push(userDataProvider); this.registerDeveloperActions(userDataProvider); } else { logService.info('Using in-memory user data provider'); diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts index 71de392b373..7e74b3ec1fc 100644 --- a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -27,12 +27,14 @@ export class CommentFormActions implements IDisposable { this._buttonElements.forEach(b => b.remove()); const groups = menu.getActions({ shouldForwardArgs: true }); + let isPrimary: boolean = true; for (const group of groups) { const [, actions] = group; this._actions = actions; - actions.forEach(action => { - const button = new Button(this.container); + for (const action of actions) { + const button = new Button(this.container, { secondary: !isPrimary }); + isPrimary = false; this._buttonElements.push(button.element); this._toDispose.add(button); @@ -41,7 +43,7 @@ export class CommentFormActions implements IDisposable { button.enabled = action.enabled; button.label = action.label; - }); + } } } diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index f30e7d7109b..c641f752f30 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -298,11 +298,6 @@ padding: 10px 0; } -.review-widget .body .edit-container .form-actions { - display: flex; - justify-content: flex-end; -} - .review-widget .body .edit-textarea { height: 90px; margin: 5px 0 10px 0; @@ -316,7 +311,7 @@ margin-bottom: 5px; } -.review-widget .body .comment-form .monaco-text-button { +.review-widget .body .form-actions .monaco-text-button { float: right; } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 611a1b5c202..b255d590c9e 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -399,6 +399,7 @@ interface IFunctionBreakpointInputTemplateData { breakpoint: IFunctionBreakpoint; toDispose: IDisposable[]; type: 'hitCount' | 'condition' | 'name'; + updating?: boolean; } interface IExceptionBreakpointInputTemplateData { @@ -802,25 +803,30 @@ class FunctionBreakpointInputRenderer implements IListRenderer { - this.view.breakpointInputFocused.set(false); - const id = template.breakpoint.getId(); + template.updating = true; + try { + this.view.breakpointInputFocused.set(false); + const id = template.breakpoint.getId(); - if (success) { - if (template.type === 'name') { - this.debugService.updateFunctionBreakpoint(id, { name: inputBox.value }); - } - if (template.type === 'condition') { - this.debugService.updateFunctionBreakpoint(id, { condition: inputBox.value }); - } - if (template.type === 'hitCount') { - this.debugService.updateFunctionBreakpoint(id, { hitCondition: inputBox.value }); - } - } else { - if (template.type === 'name' && !template.breakpoint.name) { - this.debugService.removeFunctionBreakpoints(id); + if (success) { + if (template.type === 'name') { + this.debugService.updateFunctionBreakpoint(id, { name: inputBox.value }); + } + if (template.type === 'condition') { + this.debugService.updateFunctionBreakpoint(id, { condition: inputBox.value }); + } + if (template.type === 'hitCount') { + this.debugService.updateFunctionBreakpoint(id, { hitCondition: inputBox.value }); + } } else { - this.view.renderInputBox(undefined); + if (template.type === 'name' && !template.breakpoint.name) { + this.debugService.removeFunctionBreakpoints(id); + } else { + this.view.renderInputBox(undefined); + } } + } finally { + template.updating = false; } }; @@ -834,10 +840,9 @@ class FunctionBreakpointInputRenderer implements IListRenderer { - // Need to react with a timeout on the blur event due to possible concurent splices #56443 - setTimeout(() => { + if (!template.updating) { wrapUp(!!inputBox.value); - }); + } })); template.inputBox = inputBox; diff --git a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts index 57a2abbe29e..06ea6440b6c 100644 --- a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -17,7 +17,7 @@ function sendInitializeRequest(debugAdapter: StreamDebugAdapter): Promise { debugAdapter.sendRequest('initialize', { adapterID: 'test' }, (result) => { resolve(result); - }); + }, 3000); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index 763c427e321..b0dd1d172c8 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -32,7 +32,7 @@ const NOTEBOOK_CURSOR_PAGEDOWN_COMMAND_ID = 'notebook.cell.cursorPageDown'; const NOTEBOOK_CURSOR_PAGEDOWN_SELECT_COMMAND_ID = 'notebook.cell.cursorPageDownSelect'; -registerAction2(class extends NotebookCellAction { +registerAction2(class FocusNextCellAction extends NotebookCellAction { constructor() { super({ id: NOTEBOOK_FOCUS_NEXT_EDITOR, @@ -76,13 +76,13 @@ registerAction2(class extends NotebookCellAction { const newCell = editor.cellAt(idx + 1); const newFocusMode = newCell.cellKind === CellKind.Markup && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; - editor.focusNotebookCell(newCell, newFocusMode); + await editor.focusNotebookCell(newCell, newFocusMode, { focusEditorLine: 1 }); editor.cursorNavigationMode = true; } }); -registerAction2(class extends NotebookCellAction { +registerAction2(class FocusPreviousCellAction extends NotebookCellAction { constructor() { super({ id: NOTEBOOK_FOCUS_PREVIOUS_EDITOR, @@ -118,7 +118,7 @@ registerAction2(class extends NotebookCellAction { const newCell = editor.cellAt(idx - 1); const newFocusMode = newCell.cellKind === CellKind.Markup && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; - editor.focusNotebookCell(newCell, newFocusMode); + await editor.focusNotebookCell(newCell, newFocusMode, { focusEditorLine: newCell.textBuffer.getLineCount() }); editor.cursorNavigationMode = true; } }); @@ -145,7 +145,7 @@ registerAction2(class extends NotebookAction { } const firstCell = editor.cellAt(0); - editor.focusNotebookCell(firstCell, 'container'); + await editor.focusNotebookCell(firstCell, 'container'); } }); @@ -173,7 +173,7 @@ registerAction2(class extends NotebookAction { const lastVisibleIdx = editor.getPreviousVisibleCellIndex(lastIdx); if (lastVisibleIdx) { const cell = editor.cellAt(lastVisibleIdx); - editor.focusNotebookCell(cell, 'container'); + await editor.focusNotebookCell(cell, 'container'); } } }); @@ -196,7 +196,7 @@ registerAction2(class extends NotebookCellAction { async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; const activeCell = context.cell; - editor.focusNotebookCell(activeCell, 'output'); + await editor.focusNotebookCell(activeCell, 'output'); } }); @@ -217,7 +217,7 @@ registerAction2(class extends NotebookCellAction { async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; const activeCell = context.cell; - editor.focusNotebookCell(activeCell, 'editor'); + await editor.focusNotebookCell(activeCell, 'editor'); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts index f4209f52ad9..08f3f326881 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts @@ -69,7 +69,7 @@ export async function changeCellToKind(kind: CellKind, context: INotebookActionC }; }, undefined, true); const newCell = notebookEditor.cellAt(idx); - notebookEditor.focusNotebookCell(newCell, cell.getEditState() === CellEditState.Editing ? 'editor' : 'container'); + await notebookEditor.focusNotebookCell(newCell, cell.getEditState() === CellEditState.Editing ? 'editor' : 'container'); } else if (context.selectedCells) { const selectedCells = context.selectedCells; const rawEdits: ICellEditOperation[] = []; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 8d253828d2e..7250d2dc560 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -87,7 +87,7 @@ registerAction2(class EditCellAction extends NotebookCellAction { return; } - context.notebookEditor.focusNotebookCell(context.cell, 'editor'); + await context.notebookEditor.focusNotebookCell(context.cell, 'editor'); } }); @@ -139,7 +139,7 @@ registerAction2(class QuitEditCellAction extends NotebookCellAction { context.cell.updateEditState(CellEditState.Preview, QUIT_EDIT_CELL_COMMAND_ID); } - context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); + await context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); } }); @@ -499,7 +499,7 @@ async function setCellToLanguage(languageId: string, context: IChangeCellContext const newCell = context.notebookEditor.cellAt(idx); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } else if (languageId !== 'markdown' && context.cell?.cellKind === CellKind.Markup) { await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, languageId); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index 063d3812d0e..564cd6548e5 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -194,7 +194,7 @@ registerAction2(class ExecuteCell extends NotebookMultiCellAction { async runWithContext(accessor: ServicesAccessor, context: INotebookCommandContext | INotebookCellToolbarActionContext): Promise { if (context.ui) { - context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); + await context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); } return runCell(accessor, context); @@ -235,7 +235,7 @@ registerAction2(class ExecuteAboveCells extends NotebookMultiCellAction { let endCellIdx: number | undefined = undefined; if (context.ui) { endCellIdx = context.notebookEditor.getCellIndex(context.cell); - context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); + await context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); } else { endCellIdx = Math.min(...context.selectedCells.map(cell => context.notebookEditor.getCellIndex(cell))); } @@ -282,7 +282,7 @@ registerAction2(class ExecuteCellAndBelow extends NotebookMultiCellAction { let startCellIdx: number | undefined = undefined; if (context.ui) { startCellIdx = context.notebookEditor.getCellIndex(context.cell); - context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); + await context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); } else { startCellIdx = Math.min(...context.selectedCells.map(cell => context.notebookEditor.getCellIndex(cell))); } @@ -315,12 +315,12 @@ registerAction2(class ExecuteCellFocusContainer extends NotebookMultiCellAction async runWithContext(accessor: ServicesAccessor, context: INotebookCommandContext | INotebookCellToolbarActionContext): Promise { if (context.ui) { - context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); + await context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); } else { const firstCell = context.selectedCells[0]; if (firstCell) { - context.notebookEditor.focusNotebookCell(firstCell, 'container', { skipReveal: true }); + await context.notebookEditor.focusNotebookCell(firstCell, 'container', { skipReveal: true }); } } @@ -390,7 +390,7 @@ registerAction2(class CancelExecuteCell extends NotebookMultiCellAction { async runWithContext(accessor: ServicesAccessor, context: INotebookCommandContext | INotebookCellToolbarActionContext): Promise { if (context.ui) { - context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); + await context.notebookEditor.focusNotebookCell(context.cell, 'container', { skipReveal: true }); return context.notebookEditor.cancelNotebookCells(Iterable.single(context.cell)); } else { return context.notebookEditor.cancelNotebookCells(context.selectedCells); @@ -423,12 +423,12 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { const nextCell = context.notebookEditor.cellAt(idx + 1); context.cell.updateEditState(CellEditState.Preview, EXECUTE_CELL_SELECT_BELOW); if (nextCell) { - context.notebookEditor.focusNotebookCell(nextCell, 'container'); + await context.notebookEditor.focusNotebookCell(nextCell, 'container'); } else { const newCell = insertCell(languageService, context.notebookEditor, idx, CellKind.Markup, 'below'); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } return; @@ -436,12 +436,12 @@ registerAction2(class ExecuteCellSelectBelow extends NotebookCellAction { // Try to select below, fall back on inserting const nextCell = context.notebookEditor.cellAt(idx + 1); if (nextCell) { - context.notebookEditor.focusNotebookCell(nextCell, 'container'); + await context.notebookEditor.focusNotebookCell(nextCell, 'container'); } else { const newCell = insertCell(languageService, context.notebookEditor, idx, CellKind.Code, 'below'); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } @@ -471,7 +471,7 @@ registerAction2(class ExecuteCellInsertBelow extends NotebookCellAction { const newCell = insertCell(languageService, context.notebookEditor, idx, context.cell.cellKind, 'below'); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, newFocusMode); + await context.notebookEditor.focusNotebookCell(newCell, newFocusMode); } if (context.cell.cellKind === CellKind.Markup) { diff --git a/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts index 79d40142457..6a85c41a24a 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/insertCellActions.ts @@ -54,7 +54,7 @@ abstract class InsertCellCommand extends NotebookAction { } if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, this.focusEditor ? 'editor' : 'container'); + await context.notebookEditor.focusNotebookCell(newCell, this.focusEditor ? 'editor' : 'container'); } } } @@ -189,7 +189,7 @@ registerAction2(class InsertCodeCellAtTopAction extends NotebookAction { const newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Code, 'above', undefined, true); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } }); @@ -216,7 +216,7 @@ registerAction2(class InsertMarkdownCellAtTopAction extends NotebookAction { const newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Markup, 'above', undefined, true); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts index 9bb383e2b81..26c6588808b 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -43,8 +43,8 @@ export interface INotebookTextDiffEditor { */ triggerScroll(event: IMouseWheelEvent): void; getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; - focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; - focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): Promise; + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): Promise; updateOutputHeight(cellInfo: ICommonCellInfo, output: ICellOutputViewModel, height: number, isInit: boolean): void; deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]): void; } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 9de089e2f5a..1369de699b9 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -158,11 +158,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD // throw new Error('Method not implemented.'); } - focusNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): void { + async focusNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): Promise { // throw new Error('Method not implemented.'); } - focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): void { + async focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'output' | 'editor' | 'container'): Promise { // throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 77a50b8c655..56012a1a041 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -122,6 +122,7 @@ export interface ICommonCellInfo { export interface IFocusNotebookCellOptions { readonly skipReveal?: boolean; + readonly focusEditorLine?: number; } //#endregion @@ -446,7 +447,7 @@ export interface INotebookEditor { /** * Focus the container of a cell (the monaco editor inside is not focused). */ - focusNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): void; + focusNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): Promise; /** * Execute the given notebook cells @@ -704,7 +705,8 @@ export enum CellEditState { export enum CellFocusMode { Container, - Editor + Editor, + Output } export enum CursorAtBoundary { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 9810b6e6a38..535e00b2aa6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1077,14 +1077,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); } - private _updateForCursorNavigationMode(applyFocusChange: () => void): void { + private async _updateForCursorNavigationMode(applyFocusChange: () => void): Promise { if (this._cursorNavigationMode) { // Will fire onDidChangeFocus, resetting the state to Container applyFocusChange(); const newFocusedCell = this._list.getFocusedElements()[0]; if (newFocusedCell.cellKind === CellKind.Code || newFocusedCell.getEditState() === CellEditState.Editing) { - this.focusNotebookCell(newFocusedCell, 'editor'); + await this.focusNotebookCell(newFocusedCell, 'editor'); } else { // Reset to "Editor", the state has not been consumed this._cursorNavigationMode = true; @@ -1354,9 +1354,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._localStore.add(this._webview.webview.onDidBlur(() => { this._outputFocus.set(false); - this.updateEditorFocus(); - this._webviewFocused = false; + + this.updateEditorFocus(); + this.updateCellFocusMode(); })); this._localStore.add(this._webview.webview.onDidFocus(() => { @@ -1917,6 +1918,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.viewModel?.setEditorFocus(focused); } + updateCellFocusMode() { + const activeCell = this.getActiveCell(); + + if (activeCell?.focusMode === CellFocusMode.Output && !this._webviewFocused) { + // output previously has focus, but now it's blurred. + activeCell.focusMode = CellFocusMode.Container; + } + } + hasEditorFocus() { // _editorFocus is driven by the FocusTracker, which is only guaranteed to _eventually_ fire blur. // If we need to know whether we have focus at this instant, we need to check the DOM manually. @@ -2316,7 +2326,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return selectedCellsInRange; } - focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) { + async focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) { if (this._isDisposed) { return; } @@ -2329,26 +2339,43 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD cell.updateEditState(CellEditState.Editing, 'focusNotebookCell'); cell.focusMode = CellFocusMode.Editor; if (!options?.skipReveal) { - const selectionsStartPosition = cell.getSelectionsStartPosition(); - if (selectionsStartPosition?.length) { - const firstSelectionPosition = selectionsStartPosition[0]; - this.revealRangeInCenterIfOutsideViewportAsync(cell, Range.fromPositions(firstSelectionPosition, firstSelectionPosition)); + if (typeof options?.focusEditorLine === 'number') { + await this.revealLineInViewAsync(cell, options.focusEditorLine); + const editor = this._renderedEditors.get(cell)!; + const focusEditorLine = options.focusEditorLine!; + editor?.setSelection({ + startLineNumber: focusEditorLine, + startColumn: 1, + endLineNumber: focusEditorLine, + endColumn: 1 + }); } else { - this.revealInCenterIfOutsideViewport(cell); + const selectionsStartPosition = cell.getSelectionsStartPosition(); + if (selectionsStartPosition?.length) { + const firstSelectionPosition = selectionsStartPosition[0]; + await this.revealRangeInCenterIfOutsideViewportAsync(cell, Range.fromPositions(firstSelectionPosition, firstSelectionPosition)); + } else { + this.revealInCenterIfOutsideViewport(cell); + } } + } } else if (focusItem === 'output') { this.focusElement(cell); this._cellFocusAria(cell, focusItem); - this._list.focusView(); + + if (!this.hasEditorFocus()) { + this._list.focusView(); + } if (!this._webview) { return; } - this._webview.focusOutput(cell.id); + + this._webview.focusOutput(cell.id, this._webviewFocused); cell.updateEditState(CellEditState.Preview, 'focusNotebookCell'); - cell.focusMode = CellFocusMode.Container; + cell.focusMode = CellFocusMode.Output; if (!options?.skipReveal) { this.revealInCenterIfOutsideViewport(cell); } @@ -2370,7 +2397,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } } - focusNextNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { + async focusNextNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { const idx = this.viewModel?.getCellIndex(cell); if (typeof idx !== 'number') { return; @@ -2381,7 +2408,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } - this.focusNotebookCell(newCell, focusItem); + await this.focusNotebookCell(newCell, focusItem); } //#endregion diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index 6cd5d2c242d..63cc7be3072 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -331,6 +331,7 @@ export class CodeCell extends Disposable { } this.templateData.container.classList.toggle('cell-editor-focus', this.viewCell.focusMode === CellFocusMode.Editor); + this.templateData.container.classList.toggle('cell-output-focus', this.viewCell.focusMode === CellFocusMode.Output); } private updateForCollapseState(): boolean { diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 3ef1c58649e..859e007d6c9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -176,10 +176,6 @@ export class NotebookCellList extends WorkbenchList implements ID } }); this._previousFocusedElements = e.elements; - - if (document.activeElement && document.activeElement.classList.contains('webview')) { - super.domFocus(); - } })); const notebookEditorCursorAtBoundaryContext = NOTEBOOK_EDITOR_CURSOR_BOUNDARY.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 09a5e218cdc..cc2eb8bf93a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -63,10 +63,10 @@ export interface IResolvedBackLayerWebview { export interface INotebookDelegateForWebview { readonly creationOptions: INotebookEditorCreationOptions; getCellById(cellId: string): IGenericCellViewModel | undefined; - focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): void; + focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): Promise; toggleNotebookCellSelection(cell: IGenericCellViewModel, selectFromPrevious: boolean): void; getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; - focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; + focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): Promise; updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean, source?: string): void; scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void; updateMarkupCellHeight(cellId: string, height: number, isInit: boolean): void; @@ -560,7 +560,7 @@ var requirejs = (function() { } })); - this._register(this.webview.onMessage((message) => { + this._register(this.webview.onMessage(async (message) => { const data: FromWebviewMessage | { readonly __vscode_notebook_message: undefined } = message.message; if (this._disposed) { return; @@ -617,6 +617,7 @@ var requirejs = (function() { const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); if (latestCell) { latestCell.outputIsFocused = true; + this.notebookEditor.focusNotebookCell(latestCell, 'output', { skipReveal: true }); } } break; @@ -655,7 +656,7 @@ var requirejs = (function() { if (data.focusNext) { this.notebookEditor.focusNextNotebookCell(cell, 'editor'); } else { - this.notebookEditor.focusNotebookCell(cell, 'editor'); + await this.notebookEditor.focusNotebookCell(cell, 'editor'); } } break; @@ -733,7 +734,7 @@ var requirejs = (function() { this.notebookEditor.toggleNotebookCellSelection(cell, /* fromPrevious */ data.shiftKey); } else { // Normal click - this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); + await this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); } } break; @@ -742,7 +743,7 @@ var requirejs = (function() { const cell = this.notebookEditor.getCellById(data.cellId); if (cell) { // Focus the cell first - this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); + await this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); // Then show the context menu const webviewRect = this.element.getBoundingClientRect(); @@ -766,7 +767,7 @@ var requirejs = (function() { const cell = this.notebookEditor.getCellById(data.cellId); if (cell && !this.notebookEditor.creationOptions.isReadOnly) { this.notebookEditor.setMarkupCellEditState(data.cellId, CellEditState.Editing); - this.notebookEditor.focusNotebookCell(cell, 'editor', { skipReveal: true }); + await this.notebookEditor.focusNotebookCell(cell, 'editor', { skipReveal: true }); } break; } @@ -1314,12 +1315,15 @@ var requirejs = (function() { this.webview?.focus(); } - focusOutput(cellId: string) { + focusOutput(cellId: string, viewFocused: boolean) { if (this._disposed) { return; } - this.webview?.focus(); + if (!viewFocused) { + this.webview?.focus(); + } + setTimeout(() => { // Need this, or focus decoration is not shown. No clue. this._sendMessageToWebview({ type: 'focus-output', diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 43b31f662d1..33155f8dc15 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -88,6 +88,16 @@ async function webviewPreloads(ctx: PreloadContext) { return; } + for (const node of event.composedPath()) { + if (node instanceof HTMLElement && node.classList.contains('output')) { + // output + postNotebookMessage('outputFocus', { + id: node.id, + }); + break; + } + } + for (const node of event.composedPath()) { if (node instanceof HTMLAnchorElement && node.href) { if (node.href.startsWith('blob:')) { @@ -401,17 +411,6 @@ async function webviewPreloads(ctx: PreloadContext) { }); } - function isAncestor(testChild: Node | null, testAncestor: Node | null): boolean { - while (testChild) { - if (testChild === testAncestor) { - return true; - } - testChild = testChild.parentNode; - } - - return false; - } - function _internalHighlightRange(range: Range, tagName = 'mark', attributes = {}) { // derived from https://github.com/Treora/dom-highlight-range/blob/master/highlight-range.js @@ -635,64 +634,6 @@ async function webviewPreloads(ctx: PreloadContext) { } } - class OutputFocusTracker { - private _outputId: string; - private _hasFocus: boolean = false; - private _loosingFocus: boolean = false; - private _element: HTMLElement | Window; - constructor(element: HTMLElement | Window, outputId: string) { - this._element = element; - this._outputId = outputId; - this._hasFocus = isAncestor(document.activeElement, element); - this._loosingFocus = false; - - element.addEventListener('focus', this._onFocus.bind(this), true); - element.addEventListener('blur', this._onBlur.bind(this), true); - } - - private _onFocus() { - this._loosingFocus = false; - if (!this._hasFocus) { - this._hasFocus = true; - postNotebookMessage('outputFocus', { - id: this._outputId, - }); - } - } - - private _onBlur() { - if (this._hasFocus) { - this._loosingFocus = true; - window.setTimeout(() => { - if (this._loosingFocus) { - this._loosingFocus = false; - this._hasFocus = false; - postNotebookMessage('outputBlur', { - id: this._outputId, - }); - } - }, 0); - } - } - - dispose() { - if (this._element) { - this._element.removeEventListener('focus', this._onFocus, true); - this._element.removeEventListener('blur', this._onBlur, true); - } - } - } - - const outputFocusTrackers = new Map(); - - function addOutputFocusTracker(element: HTMLElement, outputId: string): void { - if (outputFocusTrackers.has(outputId)) { - outputFocusTrackers.get(outputId)?.dispose(); - } - - outputFocusTrackers.set(outputId, new OutputFocusTracker(element, outputId)); - } - function createEmitter(listenerChange: (listeners: Set>) => void = () => undefined): EmitterLike { const listeners = new Set>(); return { @@ -1119,11 +1060,6 @@ async function webviewPreloads(ctx: PreloadContext) { renderers.clearAll(); viewModel.clearAll(); document.getElementById('container')!.innerText = ''; - - outputFocusTrackers.forEach(ft => { - ft.dispose(); - }); - outputFocusTrackers.clear(); break; case 'clearOutput': { @@ -2029,7 +1965,6 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.padding = '0px'; addMouseoverListeners(this.element, outputId); - addOutputFocusTracker(this.element, outputId); } diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index fba54d6b40e..f49b4d0acb6 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -776,6 +776,8 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I callback: () => { type ReconnectReloadClassification = { + owner: 'alexdima'; + comment: 'The reload button in the builtin permanent reconnection failure dialog was pressed'; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; @@ -820,6 +822,8 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I reconnectionAttempts = 0; type RemoteConnectionLostClassification = { + owner: 'alexdima'; + comment: 'The remote connection state is now `ConnectionLost`'; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; }; @@ -854,6 +858,8 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I reconnectionAttempts = e.attempt; type RemoteReconnectionRunningClassification = { + owner: 'alexdima'; + comment: 'The remote connection state is now `ReconnectionRunning`'; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; @@ -893,6 +899,8 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I reconnectionAttempts = e.attempt; type RemoteReconnectionPermanentFailureClassification = { + owner: 'alexdima'; + comment: 'The remote connection state is now `ReconnectionPermanentFailure`'; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; @@ -936,6 +944,8 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I reconnectionAttempts = e.attempt; type RemoteConnectionGainClassification = { + owner: 'alexdima'; + comment: 'The remote connection state is now `ConnectionGain`'; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 894ccb8be73..7f404ce1f37 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -174,6 +174,8 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio await this._remoteAgentService.getRawEnvironment(); type RemoteConnectionSuccessClassification = { + owner: 'alexdima'; + comment: 'The initial connection succeeded'; web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; }; @@ -189,6 +191,8 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio } catch (err) { type RemoteConnectionFailureClassification = { + owner: 'alexdima'; + comment: 'The initial connection failed'; web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; message: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index d77b68a8944..75929726a57 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -60,11 +60,11 @@ export class SCMStatusController implements IWorkbenchContribution { this.scmViewService.onDidFocusRepository(this.focusRepository, this, this.disposables); this.focusRepository(this.scmViewService.focusedRepository); - editorService.onDidActiveEditorChange(this.tryFocusRepositoryBasedOnActiveEditor, this, this.disposables); + editorService.onDidActiveEditorChange(() => this.tryFocusRepositoryBasedOnActiveEditor(), this, this.disposables); this.renderActivityCount(); } - private tryFocusRepositoryBasedOnActiveEditor(): boolean { + private tryFocusRepositoryBasedOnActiveEditor(repositories: Iterable = this.scmService.repositories): boolean { const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); if (!resource) { @@ -74,7 +74,7 @@ export class SCMStatusController implements IWorkbenchContribution { let bestRepository: ISCMRepository | null = null; let bestMatchLength = Number.POSITIVE_INFINITY; - for (const repository of this.scmService.repositories) { + for (const repository of repositories) { const root = repository.provider.rootUri; if (!root) { @@ -110,6 +110,8 @@ export class SCMStatusController implements IWorkbenchContribution { const disposable = combinedDisposable(changeDisposable, removeDisposable); this.repositoryDisposables.add(disposable); + + this.tryFocusRepositoryBasedOnActiveEditor(Iterable.single(repository)); } private onDidRemoveRepository(repository: ISCMRepository): void { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index e0a420f570a..825b5173d4f 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -44,9 +44,7 @@ interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposa export class DecorationAddon extends Disposable implements ITerminalAddon { protected _terminal: Terminal | undefined; private _hoverDelayer: Delayer; - private _commandStartedListener: IDisposable | undefined; - private _commandFinishedListener: IDisposable | undefined; - private _commandClearedListener: IDisposable | undefined; + private _commandDetectionListeners: IDisposable[] | undefined; private _contextMenuVisible: boolean = false; private _decorations: Map = new Map(); private _placeholderDecoration: IDecoration | undefined; @@ -113,9 +111,9 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } private _dispose(): void { - this._commandStartedListener?.dispose(); - this._commandFinishedListener?.dispose(); - this._commandClearedListener?.dispose(); + if (this._commandDetectionListeners) { + dispose(this._commandDetectionListeners); + } this._placeholderDecoration?.dispose(); this._placeholderDecoration?.marker.dispose(); for (const value of this._decorations.values()) { @@ -127,66 +125,45 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { private _attachToCommandCapability(): void { if (this._capabilities.has(TerminalCapability.CommandDetection)) { - this._addCommandFinishedListener(); - this._addCommandStartedListener(); - this._addCommandClearedListener(); + this._addCommandDetectionListeners(); } else { this._register(this._capabilities.onDidAddCapability(c => { if (c === TerminalCapability.CommandDetection) { - this._addCommandFinishedListener(); - this._addCommandStartedListener(); - this._addCommandClearedListener(); + this._addCommandDetectionListeners(); } })); } this._register(this._capabilities.onDidRemoveCapability(c => { if (c === TerminalCapability.CommandDetection) { - this._commandStartedListener?.dispose(); - this._commandFinishedListener?.dispose(); - this._commandClearedListener?.dispose(); + if (this._commandDetectionListeners) { + dispose(this._commandDetectionListeners); + this._commandDetectionListeners = undefined; + } } })); } - private _addCommandStartedListener(): void { - if (this._commandStartedListener) { + private _addCommandDetectionListeners(): void { + if (this._commandDetectionListeners) { return; } const capability = this._capabilities.get(TerminalCapability.CommandDetection); if (!capability) { return; } + this._commandDetectionListeners = []; + // Command started if (capability.executingCommandObject?.marker) { this.registerCommandDecoration(capability.executingCommandObject, true); } - this._commandStartedListener = capability.onCommandStarted(command => this.registerCommandDecoration(command, true)); - } - - - private _addCommandFinishedListener(): void { - if (this._commandFinishedListener) { - return; - } - const capability = this._capabilities.get(TerminalCapability.CommandDetection); - if (!capability) { - return; - } + this._commandDetectionListeners.push(capability.onCommandStarted(command => this.registerCommandDecoration(command, true))); + // Command finished for (const command of capability.commands) { this.registerCommandDecoration(command); } - this._commandFinishedListener = capability.onCommandFinished(command => this.registerCommandDecoration(command)); - } - - private _addCommandClearedListener(): void { - if (this._commandClearedListener) { - return; - } - const capability = this._capabilities.get(TerminalCapability.CommandDetection); - if (!capability) { - return; - } - - this._commandClearedListener = capability.onCommandInvalidated(commands => { + this._commandDetectionListeners.push(capability.onCommandFinished(command => this.registerCommandDecoration(command))); + // Command invalidated + this._commandDetectionListeners.push(capability.onCommandInvalidated(commands => { for (const command of commands) { const id = command.marker?.id; if (id) { @@ -197,7 +174,12 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } } } - }); + })); + // Current command invalidated + this._commandDetectionListeners.push(capability.onCurrentCommandInvalidated(() => { + this._placeholderDecoration?.dispose(); + this._placeholderDecoration = undefined; + })); } activate(terminal: Terminal): void { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 8cb77e7d24b..a90e8a5bf3b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -534,7 +534,7 @@ const terminalConfiguration: IConfigurationNode = { }, [TerminalSettingId.ShellIntegrationEnabled]: { restricted: true, - markdownDescription: localize('terminal.integrated.shellIntegration.enabled', "Enable features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VSCode insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, pwsh, zsh\n - Windows: pwsh\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup."), + markdownDescription: localize('terminal.integrated.shellIntegration.enabled', "Enable features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VS Code insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, pwsh, zsh\n - Windows: pwsh\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup."), type: 'boolean', default: false }, diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 330be4494f0..e486565ab36 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -700,6 +700,9 @@ class MockLabelService implements ILabelService { registerFormatter(formatter: ResourceLabelFormatter): IDisposable { throw new Error('Method not implemented.'); } + registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable { + throw new Error('Method not implemented.'); + } onDidChangeFormatters: Event = new Emitter().event; } diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index fc7e02bdcfc..e8aee60a675 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -1209,6 +1209,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx if (!this._isDev && msg.extensionId) { const { type, extensionId, extensionPointId, message } = msg; type ExtensionsMessageClassification = { + owner: 'alexdima'; + comment: 'A validation message for an extension'; type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; extensionPointId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; @@ -1283,6 +1285,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private _onDidActivateExtensionError(extensionId: ExtensionIdentifier, error: Error): void { type ExtensionActivationErrorClassification = { + owner: 'alexdima'; + comment: 'An extension failed to activate'; extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; error: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; }; diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index eb4c4437b6a..2f912754968 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -65,6 +65,8 @@ export function createExtensionHostManager(instantiationService: IInstantiationS } export type ExtensionHostStartupClassification = { + owner: 'alexdima'; + comment: 'The startup state of the extension host'; time: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; action: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; kind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 047e66bc6f5..388033c90a1 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -306,6 +306,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten private _sendExtensionHostCrashTelemetry(code: number, signal: string | null, activatedExtensions: ExtensionIdentifier[]): void { type ExtensionHostCrashClassification = { + owner: 'alexdima'; + comment: 'The extension host has terminated unexpectedly'; code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; signal: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; extensionIds: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; @@ -323,6 +325,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten for (const extensionId of activatedExtensions) { type ExtensionHostCrashExtensionClassification = { + owner: 'alexdima'; + comment: 'The extension host has terminated unexpectedly'; code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; signal: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index b8583dadaf2..c16d82ee53a 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -17,13 +17,15 @@ import { tildify, getPathLabel } from 'vs/base/common/labels'; import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting, IFormatterChangeEvent } from 'vs/platform/label/common/label'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { match } from 'vs/base/common/glob'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { OperatingSystem, OS } from 'vs/base/common/platform'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { Schemas } from 'vs/base/common/network'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Memento } from 'vs/workbench/common/memento'; const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'resourceLabelFormatters', @@ -102,15 +104,24 @@ class ResourceLabelFormattersHandler implements IWorkbenchContribution { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceLabelFormattersHandler, LifecyclePhase.Restored); +const FORMATTER_CACHE_SIZE = 50; + +interface IStoredFormatters { + formatters?: ResourceLabelFormatter[]; + i?: number; +} + export class LabelService extends Disposable implements ILabelService { declare readonly _serviceBrand: undefined; - private formatters: ResourceLabelFormatter[] = []; + private formatters: ResourceLabelFormatter[]; private readonly _onDidChangeFormatters = this._register(new Emitter({ leakWarningThreshold: 400 })); readonly onDidChangeFormatters = this._onDidChangeFormatters.event; + private readonly storedFormattersMemento: Memento; + private readonly storedFormatters: IStoredFormatters; private os: OperatingSystem; private userHome: URI | undefined; @@ -118,7 +129,9 @@ export class LabelService extends Disposable implements ILabelService { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IPathService private readonly pathService: IPathService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IStorageService storageService: IStorageService, + @ILifecycleService lifecycleService: ILifecycleService, ) { super(); @@ -129,6 +142,10 @@ export class LabelService extends Disposable implements ILabelService { this.os = OS; this.userHome = pathService.defaultUriScheme === Schemas.file ? this.pathService.userHome({ preferLocal: true }) : undefined; + const memento = this.storedFormattersMemento = new Memento('cachedResourceFormatters', storageService); + this.storedFormatters = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + this.formatters = this.storedFormatters?.formatters || []; + // Remote environment is potentially long running this.resolveRemoteEnvironment(); } @@ -334,6 +351,28 @@ export class LabelService extends Disposable implements ILabelService { return formatter?.workspaceTooltip; } + registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable { + const list = this.storedFormatters.formatters ??= []; + + let replace = list.findIndex(f => f.scheme === formatter.scheme && f.authority === formatter.authority); + if (replace === -1 && list.length >= FORMATTER_CACHE_SIZE) { + replace = FORMATTER_CACHE_SIZE - 1; // at max capacity, replace the last element + } + + if (replace === -1) { + list.unshift(formatter); + } else { + for (let i = replace; i > 0; i--) { + list[i] = list[i - 1]; + } + list[0] = formatter; + } + + this.storedFormattersMemento.saveMemento(); + + return this.registerFormatter(formatter); + } + registerFormatter(formatter: ResourceLabelFormatter): IDisposable { this.formatters.push(formatter); this._onDidChangeFormatters.fire({ scheme: formatter.scheme }); diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index ab94051a7e6..31252fd11ef 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -5,19 +5,24 @@ import * as resources from 'vs/base/common/resources'; import * as assert from 'assert'; -import { TestEnvironmentService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestLifecycleService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; -import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { isWindows } from 'vs/base/common/platform'; +import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Memento } from 'vs/workbench/common/memento'; +import { ResourceLabelFormatter } from 'vs/platform/label/common/label'; suite('URI Label', () => { let labelService: LabelService; + let storageService: TestStorageService; setup(() => { - labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService()); + storageService = new TestStorageService(); + labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService(), storageService, new TestLifecycleService()); }); test('custom scheme', function () { @@ -158,6 +163,41 @@ suite('URI Label', () => { const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); assert.strictEqual(labelService.getUriLabel(uri1, { relative: false }), 'LABEL: /END'); }); + + + test('label caching', () => { + const m = new Memento('cachedResourceFormatters', storageService).getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const makeFormatter = (scheme: string): ResourceLabelFormatter => ({ formatting: { label: `\${path} (${scheme})`, separator: '/' }, scheme }); + assert.deepStrictEqual(m, {}); + + // registers a new formatter: + labelService.registerCachedFormatter(makeFormatter('a')); + assert.deepStrictEqual(m, { formatters: [makeFormatter('a')] }); + + // registers a 2nd formatter: + labelService.registerCachedFormatter(makeFormatter('b')); + assert.deepStrictEqual(m, { formatters: [makeFormatter('b'), makeFormatter('a')] }); + + // promotes a formatter on re-register: + labelService.registerCachedFormatter(makeFormatter('a')); + assert.deepStrictEqual(m, { formatters: [makeFormatter('a'), makeFormatter('b')] }); + + // no-ops if already in first place: + labelService.registerCachedFormatter(makeFormatter('a')); + assert.deepStrictEqual(m, { formatters: [makeFormatter('a'), makeFormatter('b')] }); + + // limits the cache: + for (let i = 0; i < 100; i++) { + labelService.registerCachedFormatter(makeFormatter(`i${i}`)); + } + let expected: ResourceLabelFormatter[] = []; + for (let i = 50; i < 100; i++) { + expected.unshift(makeFormatter(`i${i}`)); + } + assert.deepStrictEqual(m, { formatters: expected }); + + delete (m as any).formatters; + }); }); @@ -178,7 +218,9 @@ suite('multi-root workspace', () => { new WorkspaceFolder({ uri: other, index: 2, name: resources.basename(other) }), ])), new TestPathService(), - new TestRemoteAgentService() + new TestRemoteAgentService(), + new TestStorageService(), + new TestLifecycleService() ); }); @@ -263,7 +305,9 @@ suite('multi-root workspace', () => { new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }), ])), new TestPathService(undefined, rootFolder.scheme), - new TestRemoteAgentService() + new TestRemoteAgentService(), + new TestStorageService(), + new TestLifecycleService() ); const generated = labelService.getUriLabel(URI.parse('myscheme://myauthority/some/folder/test.txt'), { relative: true }); @@ -288,7 +332,9 @@ suite('workspace at FSP root', () => { new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }), ])), new TestPathService(), - new TestRemoteAgentService() + new TestRemoteAgentService(), + new TestStorageService(), + new TestLifecycleService() ); labelService.registerFormatter({ scheme: 'myscheme', diff --git a/src/vs/workbench/services/label/test/electron-browser/label.test.ts b/src/vs/workbench/services/label/test/electron-browser/label.test.ts index ad2976d6bb7..c22bb5c721c 100644 --- a/src/vs/workbench/services/label/test/electron-browser/label.test.ts +++ b/src/vs/workbench/services/label/test/electron-browser/label.test.ts @@ -9,16 +9,16 @@ import { URI } from 'vs/base/common/uri'; import { sep } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; -import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestNativePathService, TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('URI Label', () => { let labelService: LabelService; setup(() => { - labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService()); + labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService(), new TestStorageService(), new TestLifecycleService()); }); test('file scheme', function () { diff --git a/src/vs/workbench/services/tunnel/browser/tunnelService.ts b/src/vs/workbench/services/tunnel/browser/tunnelService.ts index d433bb69371..622b9c7d62a 100644 --- a/src/vs/workbench/services/tunnel/browser/tunnelService.ts +++ b/src/vs/workbench/services/tunnel/browser/tunnelService.ts @@ -18,7 +18,7 @@ export class TunnelService extends AbstractTunnelService { super(logService); } - protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise | undefined { + protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; diff --git a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts index 505f814d12c..b4d9bc3fbd7 100644 --- a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts +++ b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts @@ -70,7 +70,7 @@ export class TunnelService extends AbstractTunnelService { }); } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise | undefined { + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy?: string, protocol?: string): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts index 596255d0466..5068eccd657 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { TestNativePathService, TestNativeWindowConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { TestContextService, TestProductService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestProductService, TestStorageService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { NullLogService } from 'vs/platform/log/common/log'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -57,7 +57,7 @@ export class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryServi const uriIdentityService = new UriIdentityService(fileService); - const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService()); + const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService(), new TestStorageService(), new TestLifecycleService()); const lifecycleService = new TestLifecycleService(); diff --git a/src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts b/src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts index 54a0950f9ca..24baea79897 100644 --- a/src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts @@ -5,7 +5,7 @@ declare module 'vscode' { - // https://github.com/microsoft/vscode/issues/TODO + // https://github.com/microsoft/vscode/issues/147481 /** * A file associated with a {@linkcode DataTransferItem}. diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index d544cdde350..28796e9b0d7 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -46,7 +46,7 @@ export function setup(logger: Logger) { await app.workbench.search.removeFileMatch('app.js', '2 results in 2 files'); }); - it('replaces first search result with a replace term', async function () { + it.skip('replaces first search result with a replace term', async function () { // TODO@roblourens https://github.com/microsoft/vscode/issues/137195 const app = this.app as Application; await app.workbench.search.searchFor('body');