diff --git a/code/src/vs/base/common/prefixTree.ts b/code/src/vs/base/common/prefixTree.ts index b4001390526..20ef79000e2 100644 --- a/code/src/vs/base/common/prefixTree.ts +++ b/code/src/vs/base/common/prefixTree.ts @@ -11,26 +11,51 @@ const unset = Symbol('unset'); */ export class WellDefinedPrefixTree { private readonly root = new Node(); + private _size = 0; + + public get size() { + return this._size; + } /** Inserts a new value in the prefix tree. */ insert(key: Iterable, value: V): void { - let node = this.root; + this.opNode(key, n => n.value = value); + } + + /** Mutates a value in the prefix tree. */ + mutate(key: Iterable, mutate: (value?: V) => V): void { + this.opNode(key, n => n.value = mutate(n.value === unset ? undefined : n.value)); + } + + /** Deletes a node from the prefix tree, returning the value it contained. */ + delete(key: Iterable): V | undefined { + const path = [{ part: '', node: this.root }]; + let i = 0; for (const part of key) { - if (!node.children) { - const next = new Node(); - node.children = new Map([[part, next]]); - node = next; - } else if (!node.children.has(part)) { - const next = new Node(); - node.children.set(part, next); - node = next; - } else { - node = node.children.get(part)!; + const node = path[i].node.children?.get(part); + if (!node) { + return undefined; // node not in tree } + path.push({ part, node }); + i++; + } + + const value = path[i].node.value; + if (value === unset) { + return; // not actually a real node } - node.value = value; + this._size--; + for (; i > 0; i--) { + const parent = path[i - 1]; + parent.node.children!.delete(path[i].part); + if (parent.node.children!.size > 0 || parent.node.value !== unset) { + break; + } + } + + return value; } /** Gets a value from the tree. */ @@ -80,6 +105,61 @@ export class WellDefinedPrefixTree { return true; } + + /** Gets whether the tree has the given key. */ + hasKey(key: Iterable): boolean { + let node = this.root; + for (const segment of key) { + const next = node.children?.get(segment); + if (!next) { + return false; + } + + node = next; + } + + return node.value !== unset; + } + + private opNode(key: Iterable, fn: (node: Node) => void): void { + let node = this.root; + for (const part of key) { + if (!node.children) { + const next = new Node(); + node.children = new Map([[part, next]]); + node = next; + } else if (!node.children.has(part)) { + const next = new Node(); + node.children.set(part, next); + node = next; + } else { + node = node.children.get(part)!; + } + } + + if (node.value === unset) { + this._size++; + } + + fn(node); + } + + /** Returns an iterable of the tree values in no defined order. */ + *values() { + const stack = [this.root]; + while (stack.length > 0) { + const node = stack.pop()!; + if (node.value !== unset) { + yield node.value; + } + + if (node.children) { + for (const child of node.children.values()) { + stack.push(child); + } + } + } + } } class Node { diff --git a/code/src/vs/base/test/common/prefixTree.test.ts b/code/src/vs/base/test/common/prefixTree.test.ts index 3b1cb88ae86..b93921810d3 100644 --- a/code/src/vs/base/test/common/prefixTree.test.ts +++ b/code/src/vs/base/test/common/prefixTree.test.ts @@ -44,4 +44,94 @@ suite('WellDefinedPrefixTree', () => { assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar']), true); assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar', 'baz']), false); }); + + test('hasKey', () => { + const key = ['foo', 'bar']; + tree.insert(key, 42); + + assert.strictEqual(tree.hasKey(key), true); + assert.strictEqual(tree.hasKey(['foo']), false); + assert.strictEqual(tree.hasKey(['baz']), false); + assert.strictEqual(tree.hasKey(['foo', 'bar', 'baz']), false); + }); + + test('size', () => { + const key1 = ['foo', 'bar']; + const key2 = ['foo', 'baz']; + assert.strictEqual(tree.size, 0); + tree.insert(key1, 42); + assert.strictEqual(tree.size, 1); + tree.insert(key2, 43); + assert.strictEqual(tree.size, 2); + tree.insert(key2, 44); + assert.strictEqual(tree.size, 2); + }); + + test('mutate', () => { + const key1 = ['foo', 'bar']; + const key2 = ['foo', 'baz']; + tree.insert(key1, 42); + tree.insert(key2, 43); + tree.mutate(key1, (value) => { + assert.strictEqual(value, 42); + return 44; + }); + assert.strictEqual(tree.find(key1), 44); + assert.strictEqual(tree.find(key2), 43); + }); + + test('delete', () => { + const key1 = ['foo', 'bar']; + const key2 = ['foo', 'baz']; + tree.insert(key1, 42); + tree.insert(key2, 43); + assert.strictEqual(tree.size, 2); + + assert.strictEqual(tree.delete(key1), 42); + assert.strictEqual(tree.size, 1); + assert.strictEqual(tree.find(key1), undefined); + assert.strictEqual(tree.find(key2), 43); + + assert.strictEqual(tree.delete(key2), 43); + assert.strictEqual(tree.size, 0); + assert.strictEqual(tree.find(key1), undefined); + assert.strictEqual(tree.find(key2), undefined); + + tree.delete(key2); + assert.strictEqual(tree.size, 0); + }); + + test('delete child', () => { + const key1 = ['foo', 'bar']; + const key2 = ['foo', 'bar', 'baz']; + tree.insert(key1, 42); + tree.insert(key2, 43); + assert.strictEqual(tree.size, 2); + + assert.strictEqual(tree.delete(key2), 43); + assert.strictEqual(tree.size, 1); + assert.strictEqual(tree.find(key1), 42); + assert.strictEqual(tree.find(key2), undefined); + }); + + test('delete noops if deleting parent', () => { + const key1 = ['foo', 'bar']; + const key2 = ['foo', 'bar', 'baz']; + tree.insert(key2, 43); + assert.strictEqual(tree.size, 1); + + assert.strictEqual(tree.delete(key1), undefined); + assert.strictEqual(tree.size, 1); + assert.strictEqual(tree.find(key2), 43); + assert.strictEqual(tree.find(key1), undefined); + }); + + test('values', () => { + const key1 = ['foo', 'bar']; + const key2 = ['foo', 'baz']; + tree.insert(key1, 42); + tree.insert(key2, 43); + + assert.deepStrictEqual([...tree.values()], [43, 42]); + }); }); diff --git a/code/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/code/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index db55fec521f..82640175b1c 100644 --- a/code/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/code/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -68,6 +68,20 @@ export interface ShutdownEvent { join(id: string, promise: Promise): void; } +export interface IRelaunchHandler { + + /** + * Allows a handler to deal with relaunching the application. The return + * value indicates if the relaunch is handled or not. + */ + handleRelaunch(options?: IRelaunchOptions): boolean; +} + +export interface IRelaunchOptions { + readonly addArgs?: string[]; + readonly removeArgs?: string[]; +} + export interface ILifecycleMainService { readonly _serviceBrand: undefined; @@ -130,7 +144,12 @@ export interface ILifecycleMainService { /** * Restart the application with optional arguments (CLI). All lifecycle event handlers are triggered. */ - relaunch(options?: { addArgs?: string[]; removeArgs?: string[] }): Promise; + relaunch(options?: IRelaunchOptions): Promise; + + /** + * Sets a custom handler for relaunching the application. + */ + setRelaunchHandler(handler: IRelaunchHandler): void; /** * Shutdown the application normally. All lifecycle event handlers are triggered. @@ -224,6 +243,8 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe private readonly phaseWhen = new Map(); + private relaunchHandler: IRelaunchHandler | undefined = undefined; + constructor( @ILogService private readonly logService: ILogService, @IStateService private readonly stateService: IStateService, @@ -553,6 +574,29 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } quit(willRestart?: boolean): Promise { + return this.doQuit(willRestart).then(veto => { + if (!veto && willRestart) { + // Windows: we are about to restart and as such we need to restore the original + // current working directory we had on startup to get the exact same startup + // behaviour. As such, we briefly change back to that directory and then when + // Code starts it will set it back to the installation directory again. + try { + if (isWindows) { + const currentWorkingDir = cwd(); + if (currentWorkingDir !== process.cwd()) { + process.chdir(currentWorkingDir); + } + } + } catch (err) { + this.logService.error(err); + } + } + + return veto; + }); + } + + private doQuit(willRestart?: boolean): Promise { this.trace(`Lifecycle#quit() - begin (willRestart: ${willRestart})`); if (this.pendingQuitPromise) { @@ -588,7 +632,11 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } } - async relaunch(options?: { addArgs?: string[]; removeArgs?: string[] }): Promise { + setRelaunchHandler(handler: IRelaunchHandler): void { + this.relaunchHandler = handler; + } + + async relaunch(options?: IRelaunchOptions): Promise { this.trace('Lifecycle#relaunch()'); const args = process.argv.slice(1); @@ -606,24 +654,10 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe } const quitListener = () => { - // Windows: we are about to restart and as such we need to restore the original - // current working directory we had on startup to get the exact same startup - // behaviour. As such, we briefly change back to that directory and then when - // Code starts it will set it back to the installation directory again. - try { - if (isWindows) { - const currentWorkingDir = cwd(); - if (currentWorkingDir !== process.cwd()) { - process.chdir(currentWorkingDir); - } - } - } catch (err) { - this.logService.error(err); + if (!this.relaunchHandler?.handleRelaunch(options)) { + this.trace('Lifecycle#relaunch() - calling app.relaunch()'); + app.relaunch({ args }); } - - // relaunch after we are sure there is no veto - this.trace('Lifecycle#relaunch() - calling app.relaunch()'); - app.relaunch({ args }); }; app.once('quit', quitListener); diff --git a/code/src/vs/platform/native/electron-main/nativeHostMainService.ts b/code/src/vs/platform/native/electron-main/nativeHostMainService.ts index af0e0cfe14c..a5ba0e1bb3a 100644 --- a/code/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/code/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -26,7 +26,7 @@ import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILifecycleMainService, IRelaunchOptions } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -650,7 +650,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain window?.setReady(); } - async relaunch(windowId: number | undefined, options?: { addArgs?: string[]; removeArgs?: string[] }): Promise { + async relaunch(windowId: number | undefined, options?: IRelaunchOptions): Promise { return this.lifecycleMainService.relaunch(options); } diff --git a/code/src/vs/platform/test/electron-main/workbenchTestServices.ts b/code/src/vs/platform/test/electron-main/workbenchTestServices.ts index 2d4d21be19a..639f1fab2bc 100644 --- a/code/src/vs/platform/test/electron-main/workbenchTestServices.ts +++ b/code/src/vs/platform/test/electron-main/workbenchTestServices.ts @@ -6,7 +6,7 @@ import { Promises } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { ILifecycleMainService, LifecycleMainPhase, ShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILifecycleMainService, IRelaunchHandler, LifecycleMainPhase, ShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IStateService } from 'vs/platform/state/node/state'; import { ICodeWindow, UnloadReason } from 'vs/platform/window/electron-main/window'; @@ -43,6 +43,7 @@ export class TestLifecycleMainService implements ILifecycleMainService { registerWindow(window: ICodeWindow): void { } async reload(window: ICodeWindow, cli?: NativeParsedArgs): Promise { } async unload(window: ICodeWindow, reason: UnloadReason): Promise { return true; } + setRelaunchHandler(handler: IRelaunchHandler): void { } async relaunch(options?: { addArgs?: string[] | undefined; removeArgs?: string[] | undefined }): Promise { } async quit(willRestart?: boolean): Promise { return true; } async kill(code?: number): Promise { } diff --git a/code/src/vs/platform/update/electron-main/abstractUpdateService.ts b/code/src/vs/platform/update/electron-main/abstractUpdateService.ts index 8555808caf9..bee2dec27ab 100644 --- a/code/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/code/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -46,7 +46,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } constructor( - @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @ILifecycleMainService protected readonly lifecycleMainService: ILifecycleMainService, @IConfigurationService protected configurationService: IConfigurationService, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IRequestService protected requestService: IRequestService, diff --git a/code/src/vs/platform/update/electron-main/updateService.darwin.ts b/code/src/vs/platform/update/electron-main/updateService.darwin.ts index 3a38daa5947..34f2218c013 100644 --- a/code/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/code/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -9,7 +9,7 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IRequestService } from 'vs/platform/request/common/request'; @@ -17,7 +17,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdate, State, StateType, UpdateType } from 'vs/platform/update/common/update'; import { AbstractUpdateService, createUpdateURL, UpdateNotAvailableClassification } from 'vs/platform/update/electron-main/abstractUpdateService'; -export class DarwinUpdateService extends AbstractUpdateService { +export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler { private readonly disposables = new DisposableStore(); @@ -36,6 +36,23 @@ export class DarwinUpdateService extends AbstractUpdateService { @IProductService productService: IProductService ) { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); + + lifecycleMainService.setRelaunchHandler(this); + } + + handleRelaunch(options?: IRelaunchOptions): boolean { + if (options?.addArgs || options?.removeArgs) { + return false; // we cannot apply an update and restart with different args + } + + if (this.state.type !== StateType.Ready) { + return false; // we only handle the relaunch when we have a pending update + } + + this.logService.trace('update#handleRelaunch(): running raw#quitAndInstall()'); + this.doQuitAndInstall(); + + return true; } protected override async initialize(): Promise { diff --git a/code/src/vs/platform/update/electron-main/updateService.win32.ts b/code/src/vs/platform/update/electron-main/updateService.win32.ts index 99b3602b464..05229473d33 100644 --- a/code/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/code/src/vs/platform/update/electron-main/updateService.win32.ts @@ -16,7 +16,7 @@ import * as pfs from 'vs/base/node/pfs'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { IFileService } from 'vs/platform/files/common/files'; -import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -47,7 +47,7 @@ function getUpdateType(): UpdateType { return _updateType; } -export class Win32UpdateService extends AbstractUpdateService { +export class Win32UpdateService extends AbstractUpdateService implements IRelaunchHandler { private availableUpdate: IAvailableUpdate | undefined; @@ -69,6 +69,23 @@ export class Win32UpdateService extends AbstractUpdateService { @IProductService productService: IProductService ) { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); + + lifecycleMainService.setRelaunchHandler(this); + } + + handleRelaunch(options?: IRelaunchOptions): boolean { + if (options?.addArgs || options?.removeArgs) { + return false; // we cannot apply an update and restart with different args + } + + if (this.state.type !== StateType.Ready || !this.availableUpdate) { + return false; // we only handle the relaunch when we have a pending update + } + + this.logService.trace('update#handleRelaunch(): running raw#quitAndInstall()'); + this.doQuitAndInstall(); + + return true; } protected override async initialize(): Promise { diff --git a/code/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts b/code/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts index 894c99e10af..6e5cfc4f39c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts @@ -10,13 +10,13 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget } from 'vs/editor/browser/editorBrowser'; import { KeyCode } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import * as aria from 'vs/base/browser/ui/aria/aria'; export class SlashCommandContentWidget extends Disposable implements IContentWidget { private _domNode = document.createElement('div'); private _lastSlashCommandText: string | undefined; - constructor(private _editor: ICodeEditor, private _accessibilityService: IAccessibilityService) { + constructor(private _editor: ICodeEditor) { super(); this._domNode.toggleAttribute('hidden', true); @@ -69,6 +69,6 @@ export class SlashCommandContentWidget extends Disposable implements IContentWid }]); // Announce the deletion - this._accessibilityService.alert(localize('exited slash command mode', 'Exited {0} mode', this._lastSlashCommandText)); + aria.alert(localize('exited slash command mode', 'Exited {0} mode', this._lastSlashCommandText)); } } diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 4946a11f9ee..801519bd549 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -24,7 +24,6 @@ import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const decorationDescription = 'chat'; const slashCommandPlaceholderDecorationType = 'chat-session-detail'; @@ -40,7 +39,6 @@ class InputEditorDecorations extends Disposable { @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IThemeService private readonly themeService: IThemeService, @IChatService private readonly chatService: IChatService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { super(); @@ -141,7 +139,7 @@ class InputEditorDecorations extends Disposable { if (command && inputValue.startsWith(`/${command.command} `)) { if (!this._slashCommandContentWidget) { - this._slashCommandContentWidget = new SlashCommandContentWidget(this.widget.inputEditor, this.accessibilityService); + this._slashCommandContentWidget = new SlashCommandContentWidget(this.widget.inputEditor); this._store.add(this._slashCommandContentWidget); } this._slashCommandContentWidget.setCommandText(command.command); diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5ee3feeae51..2d699a081ef 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -315,7 +315,7 @@ export class InlineChatWidget { // slash command content widget - this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor, this._accessibilityService); + this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor); this._store.add(this._slashCommandContentWidget); // toolbars diff --git a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 8c4f4fbbef0..687b9ee025a 100755 --- a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -4,7 +4,7 @@ # --------------------------------------------------------------------------------------------- # Prevent the script recursing when setting up -if [[ -n "$VSCODE_SHELL_INTEGRATION" ]]; then +if [[ -n "${VSCODE_SHELL_INTEGRATION:-}" ]]; then builtin return fi @@ -33,7 +33,7 @@ if [ "$VSCODE_INJECTION" == "1" ]; then builtin unset VSCODE_SHELL_LOGIN # Apply any explicit path prefix (see #99878) - if [ -n "$VSCODE_PATH_PREFIX" ]; then + if [ -n "${VSCODE_PATH_PREFIX:-}" ]; then export PATH=$VSCODE_PATH_PREFIX$PATH builtin unset VSCODE_PATH_PREFIX fi @@ -46,7 +46,7 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then fi # Apply EnvironmentVariableCollections if needed -if [ -n "$VSCODE_ENV_REPLACE" ]; then +if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" @@ -55,7 +55,7 @@ if [ -n "$VSCODE_ENV_REPLACE" ]; then done builtin unset VSCODE_ENV_REPLACE fi -if [ -n "$VSCODE_ENV_PREPEND" ]; then +if [ -n "${VSCODE_ENV_PREPEND:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" @@ -64,7 +64,7 @@ if [ -n "$VSCODE_ENV_PREPEND" ]; then done builtin unset VSCODE_ENV_PREPEND fi -if [ -n "$VSCODE_ENV_APPEND" ]; then +if [ -n "${VSCODE_ENV_APPEND:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" @@ -275,10 +275,10 @@ __vsc_prompt_cmd() { # PROMPT_COMMAND arrays and strings seem to be handled the same (handling only the first entry of # the array?) -__vsc_original_prompt_command=$PROMPT_COMMAND +__vsc_original_prompt_command=${PROMPT_COMMAND:-} if [[ -z "${bash_preexec_imported:-}" ]]; then - if [[ -n "$__vsc_original_prompt_command" && "$__vsc_original_prompt_command" != "__vsc_prompt_cmd" ]]; then + if [[ -n "${__vsc_original_prompt_command:-}" && "${__vsc_original_prompt_command:-}" != "__vsc_prompt_cmd" ]]; then PROMPT_COMMAND=__vsc_prompt_cmd_original else PROMPT_COMMAND=__vsc_prompt_cmd diff --git a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh index d4e21dacda7..09718cfbab2 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh +++ b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh @@ -39,7 +39,7 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then fi # Apply EnvironmentVariableCollections if needed -if [ -n "$VSCODE_ENV_REPLACE" ]; then +if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -rA ADDR <<< "$VSCODE_ENV_REPLACE" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo ${ITEM%%=*})" @@ -47,7 +47,7 @@ if [ -n "$VSCODE_ENV_REPLACE" ]; then done unset VSCODE_ENV_REPLACE fi -if [ -n "$VSCODE_ENV_PREPEND" ]; then +if [ -n "${VSCODE_ENV_PREPEND:-}" ]; then IFS=':' read -rA ADDR <<< "$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo ${ITEM%%=*})" @@ -55,7 +55,7 @@ if [ -n "$VSCODE_ENV_PREPEND" ]; then done unset VSCODE_ENV_PREPEND fi -if [ -n "$VSCODE_ENV_APPEND" ]; then +if [ -n "${VSCODE_ENV_APPEND:-}" ]; then IFS=':' read -rA ADDR <<< "$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo ${ITEM%%=*})" diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts index 1991ba84eac..094ab091f00 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts @@ -127,6 +127,10 @@ export class TerminalWordLinkDetector extends Disposable implements ITerminalLin private _refreshSeparatorCodes(): void { const separators = this._configurationService.getValue(TERMINAL_CONFIG_SECTION).wordSeparators; - this._separatorRegex = new RegExp(`[${escapeRegExpCharacters(separators)}]`, 'g'); + let powerlineSymbols = ''; + for (let i = 0xe0b0; i <= 0xe0bf; i++) { + powerlineSymbols += String.fromCharCode(i); + } + this._separatorRegex = new RegExp(`[${escapeRegExpCharacters(separators)}${powerlineSymbols}]`, 'g'); } } diff --git a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts index d274ff008a8..bdb6dc8ac97 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts @@ -72,6 +72,14 @@ suite('Workbench - TerminalWordLinkDetector', () => { }); }); + suite('should ignore powerline symbols', () => { + for (let i = 0xe0b0; i <= 0xe0bf; i++) { + test(`\\u${i.toString(16)}`, async () => { + await assertLink(`${String.fromCharCode(i)}foo${String.fromCharCode(i)}`, [{ range: [[2, 1], [4, 1]], text: 'foo' }]); + }); + } + }); + // These are failing - the link's start x is 1 px too far to the right bc it starts // with a wide character, which the terminalLinkHelper currently doesn't account for test.skip('should support wide characters', async () => { diff --git a/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index a38dc4e9611..1258916d4cf 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -69,7 +69,7 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { ITestingContinuousRunService } from 'vs/workbench/contrib/testing/common/testingContinuousRunService'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates'; -import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IActivityService, IconBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; @@ -111,6 +111,7 @@ export class TestingExplorerView extends ViewPane { @ITestingProgressUiService private readonly testProgressService: ITestingProgressUiService, @ITestProfileService private readonly testProfileService: ITestProfileService, @ICommandService private readonly commandService: ICommandService, + @ITestingContinuousRunService private readonly crService: ITestingContinuousRunService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -127,7 +128,8 @@ export class TestingExplorerView extends ViewPane { this._register(testProfileService.onDidChange(() => this.updateActions())); const onDidChangeTestingCountBadge = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('testing.countBadge')); - this._register(onDidChangeTestingCountBadge(this.renderActivityCount, this)); + this._register(onDidChangeTestingCountBadge(this.renderActivityBadge, this)); + this._register(crService.onDidChange(this.renderActivityBadge, this)); } public override shouldShowWelcome() { @@ -288,7 +290,7 @@ export class TestingExplorerView extends ViewPane { })); this._register(this.testProgressService.onCountChange((text: CountSummary) => { this.countSummary = text; - this.renderActivityCount(); + this.renderActivityBadge(); })); this.testProgressService.update(); @@ -439,13 +441,16 @@ export class TestingExplorerView extends ViewPane { } } - private renderActivityCount() { + private renderActivityBadge() { const countBadgeType = this.configurationService.getValue(TestingConfigKeys.CountBadge); - if (!this.countSummary || countBadgeType === TestingCountBadge.Off || this.countSummary[countBadgeType] === 0) { - this.badgeDisposable.value = undefined; - } else { + if (this.countSummary && countBadgeType !== TestingCountBadge.Off && this.countSummary[countBadgeType] !== 0) { const badge = new NumberBadge(this.countSummary[countBadgeType], num => this.getLocalizedBadgeString(countBadgeType, num)); this.badgeDisposable.value = this.activityService.showViewActivity(Testing.ExplorerViewId, { badge }); + } else if (this.crService.isEnabled()) { + const badge = new IconBadge(icons.testingContinuousIsOn, () => localize('testingContinuousBadge', 'Tests are being watched for changes')); + this.badgeDisposable.value = this.activityService.showViewActivity(Testing.ExplorerViewId, { badge }); + } else { + this.badgeDisposable.value = undefined; } } @@ -1074,7 +1079,6 @@ class TestsFilter implements ITreeFilter { class TreeSorter implements ITreeSorter { constructor( private readonly viewModel: TestingExplorerViewModel, - @ITestingContinuousRunService private readonly crService: ITestingContinuousRunService, ) { } public compare(a: TestExplorerTreeElement, b: TestExplorerTreeElement): number { @@ -1082,11 +1086,6 @@ class TreeSorter implements ITreeSorter { return (a instanceof TestTreeErrorMessage ? -1 : 0) + (b instanceof TestTreeErrorMessage ? 1 : 0); } - const crDelta = +this.crService.isSpecificallyEnabledFor(b.test.item.extId) - +this.crService.isSpecificallyEnabledFor(a.test.item.extId); - if (crDelta !== 0) { - return crDelta; - } - const durationDelta = (b.duration || 0) - (a.duration || 0); if (this.viewModel.viewSorting === TestExplorerViewSorting.ByDuration && durationDelta !== 0) { return durationDelta; @@ -1303,8 +1302,9 @@ class TestItemRenderer extends Disposable })); disposable.add(this.crService.onDidChange(changed => { - if (templateData.current && (!changed || changed === templateData.current.test.item.extId)) { - this.fillActionBar(templateData.current, templateData); + const id = templateData.current?.test.item.extId; + if (id && (!changed || changed === id || TestId.isChild(id, changed))) { + this.fillActionBar(templateData.current!, templateData); } })); @@ -1328,7 +1328,9 @@ class TestItemRenderer extends Disposable private fillActionBar(element: TestItemTreeElement, data: ITestElementTemplateData) { const { actions, contextOverlay } = getActionableElementActions(this.contextKeyService, this.menuService, this.testService, this.crService, this.profiles, element); - data.actionBar.domNode.classList.toggle('testing-is-continuous-run', !!contextOverlay.getContextKeyValue(TestingContextKeys.isContinuousModeOn.key)); + const crSelf = !!contextOverlay.getContextKeyValue(TestingContextKeys.isContinuousModeOn.key); + const crChild = !crSelf && this.crService.isEnabledForAChildOf(element.test.item.extId); + data.actionBar.domNode.classList.toggle('testing-is-continuous-run', crSelf || crChild); data.actionBar.clear(); data.actionBar.context = element; data.actionBar.push(actions.primary, { icon: true, label: false }); diff --git a/code/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts b/code/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts index ebdaa217c80..0e088ea4ff3 100644 --- a/code/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts +++ b/code/src/vs/workbench/contrib/testing/common/testingContinuousRunService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -15,6 +15,7 @@ import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl import { ITestRunProfile } from 'vs/workbench/contrib/testing/common/testTypes'; import { Emitter, Event } from 'vs/base/common/event'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; export const ITestingContinuousRunService = createDecorator('testingContinuousRunService'); @@ -43,6 +44,17 @@ export interface ITestingContinuousRunService { */ isEnabledForAParentOf(testId: string): boolean; + /** + * Gets whether continous run is specifically enabled for + * the given test ID, or any of its parents. + */ + isEnabledForAChildOf(testId: string): boolean; + + /** + * Gets whether it's enabled at all. + */ + isEnabled(): boolean; + /** * Starts a continuous auto run with a specific profile or set of profiles. * Globally if no test is given, for a specific test otherwise. @@ -60,7 +72,8 @@ export class TestingContinuousRunService extends Disposable implements ITestingC declare readonly _serviceBrand: undefined; private readonly changeEmitter = new Emitter(); - private readonly running = new Map(); + private globallyRunning?: CancellationTokenSource; + private readonly running = new WellDefinedPrefixTree(); private readonly lastRun: StoredValue>; private readonly isGloballyOn: IContextKey; @@ -86,30 +99,37 @@ export class TestingContinuousRunService extends Disposable implements ITestingC serialize: v => JSON.stringify([...v]) }, }, storageService)); + + this._register(toDisposable(() => { + this.globallyRunning?.dispose(); + for (const cts of this.running.values()) { + cts.dispose(); + } + })); } /** @inheritdoc */ public isSpecificallyEnabledFor(testId: string): boolean { - return this.running.has(testId); + return this.running.size > 0 && this.running.hasKey(TestId.fromString(testId).path); } /** @inheritdoc */ public isEnabledForAParentOf(testId: string): boolean { - if (!this.running.size) { - return false; - } - - if (this.running.has(undefined)) { + if (this.globallyRunning) { return true; } - for (const part of TestId.fromString(testId).idsFromRoot()) { - if (this.running.has(part.toString())) { - return true; - } - } + return this.running.size > 0 && this.running.hasKeyOrParent(TestId.fromString(testId).path); + } - return false; + /** @inheritdoc */ + public isEnabledForAChildOf(testId: string): boolean { + return this.running.size > 0 && this.running.hasKeyOrChildren(TestId.fromString(testId).path); + } + + /** @inheritdoc */ + public isEnabled(): boolean { + return !!this.globallyRunning || this.running.size > 0; } /** @inheritdoc */ @@ -120,8 +140,16 @@ export class TestingContinuousRunService extends Disposable implements ITestingC this.isGloballyOn.set(true); } - this.running.get(testId)?.dispose(true); - this.running.set(testId, cts); + if (!testId) { + this.globallyRunning?.dispose(true); + this.globallyRunning = cts; + } else { + this.running.mutate(TestId.fromString(testId).path, c => { + c?.dispose(true); + return cts; + }); + } + this.lastRun.store(new Set(profile.map(p => p.profileId))); this.testService.startContinuousRun({ @@ -139,8 +167,12 @@ export class TestingContinuousRunService extends Disposable implements ITestingC /** @inheritdoc */ public stop(testId?: string): void { - this.running.get(testId)?.dispose(true); - this.running.delete(testId); + if (!testId) { + this.globallyRunning?.dispose(true); + this.globallyRunning = undefined; + } else { + this.running.delete(TestId.fromString(testId).path)?.dispose(true); + } if (testId === undefined) { this.isGloballyOn.set(false);