diff --git a/extensions/git/package.json b/extensions/git/package.json index 641d81ae6eda0..7c28c7e4b0926 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1665,6 +1665,18 @@ "scope": "resource", "default": true, "description": "%config.showCommitInput%" + }, + "git.terminalAuthentication": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "%config.terminalAuthentication%" + }, + "git.githubAuthentication": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "%config.githubAuthentication%" } } }, @@ -1777,6 +1789,7 @@ "ignore" ], "filenames": [ + ".gitignore_global", ".gitignore" ], "configuration": "./languages/ignore.language-configuration.json" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 2ea91658e9cb7..557f0c2abdb73 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -144,6 +144,8 @@ "config.untrackedChanges.separate": "Untracked changes appear separately in the Source Control view. They are also excluded from several actions.", "config.untrackedChanges.hidden": "Untracked changes are hidden and excluded from several actions.", "config.showCommitInput": "Controls whether to show the commit input in the Git source control panel.", + "config.terminalAuthentication": "Controls whether to enable VS Code to be the authentication handler for git processes spawned in the integrated terminal.", + "config.githubAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.", "colors.added": "Color for added resources.", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 56ff982df8c21..84f503743ebc8 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -5,7 +5,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, GitExtension, RefType, RemoteSourceProvider } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, GitExtension, RefType, RemoteSourceProvider, CredentialsProvider } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands } from 'vscode'; import { mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -263,6 +263,10 @@ export class ApiImpl implements API { return this._model.registerRemoteSourceProvider(provider); } + registerCredentialsProvider(provider: CredentialsProvider): Disposable { + return this._model.registerCredentialsProvider(provider); + } + constructor(private _model: Model) { } } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 866b91100386b..46e8217ce6f80 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -204,6 +204,15 @@ export interface RemoteSourceProvider { getRemoteSources(query?: string): ProviderResult; } +export interface Credentials { + readonly username: string; + readonly password: string; +} + +export interface CredentialsProvider { + getCredentials(host: Uri): ProviderResult; +} + export type APIState = 'uninitialized' | 'initialized'; export interface API { @@ -217,7 +226,9 @@ export interface API { toGitUri(uri: Uri, ref: string): Uri; getRepository(uri: Uri): Repository | null; init(root: Uri): Promise; + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + registerCredentialsProvider(provider: CredentialsProvider): Disposable; } export interface GitExtension { diff --git a/extensions/git/src/askpass.sh b/extensions/git/src/askpass.sh index b6d374f2c38e3..d19b62affa3a6 100755 --- a/extensions/git/src/askpass.sh +++ b/extensions/git/src/askpass.sh @@ -1,5 +1,5 @@ #!/bin/sh VSCODE_GIT_ASKPASS_PIPE=`mktemp` -VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $* +ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $* cat $VSCODE_GIT_ASKPASS_PIPE -rm $VSCODE_GIT_ASKPASS_PIPE \ No newline at end of file +rm $VSCODE_GIT_ASKPASS_PIPE diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index dd7e84c2e030a..ad1b5f3a79152 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -3,36 +3,60 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, InputBoxOptions } from 'vscode'; -import { IDisposable } from './util'; +import { window, InputBoxOptions, Uri, OutputChannel, Disposable } from 'vscode'; +import { IDisposable, EmptyDisposable, toDisposable } from './util'; import * as path from 'path'; -import { IIPCHandler, IIPCServer } from './ipc/ipcServer'; - -export interface AskpassEnvironment { - GIT_ASKPASS: string; - ELECTRON_RUN_AS_NODE?: string; - VSCODE_GIT_ASKPASS_NODE?: string; - VSCODE_GIT_ASKPASS_MAIN?: string; - VSCODE_GIT_ASKPASS_HANDLE?: string; -} +import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer'; +import { CredentialsProvider, Credentials } from './api/git'; export class Askpass implements IIPCHandler { - private disposable: IDisposable; + private disposable: IDisposable = EmptyDisposable; + private cache = new Map(); + private credentialsProviders = new Set(); - static getDisabledEnv(): AskpassEnvironment { - return { - GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh') - }; + static async create(outputChannel: OutputChannel, context?: string): Promise { + try { + return new Askpass(await createIPCServer(context)); + } catch (err) { + outputChannel.appendLine(`[error] Failed to create git askpass IPC: ${err}`); + return new Askpass(); + } } - constructor(ipc: IIPCServer) { - this.disposable = ipc.registerHandler('askpass', this); + private constructor(private ipc?: IIPCServer) { + if (ipc) { + this.disposable = ipc.registerHandler('askpass', this); + } } async handle({ request, host }: { request: string, host: string }): Promise { + const uri = Uri.parse(host); + const authority = uri.authority.replace(/^.*@/, ''); + const password = /password/i.test(request); + const cached = this.cache.get(authority); + + if (cached && password) { + this.cache.delete(authority); + return cached.password; + } + + if (!password) { + for (const credentialsProvider of this.credentialsProviders) { + try { + const credentials = await credentialsProvider.getCredentials(uri); + + if (credentials) { + this.cache.set(authority, credentials); + setTimeout(() => this.cache.delete(authority), 60_000); + return credentials.username; + } + } catch { } + } + } + const options: InputBoxOptions = { - password: /password/i.test(request), + password, placeHolder: request, prompt: `Git: ${host}`, ignoreFocusOut: true @@ -41,15 +65,26 @@ export class Askpass implements IIPCHandler { return await window.showInputBox(options) || ''; } - getEnv(): AskpassEnvironment { + getEnv(): { [key: string]: string; } { + if (!this.ipc) { + return { + GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh') + }; + } + return { - ELECTRON_RUN_AS_NODE: '1', + ...this.ipc.getEnv(), GIT_ASKPASS: path.join(__dirname, 'askpass.sh'), VSCODE_GIT_ASKPASS_NODE: process.execPath, VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js') }; } + registerCredentialsProvider(provider: CredentialsProvider): Disposable { + this.credentialsProviders.add(provider); + return toDisposable(() => this.credentialsProviders.delete(provider)); + } + dispose(): void { this.disposable.dispose(); } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index d5fab6178bb93..139753b1908f8 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2560,6 +2560,14 @@ export class CommandCenter { type = 'warning'; options.modal = false; break; + case GitErrorCodes.AuthenticationFailed: + const regex = /Authentication failed for '(.*)'/i; + const match = regex.exec(err.stderr || String(err)); + + message = match + ? localize('auth failed specific', "Failed to authenticate to git remote:\n\n{0}", match[1]) + : localize('auth failed', "Failed to authenticate to git remote."); + break; case GitErrorCodes.NoUserNameConfigured: case GitErrorCodes.NoUserEmailConfigured: message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git."); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 704e1528c8f5c..691d36e937a28 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -306,7 +306,7 @@ export interface IGitOptions { function getGitErrorCode(stderr: string): string | undefined { if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(stderr)) { return GitErrorCodes.RepositoryIsLocked; - } else if (/Authentication failed/.test(stderr)) { + } else if (/Authentication failed/i.test(stderr)) { return GitErrorCodes.AuthenticationFailed; } else if (/Not a git repository/i.test(stderr)) { return GitErrorCodes.NotAGitRepository; diff --git a/extensions/git/src/github.ts b/extensions/git/src/github.ts new file mode 100644 index 0000000000000..bc2f816e52b4b --- /dev/null +++ b/extensions/git/src/github.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CredentialsProvider, Credentials } from './api/git'; +import { IDisposable, filterEvent, EmptyDisposable } from './util'; +import { workspace, Uri, AuthenticationSession, authentication } from 'vscode'; +import { Askpass } from './askpass'; + +export class GitHubCredentialProvider implements CredentialsProvider { + + async getCredentials(host: Uri): Promise { + if (!/github\.com/i.test(host.authority)) { + return; + } + + const session = await this.getSession(); + return { username: session.account.id, password: await session.getAccessToken() }; + } + + private async getSession(): Promise { + const authenticationSessions = await authentication.getSessions('github', ['repo']); + + if (authenticationSessions.length) { + return await authenticationSessions[0]; + } else { + return await authentication.login('github', ['repo']); + } + } +} + +export class GithubCredentialProviderManager { + + private providerDisposable: IDisposable = EmptyDisposable; + private readonly disposable: IDisposable; + + private _enabled = false; + private set enabled(enabled: boolean) { + if (this._enabled === enabled) { + return; + } + + this._enabled = enabled; + + if (enabled) { + this.providerDisposable = this.askpass.registerCredentialsProvider(new GitHubCredentialProvider()); + } else { + this.providerDisposable.dispose(); + } + } + + constructor(private readonly askpass: Askpass) { + this.disposable = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'))(this.refresh, this); + this.refresh(); + } + + private refresh(): void { + this.enabled = workspace.getConfiguration('git', null).get('githubAuthentication', true); + } + + dispose(): void { + this.enabled = false; + this.disposable.dispose(); + } +} diff --git a/extensions/git/src/ipc/ipcServer.ts b/extensions/git/src/ipc/ipcServer.ts index 332490896cc89..5cea9faf98fdd 100644 --- a/extensions/git/src/ipc/ipcServer.ts +++ b/extensions/git/src/ipc/ipcServer.ts @@ -11,27 +11,42 @@ import * as os from 'os'; import * as fs from 'fs'; import * as crypto from 'crypto'; -function getIPCHandlePath(nonce: string): string { +function getIPCHandlePath(id: string): string { if (process.platform === 'win32') { - return `\\\\.\\pipe\\vscode-git-ipc-${nonce}-sock`; + return `\\\\.\\pipe\\vscode-git-${id}-sock`; } if (process.env['XDG_RUNTIME_DIR']) { - return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-ipc-${nonce}.sock`); + return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-${id}.sock`); } - return path.join(os.tmpdir(), `vscode-git-ipc-${nonce}.sock`); + return path.join(os.tmpdir(), `vscode-git-${id}.sock`); } export interface IIPCHandler { handle(request: any): Promise; } -export async function createIPCServer(): Promise { +export async function createIPCServer(context?: string): Promise { const server = http.createServer(); - const buffer = await new Promise((c, e) => crypto.randomBytes(20, (err, buf) => err ? e(err) : c(buf))); - const nonce = buffer.toString('hex'); - const ipcHandlePath = getIPCHandlePath(nonce); + const hash = crypto.createHash('sha1'); + + if (!context) { + const buffer = await new Promise((c, e) => crypto.randomBytes(20, (err, buf) => err ? e(err) : c(buf))); + hash.update(buffer); + } else { + hash.update(context); + } + + const ipcHandlePath = getIPCHandlePath(hash.digest('hex').substr(0, 10)); + + if (process.platform !== 'win32') { + try { + await fs.promises.unlink(ipcHandlePath); + } catch { + // noop + } + } return new Promise((c, e) => { try { @@ -46,7 +61,7 @@ export async function createIPCServer(): Promise { export interface IIPCServer extends Disposable { readonly ipcHandlePath: string | undefined; - getEnv(): any; + getEnv(): { [key: string]: string; }; registerHandler(name: string, handler: IIPCHandler): Disposable; } @@ -91,7 +106,7 @@ class IPCServer implements IIPCServer, Disposable { }); } - getEnv(): any { + getEnv(): { [key: string]: string; } { return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath }; } diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 571c29f6f362b..17908c90ce74f 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -20,9 +20,10 @@ import { GitProtocolHandler } from './protocolHandler'; import { GitExtensionImpl } from './api/extension'; import * as path from 'path'; import * as fs from 'fs'; -import { createIPCServer, IIPCServer } from './ipc/ipcServer'; import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; +import { GithubCredentialProviderManager } from './github'; +import { TerminalEnvironmentManager } from './terminal'; const deactivateTasks: { (): Promise; }[] = []; @@ -36,27 +37,18 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann const pathHint = workspace.getConfiguration('git').get('path'); const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path))); - let env: any = {}; - let ipc: IIPCServer | undefined; + const askpass = await Askpass.create(outputChannel, context.storagePath); + disposables.push(askpass); - try { - ipc = await createIPCServer(); - disposables.push(ipc); - env = { ...env, ...ipc.getEnv() }; - } catch { - // noop - } + const env = askpass.getEnv(); + const terminalEnvironmentManager = new TerminalEnvironmentManager(context, env); + disposables.push(terminalEnvironmentManager); - if (ipc) { - const askpass = new Askpass(ipc); - disposables.push(askpass); - env = { ...env, ...askpass.getEnv() }; - } else { - env = { ...env, ...Askpass.getDisabledEnv() }; - } + const githubCredentialProviderManager = new GithubCredentialProviderManager(askpass); + context.subscriptions.push(githubCredentialProviderManager); const git = new Git({ gitPath: info.path, version: info.version, env }); - const model = new Model(git, context.globalState, outputChannel); + const model = new Model(git, askpass, context.globalState, outputChannel); disposables.push(model); const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`); diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index f08b852c22f8f..a6cbc3df30cb0 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -12,7 +12,8 @@ import * as path from 'path'; import * as fs from 'fs'; import * as nls from 'vscode-nls'; import { fromGitUri } from './uri'; -import { GitErrorCodes, APIState as State, RemoteSourceProvider } from './api/git'; +import { GitErrorCodes, APIState as State, RemoteSourceProvider, CredentialsProvider } from './api/git'; +import { Askpass } from './askpass'; const localize = nls.loadMessageBundle(); @@ -78,7 +79,7 @@ export class Model { private disposables: Disposable[] = []; - constructor(readonly git: Git, private globalState: Memento, private outputChannel: OutputChannel) { + constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannel: OutputChannel) { workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables); window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables); workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); @@ -454,6 +455,10 @@ export class Model { return toDisposable(() => this.remoteProviders.delete(provider)); } + registerCredentialsProvider(provider: CredentialsProvider): Disposable { + return this.askpass.registerCredentialsProvider(provider); + } + getRemoteProviders(): RemoteSourceProvider[] { return [...this.remoteProviders.values()]; } diff --git a/extensions/git/src/terminal.ts b/extensions/git/src/terminal.ts new file mode 100644 index 0000000000000..c5b9eb81fbb38 --- /dev/null +++ b/extensions/git/src/terminal.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionContext, workspace } from 'vscode'; +import { filterEvent, IDisposable } from './util'; + +export class TerminalEnvironmentManager { + + private readonly disposable: IDisposable; + + private _enabled = false; + private set enabled(enabled: boolean) { + if (this._enabled === enabled) { + return; + } + + this._enabled = enabled; + this.context.environmentVariableCollection.clear(); + + if (enabled) { + for (const name of Object.keys(this.env)) { + this.context.environmentVariableCollection.replace(name, this.env[name]); + } + } + } + + constructor(private readonly context: ExtensionContext, private readonly env: { [key: string]: string }) { + this.disposable = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git')) + (this.refresh, this); + + this.refresh(); + } + + private refresh(): void { + this.enabled = workspace.getConfiguration('git', null).get('terminalAuthentication', true); + } + + dispose(): void { + this.disposable.dispose(); + } +} diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index f3e56977125e4..fba7d920b50a7 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -4,9 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, Task2 } from 'vscode'; +import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, Task2, env, UIKind } from 'vscode'; -suite('vscode API - tasks', () => { +// Disable tasks tests: +// - Web https://github.com/microsoft/vscode/issues/90528 +((env.uiKind === UIKind.Web) ? suite.skip : suite)('vscode API - tasks', () => { suite('Tasks', () => { let disposables: Disposable[] = []; diff --git a/package.json b/package.json index 839d884853fd6..2c46a6aef82d7 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "opn": "^6.0.0", "optimist": "0.3.5", "p-all": "^1.0.0", - "playwright": "0.12.1", + "playwright": "0.15.0", "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", diff --git a/src/vs/base/browser/ui/aria/aria.ts b/src/vs/base/browser/ui/aria/aria.ts index b42fbf94ac2e7..a971f2f4ce3cd 100644 --- a/src/vs/base/browser/ui/aria/aria.ts +++ b/src/vs/base/browser/ui/aria/aria.ts @@ -7,6 +7,8 @@ import 'vs/css!./aria'; import { isMacintosh } from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; +// Use a max length since we are inserting the whole msg in the DOM and that can cause browsers to freeze for long messages #94233 +const MAX_MESSAGE_LENGTH = 20000; let ariaContainer: HTMLElement; let alertContainer: HTMLElement; let statusContainer: HTMLElement; @@ -54,6 +56,9 @@ function insertMessage(target: HTMLElement, msg: string): void { } dom.clearNode(target); + if (msg.length > MAX_MESSAGE_LENGTH) { + msg = msg.substr(0, MAX_MESSAGE_LENGTH); + } target.textContent = msg; // See https://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/ diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index 0d31b65448b37..6d6823524840a 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -26,5 +26,5 @@ .monaco-button > .codicon { margin: 0 0.2em; - color: inherit; + color: inherit !important; } diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon-animations.css b/src/vs/base/browser/ui/codicons/codicon/codicon-animations.css index abfde40dede6f..667002f5b7c15 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon-animations.css +++ b/src/vs/base/browser/ui/codicons/codicon/codicon-animations.css @@ -10,5 +10,6 @@ } .codicon-animation-spin { - animation: codicon-spin 1.5s linear infinite; + /* Use steps to throttle FPS to reduce CPU usage */ + animation: codicon-spin 1.5s steps(30) infinite; } diff --git a/src/vs/base/browser/ui/tree/media/tree.css b/src/vs/base/browser/ui/tree/media/tree.css index 5e9780212e0a4..06f775d624612 100644 --- a/src/vs/base/browser/ui/tree/media/tree.css +++ b/src/vs/base/browser/ui/tree/media/tree.css @@ -61,5 +61,6 @@ } .monaco-tl-twistie.codicon-tree-item-loading::before { - animation: codicon-spin 1.25s linear infinite; + /* Use steps to throttle FPS to reduce CPU usage */ + animation: codicon-spin 1.25s steps(30) infinite; } diff --git a/src/vs/base/common/comparers.ts b/src/vs/base/common/comparers.ts index a713092dbda1f..d63c325c3d3e4 100644 --- a/src/vs/base/common/comparers.ts +++ b/src/vs/base/common/comparers.ts @@ -6,30 +6,173 @@ import { sep } from 'vs/base/common/path'; import { IdleValue } from 'vs/base/common/async'; -const intlFileNameCollator: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => { - const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); +// When comparing large numbers of strings, such as in sorting large arrays, is better for +// performance to create an Intl.Collator object and use the function provided by its compare +// property than it is to use String.prototype.localeCompare() + +// A collator with numeric sorting enabled. +const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { + const collator = new Intl.Collator(undefined, { numeric: true }); + return { + collator: collator + }; +}); + +// A collator with numeric sorting enabled, and sensitivity to accents but not case. +const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { + const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' }); return { - collator: collator, - collatorIsNumeric: collator.resolvedOptions().numeric + collator: collator }; }); +// A collator with numeric sorting disabled. +const intlFileNameCollator: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { + const collator = new Intl.Collator(undefined, { numeric: false }); + return { + collator: collator + }; +}); + +// A collator with numeric sorting disabled and sensitivity to accents but not case. +const intlFileNameCollatorCaseInsensitive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { + const collator = new Intl.Collator(undefined, { numeric: false, sensitivity: 'accent' }); + return { + collator: collator + }; +}); + +/** + * Compare two filenames using a fullname numeric locale-based comparison that + * falls back to a unicode comparison. + * + * @deprecated Use compareFileNamesNumeric instead. + */ export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { - const a = one || ''; - const b = other || ''; - const result = intlFileNameCollator.getValue().collator.compare(a, b); + return compareFileNamesNumeric(one, other); +} - // Using the numeric option in the collator will - // make compare(`foo1`, `foo01`) === 0. We must disambiguate. - if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && a !== b) { - return a < b ? -1 : 1; +/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */ +export function compareFileNamesNumeric(one: string | null, other: string | null, placeholder = false): number { + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + const collatorNumeric = intlFileNameCollatorNumeric.getValue().collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.getValue().collator; + let result; + + // Check for name differences, comparing numbers numerically instead of alphabetically. + result = collatorNumeric.compare(oneName, otherName); + if (result !== 0) { + return result; } - return result; + // Using the numeric option in the collator will make compare(`foo1`, `foo01`) === 0. Sort the shorter name first. + if (oneName.length !== otherName.length) { + return oneName.length < otherName.length ? -1 : 1; + } + + // Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically. + result = collatorNumericCaseInsensitive.compare(oneExtension, otherExtension); + if (result !== 0) { + return result; + } + + // If extensions are numerically equal but not equal in length, sort the shorter extension first. + if (oneExtension.length !== otherExtension.length) { + return oneExtension.length < otherExtension.length ? -1 : 1; + } + + // Disambiguate the extension case if needed. + if (oneExtension !== otherExtension) { + return collatorNumeric.compare(oneExtension, otherExtension); + } + + return 0; } -const FileNameMatch = /^(.*?)(\.([^.]*))?$/; +/** Compares filenames by name case, then by name, then by extension. Sorts uppercase names before lowercase. */ +export function compareFileNamesUpper(one: string | null, other: string | null) { + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + const collator = intlFileNameCollator.getValue().collator; + let result; + + // Check for case differences in names. + result = compareCaseUpperFirst(oneName, otherName); + if (result !== 0) { + return result; + } + + // Extension case should not be considered when grouping files by case. + // Do not check here for case differences in extensions. + + // Check for name differences. + result = collator.compare(oneName, otherName); + if (result !== 0) { + return result; + } + + // Check for extension differences, including possible case differences. + return collator.compare(oneExtension, otherExtension); +} + +/** Compares filenames by name case, then by name, then by extension. Sorts lowercase names before uppercase. */ +export function compareFileNamesLower(one: string | null, other: string | null) { + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + const collator = intlFileNameCollator.getValue().collator; + let result; + + // Check for case differences in names. + result = compareCaseLowerFirst(oneName, otherName); + if (result !== 0) { + return result; + } + + // Extension case should not be considered when grouping files by case. + // Do not check here for case differences in extensions. + + // Check for name differences. + result = collator.compare(oneName, otherName); + if (result !== 0) { + return result; + } + + // Check for extension differences, including possible case differences. + return collator.compare(oneExtension, otherExtension); +} + +/** Compares filenames by name, then by extension. */ +export function compareFileNamesMixed(one: string | null, other: string | null) { + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + const collator = intlFileNameCollator.getValue().collator; + let result; + + // Check for name differences. + result = collator.compare(oneName, otherName); + if (result !== 0) { + return result; + } + + // Check for extension differences. + return collator.compare(oneExtension, otherExtension); +} + +/** Compares filenames by unicode value, not differentiating between names and extensions. */ +export function compareFileNamesUnicode(one: string | null, other: string | null) { + one = one || ''; + other = other || ''; + + // Simply compare both strings. No name vs extension awareness. + if (one === other) { + return 0; + } + return one < other ? -1 : 1; +} + +/** Compares filenames by name unicode value, then extension unicode value. */ export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { if (!caseSensitive) { one = one && one.toLowerCase(); @@ -39,45 +182,192 @@ export function noIntlCompareFileNames(one: string | null, other: string | null, const [oneName, oneExtension] = extractNameAndExtension(one); const [otherName, otherExtension] = extractNameAndExtension(other); + // Check for name differences. if (oneName !== otherName) { return oneName < otherName ? -1 : 1; } - if (oneExtension === otherExtension) { - return 0; + // Names are equal. Compare extensions. + if (oneExtension !== otherExtension) { + return oneExtension < otherExtension ? -1 : 1; } - return oneExtension < otherExtension ? -1 : 1; + return 0; } +/** + * Compare file names using a numeric locale-based comparison, first by extension, then by name. + * Falls back to a unicode comparison when extensions or names are otherwise equal. + * + * @deprecated Use compareFileExtensionsNumeric instead. + */ export function compareFileExtensions(one: string | null, other: string | null): number { + return compareFileExtensionsNumeric(one, other); +} + +/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */ +export function compareFileExtensionsNumeric(one: string | null, other: string | null): number { const [oneName, oneExtension] = extractNameAndExtension(one); const [otherName, otherExtension] = extractNameAndExtension(other); + const collatorNumeric = intlFileNameCollatorNumeric.getValue().collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.getValue().collator; + let result; + + // Check for extension differences, ignoring differences in case and comparing numbers numerically. + result = collatorNumericCaseInsensitive.compare(oneExtension, otherExtension); + if (result !== 0) { + return result; + } - let result = intlFileNameCollator.getValue().collator.compare(oneExtension, otherExtension); + // Disambiguate equivalent numbers in extensions. + if (oneExtension.length !== otherExtension.length) { + return oneExtension.length < otherExtension.length ? -1 : 1; + } - if (result === 0) { - // Using the numeric option in the collator will - // make compare(`foo1`, `foo01`) === 0. We must disambiguate. - if (intlFileNameCollator.getValue().collatorIsNumeric && oneExtension !== otherExtension) { - return oneExtension < otherExtension ? -1 : 1; - } + // Compare names. + result = collatorNumeric.compare(oneName, otherName); + if (result !== 0) { + return result; + } - // Extensions are equal, compare filenames - result = intlFileNameCollator.getValue().collator.compare(oneName, otherName); + // Disambiguate equivalent numbers in names. + if (oneName.length !== otherName.length) { + return oneName.length < otherName.length ? -1 : 1; + } - if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && oneName !== otherName) { - return oneName < otherName ? -1 : 1; - } + // Disambiguate extension case if needed. + if (oneExtension !== otherExtension) { + return collatorNumeric.compare(oneExtension, otherExtension); } - return result; + return 0; } +/** Compares filenames by extension, then by name case, then by name. Sorts uppercase names before lowercase. */ +export function compareFileExtensionsUpper(one: string | null, other: string | null): number { + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + const collator = intlFileNameCollator.getValue().collator; + const collatorCaseInsensitive = intlFileNameCollatorCaseInsensitive.getValue().collator; + + // Check for case insensitive differences in extensions. + let result = collatorCaseInsensitive.compare(oneExtension, otherExtension); + if (result !== 0) { + return result; + } + + // Check for case differences in names. + result = compareCaseUpperFirst(oneName, otherName); + if (result !== 0) { + return result; + } + + // Check for other differences in names. + result = collator.compare(oneName, otherName); + if (result !== 0) { + return result; + } + + // Disambiguate extension case if needed. + if (oneExtension !== otherExtension) { + return collator.compare(oneExtension, otherExtension); + } + + return 0; +} + +/** Compares filenames by extension, then by name case, then by name. Sorts lowercase names before uppercase. */ +export function compareFileExtensionsLower(one: string | null, other: string | null): number { + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + const collator = intlFileNameCollator.getValue().collator; + const collatorCaseInsensitive = intlFileNameCollatorCaseInsensitive.getValue().collator; + + // Check for case insensitive differences in extensions. + let result = collatorCaseInsensitive.compare(oneExtension, otherExtension); + if (result !== 0) { + return result; + } + + // Check for case differences in names. + result = compareCaseLowerFirst(oneName, otherName); + if (result !== 0) { + return result; + } + + // Check for other differences in names. + result = collator.compare(oneName, otherName); + if (result !== 0) { + return result; + } + + // Disambiguate extension case if needed. + if (oneExtension !== otherExtension) { + return collator.compare(oneExtension, otherExtension); + } + + return 0; +} + +/** Compares filenames by extension, then by name. */ +export function compareFileExtensionsMixed(one: string | null, other: string | null): number { + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + const collator = intlFileNameCollator.getValue().collator; + const collatorCaseInsensitive = intlFileNameCollatorCaseInsensitive.getValue().collator; + + // Check for case insensitive differences in extensions. + let result = collatorCaseInsensitive.compare(oneExtension, otherExtension); + if (result !== 0) { + return result; + } + + // Check for differences in names. + result = collator.compare(oneName, otherName); + if (result !== 0) { + return result; + } + + // Disambiguate extension case if needed. + if (oneExtension !== otherExtension) { + return collator.compare(oneExtension, otherExtension); + } + + return 0; +} + +/** Compares filenames by extension unicode value, then by name unicode value. */ +export function compareFileExtensionsUnicode(one: string | null, other: string | null) { + const [oneName, oneExtension] = extractNameAndExtension(one); + const [otherName, otherExtension] = extractNameAndExtension(other); + + // Check for extension differences + if (oneExtension !== otherExtension) { + return oneExtension < otherExtension ? -1 : 1; + } + + // Check for name differences. + if (oneName !== otherName) { + return oneName < otherName ? -1 : 1; + } + + return 0; +} + +const FileNameMatch = /^(.*?)(\.([^.]*))?$/; + +/** Extracts the name and extension from a full filename, with special handling for dotfiles */ function extractNameAndExtension(str?: string | null): [string, string] { const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); - return [(match && match[1]) || '', (match && match[3]) || '']; + let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || '']; + + // treat an empty filename with an extension, or a filename that starts with a dot, as a dotfile name + if (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.') { + result = [result[0] + '.' + result[1], '']; + } + + return result; } function comparePathComponents(one: string, other: string, caseSensitive = false): number { @@ -93,6 +383,68 @@ function comparePathComponents(one: string, other: string, caseSensitive = false return one < other ? -1 : 1; } +/** @returns `true` if the string is starts with a lowercase letter. Otherwise, `false`. */ +function isLower(string: string) { + const character = string.charAt(0); + + if (character.toLocaleUpperCase() !== character) { + return true; + } + + return false; +} + +/** @returns `true` if the string starts with an uppercase letter. Otherwise, `false`. */ +function isUpper(string: string) { + const character = string.charAt(0); + + if (character.toLocaleLowerCase() !== character) { + return true; + } + + return false; +} + +/** + * Compares the case of the provided strings - with lowercase considered less than uppercase + * + * @returns + * ```text + * -1 if one is lowercase and other is uppercase + * 1 if one is uppercase and other is lowercase + * 0 otherwise + * ``` + */ +export function compareCaseLowerFirst(one: string, other: string): number { + if (isLower(one) && isUpper(other)) { + return -1; + } + if (isUpper(one) && isLower(other)) { + return 1; + } + return 0; +} + +/** + * Compares the case of the provided strings - with uppercase considered less than lowercase + * + * @returns + * ```text + * -1 if one is uppercase and other is lowercase + * 1 if one is lowercase and other is uppercase + * 0 otherwise + * ``` + */ +export function compareCaseUpperFirst(one: string, other: string): number { + if (isUpper(one) && isLower(other)) { + return -1; + } + if (isLower(one) && isUpper(other)) { + return 1; + } + return 0; +} + export function comparePaths(one: string, other: string, caseSensitive = false): number { const oneParts = one.split(sep); const otherParts = other.split(sep); @@ -106,7 +458,7 @@ export function comparePaths(one: string, other: string, caseSensitive = false): endOther = lastOther === i; if (endOne && endOther) { - return compareFileNames(oneParts[i], otherParts[i], caseSensitive); + return compareFileNamesNumeric(oneParts[i], otherParts[i]); } else if (endOne) { return -1; } else if (endOther) { @@ -139,7 +491,7 @@ export function compareAnything(one: string, other: string, lookFor: string): nu } // Understand file names - const r = compareFileNames(elementAName, elementBName); + const r = compareFileNamesNumeric(elementAName, elementBName); if (r !== 0) { return r; } diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index ed9eea1e19b9c..adff05173563b 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -392,7 +392,7 @@ export function anyScore(pattern: string, lowPattern: string, _patternPos: numbe //#region --- fuzzyScore --- -export function createMatches(score: undefined | FuzzyScore, offset = 0): IMatch[] { +export function createMatches(score: undefined | FuzzyScore): IMatch[] { if (typeof score === 'undefined') { return []; } @@ -404,10 +404,10 @@ export function createMatches(score: undefined | FuzzyScore, offset = 0): IMatch for (let pos = wordStart; pos < _maxLen; pos++) { if (matches[matches.length - (pos + 1)] === '1') { const last = res[res.length - 1]; - if (last && last.end === pos + offset) { - last.end = pos + offset + 1; + if (last && last.end === pos) { + last.end = pos + 1; } else { - res.push({ start: pos + offset, end: pos + offset + 1 }); + res.push({ start: pos, end: pos + 1 }); } } } diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index a79a15fdb819b..8599e94720527 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareAnything } from 'vs/base/common/comparers'; -import { matchesPrefix, IMatch, matchesCamelCase, isUpper, fuzzyScore, createMatches as createFuzzyMatches } from 'vs/base/common/filters'; +import { matchesPrefix, IMatch, matchesCamelCase, isUpper, fuzzyScore, createMatches as createFuzzyMatches, matchesStrictPrefix } from 'vs/base/common/filters'; import { sep } from 'vs/base/common/path'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings'; @@ -281,24 +281,24 @@ export type FuzzyScore2 = [number | undefined /* score */, IMatch[]]; const NO_SCORE2: FuzzyScore2 = [undefined, []]; -export function scoreFuzzy2(target: string, query: IPreparedQuery | IPreparedQueryPiece, patternStart = 0, matchOffset = 0): FuzzyScore2 { +export function scoreFuzzy2(target: string, query: IPreparedQuery | IPreparedQueryPiece, patternStart = 0, wordStart = 0): FuzzyScore2 { // Score: multiple inputs const preparedQuery = query as IPreparedQuery; if (preparedQuery.values && preparedQuery.values.length > 1) { - return doScoreFuzzy2Multiple(target, preparedQuery.values, patternStart, matchOffset); + return doScoreFuzzy2Multiple(target, preparedQuery.values, patternStart, wordStart); } // Score: single input - return doScoreFuzzy2Single(target, query, patternStart, matchOffset); + return doScoreFuzzy2Single(target, query, patternStart, wordStart); } -function doScoreFuzzy2Multiple(target: string, query: IPreparedQueryPiece[], patternStart: number, matchOffset: number): FuzzyScore2 { +function doScoreFuzzy2Multiple(target: string, query: IPreparedQueryPiece[], patternStart: number, wordStart: number): FuzzyScore2 { let totalScore = 0; const totalMatches: IMatch[] = []; for (const queryPiece of query) { - const [score, matches] = doScoreFuzzy2Single(target, queryPiece, patternStart, matchOffset); + const [score, matches] = doScoreFuzzy2Single(target, queryPiece, patternStart, wordStart); if (typeof score !== 'number') { // if a single query value does not match, return with // no score entirely, we require all queries to match @@ -314,13 +314,13 @@ function doScoreFuzzy2Multiple(target: string, query: IPreparedQueryPiece[], pat return [totalScore, normalizeMatches(totalMatches)]; } -function doScoreFuzzy2Single(target: string, query: IPreparedQueryPiece, patternStart: number, matchOffset: number): FuzzyScore2 { - const score = fuzzyScore(query.original, query.originalLowercase, patternStart, target, target.toLowerCase(), 0, true); +function doScoreFuzzy2Single(target: string, query: IPreparedQueryPiece, patternStart: number, wordStart: number): FuzzyScore2 { + const score = fuzzyScore(query.original, query.originalLowercase, patternStart, target, target.toLowerCase(), wordStart, true); if (!score) { return NO_SCORE2; } - return [score[0], createFuzzyMatches(score, matchOffset)]; + return [score[0], createFuzzyMatches(score)]; } //#endregion @@ -370,9 +370,10 @@ export interface IItemAccessor { } const PATH_IDENTITY_SCORE = 1 << 18; -const LABEL_PREFIX_SCORE = 1 << 17; -const LABEL_CAMELCASE_SCORE = 1 << 16; -const LABEL_SCORE_THRESHOLD = 1 << 15; +const LABEL_PREFIX_SCORE_MATCHCASE = 1 << 17; +const LABEL_PREFIX_SCORE_IGNORECASE = 1 << 16; +const LABEL_CAMELCASE_SCORE = 1 << 15; +const LABEL_SCORE_THRESHOLD = 1 << 14; export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: FuzzyScorerCache): IItemScore { if (!item || !query.normalized) { @@ -458,13 +459,14 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined, // Prefer label matches if told so if (preferLabelMatches) { - // Treat prefix matches on the label second highest - const prefixLabelMatch = matchesPrefix(query.normalized, label); - if (prefixLabelMatch) { - return { score: LABEL_PREFIX_SCORE, labelMatch: prefixLabelMatch }; + // Treat prefix matches on the label highest + const prefixLabelMatchIgnoreCase = matchesPrefix(query.normalized, label); + if (prefixLabelMatchIgnoreCase) { + const prefixLabelMatchStrictCase = matchesStrictPrefix(query.normalized, label); + return { score: prefixLabelMatchStrictCase ? LABEL_PREFIX_SCORE_MATCHCASE : LABEL_PREFIX_SCORE_IGNORECASE, labelMatch: prefixLabelMatchStrictCase || prefixLabelMatchIgnoreCase }; } - // Treat camelcase matches on the label third highest + // Treat camelcase matches on the label second highest const camelcaseLabelMatch = matchesCamelCase(query.normalized, label); if (camelcaseLabelMatch) { return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch }; @@ -600,10 +602,10 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared } } - // 2.) prefer label prefix matches - if (scoreA === LABEL_PREFIX_SCORE || scoreB === LABEL_PREFIX_SCORE) { + // 2.) prefer label prefix matches (match case) + if (scoreA === LABEL_PREFIX_SCORE_MATCHCASE || scoreB === LABEL_PREFIX_SCORE_MATCHCASE) { if (scoreA !== scoreB) { - return scoreA === LABEL_PREFIX_SCORE ? -1 : 1; + return scoreA === LABEL_PREFIX_SCORE_MATCHCASE ? -1 : 1; } const labelA = accessor.getItemLabel(itemA) || ''; @@ -615,7 +617,22 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared } } - // 3.) prefer camelcase matches + // 3.) prefer label prefix matches (ignore case) + if (scoreA === LABEL_PREFIX_SCORE_IGNORECASE || scoreB === LABEL_PREFIX_SCORE_IGNORECASE) { + if (scoreA !== scoreB) { + return scoreA === LABEL_PREFIX_SCORE_IGNORECASE ? -1 : 1; + } + + const labelA = accessor.getItemLabel(itemA) || ''; + const labelB = accessor.getItemLabel(itemB) || ''; + + // prefer shorter names when both match on label prefix + if (labelA.length !== labelB.length) { + return labelA.length - labelB.length; + } + } + + // 4.) prefer camelcase matches if (scoreA === LABEL_CAMELCASE_SCORE || scoreB === LABEL_CAMELCASE_SCORE) { if (scoreA !== scoreB) { return scoreA === LABEL_CAMELCASE_SCORE ? -1 : 1; @@ -636,7 +653,7 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared } } - // 4.) prefer label scores + // 5.) prefer label scores if (scoreA > LABEL_SCORE_THRESHOLD || scoreB > LABEL_SCORE_THRESHOLD) { if (scoreB < LABEL_SCORE_THRESHOLD) { return -1; @@ -647,12 +664,12 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared } } - // 5.) compare by score + // 6.) compare by score if (scoreA !== scoreB) { return scoreA > scoreB ? -1 : 1; } - // 6.) prefer matches in label over non-label matches + // 7.) prefer matches in label over non-label matches const itemAHasLabelMatches = Array.isArray(itemScoreA.labelMatch) && itemScoreA.labelMatch.length > 0; const itemBHasLabelMatches = Array.isArray(itemScoreB.labelMatch) && itemScoreB.labelMatch.length > 0; if (itemAHasLabelMatches && !itemBHasLabelMatches) { @@ -661,14 +678,14 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared return 1; } - // 7.) scores are identical, prefer more compact matches (label and description) + // 8.) scores are identical, prefer more compact matches (label and description) const itemAMatchDistance = computeLabelAndDescriptionMatchDistance(itemA, itemScoreA, accessor); const itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor); if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) { return itemBMatchDistance > itemAMatchDistance ? -1 : 1; } - // 7.) at this point, scores are identical and match compactness as well + // 9.) at this point, scores are identical and match compactness as well // for both items so we start to use the fallback compare return fallbackCompare(itemA, itemB, query, accessor); } diff --git a/src/vs/base/common/jsonSchema.ts b/src/vs/base/common/jsonSchema.ts index 3af5da703672d..1c6fea1f9c7b3 100644 --- a/src/vs/base/common/jsonSchema.ts +++ b/src/vs/base/common/jsonSchema.ts @@ -53,17 +53,18 @@ export interface IJSONSchema { then?: IJSONSchema; else?: IJSONSchema; - // VSCode extensions - defaultSnippets?: IJSONSchemaSnippet[]; // VSCode extension - errorMessage?: string; // VSCode extension - patternErrorMessage?: string; // VSCode extension - deprecationMessage?: string; // VSCode extension - enumDescriptions?: string[]; // VSCode extension - markdownEnumDescriptions?: string[]; // VSCode extension - markdownDescription?: string; // VSCode extension - doNotSuggest?: boolean; // VSCode extension - allowComments?: boolean; // VSCode extension - allowTrailingCommas?: boolean; // VSCode extension + // VS Code extensions + defaultSnippets?: IJSONSchemaSnippet[]; + errorMessage?: string; + patternErrorMessage?: string; + deprecationMessage?: string; + markdownDeprecationMessage?: string; + enumDescriptions?: string[]; + markdownEnumDescriptions?: string[]; + markdownDescription?: string; + doNotSuggest?: boolean; + allowComments?: boolean; + allowTrailingCommas?: boolean; } export interface IJSONSchemaMap { diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 67fdc1f90a28a..2b93cb2e47c7a 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -853,7 +853,7 @@ class QuickPick extends QuickInput implements IQuickPi } dom.toggleClass(this.ui.container, 'hidden-input', hideInput); const visibilities: Visibilities = { - title: !!this.title || !!this.step, + title: !!this.title || !!this.step || !!this.buttons.length, description: !!this.description, checkAll: this.canSelectMany, inputBox: !hideInput, @@ -1048,7 +1048,12 @@ class InputBox extends QuickInput implements IInputBox { if (!this.visible) { return; } - this.ui.setVisibilities({ title: !!this.title || !!this.step, description: !!this.description || !!this.step, inputBox: true, message: true }); + const visibilities: Visibilities = { + title: !!this.title || !!this.step || !!this.buttons.length, + description: !!this.description || !!this.step, + inputBox: true, message: true + }; + this.ui.setVisibilities(visibilities); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 369f160803bcc..81c7c3202ca47 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -3,7 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareFileNames, compareFileExtensions } from 'vs/base/common/comparers'; +import { + compareFileNames, + compareFileExtensions, + compareFileNamesUnicode, + compareFileNamesNumeric, + compareFileNamesUpper, + compareFileNamesLower, + compareFileNamesMixed, + compareFileExtensionsUnicode, + compareFileExtensionsNumeric, + compareFileExtensionsUpper, + compareFileExtensionsLower, + compareFileExtensionsMixed, + compareCaseUpperFirst, + compareCaseLowerFirst, +} from 'vs/base/common/comparers'; import * as assert from 'assert'; suite('Comparers', () => { @@ -21,6 +36,7 @@ suite('Comparers', () => { assert(compareFileNames('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); assert(compareFileNames('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); assert(compareFileNames('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + }); test('compareFileExtensions', () => { @@ -46,5 +62,541 @@ suite('Comparers', () => { assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); assert(compareFileExtensions('file2.ext2', 'file1.ext10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('file.ext01', 'file.ext1') < 0, 'extensions with equal numbers should be in alphabetical order'); + + }); + + test('compareFileNamesUnicode', () => { + + assert(compareFileNamesUnicode('', '') === 0, 'empty strings are equal'); + assert(compareFileNamesUnicode('.', '_') < 0, 'dot comes before underscore'); + assert(compareFileNamesUnicode('.', 'A') < 0, 'dot comes before uppercase letters'); + assert(compareFileNamesUnicode('.', 'a') < 0, 'dot comes before lowercase letters'); + assert(compareFileNamesUnicode('_', 'A') > 0, 'underscore comes after uppercase letters'); + assert(compareFileNamesUnicode('_', 'a') < 0, 'underscore comes before lowercase letters'); + assert(compareFileNamesUnicode('.exe', 'a.aaa') < 0, 'dotfile sorts before regular file'); + assert(compareFileNamesUnicode('aggregate.go', 'aggregate_repo.go') < 0, 'filenames with underscores sort after'); + assert(compareFileNamesUnicode('z', 'Á') < 0, 'all characters with accents sort after regular characters'); + assert(compareFileNamesUnicode('Z', 'a') < 0, 'all capital letters sort before all lowercase letters'); + assert(compareFileNamesUnicode('art.b', 'art.a.b') > 0, 'the whole filename is compared at once'); + + }); + + test('compareFileNamesUpper', () => { + + // same-case basic comparisons + assert(compareFileNamesUpper(null, null) === 0, 'null should be equal'); + assert(compareFileNamesUpper(null, 'abc') < 0, 'null should come before real values'); + assert(compareFileNamesUpper('', '') === 0, 'empty should be equal'); + assert(compareFileNamesUpper('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesUpper('a', 'â') === 'a'.localeCompare('â'), 'the same lowercase letter with an accent should compare according to the locale'); + assert(compareFileNamesUpper('A', 'Â') === 'A'.localeCompare('Â'), 'the same uppercase letter with an accent should compare according to the locale'); + assert(compareFileNamesUpper('art', 'artichoke') < 0, 'shorter names sort before longer names with the same root'); + + // mixed-case basic comparisons + assert(compareFileNamesUpper('Z', 'a') < 0, 'all uppercase letters sort before all lowercase letters'); + assert(compareFileNamesUpper('a', 'A') > 0, 'the same letter sorts uppercase before'); + assert(compareFileNamesUpper('â', 'Â') > 0, 'the same accented letter sorts uppercase before'); + assert(compareFileNamesUpper('art', 'Art') > 0, 'the same word sorts uppercase before'); + assert(compareFileNamesUpper('art', 'Artichoke') > 0, 'a longer uppercase word that starts with the same lowercase word sorts uppercase before'); + assert(compareFileNamesUpper('artichoke', 'Art') > 0, 'a longer lowercase word that starts with the same uppercase word sorts uppercase before'); + assert(compareFileNamesUpper('école', 'École') > 0, 'accented words sort uppercase before'); + assert(compareFileNamesUpper('a.MD', 'A.md') > 0, 'files with uppercase names sort first, regardless of extension case'); + + // same-case dotfile comparisons + assert(compareFileNamesUpper('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileNamesUpper('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesUpper('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesUpper('.env', '.gitattributes') < 0, 'dotfiles sort in alphabetical order'); + assert(compareFileNamesUpper('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesUpper('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesUpper('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // mixed-case dotfile comparisons + assert(compareFileNamesUpper('A.MD', '.md') > 0, 'dotfiles are non-case and sort before uppercase files'); + assert(compareFileNamesUpper('a.md', '.MD') > 0, 'dotfiles are non-case and sort before lowercase files'); + assert(compareFileNamesUpper('.MD', '.gitattributes') > 0, 'dotfiles are non-case, regardless of the case after the dot'); + + // same-case numeric comparisons + assert(compareFileNamesUpper('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers sort alphabetically, not numerically'); + + // mixed-case numeric comparisons + assert(compareFileNamesUpper('abc2.txt', 'Abc10.txt') > 0, 'uppercase sorts before lowercase, even if numerically after'); + + // comparisons that depend on comparing names before extensions + assert(compareFileNamesUpper('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNamesUpper('aggregate.go', 'aggregate_repo.go') < 0, 'shorter filenames sort before longer names even though dot sorts after underscore'); + + }); + + test('compareFileNamesLower', () => { + + // same-case basic comparisons + assert(compareFileNamesLower(null, null) === 0, 'null should be equal'); + assert(compareFileNamesLower(null, 'abc') < 0, 'null should come before real values'); + assert(compareFileNamesLower('', '') === 0, 'empty should be equal'); + assert(compareFileNamesLower('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesLower('a', 'â') === 'a'.localeCompare('â'), 'the same lowercase letter with an accent should compare according to the locale'); + assert(compareFileNamesLower('A', 'Â') === 'A'.localeCompare('Â'), 'the same uppercase letter with an accent should compare according to the locale'); + assert(compareFileNamesLower('art', 'artichoke') < 0, 'shorter names sort before longer names with the same root'); + + // mixed-case basic comparisons + assert(compareFileNamesLower('Z', 'a') > 0, 'all lowercase letters sort before all uppercase letters'); + assert(compareFileNamesLower('a', 'A') < 0, 'the same letter sorts lowercase before'); + assert(compareFileNamesLower('â', 'Â') < 0, 'the same accented letter sorts lowercase before'); + assert(compareFileNamesLower('art', 'Art') < 0, 'the same word sorts lowercase before'); + assert(compareFileNamesLower('art', 'Artichoke') < 0, 'a longer uppercase word that starts with the same lowercase word sorts lowercase before'); + assert(compareFileNamesLower('artichoke', 'Art') < 0, 'a longer lowercase word that starts with the same uppercase word sorts lowercase before'); + assert(compareFileNamesLower('école', 'École') < 0, 'accented words sort lowercase before'); + assert(compareFileNamesLower('a.MD', 'A.md') < 0, 'files with lowercase names sort first, regardless of extension case'); + + // same-case dotfile comparisons + assert(compareFileNamesLower('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileNamesLower('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesLower('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesLower('.env', '.gitattributes') < 0, 'dotfiles sort in alphabetical order'); + assert(compareFileNamesLower('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesLower('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesLower('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // mixed-case dotfile comparisons + assert(compareFileNamesLower('A.MD', '.md') > 0, 'dotfiles are non-case and sort before uppercase files'); + assert(compareFileNamesLower('a.md', '.MD') > 0, 'dotfiles are non-case and sort before lowercase files'); + assert(compareFileNamesLower('.md', '.Gitattributes') > 0, 'dotfiles are non-case, regardless of the case after the dot'); + + // same-case numeric comparisons + assert(compareFileNamesLower('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers sort alphabetically, not numerically'); + + // mixed-case numeric comparisons + assert(compareFileNamesLower('abc10.txt', 'Abc2.txt') < 0, 'lowercase sorts before uppercase, even if numerically after'); + + // comparisons that depend on comparing names before extensions + assert(compareFileNamesLower('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNamesLower('aggregate.go', 'aggregate_repo.go') < 0, 'shorter filenames sort before longer names even though dot sorts after underscore'); + + }); + + test('compareFileNamesMixed', () => { + + // same-case basic comparisons + assert(compareFileNamesMixed(null, null) === 0, 'null should be equal'); + assert(compareFileNamesMixed(null, 'abc') < 0, 'null should come before real values'); + assert(compareFileNamesMixed('', '') === 0, 'empty should be equal'); + assert(compareFileNamesMixed('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesMixed('a', 'â') === 'a'.localeCompare('â'), 'the same lowercase letter with an accent should compare according to the locale'); + assert(compareFileNamesMixed('A', 'Â') === 'A'.localeCompare('Â'), 'the same uppercase letter with an accent should compare according to the locale'); + assert(compareFileNamesMixed('art', 'artichoke') < 0, 'shorter names sort before longer names with the same root'); + + // mixed-case basic comparisons + assert(compareFileNamesMixed('z', 'A') > 0, 'z comes is after A'); + assert(compareFileNamesMixed('Z', 'a') > 0, 'Z comes after a'); + assert(compareFileNamesMixed('a', 'A') === 'a'.localeCompare('A'), 'the same letter sorts by locale'); + assert(compareFileNamesMixed('â', 'Â') === 'â'.localeCompare('Â'), 'the same accented letter sorts by locale'); + assert(compareFileNamesMixed('art', 'Art') === 'art'.localeCompare('Art'), 'the same word sorts by locale'); + assert(compareFileNamesMixed('art', 'Artichoke') < 0, 'a longer uppercase word that starts with the same lowercase word sorts shortest first'); + assert(compareFileNamesMixed('artichoke', 'Art') > 0, 'a longer lowercase word that starts with the same uppercase word sorts shortest first'); + assert(compareFileNamesMixed('école', 'École') === 'école'.localeCompare('École'), 'the same accented words sort by locale'); + assert(compareFileNamesMixed('a.MD', 'A.md') === 'a'.localeCompare('A'), 'files different case names sort by name locale comparison'); + + // same-case dotfile comparisons + assert(compareFileNamesMixed('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileNamesMixed('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesMixed('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesMixed('.env', '.gitattributes') < 0, 'dotfiles sort in alphabetical order'); + assert(compareFileNamesMixed('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesMixed('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesMixed('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // mixed-case dotfile comparisons + assert(compareFileNamesMixed('A.MD', '.md') > 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesMixed('a.md', '.MD') > 0, 'dotfiles sort before lowercase files'); + assert(compareFileNamesMixed('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // same-case numeric comparisons + assert(compareFileNamesMixed('abc2.txt', 'abc10.txt') === 'abc2'.localeCompare('abc10', undefined, { numeric: false }), + 'filenames with numbers sort alphabetically, not numerically'); + + // mixed-case numeric comparisons + assert(compareFileNamesMixed('abc10.txt', 'Abc2.txt') === 'abc10'.localeCompare('Abc2', undefined, { numeric: false }), + 'mixed-case names with numbers sort alphabetically by locale'); + + // comparisons that depend on comparing names before extensions + assert(compareFileNamesMixed('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNamesMixed('aggregate.go', 'aggregate_repo.go') < 0, 'shorter filenames sort before longer names even though dot sorts after underscore'); + + }); + + test('compareFileNamesNumeric', () => { + + // same-case basic comparisons + assert(compareFileNamesNumeric(null, null) === 0, 'null should be equal'); + assert(compareFileNamesNumeric(null, 'abc') < 0, 'null should come before real values'); + assert(compareFileNamesNumeric('', '') === 0, 'empty should be equal'); + assert(compareFileNamesNumeric('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesNumeric('a', 'â') === 'a'.localeCompare('â'), 'the same lowercase letter with an accent should compare according to the locale'); + assert(compareFileNamesNumeric('A', 'Â') === 'A'.localeCompare('Â'), 'the same uppercase letter with an accent should compare according to the locale'); + assert(compareFileNamesNumeric('art', 'artichoke') < 0, 'shorter names sort before longer names with the same root'); + + // mixed-case basic comparisons + assert(compareFileNamesNumeric('z', 'A') > 0, 'z comes is after A'); + assert(compareFileNamesNumeric('Z', 'a') > 0, 'Z comes after a'); + assert(compareFileNamesNumeric('a', 'A') === 'a'.localeCompare('A'), 'the same letter sorts by locale'); + assert(compareFileNamesNumeric('â', 'Â') === 'â'.localeCompare('Â'), 'the same accented letter sorts by locale'); + assert(compareFileNamesNumeric('art', 'Art') === 'art'.localeCompare('Art'), 'the same word sorts by locale'); + assert(compareFileNamesNumeric('art', 'Artichoke') < 0, 'a longer uppercase word that starts with the same lowercase word sorts shortest first'); + assert(compareFileNamesNumeric('artichoke', 'Art') > 0, 'a longer lowercase word that starts with the same uppercase word sorts shortest first'); + assert(compareFileNamesNumeric('école', 'École') === 'école'.localeCompare('École'), 'the same accented words sort by locale'); + assert(compareFileNamesNumeric('a.MD', 'A.md') === 'a'.localeCompare('A'), 'files different case names sort by name locale comparison'); + + // same-case dotfile comparisons + assert(compareFileNamesNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileNamesNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesNumeric('.env', '.gitattributes') < 0, 'dotfiles sort in alphabetical order'); + assert(compareFileNamesNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesNumeric('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // mixed-case dotfile comparisons + assert(compareFileNamesNumeric('A.MD', '.md') > 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesNumeric('a.md', '.MD') > 0, 'dotfiles sort before lowercase files'); + assert(compareFileNamesNumeric('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // same-case numeric comparisons + assert(compareFileNamesNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileNamesNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileNamesNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + + // mixed-case numeric comparisons + assert(compareFileNamesNumeric('art01', 'Art01') === 'art01'.localeCompare('Art01', undefined, { numeric: true }), + 'a numerically equivalent word of a different case compares numerically based on locale'); + + // comparisons that depend on comparing names then extensions + assert(compareFileNamesNumeric('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNamesNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'shorter filenames sort before longer names even though dot sorts after underscore'); + + }); + + test('compareFileExtensionsUnicode', () => { + + assert(compareFileExtensionsUnicode('', '') === 0, 'empty strings are equal'); + assert(compareFileExtensionsUnicode('.', '_') < 0, 'dot comes before underscore'); + assert(compareFileExtensionsUnicode('b', 'a.exe') < 0, 'names with no extension come before names with extensions'); + assert(compareFileExtensionsUnicode('a.b', 'b.a') > 0, 'names sort by extension then filename'); + assert(compareFileExtensionsUnicode('a.exe', 'b.exe') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsUnicode('.exe', 'a.aaa') < 0, 'dotfile sorts before non-dotfile'); + assert(compareFileExtensionsUnicode('aggregate.go', 'aggregate_repo.go') < 0, 'filenames with underscores sort as expected'); + assert(compareFileExtensionsUnicode('z', 'Á') < 0, 'all characters with accents sort after regular characters'); + assert(compareFileExtensionsUnicode('Z', 'a') < 0, 'all capital letters sort before all lowercase letters'); + assert(compareFileExtensionsUnicode('art.b', 'art.a.b') < 0, 'extensions are compared separately from names'); + + }); + + test('compareFileExtensionsNumeric', () => { + + // same-case basic comparisons + assert(compareFileExtensionsNumeric(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsNumeric(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsNumeric(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsNumeric('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsNumeric('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsNumeric('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + + // mixed-case basic comparisons + assert(compareFileExtensionsNumeric('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensionsNumeric('Z', 'a') > 0, 'Z comes after a'); + assert(compareFileExtensionsNumeric('a', 'A') === 'a'.localeCompare('A'), 'the same letter sorts by locale'); + assert(compareFileExtensionsNumeric('â', 'Â') === 'â'.localeCompare('Â'), 'the same accented letter sorts by locale'); + assert(compareFileExtensionsNumeric('art', 'Art') === 'art'.localeCompare('Art'), 'the same word sorts by locale'); + assert(compareFileExtensionsNumeric('art', 'Artichoke') < 0, 'a longer uppercase word that starts with the same lowercase word sorts shortest first'); + assert(compareFileExtensionsNumeric('artichoke', 'Art') > 0, 'a longer lowercase word that starts with the same uppercase word sorts shortest first'); + assert(compareFileExtensionsNumeric('école', 'École') === 'école'.localeCompare('École'), 'the same accented words sort by locale'); + + // same-case dotfile comparisons + assert(compareFileExtensionsNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsNumeric('.env', '.gitattributes') < 0, 'dotfiles sort in alphabetical order'); + assert(compareFileExtensionsNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileExtensionsNumeric('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // mixed-case dotfile comparisons + assert(compareFileExtensionsNumeric('A.MD', '.md') > 0, 'dotfiles sort before uppercase files'); + assert(compareFileExtensionsNumeric('a.md', '.MD') > 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsNumeric('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // same-case numeric comparisons + assert(compareFileExtensionsNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + + // mixed-case numeric comparisons + assert(compareFileExtensionsNumeric('art01', 'Art01') === 'art01'.localeCompare('Art01', undefined, { numeric: true }), + 'a numerically equivalent word of a different case compares numerically based on locale'); + + // comparisons that depend on comparing extensions then names + assert(compareFileExtensionsNumeric('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsNumeric('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsNumeric('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); + + // extensions with numbers comparisons + assert(compareFileExtensionsNumeric('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsNumeric('abc.txt2', 'abc.txt10') < 0, 'filenames with numbers in their extensions should sort numerically when they are multiple digits long'); + + // comparisons that depend on how extensions plus case are handled + assert(compareFileExtensionsNumeric('a.MD', 'a.md') === 'MD'.localeCompare('md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsNumeric('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsNumeric('a.MD', 'A.md') === 'a'.localeCompare('A'), 'case differences in names sort by locale'); + + + // comparisons that depend on how case and numbers are handled + assert(compareFileExtensionsNumeric('a10.txt', 'A2.txt') === 'a10.txt'.localeCompare('A2.txt', undefined, { numeric: true }), 'filenames with numbers and case differences sort numerically with case by locale'); + + }); + + test('compareFileExtensionsUpper', () => { + // same-case basic comparisons + assert(compareFileExtensionsUpper(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsUpper(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsUpper(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsUpper('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsUpper('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsUpper('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsUpper('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsUpper('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + + // mixed-case basic comparisons + assert(compareFileExtensionsUpper('z', 'A') > 0, 'uppercase A comes before lowercase z'); + assert(compareFileExtensionsUpper('Z', 'a') < 0, 'uppercase Z comes before lowercase a'); + assert(compareFileExtensionsUpper('a', 'A') > 0, 'the same letter sorts uppercase first'); + assert(compareFileExtensionsUpper('â', 'Â') > 0, 'the same accented letter sorts uppercase first'); + assert(compareFileExtensionsUpper('art', 'Art') > 0, 'the same word sorts uppercase first'); + assert(compareFileExtensionsUpper('art', 'Artichoke') > 0, 'a longer uppercase word that starts with the same lowercase word sorts uppercase first'); + assert(compareFileExtensionsUpper('artichoke', 'Art') > 0, 'a longer lowercase word that starts with the same uppercase word sorts after'); + assert(compareFileExtensionsUpper('école', 'École') > 0, 'the same accented words sort uppercase first'); + + // same-case dotfile comparisons + assert(compareFileExtensionsUpper('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsUpper('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsUpper('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsUpper('.env', '.gitattributes') < 0, 'dotfiles sort in alphabetical order'); + assert(compareFileExtensionsUpper('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsUpper('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileExtensionsUpper('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // mixed-case dotfile comparisons + assert(compareFileExtensionsUpper('A.MD', '.md') > 0, 'dotfiles sort before uppercase files'); + assert(compareFileExtensionsUpper('a.md', '.MD') > 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsUpper('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // same-case numeric comparisons + assert(compareFileExtensionsUpper('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in alphabetical order'); + assert(compareFileExtensionsUpper('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort alphabetically'); + assert(compareFileExtensionsUpper('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort alphabetically'); + assert(compareFileExtensionsUpper('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort alphabetically'); + assert(compareFileExtensionsUpper('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between sort alphabetically'); + + // mixed-case numeric comparisons + assert(compareFileExtensionsUpper('art01', 'Art01') > 0, 'a numerically equivalent word of a different case sorts uppercase first'); + + // comparisons that depend on comparing extensions then names + assert(compareFileExtensionsUpper('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsUpper('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsUpper('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); + assert(compareFileExtensionsUpper('abc2.04txt', 'abc2.2txt') < 0, 'numbers around a the extension dot separator are not compared alphabetically'); + assert(compareFileExtensionsUpper('abc.02txt', 'abc.2txt') < 0, 'equivalent numbers in extensions sort alphabetically'); + + // comparisons that depend on how extensions plus case are handled + assert(compareFileExtensionsUpper('a.MD', 'a.md') === 'MD'.localeCompare('md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsUpper('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsUpper('art.MD', 'Art.MD') > 0, 'when extensions are the same, files with uppercase names sort first'); + assert(compareFileExtensionsUpper('art.MD', 'Art.md') > 0, 'when extensions are the same except in case, files with uppercase names sort first'); + + // comparisons that depend on how case and numbers are handled + assert(compareFileExtensionsUpper('a10.txt', 'A2.txt') > 0, 'filenames with numbers and case differences sort uppercase first'); + + }); + + test('compareFileExtensionsLower', () => { + + // same-case basic comparisons + assert(compareFileExtensionsLower(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsLower(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsLower(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsLower('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsLower('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsLower('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsLower('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsLower('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + + // mixed-case basic comparisons + assert(compareFileExtensionsLower('z', 'A') < 0, 'uppercase A comes after lowercase z'); + assert(compareFileExtensionsLower('Z', 'a') > 0, 'uppercase Z comes after lowercase a'); + assert(compareFileExtensionsLower('a', 'A') < 0, 'the same letter sorts lowercase first'); + assert(compareFileExtensionsLower('â', 'Â') < 0, 'the same accented letter sorts lowercase first'); + assert(compareFileExtensionsLower('art', 'Art') < 0, 'the same word sorts lowercase first'); + assert(compareFileExtensionsLower('art', 'Artichoke') < 0, 'a longer uppercase word that starts with the same lowercase word sorts lowercase first'); + assert(compareFileExtensionsLower('artichoke', 'Art') < 0, 'a longer lowercase word that starts with the same uppercase word sorts before'); + assert(compareFileExtensionsLower('école', 'École') < 0, 'the same accented words sort lowercase first'); + + // same-case dotfile comparisons + assert(compareFileExtensionsLower('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsLower('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsLower('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsLower('.env', '.gitattributes') < 0, 'dotfiles sort in alphabetical order'); + assert(compareFileExtensionsLower('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsLower('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileExtensionsLower('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // mixed-case dotfile comparisons + assert(compareFileExtensionsLower('A.MD', '.md') > 0, 'dotfiles sort before uppercase files'); + assert(compareFileExtensionsLower('a.md', '.MD') > 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsLower('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // same-case numeric comparisons + assert(compareFileExtensionsLower('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in alphabetical order'); + assert(compareFileExtensionsLower('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort alphabetically'); + assert(compareFileExtensionsLower('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort alphabetically'); + assert(compareFileExtensionsLower('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort alphabetically'); + assert(compareFileExtensionsLower('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between sort alphabetically'); + + // mixed-case numeric comparisons + assert(compareFileExtensionsLower('art01', 'Art01') < 0, 'a numerically equivalent word of a different case sorts lowercase first'); + + // comparisons that depend on comparing extensions then names + assert(compareFileExtensionsLower('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsLower('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsLower('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); + assert(compareFileExtensionsLower('abc2.04txt', 'abc2.2txt') < 0, 'numbers around a the extension dot separator are not compared alphabetically'); + assert(compareFileExtensionsLower('abc.02txt', 'abc.2txt') < 0, 'equivalent numbers in extensions sort alphabetically'); + + // comparisons that depend on how extensions plus case are handled + assert(compareFileExtensionsLower('a.MD', 'a.md') === 'MD'.localeCompare('md'), 'case differences in extensions are sorted by locale'); + assert(compareFileExtensionsLower('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsLower('art.MD', 'Art.MD') < 0, 'when extensions are the same, files with lowercase names sort first'); + assert(compareFileExtensionsLower('art.MD', 'Art.md') < 0, 'when extensions are the same except in case, files with lowercase names sort first'); + + // comparisons that depend on how case and numbers are handled + assert(compareFileExtensionsLower('a10.txt', 'A2.txt') < 0, 'filenames with numbers and case differences sort lowercase first'); + + }); + + test('compareFileExtensionsMixed', () => { + + // same-case basic comparisons + assert(compareFileExtensionsMixed(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsMixed(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsMixed(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsMixed('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsMixed('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsMixed('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsMixed('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsMixed('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + + // mixed-case basic comparisons + assert(compareFileExtensionsMixed('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensionsMixed('Z', 'a') > 0, 'Z comes after a'); + assert(compareFileExtensionsMixed('a', 'A') === 'a'.localeCompare('A'), 'the same letter sorts by locale'); + assert(compareFileExtensionsMixed('â', 'Â') === 'â'.localeCompare('Â'), 'the same accented letter sorts by locale'); + assert(compareFileExtensionsMixed('art', 'Art') === 'art'.localeCompare('Art'), 'the same word sorts by locale'); + assert(compareFileExtensionsMixed('art', 'Artichoke') < 0, 'a longer uppercase word that starts with the same lowercase word sorts shortest first'); + assert(compareFileExtensionsMixed('artichoke', 'Art') > 0, 'a longer lowercase word that starts with the same uppercase word sorts shortest first'); + assert(compareFileExtensionsMixed('école', 'École') === 'école'.localeCompare('École'), 'the same accented words sort by locale'); + + // same-case dotfile comparisons + assert(compareFileExtensionsMixed('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsMixed('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsMixed('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsMixed('.env', '.gitattributes') < 0, 'dotfiles sort in alphabetical order'); + assert(compareFileExtensionsMixed('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsMixed('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileExtensionsMixed('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // mixed-case dotfile comparisons + assert(compareFileExtensionsMixed('A.MD', '.md') > 0, 'dotfiles sort before uppercase files'); + assert(compareFileExtensionsMixed('a.md', '.MD') > 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsMixed('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // same-case numeric comparisons + assert(compareFileExtensionsMixed('abc2.txt', 'abc10.txt') > 0, 'filenames with numbers should be in alphabetical order'); + assert(compareFileExtensionsMixed('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort alphabetically'); + assert(compareFileExtensionsMixed('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort alphabetically'); + assert(compareFileExtensionsMixed('abc02.txt', 'abc010.txt') > 0, 'filenames with numbers that have leading zeros sort alphabetically'); + assert(compareFileExtensionsMixed('abc1.10.txt', 'abc1.2.txt') < 0, 'numbers with dots between sort alphabetically'); + + // mixed-case numeric comparisons + assert(compareFileExtensionsMixed('art01', 'Art01') < 0, 'a numerically equivalent word of a different case sorts lowercase first'); + + // comparisons that depend on comparing extensions then names + assert(compareFileExtensionsMixed('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsMixed('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsMixed('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); + assert(compareFileExtensionsMixed('abc2.04txt', 'abc2.2txt') < 0, 'numbers around a the extension dot separator are not compared alphabetically'); + assert(compareFileExtensionsMixed('abc.02txt', 'abc.2txt') < 0, 'equivalent numbers in extensions sort alphabetically'); + + // comparisons that depend on how extensions plus case are handled + assert(compareFileExtensionsMixed('a.MD', 'a.md') === 'MD'.localeCompare('md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsMixed('a.MD', 'b.md') < 0, 'when extensions are the same except in case, the files sort by name'); + assert(compareFileExtensionsMixed('art.MD', 'Art.MD') === 'art'.localeCompare('Art'), 'when extensions are the same, files sort by name and locale'); + assert(compareFileExtensionsMixed('art.MD', 'Art.md') === 'art'.localeCompare('Art'), 'when extensions are the same except in case, files sort by name and locale'); + + // comparisons that depend on how case and numbers are handled + assert(compareFileExtensionsMixed('a10.txt', 'A2.txt') === 'a10'.localeCompare('A2'), 'filenames with numbers and case differences sort by locale'); + + }); + + test('compareCaseLowerFirst', () => { + + assert(compareCaseLowerFirst('', '') === 0, 'empty strings have equal case'); + assert(compareCaseLowerFirst('.', ',') === 0, 'punctuation characters have equal case'); + assert(compareCaseLowerFirst('1', '2') === 0, 'number characters have equal case'); + assert(compareCaseLowerFirst('\n', ' ') === 0, 'whitespace characters have equal case'); + assert(compareCaseLowerFirst('b', 'A') < 0, 'lowercase a is less than uppercase A'); + assert(compareCaseLowerFirst('a', 'b') === 0, 'two lowercase characters have equal case'); + assert(compareCaseLowerFirst('A', 'B') === 0, 'two uppercase characters have equal case'); + + // assert(compareCaseLowerFirst('a', ' ') > 0, 'lowercase is greater than noncase'); + // assert(compareCaseLowerFirst('A', ' ') > 0, 'uppercase is greater than noncase'); + // assert(compareCaseLowerFirst('a', '') > 0, 'lowercase is greater than empty string'); + // assert(compareCaseLowerFirst('A', '') > 0, 'uppercase is greater than empty string'); + assert(compareCaseLowerFirst('a', ' ') === 0, 'lowercase vs noncase is treated as equal'); + assert(compareCaseLowerFirst('A', ' ') === 0, 'uppercase vs noncase is treated as equal'); + assert(compareCaseLowerFirst('a', '') === 0, 'lowercase vs empty string is treated as equal'); + assert(compareCaseLowerFirst('A', '') === 0, 'uppercase vs empty string is treated as equal'); + + assert(compareCaseUpperFirst(' ', '_A') === 0, 'a leading noncase character followed by uppercase is still noncase'); + assert(compareCaseUpperFirst('1', '_a') === 0, 'a leading non-case character followed by lowercase is still noncase'); + }); + + test('compareCaseUpperFirst', () => { + + assert(compareCaseUpperFirst('', '') === 0, 'empty strings have equal case'); + assert(compareCaseUpperFirst('.', ',') === 0, 'punctuation characters have equal case'); + assert(compareCaseUpperFirst('1', '2') === 0, 'number characters have equal case'); + assert(compareCaseUpperFirst('\n', ' ') === 0, 'whitespace characters have equal case'); + assert(compareCaseUpperFirst('b', 'A') > 0, 'lowercase a is less than uppercase A'); + assert(compareCaseUpperFirst('a', 'b') === 0, 'two lowercase characters have equal case'); + assert(compareCaseUpperFirst('A', 'B') === 0, 'two uppercase characters have equal case'); + + // assert(compareCaseUpperFirst('a', ' ') > 0, 'lowercase is greater than noncase'); + // assert(compareCaseUpperFirst('A', ' ') > 0, 'uppercase is greater than noncase'); + // assert(compareCaseUpperFirst('a', '') > 0, 'lowercase is greater than empty string'); + // assert(compareCaseUpperFirst('A', '') > 0, 'uppercase is greater than empty string'); + assert(compareCaseUpperFirst('a', ' ') === 0, 'lowercase vs noncase is treated as equal'); + assert(compareCaseUpperFirst('A', ' ') === 0, 'uppercase vs noncase is treated as equal'); + assert(compareCaseUpperFirst('a', '') === 0, 'lowercase vs empty string is treated as equal'); + assert(compareCaseUpperFirst('A', '') === 0, 'uppercase vs empty string is treated as equal'); + + assert(compareCaseUpperFirst(' ', '_A') === 0, 'a leading noncase character followed by uppercase is still noncase'); + assert(compareCaseUpperFirst('1', '_a') === 0, 'a leading non-case character followed by lowercase is still noncase'); + + }); + }); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index bc961cb66d841..569becfed7b06 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -912,6 +912,19 @@ suite('Fuzzy Scorer', () => { assert.equal(res[0], resourceB); }); + test('compareFilesByScore - prefer case match (bug #96122)', function () { + const resourceA = URI.file('lists.php'); + const resourceB = URI.file('lib/Lists.php'); + + let query = 'Lists.php'; + + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + }); + test('prepareQuery', () => { assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); @@ -977,14 +990,14 @@ suite('Fuzzy Scorer', () => { const target = 'HeLlo-World'; for (const offset of [0, 3]) { - let [score, matches] = _doScore2(target, 'HeLlo-World', offset); + let [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HeLlo-World', offset); assert.ok(score); assert.equal(matches.length, 1); assert.equal(matches[0].start, 0 + offset); assert.equal(matches[0].end, target.length + offset); - [score, matches] = _doScore2(target, 'HW', offset); + [score, matches] = _doScore2(offset === 0 ? target : `123${target}`, 'HW', offset); assert.ok(score); assert.equal(matches.length, 2); diff --git a/src/vs/editor/browser/core/editorState.ts b/src/vs/editor/browser/core/editorState.ts index 9d469299bbbfe..05d9f6b0f3a72 100644 --- a/src/vs/editor/browser/core/editorState.ts +++ b/src/vs/editor/browser/core/editorState.ts @@ -6,7 +6,7 @@ import * as strings from 'vs/base/common/strings'; import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { Range, IRange } from 'vs/editor/common/core/range'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModel } from 'vs/editor/common/model'; @@ -87,19 +87,28 @@ export class EditorState { /** * A cancellation token source that cancels when the editor changes as expressed * by the provided flags + * @param range If provided, changes in position and selection within this range will not trigger cancellation */ export class EditorStateCancellationTokenSource extends EditorKeybindingCancellationTokenSource implements IDisposable { private readonly _listener = new DisposableStore(); - constructor(readonly editor: IActiveCodeEditor, flags: CodeEditorStateFlag, parent?: CancellationToken) { + constructor(readonly editor: IActiveCodeEditor, flags: CodeEditorStateFlag, range?: IRange, parent?: CancellationToken) { super(editor, parent); if (flags & CodeEditorStateFlag.Position) { - this._listener.add(editor.onDidChangeCursorPosition(_ => this.cancel())); + this._listener.add(editor.onDidChangeCursorPosition(e => { + if (!range || !Range.containsPosition(range, e.position)) { + this.cancel(); + } + })); } if (flags & CodeEditorStateFlag.Selection) { - this._listener.add(editor.onDidChangeCursorSelection(_ => this.cancel())); + this._listener.add(editor.onDidChangeCursorSelection(e => { + if (!range || !Range.containsRange(range, e.selection)) { + this.cancel(); + } + })); } if (flags & CodeEditorStateFlag.Scroll) { this._listener.add(editor.onDidScrollChange(_ => this.cancel())); diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index 7e4ae7c0d280e..8e4dcc659bdc4 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -27,11 +27,6 @@ white-space: nowrap; } -.monaco-editor.vs-dark.mac .view-lines, -.monaco-editor.hc-black.mac .view-lines { - cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text; -} - .monaco-editor .view-line { position: absolute; width: 100%; diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index bd1aa10f8a8f0..287011babea04 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -10,7 +10,7 @@ import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { Position } from 'vs/editor/common/core/position'; import { IConfiguration } from 'vs/editor/common/editorCommon'; import { TokenizationRegistry } from 'vs/editor/common/modes'; -import { editorCursorForeground, editorOverviewRulerBorder } from 'vs/editor/common/view/editorColorRegistry'; +import { editorCursorForeground, editorOverviewRulerBorder, editorOverviewRulerBackground } from 'vs/editor/common/view/editorColorRegistry'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; import { ViewContext, EditorTheme } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; @@ -60,7 +60,10 @@ class Settings { const minimapOpts = options.get(EditorOption.minimap); const minimapEnabled = minimapOpts.enabled; const minimapSide = minimapOpts.side; - const backgroundColor = (minimapEnabled ? TokenizationRegistry.getDefaultBackground() : null); + const backgroundColor = minimapEnabled + ? theme.getColor(editorOverviewRulerBackground) || TokenizationRegistry.getDefaultBackground() + : null; + if (backgroundColor === null || minimapSide === 'left') { this.backgroundColor = null; } else { diff --git a/src/vs/editor/common/commands/shiftCommand.ts b/src/vs/editor/common/commands/shiftCommand.ts index 742ad42fa9c88..52473fd2793a6 100644 --- a/src/vs/editor/common/commands/shiftCommand.ts +++ b/src/vs/editor/common/commands/shiftCommand.ts @@ -103,14 +103,14 @@ export class ShiftCommand implements ICommand { const { tabSize, indentSize, insertSpaces } = this._opts; const shouldIndentEmptyLines = (startLine === endLine); - // if indenting or outdenting on a whitespace only line - if (this._selection.isEmpty()) { - if (/^\s*$/.test(model.getLineContent(startLine))) { - this._useLastEditRangeForCursorEndPosition = true; + if (this._opts.useTabStops) { + // if indenting or outdenting on a whitespace only line + if (this._selection.isEmpty()) { + if (/^\s*$/.test(model.getLineContent(startLine))) { + this._useLastEditRangeForCursorEndPosition = true; + } } - } - if (this._opts.useTabStops) { // keep track of previous line's "miss-alignment" let previousLineExtraSpaces = 0, extraSpaces = 0; for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++, previousLineExtraSpaces = extraSpaces) { @@ -188,6 +188,11 @@ export class ShiftCommand implements ICommand { } } else { + // if indenting or outdenting on a whitespace only line + if (!this._opts.isUnshift && this._selection.isEmpty() && model.getLineLength(startLine) === 0) { + this._useLastEditRangeForCursorEndPosition = true; + } + const oneIndent = (insertSpaces ? cachedStringRepeat(' ', indentSize) : '\t'); for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 81a7be60be805..9774fc60eaf18 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -34,7 +34,6 @@ import { withUndefinedAsNull } from 'vs/base/common/types'; import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore'; import { Color } from 'vs/base/common/color'; -import { Constants } from 'vs/base/common/uint'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TextChange } from 'vs/editor/common/model/textChange'; @@ -172,6 +171,21 @@ const enum StringOffsetValidationType { SurrogatePairs = 1, } +type ContinueBracketSearchPredicate = null | (() => boolean); + +class BracketSearchCanceled { + public static INSTANCE = new BracketSearchCanceled(); + _searchCanceledBrand = undefined; + private constructor() { } +} + +function stripBracketSearchCanceled(result: T | null | BracketSearchCanceled): T | null { + if (result instanceof BracketSearchCanceled) { + return null; + } + return result; +} + export class TextModel extends Disposable implements model.ITextModel { private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB @@ -2014,7 +2028,7 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - return this._findMatchingBracketUp(data, position); + return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, null)); } public matchBracket(position: IPosition): [Range, Range] | null { @@ -2062,8 +2076,11 @@ export class TextModel extends Disposable implements model.ITextModel { // check that we didn't hit a bracket too far away from position if (foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); - const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText]); + const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText], null); if (r) { + if (r instanceof BracketSearchCanceled) { + return null; + } bestResult = r; } } @@ -2100,8 +2117,11 @@ export class TextModel extends Disposable implements model.ITextModel { // check that we didn't hit a bracket too far away from position if (foundBracket && foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); - const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText]); + const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText], null); if (r) { + if (r instanceof BracketSearchCanceled) { + return null; + } return r; } } @@ -2111,35 +2131,41 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean): [Range, Range] | null { + private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null | BracketSearchCanceled { if (!data) { return null; } - if (isOpen) { - let matched = this._findMatchingBracketDown(data, foundBracket.getEndPosition()); - if (matched) { - return [foundBracket, matched]; - } - } else { - let matched = this._findMatchingBracketUp(data, foundBracket.getStartPosition()); - if (matched) { - return [foundBracket, matched]; - } + const matched = ( + isOpen + ? this._findMatchingBracketDown(data, foundBracket.getEndPosition(), continueSearchPredicate) + : this._findMatchingBracketUp(data, foundBracket.getStartPosition(), continueSearchPredicate) + ); + + if (!matched) { + return null; } - return null; + if (matched instanceof BracketSearchCanceled) { + return matched; + } + + return [foundBracket, matched]; } - private _findMatchingBracketUp(bracket: RichEditBracket, position: Position): Range | null { + private _findMatchingBracketUp(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { // console.log('_findMatchingBracketUp: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); const languageId = bracket.languageIdentifier.id; const reversedBracketRegex = bracket.reversedRegex; let count = -1; - const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null => { + let totalCallCount = 0; + const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } const r = BracketsUtils.findPrevBracketInRange(reversedBracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (!r) { break; @@ -2214,15 +2240,19 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - private _findMatchingBracketDown(bracket: RichEditBracket, position: Position): Range | null { + private _findMatchingBracketDown(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { // console.log('_findMatchingBracketDown: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); const languageId = bracket.languageIdentifier.id; const bracketRegex = bracket.forwardRegex; let count = 1; - const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null => { + let totalCallCount = 0; + const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } const r = BracketsUtils.findNextBracketInRange(bracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (!r) { break; @@ -2452,7 +2482,16 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - public findEnclosingBrackets(_position: IPosition, maxDuration = Constants.MAX_SAFE_SMALL_INTEGER): [Range, Range] | null { + public findEnclosingBrackets(_position: IPosition, maxDuration?: number): [Range, Range] | null { + let continueSearchPredicate: ContinueBracketSearchPredicate; + if (typeof maxDuration === 'undefined') { + continueSearchPredicate = null; + } else { + const startTime = Date.now(); + continueSearchPredicate = () => { + return (Date.now() - startTime <= maxDuration); + }; + } const position = this.validatePosition(_position); const lineCount = this.getLineCount(); const savedCounts = new Map(); @@ -2468,8 +2507,13 @@ export class TextModel extends Disposable implements model.ITextModel { } counts = savedCounts.get(languageId)!; }; - const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null => { + + let totalCallCount = 0; + const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null | BracketSearchCanceled => { while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (!r) { break; @@ -2485,7 +2529,7 @@ export class TextModel extends Disposable implements model.ITextModel { } if (counts[bracket.index] === -1) { - return this._matchFoundBracket(r, bracket, false); + return this._matchFoundBracket(r, bracket, false, continueSearchPredicate); } } @@ -2496,12 +2540,7 @@ export class TextModel extends Disposable implements model.ITextModel { let languageId: LanguageId = -1; let modeBrackets: RichEditBrackets | null = null; - const startTime = Date.now(); for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const elapsedTime = Date.now() - startTime; - if (elapsedTime > maxDuration) { - return null; - } const lineTokens = this._getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this._buffer.getLineContent(lineNumber); @@ -2530,7 +2569,7 @@ export class TextModel extends Disposable implements model.ITextModel { if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return r; + return stripBracketSearchCanceled(r); } prevSearchInToken = false; } @@ -2555,7 +2594,7 @@ export class TextModel extends Disposable implements model.ITextModel { if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return r; + return stripBracketSearchCanceled(r); } } } @@ -2566,7 +2605,7 @@ export class TextModel extends Disposable implements model.ITextModel { if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return r; + return stripBracketSearchCanceled(r); } } } diff --git a/src/vs/editor/common/model/wordHelper.ts b/src/vs/editor/common/model/wordHelper.ts index 5f7eabe5e40cc..e857ddabb53dc 100644 --- a/src/vs/editor/common/model/wordHelper.ts +++ b/src/vs/editor/common/model/wordHelper.ts @@ -93,13 +93,15 @@ export function getWordAtText(column: number, wordDefinition: RegExp, text: stri // should stop so that subsequent search don't repeat previous searches const regexIndex = pos - config.windowSize * i; wordDefinition.lastIndex = Math.max(0, regexIndex); - match = _findRegexMatchEnclosingPosition(wordDefinition, text, pos, prevRegexIndex); + const thisMatch = _findRegexMatchEnclosingPosition(wordDefinition, text, pos, prevRegexIndex); - // stop: found something - if (match) { + if (!thisMatch && match) { + // stop: we have something break; } + match = thisMatch; + // stop: searched at start if (regexIndex <= 0) { break; @@ -111,7 +113,7 @@ export function getWordAtText(column: number, wordDefinition: RegExp, text: stri let result = { word: match[0], startColumn: textOffset + 1 + match.index!, - endColumn: textOffset + 1 + wordDefinition.lastIndex + endColumn: textOffset + 1 + match.index! + match[0].length }; wordDefinition.lastIndex = 0; return result; diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index d8645e68049cc..89c3c90dc59f6 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -36,6 +36,7 @@ export const editorBracketMatchBackground = registerColor('editorBracketMatch.ba export const editorBracketMatchBorder = registerColor('editorBracketMatch.border', { dark: '#888', light: '#B9B9B9', hc: contrastBorder }, nls.localize('editorBracketMatchBorder', 'Color for matching brackets boxes')); export const editorOverviewRulerBorder = registerColor('editorOverviewRuler.border', { dark: '#7f7f7f4d', light: '#7f7f7f4d', hc: '#7f7f7f4d' }, nls.localize('editorOverviewRulerBorder', 'Color of the overview ruler border.')); +export const editorOverviewRulerBackground = registerColor('editorOverviewRuler.background', null, nls.localize('editorOverviewRulerBackground', 'Background color of the editor overview ruler. Only used when the minimap is enabled and placed on the right side of the editor.')); export const editorGutter = registerColor('editorGutter.background', { dark: editorBackground, light: editorBackground, hc: editorBackground }, nls.localize('editorGutter', 'Background color of the editor gutter. The gutter contains the glyph margins and the line numbers.')); diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 14dac4565c1ac..2f619c371d859 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -150,7 +150,7 @@ export async function formatDocumentRangeWithProvider( let cts: CancellationTokenSource; if (isCodeEditor(editorOrModel)) { model = editorOrModel.getModel(); - cts = new EditorStateCancellationTokenSource(editorOrModel, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position, token); + cts = new EditorStateCancellationTokenSource(editorOrModel, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position, undefined, token); } else { model = editorOrModel; cts = new TextModelCancellationTokenSource(editorOrModel, token); @@ -238,7 +238,7 @@ export async function formatDocumentWithProvider( let cts: CancellationTokenSource; if (isCodeEditor(editorOrModel)) { model = editorOrModel.getModel(); - cts = new EditorStateCancellationTokenSource(editorOrModel, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position, token); + cts = new EditorStateCancellationTokenSource(editorOrModel, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position, undefined, token); } else { model = editorOrModel; cts = new TextModelCancellationTokenSource(editorOrModel, token); diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index 6090bd19e7ebe..d0dc6b5728558 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -960,6 +960,25 @@ suite('Editor Contrib - Line Operations', () => { model.dispose(); }); + test('Indenting on empty line should move cursor', () => { + const model = createTextModel( + [ + '' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model, useTabStops: false }, (editor) => { + const indentLinesAction = new IndentLinesAction(); + editor.setPosition(new Position(1, 1)); + + executeAction(indentLinesAction, editor); + assert.equal(model.getLineContent(1), ' '); + assert.deepEqual(editor.getSelection(), new Selection(1, 5, 1, 5)); + }); + + model.dispose(); + }); + test('issue #62112: Delete line does not work properly when multiple cursors are on line', () => { const TEXT = [ 'a', diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index b94bfc7977553..db4cc2ecbbbc5 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -257,7 +257,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // case we want to skip the container query altogether. let skipContainerQuery = false; if (symbolQuery !== query) { - [symbolScore, symbolMatches] = scoreFuzzy2(symbolLabel, { ...query, values: undefined /* disable multi-query support */ }, filterPos, symbolLabelIconOffset); + [symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, { ...query, values: undefined /* disable multi-query support */ }, filterPos, symbolLabelIconOffset); if (typeof symbolScore === 'number') { skipContainerQuery = true; // since we consumed the query, skip any container matching } @@ -265,7 +265,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Otherwise: score on the symbol query and match on the container later if (typeof symbolScore !== 'number') { - [symbolScore, symbolMatches] = scoreFuzzy2(symbolLabel, symbolQuery, filterPos, symbolLabelIconOffset); + [symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, symbolQuery, filterPos, symbolLabelIconOffset); if (typeof symbolScore !== 'number') { continue; } diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 360986dd666f7..ae87a914c9b9e 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -168,6 +168,8 @@ class RenameController implements IEditorContribution { if (this._cts.token.isCancellationRequested) { return undefined; } + this._cts.dispose(); + this._cts = new EditorStateCancellationTokenSource(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value, loc.range); // do rename at location let selection = this.editor.getSelection(); @@ -180,7 +182,7 @@ class RenameController implements IEditorContribution { } const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); - const inputFieldResult = await this._renameInputField.getValue().getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview); + const inputFieldResult = await this._renameInputField.getValue().getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, this._cts.token); // no result, only hint to focus the editor or not if (typeof inputFieldResult === 'boolean') { diff --git a/src/vs/editor/contrib/rename/renameInputField.ts b/src/vs/editor/contrib/rename/renameInputField.ts index 78fafad59a0bd..3767c0157304c 100644 --- a/src/vs/editor/contrib/rename/renameInputField.ts +++ b/src/vs/editor/contrib/rename/renameInputField.ts @@ -7,7 +7,7 @@ import 'vs/css!./renameInputField'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { IRange } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -16,6 +16,7 @@ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeServic import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { toggleClass } from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false); @@ -149,7 +150,7 @@ export class RenameInputField implements IContentWidget { } } - getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number, supportPreview: boolean): Promise { + getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number, supportPreview: boolean, token: CancellationToken): Promise { toggleClass(this._domNode!, 'preview', supportPreview); @@ -185,14 +186,7 @@ export class RenameInputField implements IContentWidget { }); }; - let onCursorChanged = () => { - const editorPosition = this._editor.getPosition(); - if (!editorPosition || !Range.containsPosition(where, editorPosition)) { - this.cancelInput(true); - } - }; - - disposeOnDone.add(this._editor.onDidChangeCursorSelection(onCursorChanged)); + token.onCancellationRequested(() => this.cancelInput(true)); disposeOnDone.add(this._editor.onDidBlurEditorWidget(() => this.cancelInput(false))); this._show(); diff --git a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts index fa6b3cbd8974c..d9fde48698281 100644 --- a/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/wordHighlighter.ts @@ -483,20 +483,20 @@ class WordHighlighterContribution extends Disposable implements IEditorContribut return editor.getContribution(WordHighlighterContribution.ID); } - private wordHighligher: WordHighlighter | null; + private wordHighlighter: WordHighlighter | null; constructor(editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService) { super(); - this.wordHighligher = null; + this.wordHighlighter = null; const createWordHighlighterIfPossible = () => { if (editor.hasModel()) { - this.wordHighligher = new WordHighlighter(editor, contextKeyService); + this.wordHighlighter = new WordHighlighter(editor, contextKeyService); } }; this._register(editor.onDidChangeModel((e) => { - if (this.wordHighligher) { - this.wordHighligher.dispose(); - this.wordHighligher = null; + if (this.wordHighlighter) { + this.wordHighlighter.dispose(); + this.wordHighlighter = null; } createWordHighlighterIfPossible(); })); @@ -504,34 +504,34 @@ class WordHighlighterContribution extends Disposable implements IEditorContribut } public saveViewState(): boolean { - if (this.wordHighligher && this.wordHighligher.hasDecorations()) { + if (this.wordHighlighter && this.wordHighlighter.hasDecorations()) { return true; } return false; } public moveNext() { - if (this.wordHighligher) { - this.wordHighligher.moveNext(); + if (this.wordHighlighter) { + this.wordHighlighter.moveNext(); } } public moveBack() { - if (this.wordHighligher) { - this.wordHighligher.moveBack(); + if (this.wordHighlighter) { + this.wordHighlighter.moveBack(); } } public restoreViewState(state: boolean | undefined): void { - if (this.wordHighligher && state) { - this.wordHighligher.restore(); + if (this.wordHighlighter && state) { + this.wordHighlighter.restore(); } } public dispose(): void { - if (this.wordHighligher) { - this.wordHighligher.dispose(); - this.wordHighligher = null; + if (this.wordHighlighter) { + this.wordHighlighter.dispose(); + this.wordHighlighter = null; } super.dispose(); } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 72fcf78f900cc..b37d17d3ce69a 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -1428,6 +1428,28 @@ suite('Editor Controller - Regression tests', () => { model.dispose(); }); + test('issue #95591: Unindenting moves cursor to beginning of line', () => { + let model = createTextModel( + [ + ' ' + ].join('\n') + ); + + withTestCodeEditor(null, { + model: model, + useTabStops: false + }, (editor, cursor) => { + moveTo(cursor, 1, 9, false); + assertCursor(cursor, new Selection(1, 9, 1, 9)); + + CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); + assert.equal(model.getLineContent(1), ' '); + assertCursor(cursor, new Selection(1, 5, 1, 5)); + }); + + model.dispose(); + }); + test('Bug #16657: [editor] Tab on empty line of zero indentation moves cursor to position (1,1)', () => { let model = createTextModel( [ diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index cd74d415f26ed..184a9a38fa8fc 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -98,8 +98,13 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin return { conflictsSettings: [], localContent: updateIgnoredSettings(originalRemoteContent, originalLocalContent, ignoredSettings, formattingOptions), remoteContent: null, hasConflicts: false }; } - /* remote and local has changed */ + /* local is empty and not synced before */ + if (baseContent === null && isEmpty(originalLocalContent)) { + const localContent = areSame(originalLocalContent, originalRemoteContent, ignoredSettings) ? null : updateIgnoredSettings(originalRemoteContent, originalLocalContent, ignoredSettings, formattingOptions); + return { conflictsSettings: [], localContent, remoteContent: null, hasConflicts: false }; + } + /* remote and local has changed */ let localContent = originalLocalContent; let remoteContent = originalRemoteContent; const local = parse(originalLocalContent); @@ -258,6 +263,11 @@ export function areSame(localContent: string, remoteContent: string, ignoredSett return true; } +export function isEmpty(content: string): boolean { + const nodes = parseSettings(content); + return nodes.length === 0; +} + function compare(from: IStringDictionary | null, to: IStringDictionary, ignored: Set): { added: Set, removed: Set, updated: Set } { const fromKeys = from ? Object.keys(from).filter(key => !ignored.has(key)) : []; const toKeys = Object.keys(to).filter(key => !ignored.has(key)); diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index e159e8188c3e8..ce6c56c87b593 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -6,15 +6,13 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; -import { parse } from 'vs/base/common/json'; import { localize } from 'vs/nls'; import { Event } from 'vs/base/common/event'; import { createCancelablePromise } from 'vs/base/common/async'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { updateIgnoredSettings, merge, getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; -import { isEmptyObject } from 'vs/base/common/types'; +import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge'; import { edit } from 'vs/platform/userDataSync/common/content'; import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -160,10 +158,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { if (localFileContent) { const formatUtils = await this.getFormattingOptions(); const content = edit(localFileContent.value.toString(), [CONFIGURATION_SYNC_STORE_KEY], undefined, formatUtils); - const settings = parse(content); - if (!isEmptyObject(settings)) { - return true; - } + return !isEmpty(content); } } catch (error) { if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 4d5e4350c8bd2..eec0c4502a187 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -26,19 +26,6 @@ import { isArray, isString, isObject } from 'vs/base/common/types'; export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; -export interface ISyncConfiguration { - sync: { - enable: boolean, - enableSettings: boolean, - enableKeybindings: boolean, - enableUIState: boolean, - enableExtensions: boolean, - keybindingsPerPlatform: boolean, - ignoredExtensions: string[], - ignoredSettings: string[] - } -} - export function getDisallowedIgnoredSettings(): string[] { const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); return Object.keys(allSettings).filter(setting => !!allSettings[setting].disallowSyncIgnore); diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index bbe7afd722e2b..c620e9ba87673 100644 --- a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts @@ -723,6 +723,23 @@ suite('SettingsMerge - Merge', () => { assert.deepEqual(actual.conflictsSettings, expectedConflicts); assert.ok(actual.hasConflicts); }); + + test('merge when remote has comments and local is empty', async () => { + const localContent = ` +{ + +}`; + const remoteContent = stringify` +{ + // this is a comment + "a": 1, +}`; + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); + assert.equal(actual.localContent, remoteContent); + assert.equal(actual.remoteContent, null); + assert.equal(actual.conflictsSettings.length, 0); + assert.ok(!actual.hasConflicts); + }); }); suite('SettingsMerge - Compute Remote Content', () => { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 5c1a9237633a2..fbec8b1f552f4 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -8173,6 +8173,7 @@ declare module 'vscode' { * } * }; * vscode.window.createTerminal({ name: 'Exit example', pty }); + * ``` */ onDidClose?: Event; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 318a4e24f419c..f0da460319c09 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -720,12 +720,12 @@ declare module 'vscode' { //#region debug: https://github.com/microsoft/vscode/issues/88230 /** - * A DebugConfigurationProviderTrigger specifies when the `provideDebugConfigurations` method of a `DebugConfigurationProvider` is triggered. + * A DebugConfigurationProviderTriggerKind specifies when the `provideDebugConfigurations` method of a `DebugConfigurationProvider` is triggered. * Currently there are two situations: to provide the initial debug configurations for a newly created launch.json or * to provide dynamically generated debug configurations when the user asks for them through the UI (e.g. via the "Select and Start Debugging" command). - * A trigger is used when registering a `DebugConfigurationProvider` with #debug.registerDebugConfigurationProvider. + * A trigger kind is used when registering a `DebugConfigurationProvider` with #debug.registerDebugConfigurationProvider. */ - export enum DebugConfigurationProviderTrigger { + export enum DebugConfigurationProviderTriggerKind { /** * `DebugConfigurationProvider.provideDebugConfigurations` is called to provide the initial debug configurations for a newly created launch.json. */ @@ -739,19 +739,19 @@ declare module 'vscode' { export namespace debug { /** * Register a [debug configuration provider](#DebugConfigurationProvider) for a specific debug type. - * The optional [trigger](#DebugConfigurationProviderTrigger) can be used to specify when the `provideDebugConfigurations` method of the provider is triggered. - * Currently two triggers are possible: with the value `Initial` (or if no trigger argument is given) the `provideDebugConfigurations` method is used to provide the initial debug configurations to be copied into a newly created launch.json. - * With the trigger `Dynamic` the `provideDebugConfigurations` method is used to dynamically determine debug configurations to be presented to the user (in addition to the static configurations from the launch.json). - * Please note that the `trigger` argument only applies to the `provideDebugConfigurations` method: so the `resolveDebugConfiguration` methods are not affected at all. - * Registering a single provider with resolve methods for different triggers, results in the same resolve methods called multiple times. + * The optional [triggerKind](#DebugConfigurationProviderTriggerKind) can be used to specify when the `provideDebugConfigurations` method of the provider is triggered. + * Currently two trigger kinds are possible: with the value `Initial` (or if no trigger kind argument is given) the `provideDebugConfigurations` method is used to provide the initial debug configurations to be copied into a newly created launch.json. + * With the trigger kind `Dynamic` the `provideDebugConfigurations` method is used to dynamically determine debug configurations to be presented to the user (in addition to the static configurations from the launch.json). + * Please note that the `triggerKind` argument only applies to the `provideDebugConfigurations` method: so the `resolveDebugConfiguration` methods are not affected at all. + * Registering a single provider with resolve methods for different trigger kinds, results in the same resolve methods called multiple times. * More than one provider can be registered for the same type. * * @param type The debug type for which the provider is registered. * @param provider The [debug configuration provider](#DebugConfigurationProvider) to register. - * @param trigger The [trigger](#DebugConfigurationProviderTrigger) for which the 'provideDebugConfiguration' method of the provider is registered. + * @param triggerKind The [trigger](#DebugConfigurationProviderTrigger) for which the 'provideDebugConfiguration' method of the provider is registered. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider, trigger?: DebugConfigurationProviderTrigger): Disposable; + export function registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider, triggerKind?: DebugConfigurationProviderTriggerKind): Disposable; } // deprecated debug API diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 78d8317731c7e..8477fb16a2985 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -15,7 +15,7 @@ import severity from 'vs/base/common/severity'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { DebugConfigurationProviderTrigger } from 'vs/workbench/api/common/extHostTypes'; +import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -155,11 +155,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.resolve(); } - public $registerDebugConfigurationProvider(debugType: string, providerTrigger: DebugConfigurationProviderTrigger, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise { + public $registerDebugConfigurationProvider(debugType: string, providerTriggerKind: DebugConfigurationProviderTriggerKind, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise { const provider = { type: debugType, - trigger: providerTrigger + triggerKind: providerTriggerKind }; if (hasProvide) { provider.provideDebugConfigurations = (folder, token) => { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fcbfb514631fb..1fe058f7251a5 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -844,8 +844,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidChangeBreakpoints(listener, thisArgs?, disposables?) { return extHostDebugService.onDidChangeBreakpoints(listener, thisArgs, disposables); }, - registerDebugConfigurationProvider(debugType: string, provider: vscode.DebugConfigurationProvider, trigger?: vscode.DebugConfigurationProviderTrigger) { - return extHostDebugService.registerDebugConfigurationProvider(debugType, provider, trigger || extHostTypes.DebugConfigurationProviderTrigger.Initial); + registerDebugConfigurationProvider(debugType: string, provider: vscode.DebugConfigurationProvider, triggerKind?: vscode.DebugConfigurationProviderTriggerKind) { + return extHostDebugService.registerDebugConfigurationProvider(debugType, provider, triggerKind || extHostTypes.DebugConfigurationProviderTriggerKind.Initial); }, registerDebugAdapterDescriptorFactory(debugType: string, factory: vscode.DebugAdapterDescriptorFactory) { return extHostDebugService.registerDebugAdapterDescriptorFactory(extension, debugType, factory); @@ -1045,7 +1045,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall, CallHierarchyItem: extHostTypes.CallHierarchyItem, DebugConsoleMode: extHostTypes.DebugConsoleMode, - DebugConfigurationProviderTrigger: extHostTypes.DebugConfigurationProviderTrigger, + DebugConfigurationProviderTriggerKind: extHostTypes.DebugConfigurationProviderTriggerKind, Decoration: extHostTypes.Decoration, UIKind: UIKind, ColorThemeKind: extHostTypes.ColorThemeKind, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 71a10aac5d869..59bd678a98fd9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -55,7 +55,7 @@ import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCell import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { DebugConfigurationProviderTrigger } from 'vs/workbench/api/common/extHostTypes'; +import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -846,7 +846,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; $acceptDAError(handle: number, name: string, message: string, stack: string | undefined): void; $acceptDAExit(handle: number, code: number | undefined, signal: string | undefined): void; - $registerDebugConfigurationProvider(type: string, trigger: DebugConfigurationProviderTrigger, hasProvideMethod: boolean, hasResolveMethod: boolean, hasResolve2Method: boolean, hasProvideDaMethod: boolean, handle: number): Promise; + $registerDebugConfigurationProvider(type: string, triggerKind: DebugConfigurationProviderTriggerKind, hasProvideMethod: boolean, hasResolveMethod: boolean, hasResolve2Method: boolean, hasProvideDaMethod: boolean, handle: number): Promise; $registerDebugAdapterDescriptorFactory(type: string, handle: number): Promise; $unregisterDebugConfigurationProvider(handle: number): void; $unregisterDebugAdapterDescriptorFactory(handle: number): void; diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index f71c17f3cc257..91fc7bbcc9b56 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -143,22 +143,22 @@ const newCommands: ApiCommand[] = [ new ApiCommand( 'vscode.executeDefinitionProvider', '_executeDefinitionProvider', 'Execute all definition providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], - new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location-instances.', mapLocationOrLocationLink) + new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), new ApiCommand( 'vscode.executeTypeDefinitionProvider', '_executeTypeDefinitionProvider', 'Execute all type definition providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], - new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location-instances.', mapLocationOrLocationLink) + new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), new ApiCommand( 'vscode.executeDeclarationProvider', '_executeDeclarationProvider', 'Execute all declaration providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], - new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location-instances.', mapLocationOrLocationLink) + new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), new ApiCommand( 'vscode.executeImplementationProvider', '_executeImplementationProvider', 'Execute all implementation providers.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], - new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location-instances.', mapLocationOrLocationLink) + new ApiCommandResult<(modes.Location | modes.LocationLink)[], (types.Location | vscode.LocationLink)[] | undefined>('A promise that resolves to an array of Location or LocationLink instances.', mapLocationOrLocationLink) ), new ApiCommand( 'vscode.executeReferenceProvider', '_executeReferenceProvider', 'Execute all reference providers.', diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index bb867725a969e..94132fed5b868 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -51,7 +51,7 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { addBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; removeBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise; - registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider, trigger: vscode.DebugConfigurationProviderTrigger): vscode.Disposable; + registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider, trigger: vscode.DebugConfigurationProviderTriggerKind): vscode.Disposable; registerDebugAdapterDescriptorFactory(extension: IExtensionDescription, type: string, factory: vscode.DebugAdapterDescriptorFactory): vscode.Disposable; registerDebugAdapterTrackerFactory(type: string, factory: vscode.DebugAdapterTrackerFactory): vscode.Disposable; asDebugSourceUri(source: vscode.DebugProtocolSource, session?: vscode.DebugSession): vscode.Uri; @@ -299,7 +299,7 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb }); } - public registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider, trigger: vscode.DebugConfigurationProviderTrigger): vscode.Disposable { + public registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider, trigger: vscode.DebugConfigurationProviderTriggerKind): vscode.Disposable { if (!provider) { return new Disposable(() => { }); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 7f27426594cc5..89aad35b2a5c2 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2652,7 +2652,7 @@ export enum DebugConsoleMode { MergeWithParent = 1 } -export enum DebugConfigurationProviderTrigger { +export enum DebugConfigurationProviderTriggerKind { /** * `DebugConfigurationProvider.provideDebugConfigurations` is called to provide the initial debug configurations for a newly created launch.json. */ diff --git a/src/vs/workbench/browser/panecomposite.ts b/src/vs/workbench/browser/panecomposite.ts index 341355e0efd54..34de2b1a02c8a 100644 --- a/src/vs/workbench/browser/panecomposite.ts +++ b/src/vs/workbench/browser/panecomposite.ts @@ -59,8 +59,8 @@ export class PaneComposite extends Composite implements IPaneComposite { return this.viewPaneContainer.getOptimalWidth(); } - openView(id: string, focus?: boolean): IView { - return this.viewPaneContainer.openView(id, focus); + openView(id: string, focus?: boolean): T | undefined { + return this.viewPaneContainer.openView(id, focus) as T; } getViewPaneContainer(): ViewPaneContainer { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 0312854aec05d..cae1413a7c2e0 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -87,6 +87,9 @@ export class ActivitybarPart extends Part implements IActivityBarService { private content: HTMLElement | undefined; + private homeBar: ActionBar | undefined; + private homeBarContainer: HTMLElement | undefined; + private menuBar: CustomMenubarControl | undefined; private menuBarContainer: HTMLElement | undefined; @@ -353,20 +356,20 @@ export class ActivitybarPart extends Part implements IActivityBarService { } private createHomeBar(command: string, title: string, icon: Codicon): void { - const homeBarContainer = document.createElement('div'); - homeBarContainer.setAttribute('aria-label', nls.localize('homeIndicator', "Home")); - homeBarContainer.setAttribute('role', 'toolbar'); - addClass(homeBarContainer, 'home-bar'); + this.homeBarContainer = document.createElement('div'); + this.homeBarContainer.setAttribute('aria-label', nls.localize('homeIndicator', "Home")); + this.homeBarContainer.setAttribute('role', 'toolbar'); + addClass(this.homeBarContainer, 'home-bar'); - const homeActionBar = this._register(new ActionBar(homeBarContainer, { + this.homeBar = this._register(new ActionBar(this.homeBarContainer, { orientation: ActionsOrientation.VERTICAL, animated: false })); - homeActionBar.push(this._register(this.instantiationService.createInstance(HomeAction, command, title, icon)), { icon: true, label: false }); + this.homeBar.push(this._register(this.instantiationService.createInstance(HomeAction, command, title, icon)), { icon: true, label: false }); const content = assertIsDefined(this.content); - content.prepend(homeBarContainer); + content.prepend(this.homeBarContainer); } updateStyles(): void { @@ -582,12 +585,15 @@ export class ActivitybarPart extends Part implements IActivityBarService { // Layout composite bar let availableHeight = contentAreaSize.height; - if (this.globalActivityActionBar) { - availableHeight -= (this.globalActivityActionBar.viewItems.length * ActivitybarPart.ACTION_HEIGHT); // adjust height for global actions showing + if (this.homeBarContainer) { + availableHeight -= this.homeBarContainer.clientHeight; } if (this.menuBarContainer) { availableHeight -= this.menuBarContainer.clientHeight; } + if (this.globalActivityActionBar) { + availableHeight -= (this.globalActivityActionBar.viewItems.length * ActivitybarPart.ACTION_HEIGHT); // adjust height for global actions showing + } this.compositeBar.layout(new Dimension(width, availableHeight)); } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 7311f8f8746a7..7f52572c03b4e 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { compareFileNames } from 'vs/base/common/comparers'; +import { compareFileNamesNumeric } from 'vs/base/common/comparers'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; @@ -351,7 +351,7 @@ export class FileSorter implements ITreeSorter { } if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) { // same type -> compare on names - return compareFileNames(a.name, b.name); + return compareFileNamesNumeric(a.name, b.name); } else if ((a as IFileStat).isDirectory) { return -1; } else { diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 255a3b71784c9..482abde418911 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -52,9 +52,7 @@ export function getEditorPartOptions(config: IWorkbenchEditorConfiguration): IEd return options; } - if (typeof config.workbench.iconTheme === 'string') { - options.iconTheme = config.workbench.iconTheme; - } + options.iconTheme = config.workbench.iconTheme; if (config.workbench.editor) { Object.assign(options, config.workbench.editor); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 42771f4ae67d3..4e69cc1670aae 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -441,7 +441,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.screenRedearModeElement.value = this.statusbarService.addEntry({ text, ariaLabel: text, - tooltip: nls.localize('screenReaderDetectedExtra', "If you are not using a Screen Reader, please change the setting `editor.accessibilitySupport` to \"off\"."), command: 'showEditorScreenReaderNotification', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index bab188e9b6fca..f3323d215ed22 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -695,6 +695,8 @@ class StatusbarEntryItem extends Disposable { } if (!this.entry || entry.ariaLabel !== this.entry.ariaLabel) { + // Set the aria label on both elements so screen readers would read the correct thing without duplication #96210 + this.container.setAttribute('aria-label', entry.ariaLabel); this.labelContainer.setAttribute('aria-label', entry.ariaLabel); } diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 57c20448adca4..0430e16da41bd 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -1140,15 +1140,17 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { }); } - openView(id: string, focus?: boolean): IView { + openView(id: string, focus?: boolean): IView | undefined { let view = this.getView(id); if (!view) { this.toggleViewVisibility(id); } - view = this.getView(id)!; - view.setExpanded(true); - if (focus) { - view.focus(); + view = this.getView(id); + if (view) { + view.setExpanded(true); + if (focus) { + view.focus(); + } } return view; } @@ -1202,13 +1204,16 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } protected toggleViewVisibility(viewId: string): void { - const visible = !this.viewContainerModel.isVisible(viewId); - type ViewsToggleVisibilityClassification = { - viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - visible: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - this.telemetryService.publicLog2<{ viewId: String, visible: boolean }, ViewsToggleVisibilityClassification>('views.toggleVisibility', { viewId, visible }); - this.viewContainerModel.setVisible(viewId, visible); + // Check if view is active + if (this.viewContainerModel.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === viewId)) { + const visible = !this.viewContainerModel.isVisible(viewId); + type ViewsToggleVisibilityClassification = { + viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + visible: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + }; + this.telemetryService.publicLog2<{ viewId: String, visible: boolean }, ViewsToggleVisibilityClassification>('views.toggleVisibility', { viewId, visible }); + this.viewContainerModel.setVisible(viewId, visible); + } } private addPane(pane: ViewPane, size: number, index = this.paneItems.length - 1): void { diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index fc4125bdcaba1..2dd7417f4943f 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -149,6 +149,7 @@ export class ViewsService extends Disposable implements IViewsService { category: composite ? composite.name : localize('view category', "View"), menu: [{ id: MenuId.CommandPalette, + when: viewDescriptor.when, }], keybinding: { when: ContextKeyExpr.has(`${viewDescriptor.id}.active`), @@ -243,16 +244,22 @@ export class ViewsService extends Disposable implements IViewsService { async openView(id: string, focus: boolean): Promise { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(id); - if (viewContainer) { - const location = this.viewDescriptorService.getViewContainerLocation(viewContainer); - const compositeDescriptor = this.getComposite(viewContainer.id, location!); - if (compositeDescriptor) { - const paneComposite = await this.openComposite(compositeDescriptor.id, location!) as IPaneComposite | undefined; - if (paneComposite && paneComposite.openView) { - return paneComposite.openView(id, focus) as T; - } else if (focus) { - paneComposite?.focus(); - } + if (!viewContainer) { + return null; + } + + if (!this.viewDescriptorService.getViewContainerModel(viewContainer).activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === id)) { + return null; + } + + const location = this.viewDescriptorService.getViewContainerLocation(viewContainer); + const compositeDescriptor = this.getComposite(viewContainer.id, location!); + if (compositeDescriptor) { + const paneComposite = await this.openComposite(compositeDescriptor.id, location!) as IPaneComposite | undefined; + if (paneComposite && paneComposite.openView) { + return paneComposite.openView(id, focus) || null; + } else if (focus) { + paneComposite?.focus(); } } diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 3ff8760e8e3c6..78d6a8400bec6 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -383,7 +383,7 @@ export class EditorGroup extends Disposable { moveEditor(candidate: EditorInput, toIndex: number): EditorInput | undefined { const index = this.indexOf(candidate); - if (index < 0) { + if (index < 0 || toIndex === index) { return; } @@ -481,6 +481,10 @@ export class EditorGroup extends Disposable { isPinned(editor: EditorInput): boolean; isPinned(index: number): boolean; isPinned(arg1: EditorInput | number): boolean { + if (!this.preview) { + return true; // no preview editor + } + let editor: EditorInput; let index: number; if (typeof arg1 === 'number') { @@ -495,10 +499,6 @@ export class EditorGroup extends Disposable { return false; // editor not found } - if (!this.preview) { - return true; // no preview editor - } - return !this.matches(this.preview, editor); } diff --git a/src/vs/workbench/common/panecomposite.ts b/src/vs/workbench/common/panecomposite.ts index e33f240f14780..89ba702730528 100644 --- a/src/vs/workbench/common/panecomposite.ts +++ b/src/vs/workbench/common/panecomposite.ts @@ -7,7 +7,7 @@ import { IView, IViewPaneContainer } from 'vs/workbench/common/views'; import { IComposite } from 'vs/workbench/common/composite'; export interface IPaneComposite extends IComposite { - openView(id: string, focus?: boolean): IView; + openView(id: string, focus?: boolean): T | undefined; getViewPaneContainer(): IViewPaneContainer; saveState(): void; } diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 8f4c462da5b80..4a80422e37815 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -8,7 +8,6 @@ import { UriComponents, URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { RawContextKey, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; -import { IViewlet } from 'vs/workbench/common/viewlet'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -443,12 +442,6 @@ export interface IView { getProgressIndicator(): IProgressIndicator | undefined; } -export interface IViewsViewlet extends IViewlet { - - openView(id: string, focus?: boolean): IView; - -} - export const IViewsService = createDecorator('viewsService'); export interface IViewsService { diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css index d7736b342da06..63601ef1c7918 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css @@ -32,6 +32,7 @@ .tiw-metadata-value { font-family: var(--monaco-monospace-font); text-align: right; + word-break: break-word; } .tiw-metadata-key { vertical-align: top; diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index a8810e4b0cf87..133f4febc65ca 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -251,6 +251,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { } let text = this._compute(grammar, semanticTokens, position); this._domNode.innerHTML = text; + this._domNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; this._editor.layoutContentWidget(this); }, (err) => { this._notificationService.warn(err); @@ -552,7 +553,8 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { theme.resolveScopes(definition, scopesDefinition); const matchingRule = scopesDefinition[property]; if (matchingRule && scopesDefinition.scope) { - return `${escape(scopesDefinition.scope.join(' '))}
${matchingRule.scope}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; + const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope.join(', ') : String(matchingRule.scope); + return `${escape(scopesDefinition.scope.join(' '))}
${strScopes}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; } return ''; } else if (TokenStylingRule.is(definition)) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index ea39adf00d608..0d1a608e24997 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IKeyMods, IQuickPickSeparator, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPickSeparator, IQuickInputService, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; import { IEditor } from 'vs/editor/common/editorCommon'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IRange } from 'vs/editor/common/core/range'; @@ -12,16 +12,19 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions as QuickaccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration, IEditorPane } from 'vs/workbench/common/editor'; import { ITextModel } from 'vs/editor/common/model'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Action } from 'vs/base/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { prepareQuery } from 'vs/base/common/fuzzyScorer'; +import { SymbolKind } from 'vs/editor/common/modes'; +import { fuzzyScore, createMatches } from 'vs/base/common/filters'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -36,6 +39,8 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess }); } + //#region DocumentSymbols (text editor required) + private get configuration() { const editorConfig = this.configurationService.getValue().workbench.editor; @@ -66,6 +71,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess } } + //#endregion //#region public methods to use this picker from other pickers @@ -98,6 +104,81 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess } //#endregion + + protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { + const pane = this.editorService.activeEditorPane; + if (!pane || !TableOfContentsProviderRegistry.has(pane.getId())) { + // + return super.provideWithoutTextEditor(picker); + } + + const provider = TableOfContentsProviderRegistry.get(pane.getId())!; + const cts = new CancellationTokenSource(); + + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => cts.dispose(true))); + + picker.busy = true; + + provider.provideTableOfContents(pane, cts.token).then(entries => { + + picker.busy = false; + + if (cts.token.isCancellationRequested || !entries || entries.length === 0) { + return; + } + + const items: IGotoSymbolQuickPickItem[] = entries.map((entry, idx) => { + return { + kind: SymbolKind.File, + index: idx, + score: 0, + label: entry.label, + detail: entry.detail, + description: entry.description, + }; + }); + + disposables.add(picker.onDidAccept(() => { + picker.hide(); + const [entry] = picker.selectedItems; + entries[entry.index]?.reveal(); + })); + + const updatePickerItems = () => { + const filteredItems = items.filter(item => { + if (picker.value === '@') { + // default, no filtering, scoring... + item.score = 0; + item.highlights = undefined; + return true; + } + const score = fuzzyScore(picker.value, picker.value.toLowerCase(), 1 /*@-character*/, item.label, item.label.toLowerCase(), 0, true); + if (!score) { + return false; + } + item.score = score[1]; + item.highlights = { label: createMatches(score) }; + return true; + }); + if (filteredItems.length === 0) { + const label = localize('empty', 'No matching entries'); + picker.items = [{ label, index: -1, kind: SymbolKind.String }]; + picker.ariaLabel = label; + } else { + picker.items = filteredItems; + } + }; + updatePickerItems(); + disposables.add(picker.onDidChangeValue(updatePickerItems)); + + }).catch(err => { + onUnexpectedError(err); + picker.hide(); + }); + + return disposables; + } } Registry.as(QuickaccessExtensions.Quickaccess).registerQuickAccessProvider({ @@ -132,3 +213,43 @@ export class GotoSymbolAction extends Action { Registry.as(ActionExtensions.WorkbenchActions).registerWorkbenchAction(SyncActionDescriptor.from(GotoSymbolAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O }), 'Go to Symbol in Editor...'); + + +//#region toc definition and logic + +export interface ITableOfContentsEntry { + label: string; + detail?: string; + description?: string; + reveal(): any; +} + +export interface ITableOfContentsProvider { + provideTableOfContents(editor: T, token: CancellationToken): Promise; +} + +class ProviderRegistry { + + private readonly _provider = new Map(); + + register(type: string, provider: ITableOfContentsProvider): IDisposable { + this._provider.set(type, provider); + return toDisposable(() => { + if (this._provider.get(type) === provider) { + this._provider.delete(type); + } + }); + } + + get(type: string): ITableOfContentsProvider | undefined { + return this._provider.get(type); + } + + has(type: string): boolean { + return this._provider.has(type); + } +} + +export const TableOfContentsProviderRegistry = new ProviderRegistry(); + +//#endregion diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 5a4ce0ac6415e..96daf4773eadb 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -11,7 +11,7 @@ import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALL import { Thread, StackFrame, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MenuId, IMenu, IMenuService } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenu, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IAction, Action } from 'vs/base/common/actions'; @@ -21,7 +21,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { createAndFillInContextMenuActions, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { TreeResourceNavigator, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -40,6 +40,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { INotificationService } from 'vs/platform/notification/common/notification'; const $ = dom.$; @@ -171,8 +172,9 @@ export class CallStackView extends ViewPane { const treeContainer = renderViewTree(container); this.dataSource = new CallStackDataSource(this.debugService); + const sessionsRenderer = this.instantiationService.createInstance(SessionsRenderer, this.menu); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'CallStackView', treeContainer, new CallStackDelegate(), [ - new SessionsRenderer(this.menu, this.instantiationService, this.debugService), + sessionsRenderer, new ThreadsRenderer(this.instantiationService), this.instantiationService.createInstance(StackFramesRenderer), new ErrorsRenderer(), @@ -382,7 +384,7 @@ export class CallStackView extends ViewPane { const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, this.contextMenuService, g => g === 'inline'); + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, @@ -435,8 +437,11 @@ class SessionsRenderer implements ITreeRenderer { + if (action instanceof MenuItemAction) { + // We need the MenuEntryActionViewItem so the icon would get rendered + return new MenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + } + + return undefined; + } + }); return { session, name, state, stateLabel, label, actionBar, elementDisposable: [] }; } @@ -462,14 +476,14 @@ class SessionsRenderer implements ITreeRenderer t.stopped).pop(); const setActionBar = () => { - data.actionBar.clear(); const actions = getActions(this.instantiationService, element.element); const primary: IAction[] = actions; const secondary: IAction[] = []; const result = { primary, secondary }; - data.elementDisposable.push(createAndFillInActionBarActions(this.menu, { arg: getContextForContributedActions(session), shouldForwardArgs: true }, result, g => g === 'inline')); + data.elementDisposable.push(createAndFillInActionBarActions(this.menu, { arg: getContextForContributedActions(session), shouldForwardArgs: true }, result, g => /^inline/.test(g))); + data.actionBar.clear(); data.actionBar.push(primary, { icon: true, label: false }); }; setActionBar(); diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 0040c62c65de9..d7de689cf848d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -38,7 +38,7 @@ import { sequence } from 'vs/base/common/async'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { first } from 'vs/base/common/arrays'; import { getVisibleAndSorted } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { DebugConfigurationProviderTrigger } from 'vs/workbench/api/common/extHostTypes'; +import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); jsonRegistry.registerSchema(launchSchemaId, launchSchema); @@ -197,12 +197,12 @@ export class ConfigurationManager implements IConfigurationManager { /** * if scope is not specified,a value of DebugConfigurationProvideTrigger.Initial is assumed. */ - hasDebugConfigurationProvider(debugType: string, trigger?: DebugConfigurationProviderTrigger): boolean { - if (trigger === undefined) { - trigger = DebugConfigurationProviderTrigger.Initial; + hasDebugConfigurationProvider(debugType: string, triggerKind?: DebugConfigurationProviderTriggerKind): boolean { + if (triggerKind === undefined) { + triggerKind = DebugConfigurationProviderTriggerKind.Initial; } // check if there are providers for the given type that contribute a provideDebugConfigurations method - const providers = this.configProviders.filter(p => p.provideDebugConfigurations && (p.type === debugType) && (p.trigger === trigger)); + const providers = this.configProviders.filter(p => p.provideDebugConfigurations && (p.type === debugType) && (p.triggerKind === triggerKind)); return providers.length > 0; } @@ -241,14 +241,14 @@ export class ConfigurationManager implements IConfigurationManager { async provideDebugConfigurations(folderUri: uri | undefined, type: string, token: CancellationToken): Promise { await this.activateDebuggers('onDebugInitialConfigurations'); - const results = await Promise.all(this.configProviders.filter(p => p.type === type && p.trigger === DebugConfigurationProviderTrigger.Initial && p.provideDebugConfigurations).map(p => p.provideDebugConfigurations!(folderUri, token))); + const results = await Promise.all(this.configProviders.filter(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Initial && p.provideDebugConfigurations).map(p => p.provideDebugConfigurations!(folderUri, token))); return results.reduce((first, second) => first.concat(second), []); } async getDynamicProviders(): Promise<{ label: string, pick: () => Promise<{ launch: ILaunch, config: IConfig } | undefined> }[]> { await this.activateDebuggers('onDebugDynamicConfigurations'); - const dynamicProviders = this.configProviders.filter(p => p.trigger === DebugConfigurationProviderTrigger.Dynamic && p.provideDebugConfigurations); + const dynamicProviders = this.configProviders.filter(p => p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations); return dynamicProviders.map(provider => { return { label: this.getDebuggerLabel(provider.type)!, diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 1502adc721904..eb9ba1788f124 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -159,7 +159,9 @@ height: 100%; line-height: 22px; margin-right: 8px; - vertical-align: text-top; + background-size: 16px; + background-position: center center; + background-repeat: no-repeat; } .debug-pane .debug-call-stack .thread > .state > .label, diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index cd72f60ab7d18..9436fa087163b 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -23,7 +23,7 @@ import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { DebugConfigurationProviderTrigger } from 'vs/workbench/api/common/extHostTypes'; +import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -603,7 +603,7 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut export interface IDebugConfigurationProvider { readonly type: string; - readonly trigger: DebugConfigurationProviderTrigger; + readonly triggerKind: DebugConfigurationProviderTriggerKind; resolveDebugConfiguration?(folderUri: uri | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; resolveDebugConfigurationWithSubstitutedVariables?(folderUri: uri | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; provideDebugConfigurations?(folderUri: uri | undefined, token: CancellationToken): Promise; diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index c25220f2c9aa6..02cb9eb1768f3 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/explorerviewlet'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; -import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { ExplorerView } from 'vs/workbench/contrib/files/browser/views/explorerView'; @@ -136,7 +136,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor private createExplorerViewDescriptor(): IViewDescriptor { return { - id: ExplorerView.ID, + id: VIEW_ID, name: localize('folders', "Folders"), containerIcon: Codicon.files.classNames, ctorDescriptor: new SyncDescriptor(ExplorerView), @@ -191,7 +191,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { } protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewPane { - if (viewDescriptor.id === ExplorerView.ID) { + if (viewDescriptor.id === VIEW_ID) { // Create a delegating editor service for the explorer to be able to delay the refresh in the opened // editors view above. This is a workaround for being able to double click on a file to make it pinned // without causing the animation in the opened editors view to kick in and change scroll position. @@ -232,7 +232,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { } public getExplorerView(): ExplorerView { - return this.getView(ExplorerView.ID); + return this.getView(VIEW_ID); } public getOpenEditorsView(): OpenEditorsView { @@ -245,7 +245,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { } focus(): void { - const explorerView = this.getView(ExplorerView.ID); + const explorerView = this.getView(VIEW_ID); if (explorerView?.isExpanded()) { explorerView.focus(); } else { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 870062702aeb5..154344ce1359f 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -13,7 +13,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as strings from 'vs/base/common/strings'; import { Action } from 'vs/base/common/actions'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { VIEWLET_ID, IExplorerService, IFilesConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID, IExplorerService, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; import { toResource, SideBySideEditor, IEditorInput } from 'vs/workbench/common/editor'; @@ -51,6 +51,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Codicon } from 'vs/base/common/codicons'; import { openEditorWith } from 'vs/workbench/contrib/files/common/openWith'; +import { IViewsService } from 'vs/workbench/common/views'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -93,7 +94,6 @@ export class NewFileAction extends Action { static readonly LABEL = nls.localize('createNewFile', "New File"); constructor( - @IExplorerService explorerService: IExplorerService, @ICommandService private commandService: ICommandService ) { super('explorer.newFile', NEW_FILE_LABEL); @@ -111,7 +111,6 @@ export class NewFolderAction extends Action { static readonly LABEL = nls.localize('createNewFolder', "New Folder"); constructor( - @IExplorerService explorerService: IExplorerService, @ICommandService private commandService: ICommandService ) { super('explorer.newFolder', NEW_FOLDER_LABEL); @@ -707,6 +706,11 @@ export class CollapseExplorerView extends Action { const explorerView = explorerViewlet.getExplorerView(); if (explorerView) { explorerView.collapseAll(); + // If there is something being edited via input box make sure to close it #96198 + const editable = this.explorerService.getEditable(); + if (editable) { + await this.explorerService.setEditable(editable.stat, null); + } } } } @@ -915,10 +919,10 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole const fileService = accessor.get(IFileService); const textFileService = accessor.get(ITextFileService); const editorService = accessor.get(IEditorService); - const viewletService = accessor.get(IViewletService); + const viewsService = accessor.get(IViewsService); const notificationService = accessor.get(INotificationService); - await viewletService.openViewlet(VIEWLET_ID, true); + await viewsService.openView(VIEW_ID, true); const stats = explorerService.getContext(false); const stat = stats.length > 0 ? stats[0] : undefined; diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 605a43c8ba506..bafd88645c07d 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -14,7 +14,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { AutoSaveConfiguration, HotExitConfiguration, FILES_EXCLUDE_CONFIG, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; -import { VIEWLET_ID, SortOrder, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID, SortOrder, SortOrderOption, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; import { TextFileSaveErrorHandler } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; @@ -398,6 +398,19 @@ configurationRegistry.registerConfiguration({ ], 'description': nls.localize('sortOrder', "Controls sorting order of files and folders in the explorer.") }, + 'explorer.sortOrderOption': { + 'type': 'string', + 'enum': [SortOrderOption.Numeric, SortOrderOption.Upper, SortOrderOption.Lower, SortOrderOption.Mixed, SortOrderOption.Unicode], + 'default': SortOrderOption.Numeric, + 'enumDescriptions': [ + nls.localize('sortOrderOption.numeric', 'Mixes uppercase and lowercase names together. Numbers are sorted numerically, not alphabetically.'), + nls.localize('sortOrderOption.upper', 'Groups uppercase names before lowercase names. Numbers are sorted alphabetically.'), + nls.localize('sortOrderOption.lower', 'Groups lowercase names before uppercase names. Numbers are sorted alphabetically.'), + nls.localize('sortOrderOption.mixed', 'Mixes uppercase and lowercase names together. Numbers are sorted alphabetically.'), + nls.localize('sortOrderOption.unicode', 'Sorts names in unicode order.') + ], + 'description': nls.localize('SortOrderOption', "Further specifies the file and directory sort order.") + }, 'explorer.decorations.colors': { type: 'boolean', description: nls.localize('explorer.decorations.colors', "Controls whether file decorations should use colors."), diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 1bf660aa0df3d..a6fb47a046f85 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -123,7 +123,6 @@ export function getContext(focus: ExplorerItem[], selection: ExplorerItem[], res } export class ExplorerView extends ViewPane { - static readonly ID: string = 'workbench.explorer.fileView'; static readonly TREE_VIEW_STATE_STORAGE_KEY: string = 'workbench.explorer.treeViewState'; private tree!: WorkbenchCompressibleAsyncDataTree; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 79836599b9e61..91eda6b137e36 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -29,7 +29,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers'; +import { compareFileExtensionsNumeric, compareFileNamesUnicode, compareFileExtensionsUnicode, compareFileExtensionsUpper, compareFileExtensionsLower, compareFileExtensionsMixed, compareFileNamesNumeric, compareFileNamesUpper, compareFileNamesLower, compareFileNamesMixed } from 'vs/base/common/comparers'; import { fillResourceDataTransfers, CodeDataTransfers, extractResources, containsDragType } from 'vs/workbench/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; @@ -641,6 +641,33 @@ export class FileSorter implements ITreeSorter { } const sortOrder = this.explorerService.sortOrder; + const sortOrderOption = this.explorerService.sortOrderOption; + + let compareFileNames; + let compareFileExtensions; + switch (sortOrderOption) { + case 'upper': + compareFileNames = compareFileNamesUpper; + compareFileExtensions = compareFileExtensionsUpper; + break; + case 'lower': + compareFileNames = compareFileNamesLower; + compareFileExtensions = compareFileExtensionsLower; + break; + case 'mixed': + compareFileNames = compareFileNamesMixed; + compareFileExtensions = compareFileExtensionsMixed; + break; + case 'unicode': + compareFileNames = compareFileNamesUnicode; + compareFileExtensions = compareFileExtensionsUnicode; + break; + default: + // 'numeric' + compareFileNames = compareFileNamesNumeric; + compareFileExtensions = compareFileExtensionsNumeric; + + } // Sort Directories switch (sortOrder) { diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index bb0b8b9989a8a..c6912a80115f7 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExplorerService, IFilesConfiguration, SortOrder, IExplorerView } from 'vs/workbench/contrib/files/common/files'; +import { IExplorerService, IFilesConfiguration, SortOrder, SortOrderOption, IExplorerView } from 'vs/workbench/contrib/files/common/files'; import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { FileOperationEvent, FileOperation, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; @@ -35,6 +35,7 @@ export class ExplorerService implements IExplorerService { private readonly disposables = new DisposableStore(); private editable: { stat: ExplorerItem, data: IEditableData } | undefined; private _sortOrder: SortOrder; + private _sortOrderOption: SortOrderOption; private cutItems: ExplorerItem[] | undefined; private view: IExplorerView | undefined; private model: ExplorerModel; @@ -48,6 +49,7 @@ export class ExplorerService implements IExplorerService { @IEditorService private editorService: IEditorService, ) { this._sortOrder = this.configurationService.getValue('explorer.sortOrder'); + this._sortOrderOption = this.configurationService.getValue('explorer.sortOrderOption'); this.model = new ExplorerModel(this.contextService, this.fileService); this.disposables.add(this.model); @@ -83,6 +85,10 @@ export class ExplorerService implements IExplorerService { return this._sortOrder; } + get sortOrderOption(): SortOrderOption { + return this._sortOrderOption; + } + registerView(contextProvider: IExplorerView): void { this.view = contextProvider; } @@ -379,13 +385,22 @@ export class ExplorerService implements IExplorerService { } private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise { - const configSortOrder = configuration?.explorer?.sortOrder || 'default'; + let shouldRefresh = false; + + const configSortOrder = configuration?.explorer?.sortOrder || SortOrder.Default; if (this._sortOrder !== configSortOrder) { - const shouldRefresh = this._sortOrder !== undefined; + shouldRefresh = this._sortOrder !== undefined; this._sortOrder = configSortOrder; - if (shouldRefresh) { - await this.refresh(); - } + } + + const configSortOrderOption = configuration?.explorer?.sortOrderOption || SortOrderOption.Numeric; + if (this._sortOrderOption !== configSortOrderOption) { + shouldRefresh = shouldRefresh || this._sortOrderOption !== undefined; + this._sortOrderOption = configSortOrderOption; + } + + if (shouldRefresh) { + await this.refresh(); } } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index e60aac62a4e57..12fdd49675506 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -28,6 +28,11 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic */ export const VIEWLET_ID = 'workbench.view.explorer'; +/** + * Explorer file view id. + */ +export const VIEW_ID = 'workbench.explorer.fileView'; + /** * Id of the default editor for open with. */ @@ -37,6 +42,7 @@ export interface IExplorerService { _serviceBrand: undefined; readonly roots: ExplorerItem[]; readonly sortOrder: SortOrder; + readonly sortOrderOption: SortOrderOption; getContext(respectMultiSelection: boolean): ExplorerItem[]; setEditable(stat: ExplorerItem, data: IEditableData | null): Promise; @@ -120,6 +126,7 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb enableDragAndDrop: boolean; confirmDelete: boolean; sortOrder: SortOrder; + sortOrderOption: SortOrderOption; decorations: { colors: boolean; badges: boolean; @@ -142,6 +149,14 @@ export const enum SortOrder { Modified = 'modified' } +export const enum SortOrderOption { + Numeric = 'numeric', + Upper = 'upper', + Lower = 'lower', + Mixed = 'mixed', + Unicode = 'unicode', +} + export class TextFileContentProvider extends Disposable implements ITextModelContentProvider { private readonly fileWatcherDisposable = this._register(new MutableDisposable()); diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 8583446a9450b..430cecfcc6221 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -335,6 +335,10 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e, this.filterInputBox!))); this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent)); this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_UP, this.handleKeyboardEvent)); + this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.CLICK, (e) => { + e.stopPropagation(); + e.preventDefault(); + })); const focusTracker = this._register(DOM.trackFocus(this.filterInputBox.inputElement)); this._register(focusTracker.onDidFocus(() => this.focusContextKey.set(true))); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index ffd9043e2d534..cc3e2d8bcb22c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -58,7 +58,7 @@ const EXECUTE_CELL_INSERT_BELOW = 'notebook.cell.executeAndInsertBelow'; const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs'; -const NOTEBOOK_ACTIONS_CATEGORY = localize('notebookActions.category', "Notebook"); +export const NOTEBOOK_ACTIONS_CATEGORY = localize('notebookActions.category', "Notebook"); const EDITOR_WIDGET_ACTION_WEIGHT = KeybindingWeight.EditorContrib; // smaller than Suggest Widget, etc @@ -168,6 +168,7 @@ registerAction2(class extends Action2 { super({ id: EXECUTE_CELL_SELECT_BELOW, title: localize('notebookActions.executeAndSelectBelow', "Execute Notebook Cell and Select Below"), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.Shift | KeyCode.Enter, @@ -211,6 +212,7 @@ registerAction2(class extends Action2 { super({ id: EXECUTE_CELL_INSERT_BELOW, title: localize('notebookActions.executeAndInsertBelow', "Execute Notebook Cell and Insert Below"), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.Alt | KeyCode.Enter, @@ -285,6 +287,7 @@ registerAction2(class extends Action2 { super({ id: QUIT_EDIT_CELL_COMMAND_ID, title: localize('notebookActions.quitEditing', "Quit Notebook Cell Editing"), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), primary: KeyCode.Escape, @@ -316,6 +319,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: EXECUTE_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.menu.executeNotebook', "Execute Notebook (Run all cells)"), + category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/run-all' } }, order: -1, @@ -327,6 +331,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: CANCEL_NOTEBOOK_COMMAND_ID, title: localize('notebookActions.menu.cancelNotebook', "Stop Notebook Execution"), + category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/primitive-square' } }, order: -1, @@ -339,6 +344,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: EXECUTE_CELL_COMMAND_ID, title: localize('notebookActions.menu.execute', "Execute Notebook Cell"), + category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/run' } }, order: 0, @@ -983,7 +989,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: NOTEBOOK_CURSOR_DOWN, - title: 'Notebook Cursor Move Down', + title: localize('cursorMoveDown', 'Cursor Move Down'), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), EditorContextKeys.editorTextFocus, NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('top'), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none')), primary: KeyCode.DownArrow, @@ -1022,7 +1029,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: NOTEBOOK_CURSOR_UP, - title: 'Notebook Cursor Move Up', + title: localize('cursorMoveUp', 'Cursor Move Up'), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), EditorContextKeys.editorTextFocus, NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('bottom'), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none')), primary: KeyCode.UpArrow, @@ -1066,7 +1074,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: NOTEBOOK_UNDO, - title: 'Notebook Undo', + title: localize('undo', 'Undo'), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, @@ -1097,7 +1106,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: NOTEBOOK_REDO, - title: 'Notebook Redo', + title: localize('redo', 'Redo'), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z, @@ -1128,7 +1138,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: NOTEBOOK_FOCUS_TOP, - title: 'Notebook Focus First Cell', + title: localize('focusFirstCell', 'Focus First Cell'), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyMod.CtrlCmd | KeyCode.Home, @@ -1161,7 +1172,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: NOTEBOOK_FOCUS_BOTTOM, - title: 'Notebook Focus Last Cell', + title: localize('focusLastCell', 'Focus Last Cell'), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyMod.CtrlCmd | KeyCode.End, @@ -1194,7 +1206,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: CLEAR_CELL_OUTPUTS_COMMAND_ID, - title: 'Notebook Clear Active Cell Outputs', + title: localize('clearActiveCellOutputs', 'Clear Active Cell Outputs'), + category: NOTEBOOK_ACTIONS_CATEGORY, menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_RUNNABLE), @@ -1226,7 +1239,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID, - title: 'Notebook Clear All Cells Outputs', + title: localize('clearAllCellsOutputs', 'Clear All Cells Outputs'), + category: NOTEBOOK_ACTIONS_CATEGORY, menu: { id: MenuId.EditorTitle, when: NOTEBOOK_EDITOR_FOCUSED, @@ -1254,4 +1268,3 @@ registerAction2(class extends Action2 { editor.viewModel.notebookDocument.clearAllCellOutputs(); } }); - diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index f4ae1cac4c79c..5f0605296dc88 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -16,7 +16,8 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { getActiveNotebookEditor } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { getActiveNotebookEditor, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { localize } from 'vs/nls'; export class FoldingController extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.findController'; @@ -133,7 +134,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'notebook.fold', - title: 'Notebook Fold Cell', + title: localize('fold.cell', 'Fold Cell'), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyCode.LeftArrow, @@ -170,7 +172,8 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'notebook.unfold', - title: 'Notebook Unfold Cell', + title: localize('unfold.cell', 'Unfold Cell'), + category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), primary: KeyCode.RightArrow, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts new file mode 100644 index 0000000000000..0a0614e456de3 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { localize } from 'vs/nls'; +import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { getActiveNotebookEditor, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/format'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.format', + title: localize('format.title', 'Format Notebook'), + category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR), + keybinding: { + when: EditorContextKeys.editorTextFocus.toNegated(), + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F, + linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I }, + weight: KeybindingWeight.WorkbenchContrib + }, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const textModelService = accessor.get(ITextModelService); + const editorWorkerService = accessor.get(IEditorWorkerService); + const bulkEditService = accessor.get(IBulkEditService); + + const editor = getActiveNotebookEditor(editorService); + if (!editor || !editor.viewModel) { + return; + } + + const notebook = editor.viewModel.notebookDocument; + const dispoables = new DisposableStore(); + try { + + const edits: WorkspaceTextEdit[] = []; + + for (let cell of notebook.cells) { + + const ref = await textModelService.createModelReference(cell.uri); + dispoables.add(ref); + + const model = ref.object.textEditorModel; + + const formatEdits = await getDocumentFormattingEditsUntilResult( + editorWorkerService, model, + model.getOptions(), CancellationToken.None + ); + + if (formatEdits) { + formatEdits.forEach(edit => edits.push({ + edit, + resource: model.uri, + modelVersionId: model.getVersionId() + })); + } + } + + await bulkEditService.apply( + { edits }, + { label: localize('label', "Format Notebook") } + ); + + } finally { + dispoables.dispose(); + } + + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts new file mode 100644 index 0000000000000..838ffdd792628 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TableOfContentsProviderRegistry, ITableOfContentsProvider, ITableOfContentsEntry } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + + +TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements ITableOfContentsProvider { + async provideTableOfContents(editor: NotebookEditor) { + if (!editor.viewModel) { + return undefined; + } + // return an entry per markdown header + const result: ITableOfContentsEntry[] = []; + for (let cell of editor.viewModel.viewCells) { + if (cell.cellKind === CellKind.Code) { + continue; + } + const content = cell.getText(); + const matches = content.match(/^[ \t]*(\#+)(.+)$/gm); + if (matches && matches.length) { + for (let j = 0; j < matches.length; j++) { + result.push({ + label: matches[j].replace(/^[ \t]*(\#+)/, ''), + reveal: () => editor.revealInCenterIfOutsideViewport(cell) + }); + } + } + } + return result; + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index ed8ba78560f0e..96fa760072eb7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -39,8 +39,10 @@ import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/custo // Editor Contribution import 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding'; import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; +import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding'; +import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; +import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; // Output renderers registration diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index aa1de467fac76..96571b999ff6c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -17,7 +17,7 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch } from 'vs/editor/common/model'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -26,6 +26,9 @@ import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); +// Is Notebook +export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', 'workbench.editor.notebook'); + // Editor keys export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 87b52a7d06dd0..ba98354fade78 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -493,14 +493,14 @@ export class SettingsEditor2 extends BaseEditor { private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void { const elements = this.currentSettingsModel.getElementsByName(evt.targetKey); if (elements && elements[0]) { - let sourceTop = this.settingsTree.getRelativeTop(evt.source); - if (typeof sourceTop !== 'number') { - return; - } - - if (sourceTop < 0) { + let sourceTop = 0.5; + try { + const _sourceTop = this.settingsTree.getRelativeTop(evt.source); + if (_sourceTop !== null) { + sourceTop = _sourceTop; + } + } catch { // e.g. clicked a searched element, now the search has been cleared - sourceTop = 0.5; } this.settingsTree.reveal(elements[0], sourceTop); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index a17b5450d43c2..21738f1e575fd 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -26,7 +26,7 @@ import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { isIOS } from 'vs/base/common/platform'; import { ISpliceable } from 'vs/base/common/sequence'; import { escapeRegExpCharacters, startsWith } from 'vs/base/common/strings'; @@ -193,7 +193,7 @@ function getFlatSettings(settingsGroups: ISettingsGroup[]) { } interface IDisposableTemplate { - toDispose: IDisposable[]; + toDispose: DisposableStore; } interface ISettingItemTemplate extends IDisposableTemplate { @@ -209,7 +209,7 @@ interface ISettingItemTemplate extends IDisposableTemplate { otherOverridesElement: HTMLElement; syncIgnoredElement: HTMLElement; toolbar: ToolBar; - elementDisposables: IDisposable[]; + elementDisposables: DisposableStore; } interface ISettingBoolItemTemplate extends ISettingItemTemplate { @@ -370,14 +370,14 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); - const toDispose: IDisposable[] = []; + const toDispose = new DisposableStore(); const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); const toolbar = this.renderSettingToolbar(toolbarContainer); const template: ISettingItemTemplate = { toDispose, - elementDisposables: [], + elementDisposables: new DisposableStore(), containerElement: container, categoryElement, @@ -391,17 +391,17 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre }; // Prevent clicks from being handled by list - toDispose.push(DOM.addDisposableListener(controlElement, 'mousedown', e => e.stopPropagation())); + toDispose.add(DOM.addDisposableListener(controlElement, 'mousedown', e => e.stopPropagation())); - toDispose.push(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); - toDispose.push(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); + toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); + toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); return template; } protected addSettingElementFocusHandler(template: ISettingItemTemplate): void { const focusTracker = DOM.trackFocus(template.containerElement); - template.toDispose.push(focusTracker); + template.toDispose.add(focusTracker); focusTracker.onDidBlur(() => { if (template.containerElement.classList.contains('focused')) { template.containerElement.classList.remove('focused'); @@ -446,7 +446,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.context = element; template.toolbar.context = element; const actions = this.disposableActionFactory(element.setting); - template.elementDisposables?.push(...actions); + actions.forEach(a => template.elementDisposables?.add(a)); template.toolbar.setActions([], [...this.settingActions, ...actions])(); this.fixToolbarIcon(template.toolbar); @@ -466,8 +466,8 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.descriptionElement.innerHTML = ''; if (element.setting.descriptionIsMarkdown) { const disposables = new DisposableStore(); - template.toDispose.push(disposables); - const renderedDescription = this.renderDescriptionMarkdown(element, element.description, disposables); + template.toDispose.add(disposables); + const renderedDescription = this.renderSettingMarkdown(element, element.description, disposables); template.descriptionElement.appendChild(renderedDescription); } else { template.descriptionElement.innerText = element.description; @@ -496,7 +496,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre DOM.append(template.otherOverridesElement, $('span', undefined, ')')); } - template.elementDisposables.push( + template.elementDisposables.add( DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { this._onDidClickOverrideElement.fire({ targetKey: element.setting.key, @@ -510,7 +510,14 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const onChange = (value: any) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context!.valueType }); const deprecationText = element.setting.deprecationMessage || ''; - template.deprecationWarningElement.innerText = deprecationText; + if (deprecationText && element.setting.deprecationMessageIsMarkdown) { + const disposables = new DisposableStore(); + template.elementDisposables.add(disposables); + template.deprecationWarningElement.innerHTML = ''; + template.deprecationWarningElement.appendChild(this.renderSettingMarkdown(element, element.setting.deprecationMessage!, template.elementDisposables)); + } else { + template.deprecationWarningElement.innerText = deprecationText; + } DOM.toggleClass(template.containerElement, 'is-deprecated', !!deprecationText); this.renderValue(element, template, onChange); @@ -519,12 +526,12 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.syncIgnoredElement.style.display = this.ignoredSettings.includes(element.setting.key) ? 'inline' : 'none'; }; update(); - template.elementDisposables.push(this.onDidChangeIgnoredSettings(() => { + template.elementDisposables.add(this.onDidChangeIgnoredSettings(() => { update(); })); } - private renderDescriptionMarkdown(element: SettingsTreeSettingElement, text: string, disposeables: DisposableStore): HTMLElement { + private renderSettingMarkdown(element: SettingsTreeSettingElement, text: string, disposeables: DisposableStore): HTMLElement { // Rewrite `#editor.fontSize#` to link format text = fixSettingLinks(text); @@ -545,7 +552,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } }); - renderedMarkdown.classList.add('setting-item-description-markdown'); + renderedMarkdown.classList.add('setting-item-markdown'); cleanRenderedMarkdown(renderedMarkdown); return renderedMarkdown; } @@ -615,7 +622,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre disposeElement(_element: ITreeNode, _index: number, template: IDisposableTemplate, _height: number | undefined): void { if ((template as ISettingItemTemplate).elementDisposables) { - dispose((template as ISettingItemTemplate).elementDisposables); + (template as ISettingItemTemplate).elementDisposables.clear(); } } } @@ -626,10 +633,9 @@ export class SettingGroupRenderer implements ITreeRenderer { + toDispose.add(button); + toDispose.add(button.onDidClick(() => { if (template.context) { this._commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', template.context.extensionIds); } })); button.label = localize('newExtensionsButtonLabel', "Show matching extensions"); button.element.classList.add('settings-new-extensions-button'); - toDispose.push(attachButtonStyler(button, this._themeService)); + toDispose.add(attachButtonStyler(button, this._themeService)); const template: ISettingNewExtensionsTemplate = { button, @@ -699,12 +705,12 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I const common = this.renderCommonTemplate(null, container, 'complex'); const openSettingsButton = new Button(common.controlElement, { title: true, buttonBackground: undefined, buttonHoverBackground: undefined }); - common.toDispose.push(openSettingsButton); - common.toDispose.push(openSettingsButton.onDidClick(() => template.onChange!())); + common.toDispose.add(openSettingsButton); + common.toDispose.add(openSettingsButton.onDidClick(() => template.onChange!())); openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); openSettingsButton.element.classList.add('edit-in-settings-button'); - common.toDispose.push(attachButtonStyler(openSettingsButton, this._themeService, { + common.toDispose.add(attachButtonStyler(openSettingsButton, this._themeService, { buttonBackground: Color.transparent.toString(), buttonHoverBackground: Color.transparent.toString(), buttonForeground: 'foreground' @@ -756,7 +762,7 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr const listWidget = this._instantiationService.createInstance(ListSettingWidget, common.controlElement); listWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); - common.toDispose.push(listWidget); + common.toDispose.add(listWidget); const template: ISettingListItemTemplate = { ...common, @@ -766,7 +772,7 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr this.addSettingElementFocusHandler(template); - common.toDispose.push( + common.toDispose.add( listWidget.onDidChangeList(e => { const newList = this.computeNewList(template, e); this.onDidChangeList(template, newList); @@ -862,7 +868,7 @@ export class SettingExcludeRenderer extends AbstractSettingRenderer implements I const excludeWidget = this._instantiationService.createInstance(ExcludeSettingWidget, common.controlElement); excludeWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); - common.toDispose.push(excludeWidget); + common.toDispose.add(excludeWidget); const template: ISettingExcludeItemTemplate = { ...common, @@ -871,7 +877,7 @@ export class SettingExcludeRenderer extends AbstractSettingRenderer implements I this.addSettingElementFocusHandler(template); - common.toDispose.push(excludeWidget.onDidChangeList(e => this.onDidChangeExclude(template, e))); + common.toDispose.add(excludeWidget.onDidChangeList(e => this.onDidChangeExclude(template, e))); return template; } @@ -938,19 +944,19 @@ export class SettingTextRenderer extends AbstractSettingRenderer implements ITre const validationErrorMessageElement = DOM.append(common.containerElement, $('.setting-item-validation-message')); const inputBox = new InputBox(common.controlElement, this._contextViewService); - common.toDispose.push(inputBox); - common.toDispose.push(attachInputBoxStyler(inputBox, this._themeService, { + common.toDispose.add(inputBox); + common.toDispose.add(attachInputBoxStyler(inputBox, this._themeService, { inputBackground: settingsTextInputBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); - common.toDispose.push( + common.toDispose.add( inputBox.onDidChange(e => { if (template.onChange) { template.onChange(e); } })); - common.toDispose.push(inputBox); + common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); const template: ISettingTextItemTemplate = { @@ -989,8 +995,8 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre useCustomDrawn: !(isIOS && BrowserFeatures.pointerEvents) }); - common.toDispose.push(selectBox); - common.toDispose.push(attachSelectBoxStyler(selectBox, this._themeService, { + common.toDispose.add(selectBox); + common.toDispose.add(attachSelectBoxStyler(selectBox, this._themeService, { selectBackground: settingsSelectBackground, selectForeground: settingsSelectForeground, selectBorder: settingsSelectBorder, @@ -1002,7 +1008,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre selectElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); } - common.toDispose.push( + common.toDispose.add( selectBox.onDidSelect(e => { if (template.onChange) { template.onChange(e.index); @@ -1031,7 +1037,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const enumDescriptionsAreMarkdown = dataElement.setting.enumDescriptionsAreMarkdown; const disposables = new DisposableStore(); - template.toDispose.push(disposables); + template.toDispose.add(disposables); const displayOptions = dataElement.setting.enum! .map(String) @@ -1078,19 +1084,19 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT const validationErrorMessageElement = DOM.append(common.containerElement, $('.setting-item-validation-message')); const inputBox = new InputBox(common.controlElement, this._contextViewService, { type: 'number' }); - common.toDispose.push(inputBox); - common.toDispose.push(attachInputBoxStyler(inputBox, this._themeService, { + common.toDispose.add(inputBox); + common.toDispose.add(attachInputBoxStyler(inputBox, this._themeService, { inputBackground: settingsNumberInputBackground, inputForeground: settingsNumberInputForeground, inputBorder: settingsNumberInputBorder })); - common.toDispose.push( + common.toDispose.add( inputBox.onDidChange(e => { if (template.onChange) { template.onChange(e); } })); - common.toDispose.push(inputBox); + common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); const template: ISettingNumberItemTemplate = { @@ -1184,8 +1190,8 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre toDispose.add(toolbar); const template: ISettingBoolItemTemplate = { - toDispose: [toDispose], - elementDisposables: [], + toDispose, + elementDisposables: new DisposableStore(), containerElement: container, categoryElement, @@ -1627,7 +1633,7 @@ export class SettingsTree extends ObjectTree { const focusBorderColor = theme.getColor(focusBorder); if (focusBorderColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description-markdown a:focus { outline-color: ${focusBorderColor} }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:focus { outline-color: ${focusBorderColor} }`); } })); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 921a26078af0f..d17e608bb9024 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -69,16 +69,16 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description-markdown a { color: ${link}; }`); - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description-markdown a > code { color: ${link}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a { color: ${link}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a > code { color: ${link}; }`); collector.addRule(`.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a { color: ${link}; }`); collector.addRule(`.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a > code { color: ${link}; }`); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description-markdown a:hover, .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description-markdown a:active { color: ${activeLink}; }`); - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description-markdown a:hover > code, .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description-markdown a:active > code { color: ${activeLink}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:hover, .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:active { color: ${activeLink}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:hover > code, .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:active > code { color: ${activeLink}; }`); collector.addRule(`.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a:hover, .monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a:active { color: ${activeLink}; }`); collector.addRule(`.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a:hover > code, .monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a:active > code { color: ${activeLink}; }`); } @@ -126,7 +126,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const codeTextForegroundColor = theme.getColor(textPreformatForeground); if (codeTextForegroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description-markdown code { color: ${codeTextForegroundColor} }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-markdown code { color: ${codeTextForegroundColor} }`); collector.addRule(`.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown code { color: ${codeTextForegroundColor} }`); } diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index e90af1a664ba1..a22d0442bf819 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -39,7 +39,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { URI } from 'vs/base/common/uri'; import { FileKind } from 'vs/platform/files/common/files'; -import { compareFileNames } from 'vs/base/common/comparers'; +import { compareFileNamesNumeric } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IViewDescriptor, IViewDescriptorService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; @@ -352,7 +352,7 @@ export class SCMTreeSorter implements ITreeSorter { const oneName = ResourceTree.isResourceNode(one) ? one.name : basename((one as ISCMResource).sourceUri); const otherName = ResourceTree.isResourceNode(other) ? other.name : basename((other as ISCMResource).sourceUri); - return compareFileNames(oneName, otherName); + return compareFileNamesNumeric(oneName, otherName); } } @@ -865,13 +865,7 @@ export class RepositoryPane extends ViewPane { const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); this._register(actionRunner); - this._register(actionRunner.onDidRun(() => { - if (this.repository.input.visible && this.inputEditor.hasWidgetFocus()) { - return; - } - - this.tree.domFocus(); - })); + this._register(actionRunner.onDidBeforeRun(() => this.tree.domFocus())); const renderers = [ new ResourceGroupRenderer(actionViewItemProvider, this.themeService, this.menus), @@ -1083,7 +1077,7 @@ export class RepositoryPane extends ViewPane { } const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); - actionRunner.onDidRun(() => this.tree.domFocus()); + actionRunner.onDidBeforeRun(() => this.tree.domFocus()); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index 6a725605843d5..b8c5366d414d5 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -144,7 +144,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider { for (let task of tasks) { - if (ContributedTask.is(task) && task.defines.type === filter.type) { + if (ContributedTask.is(task) && ((task.defines.type === filter.type) || (task._source.label === filter.type))) { result.push(task); } else if (CustomTask.is(task)) { if (task.type === filter.type) { @@ -1574,7 +1574,17 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer for (const [handle, provider] of this._providers) { if ((type === undefined) || (type === this._providerTypes.get(handle))) { counter++; - provider.provideTasks(validTypes).then(done, error); + provider.provideTasks(validTypes).then((taskSet: TaskSet) => { + // Check that the tasks provided are of the correct type + for (const task of taskSet.tasks) { + if (task.type !== this._providerTypes.get(handle)) { + this._outputChannel.append(nls.localize('unexpectedTaskType', "The task provider for \"{0}\" tasks unexpectedly provided a task of type \"{1}\".\n", this._providerTypes.get(handle), task.type)); + this.showOutput(); + break; + } + } + return done(taskSet); + }, error); } } } else { @@ -2213,7 +2223,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } private async showTwoLevelQuickPick(placeHolder: string, defaultEntry?: TaskQuickPickEntry) { - return TaskQuickPick.show(this, this.configurationService, this.quickInputService, placeHolder, defaultEntry); + return TaskQuickPick.show(this, this.configurationService, this.quickInputService, this.notificationService, placeHolder, defaultEntry); } private async showQuickPick(tasks: Promise | Task[], placeHolder: string, defaultEntry?: TaskQuickPickEntry, group: boolean = false, sort: boolean = false, selectedEntry?: TaskQuickPickEntry, additionalEntries?: TaskQuickPickEntry[]): Promise { diff --git a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts index 24ebde4a6cbfc..f0519eda5a845 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts @@ -14,6 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail'; export const QUICKOPEN_SKIP_CONFIG = 'task.quickOpen.skip'; @@ -37,7 +38,8 @@ export class TaskQuickPick extends Disposable { constructor( private taskService: ITaskService, private configurationService: IConfigurationService, - private quickInputService: IQuickInputService) { + private quickInputService: IQuickInputService, + private notificationService: INotificationService) { super(); this.sorter = this.taskService.createSorter(); } @@ -262,11 +264,16 @@ export class TaskQuickPick extends Disposable { return task; } - return this.taskService.tryResolveTask(task); + const resolvedTask = await this.taskService.tryResolveTask(task); + + if (!resolvedTask) { + this.notificationService.error(nls.localize('noProviderForTask', "There is no task provider registered for tasks of type \"{0}\".", task.type)); + } + return resolvedTask; } - static async show(taskService: ITaskService, configurationService: IConfigurationService, quickInputService: IQuickInputService, placeHolder: string, defaultEntry?: TaskQuickPickEntry) { - const taskQuickPick = new TaskQuickPick(taskService, configurationService, quickInputService); + static async show(taskService: ITaskService, configurationService: IConfigurationService, quickInputService: IQuickInputService, notificationService: INotificationService, placeHolder: string, defaultEntry?: TaskQuickPickEntry) { + const taskQuickPick = new TaskQuickPick(taskService, configurationService, quickInputService, notificationService); return taskQuickPick.show(placeHolder, defaultEntry); } } diff --git a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts index 8fc0b0f8ad2be..89b4ff976926c 100644 --- a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts +++ b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts @@ -15,6 +15,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { TaskQuickPick, TaskTwoLevelQuickPickEntry } from 'vs/workbench/contrib/tasks/browser/taskQuickPick'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isString } from 'vs/base/common/types'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class TasksQuickAccessProvider extends PickerQuickAccessProvider { @@ -26,7 +27,8 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider = []; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 5a2cbbd4ee876..34cc78001cfec 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -29,7 +29,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { - CONTEXT_SYNC_STATE, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration, + CONTEXT_SYNC_STATE, IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT, SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview } from 'vs/platform/userDataSync/common/userDataSync'; @@ -101,10 +101,14 @@ const showSyncActivityCommand = { }; const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Preferences Sync: Show Settings"), }; +const CONTEXT_TURNING_ON_STATE = new RawContextKey('userDataSyncTurningOn', false); const CONTEXT_ACCOUNT_STATE = new RawContextKey('userDataSyncAccountStatus', AccountStatus.Uninitialized); export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution { + private static TURNING_ON_SYNC_KEY = 'userDataSync.turningOn'; + + private readonly turningOnSyncContext: IContextKey; private readonly syncEnablementContext: IContextKey; private readonly syncStatusContext: IContextKey; private readonly accountStatusContext: IContextKey; @@ -137,6 +141,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo ) { super(); + this.turningOnSyncContext = CONTEXT_TURNING_ON_STATE.bindTo(contextKeyService); this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService); @@ -147,11 +152,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (this.userDataSyncAccounts.authenticationProviders.length) { registerConfiguration(); + this.onDidChangeTurningOnState(); this.onDidChangeSyncStatus(this.userDataSyncService.status); this.onDidChangeConflicts(this.userDataSyncService.conflicts); this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled()); this.onDidChangeAccountStatus(this.userDataSyncAccounts.status); + this._register(Event.filter(storageService.onDidChangeStorage, e => e.scope === StorageScope.GLOBAL && e.key === UserDataSyncWorkbenchContribution.TURNING_ON_SYNC_KEY)(() => this.onDidChangeTurningOnState())); this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors))); @@ -170,6 +177,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } + private onDidChangeTurningOnState(): void { + this.turningOnSyncContext.set(this.storageService.getBoolean(UserDataSyncWorkbenchContribution.TURNING_ON_SYNC_KEY, StorageScope.GLOBAL, false)); + } + private onDidChangeAccountStatus(status: AccountStatus): void { this.accountStatusContext.set(status); this.updateBadge(); @@ -398,26 +409,46 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async turnOn(): Promise { - if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) { - const result = await this.dialogService.show( - Severity.Info, - localize('sync preview message', "Synchronizing your preferences is a preview feature, please read the documentation before turning it on."), - [ - localize('open doc', "Open Documentation"), - localize('turn on', "Turn On"), - localize('cancel', "Cancel"), - ], - { - cancelId: 2 + this.storageService.store(UserDataSyncWorkbenchContribution.TURNING_ON_SYNC_KEY, true, StorageScope.GLOBAL); + try { + if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) { + if (!await this.askForConfirmation()) { + return; } - ); - switch (result.choice) { - case 0: this.openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); return; - case 2: return; } + const turnOn = await this.askToConfigure(); + if (!turnOn) { + return; + } + await this.doTurnOn(); + this.storageService.store('sync.donotAskPreviewConfirmation', true, StorageScope.GLOBAL); + } finally { + this.storageService.store(UserDataSyncWorkbenchContribution.TURNING_ON_SYNC_KEY, false, StorageScope.GLOBAL); } + } - return new Promise((c, e) => { + private async askForConfirmation(): Promise { + const result = await this.dialogService.show( + Severity.Info, + localize('sync preview message', "Synchronizing your preferences is a preview feature, please read the documentation before turning it on."), + [ + localize('open doc', "Open Documentation"), + localize('turn on', "Turn On"), + localize('cancel', "Cancel"), + ], + { + cancelId: 2 + } + ); + switch (result.choice) { + case 0: this.openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); return false; + case 2: return false; + } + return true; + } + + private async askToConfigure(): Promise { + return new Promise((c, e) => { const disposables: DisposableStore = new DisposableStore(); const quickPick = this.quickInputService.createQuickPick(); disposables.add(quickPick); @@ -440,14 +471,23 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const items = this.getConfigureSyncQuickPickItems(); quickPick.items = items; quickPick.selectedItems = items.filter(item => this.userDataSyncEnablementService.isResourceEnabled(item.id)); - disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(async () => { - if (quickPick.selectedItems.length) { - this.updateConfiguration(items, quickPick.selectedItems); - this.doTurnOn().then(c, e); - quickPick.hide(); + let accepted: boolean = false; + disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(() => { + accepted = true; + quickPick.hide(); + })); + disposables.add(quickPick.onDidHide(() => { + try { + if (accepted) { + this.updateConfiguration(items, quickPick.selectedItems); + } + c(accepted); + } catch (error) { + e(error); + } finally { + disposables.dispose(); } })); - disposables.add(quickPick.onDidHide(() => disposables.dispose())); quickPick.show(); }); } @@ -466,7 +506,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo await this.handleFirstTimeSync(); this.userDataSyncEnablementService.setEnablement(true); this.notificationService.info(localize('sync turned on', "Preferences sync is turned on")); - this.storageService.store('sync.donotAskPreviewConfirmation', true, StorageScope.GLOBAL); } private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] { @@ -498,7 +537,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private async configureSyncOptions(): Promise { + private async configureSyncOptions(): Promise { return new Promise((c, e) => { const disposables: DisposableStore = new DisposableStore(); const quickPick = this.quickInputService.createQuickPick(); @@ -513,7 +552,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo quickPick.selectedItems = items.filter(item => this.userDataSyncEnablementService.isResourceEnabled(item.id)); disposables.add(quickPick.onDidAccept(async () => { if (quickPick.selectedItems.length) { - await this.updateConfiguration(items, quickPick.selectedItems); + this.updateConfiguration(items, quickPick.selectedItems); quickPick.hide(); } })); @@ -655,6 +694,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerTurnOnSyncAction(); this.registerTurnOffSyncAction(); } + this.registerTurninOnSyncAction(); this.registerSignInAction(); // When Sync is turned on from CLI this.registerShowSettingsConflictsAction(); this.registerShowKeybindingsConflictsAction(); @@ -667,7 +707,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private registerTurnOnSyncAction(): void { - const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Uninitialized)); + const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Uninitialized), CONTEXT_TURNING_ON_STATE.negate()); CommandsRegistry.registerCommand(turnOnSyncCommand.id, async () => { try { await this.turnOn(); @@ -708,6 +748,30 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } + private registerTurninOnSyncAction(): void { + const when = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Uninitialized), CONTEXT_TURNING_ON_STATE); + this._register(registerAction2(class TurningOnSyncAction extends Action2 { + constructor() { + super({ + id: 'workbench.userData.actions.turningOn', + title: localize('turnin on sync', "Turning on Preferences Sync..."), + precondition: ContextKeyExpr.false(), + menu: [{ + group: '5_sync', + id: MenuId.GlobalActivity, + when, + order: 2 + }, { + group: '1_sync', + id: MenuId.AccountsContext, + when, + }] + }); + } + async run(): Promise { } + })); + } + private registerSignInAction(): void { const that = this; const id = 'workbench.userData.actions.signin'; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts index 89ba4e168f5a9..14f16695aa6e7 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -5,7 +5,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IViewsRegistry, Extensions, ITreeViewDescriptor, ITreeViewDataProvider, ITreeItem, TreeItemCollapsibleState, IViewsService, TreeViewItemHandleArg, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; +import { IViewsRegistry, Extensions, ITreeViewDescriptor, ITreeViewDataProvider, ITreeItem, TreeItemCollapsibleState, IViewsService, TreeViewItemHandleArg, IViewContainersRegistry, ViewContainerLocation, ViewContainer, IViewDescriptorService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView'; @@ -89,8 +89,24 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { }); } async run(accessor: ServicesAccessor): Promise { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const viewsService = accessor.get(IViewsService); viewEnablementContext.set(true); - accessor.get(IViewsService).openView(id, true); + const viewContainer = viewDescriptorService.getViewContainerByViewId(id); + if (viewContainer) { + const model = viewDescriptorService.getViewContainerModel(viewContainer); + if (model.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === id)) { + viewsService.openView(id, true); + } else { + const disposable = model.onDidChangeActiveViewDescriptors(e => { + if (e.added.some(viewDescriptor => viewDescriptor.id === id)) { + disposable.dispose(); + viewsService.openView(id, true); + } + }); + } + + } } }); diff --git a/src/vs/workbench/services/keybinding/common/keybindingIO.ts b/src/vs/workbench/services/keybinding/common/keybindingIO.ts index 2d4da1d3ff30f..6a77a6bf1a213 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingIO.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingIO.ts @@ -31,12 +31,16 @@ export class KeybindingIO { if (quotedSerializedWhen.length > 0) { out.write(`${quotedSerializeCommand},`); out.writeLine(); - out.write(` "when": ${quotedSerializedWhen} `); + out.write(` "when": ${quotedSerializedWhen}`); } else { - out.write(`${quotedSerializeCommand} `); + out.write(`${quotedSerializeCommand}`); } - // out.write(String(item.weight1 + '-' + item.weight2)); - out.write('}'); + if (item.commandArgs) { + out.write(','); + out.writeLine(); + out.write(` "args": ${JSON.stringify(item.commandArgs)}`); + } + out.write(' }'); } public static readUserKeybindingItem(input: IUserFriendlyKeybinding): IUserKeybindingItem { diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 093f10ecaa565..e39388c5e7114 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -59,6 +59,7 @@ export interface ISetting { overrides?: ISetting[]; overrideOf?: ISetting; deprecationMessage?: string; + deprecationMessageIsMarkdown?: boolean; scope?: ConfigurationScope; type?: string | string[]; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 93cfa52a02e09..c59948ab5d577 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -639,7 +639,8 @@ export class DefaultSettings extends Disposable { tags: prop.tags, disallowSyncIgnore: prop.disallowSyncIgnore, extensionInfo: extensionInfo, - deprecationMessage: prop.deprecationMessage, + deprecationMessage: prop.deprecationMessage || prop.markdownDeprecationMessage, + deprecationMessageIsMarkdown: !prop.deprecationMessage, validator: createValidator(prop) }); } diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 586b62f74754f..564f59865d19e 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -75,7 +75,7 @@ export class ProgressService extends Disposable implements IProgressService { // Window progress without command can be shown as silent notification // which will first appear in the status bar and can then be brought to // the front when clicking. - return this.withNotificationProgress({ ...options, silent: true, location: ProgressLocation.Notification }, task, onDidCancel); + return this.withNotificationProgress({ delay: 150 /* default for ProgressLocation.Window */, ...options, silent: true, location: ProgressLocation.Notification }, task, onDidCancel); case ProgressLocation.Explorer: return this.withViewletProgress('workbench.view.explorer', task, { ...options, location }); case ProgressLocation.Scm: diff --git a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts index 6893e8114cd0a..cdc53cda42d77 100644 --- a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts +++ b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts @@ -30,7 +30,7 @@ class TestViewlet implements IViewlet { getControl(): IEditorControl { return null!; } focus(): void { } getOptimalWidth(): number { return 10; } - openView(id: string, focus?: boolean): IView { return null!; } + openView(id: string, focus?: boolean): T | undefined { return undefined; } getViewPaneContainer(): IViewPaneContainer { return null!; } saveState(): void { } } diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index d24fd96abea03..3feced17fa14a 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -381,11 +381,11 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode const viewDescriptor = viewDescriptorItem.viewDescriptor; if (!viewDescriptor.canToggleVisibility) { - throw new Error(`Can't toggle this view's visibility`); + continue; } - if (this.isViewDescriptorVisible(viewDescriptorItem) === visible) { - return; + if (this.isViewDescriptorVisibleWhenActive(viewDescriptorItem) === visible) { + continue; } if (viewDescriptor.workspace) { @@ -398,6 +398,11 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode viewDescriptorItem.state.size = size; } + if (this.isViewDescriptorVisible(viewDescriptorItem) !== visible) { + // do not add events if visibility is not changed + continue; + } + if (visible) { added.push({ index: visibleIndex, viewDescriptor, size: viewDescriptorItem.state.size, collapsed: !!viewDescriptorItem.state.collapsed }); } else { diff --git a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts index e6d4fd83eab9f..cf5401adde59e 100644 --- a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts @@ -382,4 +382,86 @@ suite('ViewContainerModel', () => { assert.ok(!targetEvent.called, 'remove event should not be called since it is already hidden'); }); + test('add event is not triggered if view was set visible (when visible) and not active', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true), + canToggleVisibility: true + }; + + const key = contextKeyService.createKey('showview1', true); + key.set(false); + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + const targetEvent = sinon.spy(testObject.onDidAddVisibleViewDescriptors); + testObject.setVisible('view1', true); + assert.ok(!targetEvent.called, 'add event should not be called since it is already visible'); + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + }); + + test('remove event is not triggered if view was hidden and not active', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true), + canToggleVisibility: true + }; + + const key = contextKeyService.createKey('showview1', true); + key.set(false); + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + const targetEvent = sinon.spy(testObject.onDidAddVisibleViewDescriptors); + testObject.setVisible('view1', false); + assert.ok(!targetEvent.called, 'add event should not be called since it is disabled'); + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + }); + + test('add event is not triggered if view was set visible (when not visible) and not active', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true), + canToggleVisibility: true + }; + + const key = contextKeyService.createKey('showview1', true); + key.set(false); + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + testObject.setVisible('view1', false); + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + const targetEvent = sinon.spy(testObject.onDidAddVisibleViewDescriptors); + testObject.setVisible('view1', true); + assert.ok(!targetEvent.called, 'add event should not be called since it is disabled'); + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + }); + }); diff --git a/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts b/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts index 240796587facd..a016d6560b810 100644 --- a/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts +++ b/src/vs/workbench/test/browser/api/extHostDocumentData.test.ts @@ -315,6 +315,22 @@ suite('ExtHostDocumentData', () => { assert.ok(range.contains(pos)); assert.equal(data.document.getText(range), 'TaskDefinition'); }); + + test('Rename popup sometimes populates with text on the left side omitted #96013', function () { + + const regex = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; + const line = 'int abcdefhijklmnopqwvrstxyz;'; + + data = new ExtHostDocumentData(undefined!, URI.file(''), [ + line + ], '\n', 'text', 1, false); + + let range = data.document.getWordRangeAtPosition(new Position(0, 27), regex)!; + assert.equal(range.start.line, 0); + assert.equal(range.end.line, 0); + assert.equal(range.start.character, 4); + assert.equal(range.end.character, 28); + }); }); enum AssertDocumentLineMappingDirection { diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index cc0c5547db34a..ca40282175fa9 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -141,7 +141,7 @@ function waitForEndpoint(): Promise { export function connect(browserType: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> { return new Promise(async (c) => { - const browser = await playwright[browserType].launch({ headless: false, dumpio: true }); + const browser = await playwright[browserType].launch({ headless: false }); const context = await browser.newContext(); const page = await context.newPage(); await page.setViewportSize({ width, height }); diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 99a66c318f853..7058f92ecf225 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -31,7 +31,7 @@ const height = 800; async function runTestsInBrowser(browserType: 'chromium' | 'firefox' | 'webkit', endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros - const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug), dumpio: true, args }); + const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug), args }); const context = await browser.newContext(); const page = await context.newPage(); await page.setViewportSize({ width, height }); diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 923bc7d52cf45..71514111a1069 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -119,7 +119,7 @@ const testModules = (async function () { async function runTestsInBrowser(testModules, browserType) { const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros - const browser = await playwright[browserType].launch({ headless: !Boolean(argv.debug), dumpio: true, args }); + const browser = await playwright[browserType].launch({ headless: !Boolean(argv.debug), args }); const context = await browser.newContext(); const page = await context.newPage(); const target = url.pathToFileURL(path.join(__dirname, 'renderer.html')); diff --git a/yarn.lock b/yarn.lock index d48135760c7cc..4f58d84d7b050 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3221,7 +3221,7 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@^1.0.3, extract-zip@^1.6.6: +extract-zip@^1.0.3: version "1.6.7" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= @@ -3231,6 +3231,17 @@ extract-zip@^1.0.3, extract-zip@^1.6.6: mkdirp "0.5.1" yauzl "2.4.1" +extract-zip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.0.tgz#f53b71d44f4ff5a4527a2259ade000fb8b303492" + integrity sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -5199,10 +5210,10 @@ jade@0.26.3: commander "0.6.1" mkdirp "0.3.0" -jpeg-js@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.3.6.tgz#c40382aac9506e7d1f2d856eb02f6c7b2a98b37c" - integrity sha512-MUj2XlMB8kpe+8DJUGH/3UJm4XpI8XEgZQ+CiHDeyrGoKPdW/8FJv6ku+3UiYm5Fz3CWaL+iXmD8Q4Ap6aC1Jw== +jpeg-js@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.3.7.tgz#471a89d06011640592d314158608690172b1028d" + integrity sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ== js-base64@^2.1.9: version "2.3.2" @@ -5949,6 +5960,11 @@ mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -6996,27 +7012,28 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -playwright-core@=0.12.1: - version "0.12.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-0.12.1.tgz#07581d1cbe84bb1e438ecdb188de3ed6d5e81ee0" - integrity sha512-NZ8Qe/kqsgAmFBxWZnUeE+MoZ04UzNI0DHOKA+I1p/5rbpaWhe1Vx5zVNa05A1iEvOtnKV1PdIEe4IPumG2y2w== +playwright-core@=0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-0.15.0.tgz#c605c98a13c81d5a2e2691f15d04758cf302c20a" + integrity sha512-uTm4PoF2U3iXkLMMG9vlTxlGfO8atQGAHDxqi8xV7hEjNSYeLTU7c6HN5zwadeHRVuBbNsZ4yqu9u4hoqC7uxQ== dependencies: - debug "^4.1.0" - extract-zip "^1.6.6" + debug "^4.1.1" + extract-zip "^2.0.0" https-proxy-agent "^3.0.0" - jpeg-js "^0.3.6" - pngjs "^3.4.0" + jpeg-js "^0.3.7" + mime "^2.4.4" + pngjs "^5.0.0" progress "^2.0.3" proxy-from-env "^1.1.0" rimraf "^3.0.2" ws "^6.1.0" -playwright@0.12.1: - version "0.12.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-0.12.1.tgz#59445ede1aecec120091db7bc95b4e626451e0b0" - integrity sha512-icF4+I8y7A5HjhbTsa4Eqtl2fuGe3ECvW0Wrn6aRM5eL5/AqUIgIf2U/0e1S1bEsDfz1JVvClGl5Gqw4aI5H4w== +playwright@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-0.15.0.tgz#bf5c3bb8404975aba78459310742388c08438431" + integrity sha512-UGHkQz8DT43uJ0KgMh2rmj8BI4FE5ReQJ9nm5mG68tt1Cj2sXPdM2b05qptfYYBPtQRetQqtJTauZ6rlCDemaQ== dependencies: - playwright-core "=0.12.1" + playwright-core "=0.15.0" plist@^3.0.1: version "3.0.1" @@ -7048,10 +7065,10 @@ plugin-error@^1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -pngjs@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" - integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== posix-character-classes@^0.1.0: version "0.1.1" @@ -10133,6 +10150,14 @@ yauzl@2.4.1: dependencies: fd-slicer "~1.0.1" +yauzl@^2.10.0, yauzl@^2.9.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yauzl@^2.2.1, yauzl@^2.3.1: version "2.9.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.9.1.tgz#a81981ea70a57946133883f029c5821a89359a7f" @@ -10141,14 +10166,6 @@ yauzl@^2.2.1, yauzl@^2.3.1: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1" -yauzl@^2.9.2: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - yazl@^2.2.1, yazl@^2.2.2, yazl@^2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.4.3.tgz#ec26e5cc87d5601b9df8432dbdd3cd2e5173a071"