diff --git a/code/build/.moduleignore b/code/build/.moduleignore index 97b504d8522..3f573e06078 100644 --- a/code/build/.moduleignore +++ b/code/build/.moduleignore @@ -163,7 +163,7 @@ typescript/lib/tsserverlibrary.js jschardet/index.js jschardet/src/** -jschardet/dist/jschardet.js +# TODO@esm uncomment when we can use jschardet.min.js again jschardet/dist/jschardet.js es6-promise/lib/** diff --git a/code/build/.webignore b/code/build/.webignore index d42f9775ba9..837366b67f7 100644 --- a/code/build/.webignore +++ b/code/build/.webignore @@ -14,7 +14,7 @@ jschardet/index.js jschardet/src/** -jschardet/dist/jschardet.js +# TODO@esm uncomment when we can use jschardet.min.js again jschardet/dist/jschardet.js vscode-textmate/webpack.config.js diff --git a/code/extensions/typescript-language-features/package.json b/code/extensions/typescript-language-features/package.json index f234a72645a..e93299d8169 100644 --- a/code/extensions/typescript-language-features/package.json +++ b/code/extensions/typescript-language-features/package.json @@ -1116,7 +1116,13 @@ "inline", "first" ], - "default": "auto" + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] }, "unicodeCollation": { "type": "string", @@ -1180,7 +1186,13 @@ "inline", "first" ], - "default": "auto" + "default": "auto", + "markdownEnumDescriptions": [ + "%typescript.preferences.organizeImports.typeOrder.auto%", + "%typescript.preferences.organizeImports.typeOrder.last%", + "%typescript.preferences.organizeImports.typeOrder.inline%", + "%typescript.preferences.organizeImports.typeOrder.first%" + ] }, "unicodeCollation": { "type": "string", diff --git a/code/extensions/typescript-language-features/package.nls.json b/code/extensions/typescript-language-features/package.nls.json index 6b1946b98ad..d2a0ca892fa 100644 --- a/code/extensions/typescript-language-features/package.nls.json +++ b/code/extensions/typescript-language-features/package.nls.json @@ -188,21 +188,21 @@ "typescript.preferences.renameShorthandProperties.deprecationMessage": "The setting 'typescript.preferences.renameShorthandProperties' has been deprecated in favor of 'typescript.preferences.useAliasesForRenames'", "typescript.preferences.useAliasesForRenames": "Enable/disable introducing aliases for object shorthand properties during renames.", "typescript.preferences.renameMatchingJsxTags": "When on a JSX tag, try to rename the matching tag instead of renaming the symbol. Requires using TypeScript 5.1+ in the workspace.", - "typescript.preferences.organizeImports": "Advanced preferences that control how imports are ordered. Presets are available in `#typescript.preferences.organizeImports.presets#`", - "javascript.preferences.organizeImports": "Advanced preferences that control how imports are ordered. Presets are available in `#javascript.preferences.organizeImports.presets#`", - "typescript.preferences.organizeImports.caseSensitivity.auto": "Detect case-sensitivity for import sorting", - "typescript.preferences.organizeImports.caseSensitivity.insensitive": "Sort imports case-insensitively", - "typescript.preferences.organizeImports.caseSensitivity.sensitive": "Sort imports case-sensitively", - "typescript.preferences.organizeImports.typeOrder.auto": "Detect where type-only named imports should be sorted", - "typescript.preferences.organizeImports.typeOrder.last": "Type only named imports are sorted to the end of the import list", - "typescript.preferences.organizeImports.typeOrder.inline": "Named imports are sorted by name only", - "typescript.preferences.organizeImports.typeOrder.first": "Type only named imports are sorted to the end of the import list", - "typescript.preferences.organizeImports.unicodeCollation.ordinal": "Sort imports using the numeric value of each code point", - "typescript.preferences.organizeImports.unicodeCollation.unicode": "Sort imports using the Unicode code collation", - "typescript.preferences.organizeImports.locale": "Overrides the locale used for collation. Specify `auto` to use the UI locale. Only applies to `organizeImportsCollation: 'unicode'`", - "typescript.preferences.organizeImports.caseFirst": "Indicates whether upper-case comes before lower-case. Only applies to `organizeImportsCollation: 'unicode'`", - "typescript.preferences.organizeImports.numericCollation": "Sort numeric strings by integer value", - "typescript.preferences.organizeImports.accentCollation": "Compare characters with diacritical marks as unequal to base character", + "typescript.preferences.organizeImports": "Advanced preferences that control how imports are ordered.", + "javascript.preferences.organizeImports": "Advanced preferences that control how imports are ordered.", + "typescript.preferences.organizeImports.caseSensitivity.auto": "Detect case-sensitivity for import sorting.", + "typescript.preferences.organizeImports.caseSensitivity.insensitive": "Sort imports case-insensitively.", + "typescript.preferences.organizeImports.caseSensitivity.sensitive": "Sort imports case-sensitively.", + "typescript.preferences.organizeImports.typeOrder.auto": "Detect where type-only named imports should be sorted.", + "typescript.preferences.organizeImports.typeOrder.last": "Type only named imports are sorted to the end of the import list.", + "typescript.preferences.organizeImports.typeOrder.inline": "Named imports are sorted by name only.", + "typescript.preferences.organizeImports.typeOrder.first": "Type only named imports are sorted to the end of the import list.", + "typescript.preferences.organizeImports.unicodeCollation.ordinal": "Sort imports using the numeric value of each code point.", + "typescript.preferences.organizeImports.unicodeCollation.unicode": "Sort imports using the Unicode code collation.", + "typescript.preferences.organizeImports.locale": "Overrides the locale used for collation. Specify `auto` to use the UI locale. Only applies to `organizeImportsCollation: 'unicode'`.", + "typescript.preferences.organizeImports.caseFirst": "Indicates whether upper-case comes before lower-case. Only applies to `organizeImportsCollation: 'unicode'`.", + "typescript.preferences.organizeImports.numericCollation": "Sort numeric strings by integer value.", + "typescript.preferences.organizeImports.accentCollation": "Compare characters with diacritical marks as unequal to base character.", "typescript.workspaceSymbols.scope": "Controls which files are searched by [Go to Symbol in Workspace](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name).", "typescript.workspaceSymbols.scope.allOpenProjects": "Search all open JavaScript or TypeScript projects for symbols.", "typescript.workspaceSymbols.scope.currentProject": "Only search for symbols in the current JavaScript or TypeScript project.", diff --git a/code/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/code/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 484ea640295..6e1ae9051b1 100644 --- a/code/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/code/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -201,7 +201,7 @@ export default class FileConfigurationManager extends Disposable { interactiveInlayHints: true, includeCompletionsForModuleExports: config.get('suggest.autoImports'), ...getInlayHintsPreferences(config), - ...this.getOrganizeImportsPreferences(config), + ...this.getOrganizeImportsPreferences(preferencesConfig), }; return preferences; diff --git a/code/product.json b/code/product.json index 65bd93f56a6..fb411363179 100644 --- a/code/product.json +++ b/code/product.json @@ -51,8 +51,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.92.0", - "sha256": "e5d0a74728292423631f79d076ecb2bc129f9637bcbc2529e48a0fd53baa69cc", + "version": "1.93.0", + "sha256": "9339cb8e6b77f554df54d79e71f533279cb76b0f9b04c207f633bfd507442b6a", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/code/remote/yarn.lock b/code/remote/yarn.lock index 2a55b47ce62..9d8229d92cd 100644 --- a/code/remote/yarn.lock +++ b/code/remote/yarn.lock @@ -209,7 +209,7 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -braces@^3.0.2: +braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -422,11 +422,11 @@ lru-cache@^6.0.0: yallist "^4.0.0" micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mimic-response@^3.1.0: diff --git a/code/src/vs/base/browser/ui/radio/radio.ts b/code/src/vs/base/browser/ui/radio/radio.ts index 2f57e4ace30..5ab1edc41b2 100644 --- a/code/src/vs/base/browser/ui/radio/radio.ts +++ b/code/src/vs/base/browser/ui/radio/radio.ts @@ -11,7 +11,7 @@ import { $ } from 'vs/base/browser/dom'; import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { Button } from 'vs/base/browser/ui/button/button'; import { DisposableMap, DisposableStore } from 'vs/base/common/lifecycle'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; export interface IRadioStyles { readonly activeForeground?: string; @@ -53,7 +53,7 @@ export class Radio extends Widget { constructor(opts: IRadioOptions) { super(); - this.hoverDelegate = opts.hoverDelegate ?? getDefaultHoverDelegate('element'); + this.hoverDelegate = opts.hoverDelegate ?? this._register(createInstantHoverDelegate()); this.domNode = $('.monaco-custom-radio'); this.domNode.setAttribute('role', 'radio'); diff --git a/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts index ed929bc965c..2d8e9ae9392 100644 --- a/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/code/src/vs/editor/browser/services/hoverService/hoverWidget.ts @@ -222,7 +222,7 @@ export class HoverWidget extends Widget implements IHoverWidget { } // Show the hover hint if needed - if (hideOnHover && options.appearance?.showHoverHint) { + if (options.appearance?.showHoverHint) { const statusBarElement = $('div.hover-row.status-bar'); const infoElement = $('div.info'); infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt'); diff --git a/code/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/code/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index dbbe9a912fd..862459df98c 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -180,6 +180,12 @@ export class CodeActionController extends Disposable implements IEditorContribut return; } + + const selection = this._editor.getSelection(); + if (selection?.startLineNumber !== newState.position.lineNumber) { + return; + } + this._lightBulbWidget.value?.update(actions, newState.trigger, newState.position); if (newState.trigger.type === CodeActionTriggerType.Invoke) { diff --git a/code/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/code/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 0395f421600..8f0e98d5bf3 100644 --- a/code/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/code/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -271,7 +271,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget { const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber); const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented; - // check above and below. if both are blocked, display lightbulb in the gutter. if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) { this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { @@ -280,7 +279,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { }); this.renderGutterLightbub(); return this.hide(); - } else if (prevLineEmptyOrIndented || endLine || (notEmpty && !currLineEmptyOrIndented)) { + } else if (prevLineEmptyOrIndented || endLine || (prevLineEmptyOrIndented && !currLineEmptyOrIndented)) { effectiveLineNumber -= 1; } else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) { effectiveLineNumber += 1; diff --git a/code/src/vs/editor/contrib/rename/browser/renameWidget.ts b/code/src/vs/editor/contrib/rename/browser/renameWidget.ts index b284cd519fd..1f6fcc14106 100644 --- a/code/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/code/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -318,7 +318,8 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable } afterRender(position: ContentWidgetPositionPreference | null): void { - this._trace('invoking afterRender, position: ', position ? 'not null' : 'null'); + // FIXME@ulugbekna: commenting trace log out until we start unmounting the widget from editor properly - https://github.com/microsoft/vscode/issues/226975 + // this._trace('invoking afterRender, position: ', position ? 'not null' : 'null'); if (position === null) { // cancel rename when input widget isn't rendered anymore this.cancelInput(true, 'afterRender (because position is null)'); @@ -363,7 +364,7 @@ export class RenameWidget implements IRenameWidget, IContentWidget, IDisposable } cancelInput(focusEditor: boolean, caller: string): void { - this._trace(`invoking cancelInput, caller: ${caller}, _currentCancelInput: ${this._currentAcceptInput ? 'not undefined' : 'undefined'}`); + // this._trace(`invoking cancelInput, caller: ${caller}, _currentCancelInput: ${this._currentAcceptInput ? 'not undefined' : 'undefined'}`); this._currentCancelInput?.(focusEditor); } diff --git a/code/src/vs/platform/actionWidget/browser/actionList.ts b/code/src/vs/platform/actionWidget/browser/actionList.ts index 98403a2a4c2..8eeeee2df29 100644 --- a/code/src/vs/platform/actionWidget/browser/actionList.ts +++ b/code/src/vs/platform/actionWidget/browser/actionList.ts @@ -166,8 +166,8 @@ export class ActionList extends Disposable { private readonly _list: List>; - private readonly _actionLineHeight = 28; - private readonly _headerLineHeight = 28; + private readonly _actionLineHeight = 24; + private readonly _headerLineHeight = 26; private readonly _allMenuItems: readonly IActionListItem[]; diff --git a/code/src/vs/platform/actionWidget/browser/actionWidget.css b/code/src/vs/platform/actionWidget/browser/actionWidget.css index b9ebc031df3..d205c7ab791 100644 --- a/code/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/code/src/vs/platform/actionWidget/browser/actionWidget.css @@ -132,8 +132,9 @@ /* Action bar */ .action-widget .action-widget-action-bar { - background-color: var(--vscode-editorHoverWidget-statusBarBackground); + background-color: var(--vscode-editorActionList-background); border-top: 1px solid var(--vscode-editorHoverWidget-border); + margin-top: 2px; } .action-widget .action-widget-action-bar::before { @@ -143,7 +144,7 @@ } .action-widget .action-widget-action-bar .actions-container { - padding: 0 8px; + padding: 3px 8px 0; } .action-widget-action-bar .action-label { diff --git a/code/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/code/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 61c8816e3aa..7637899aea1 100644 --- a/code/src/vs/platform/extensionManagement/common/extensionEnablementService.ts +++ b/code/src/vs/platform/extensionManagement/common/extensionEnablementService.ts @@ -16,15 +16,15 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo private _onDidChangeEnablement = new Emitter<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }>(); readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }> = this._onDidChangeEnablement.event; - private readonly storageManger: StorageManager; + private readonly storageManager: StorageManager; constructor( @IStorageService storageService: IStorageService, @IExtensionManagementService extensionManagementService: IExtensionManagementService, ) { super(); - this.storageManger = this._register(new StorageManager(storageService)); - this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' }))); + this.storageManager = this._register(new StorageManager(storageService)); + this._register(this.storageManager.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' }))); this._register(extensionManagementService.onDidInstallExtensions(e => e.forEach(({ local, operation }) => { if (local && operation === InstallOperation.Migrate) { this._removeFromDisabledExtensions(local.identifier); /* Reset migrated extensions */ @@ -84,11 +84,11 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo } private _getExtensions(storageId: string): IExtensionIdentifier[] { - return this.storageManger.get(storageId, StorageScope.PROFILE); + return this.storageManager.get(storageId, StorageScope.PROFILE); } private _setExtensions(storageId: string, extensions: IExtensionIdentifier[]): void { - this.storageManger.set(storageId, extensions, StorageScope.PROFILE); + this.storageManager.set(storageId, extensions, StorageScope.PROFILE); } } diff --git a/code/src/vs/platform/theme/common/colorUtils.ts b/code/src/vs/platform/theme/common/colorUtils.ts index 14ceea8847b..a237166f6f6 100644 --- a/code/src/vs/platform/theme/common/colorUtils.ts +++ b/code/src/vs/platform/theme/common/colorUtils.ts @@ -159,7 +159,7 @@ class ColorRegistry implements IColorRegistry { public registerColor(id: string, defaults: ColorDefaults | ColorValue | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { const colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage }; this.colorsById[id] = colorContribution; - const propertySchema: IJSONSchemaWithSnippets = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; + const propertySchema: IJSONSchemaWithSnippets = { type: 'string', format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; if (deprecationMessage) { propertySchema.deprecationMessage = deprecationMessage; } @@ -168,6 +168,7 @@ class ColorRegistry implements IColorRegistry { propertySchema.patternErrorMessage = nls.localize('transparecyRequired', 'This color must be transparent or it will obscure content'); } this.colorSchema.properties[id] = { + description, oneOf: [ propertySchema, { type: 'string', const: DEFAULT_COLOR_CONFIG_VALUE, description: nls.localize('useDefault', 'Use the default color.') } diff --git a/code/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/code/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 12441286ddc..880a6026dc7 100644 --- a/code/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/code/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -28,6 +28,7 @@ import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSe import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; import { ICanonicalUriService } from 'vs/platform/workspace/common/canonicalUri'; +import { revive } from 'vs/base/common/marshalling'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -146,7 +147,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { const query = this._queryBuilder.file( includeFolder ? [includeFolder] : workspace.folders, - options + revive(options) ); return this._searchService.fileSearch(query, token).then(result => { @@ -164,7 +165,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { const workspace = this._contextService.getWorkspace(); const folders = folder ? [folder] : workspace.folders.map(folder => folder.uri); - const query = this._queryBuilder.text(pattern, folders, options); + const query = this._queryBuilder.text(pattern, folders, revive(options)); query._reason = 'startTextSearch'; const onProgress = (p: ISearchProgressItem) => { diff --git a/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 50ab3ea6729..5bb629a6c09 100644 --- a/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/code/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2312,15 +2312,15 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise { - return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource), token), undefined, token); + return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.provideCodeLenses(URI.revive(resource), token), undefined, token, resource.scheme === 'output'); } $resolveCodeLens(handle: number, symbol: extHostProtocol.ICodeLensDto, token: CancellationToken): Promise { - return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(symbol, token), undefined, undefined); + return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(symbol, token), undefined, undefined, true); } $releaseCodeLenses(handle: number, cacheId: number): void { - this._withAdapter(handle, CodeLensAdapter, adapter => Promise.resolve(adapter.releaseCodeLenses(cacheId)), undefined, undefined); + this._withAdapter(handle, CodeLensAdapter, adapter => Promise.resolve(adapter.releaseCodeLenses(cacheId)), undefined, undefined, true); } // --- declaration diff --git a/code/src/vs/workbench/api/common/extHostWorkspace.ts b/code/src/vs/workbench/api/common/extHostWorkspace.ts index 33e3bafe5e7..951c87d967f 100644 --- a/code/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/code/src/vs/workbench/api/common/extHostWorkspace.ts @@ -584,7 +584,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } const parsedInclude = include ? parseSearchExcludeInclude(GlobPattern.from(include)) : undefined; - const excludePatterns = include ? globsToISearchPatternBuilder(options.exclude) : undefined; + const excludePatterns = globsToISearchPatternBuilder(options.exclude); return { options: { @@ -664,7 +664,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac async findTextInFilesBase(query: vscode.TextSearchQuery, queryOptions: QueryOptions[] | undefined, callback: (result: ITextSearchResult, uri: URI) => void, token: vscode.CancellationToken = CancellationToken.None): Promise { const requestId = this._requestIdProvider.getNext(); - const isCanceled = false; + let isCanceled = false; + token.onCancellationRequested(_ => { + isCanceled = true; + }); this._activeSearchCallbacks[requestId] = p => { if (isCanceled) { diff --git a/code/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/code/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 15afa9df6bf..937428df156 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/code/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -138,11 +138,11 @@ color: var(--vscode-titleBar-activeForeground); } -.monaco-workbench .part.titlebar.inactive > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { +.monaco-workbench .part.titlebar.inactive > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { color: var(--vscode-titleBar-inactiveForeground); } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label { color: inherit; } @@ -182,7 +182,7 @@ text-overflow: ellipsis; } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center.multiple { +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center.multiple { justify-content: flex-start; padding: 0 12px; } @@ -280,7 +280,7 @@ border-left: 1px solid transparent; } -/* Window Controls (Minimize, Max/Restore, Close) */ +/* Window Controls Container */ .monaco-workbench .part.titlebar .window-controls-container { display: flex; flex-grow: 0; @@ -292,7 +292,12 @@ height: 100%; } -/* Web WCO Sizing/Ordering */ +.monaco-workbench.fullscreen .part.titlebar .window-controls-container { + display: none; + background-color: transparent; +} + +/* Window Controls Container Web: Apply WCO environment variables (https://developer.mozilla.org/en-US/docs/Web/CSS/env#titlebar-area-x) */ .monaco-workbench.web .part.titlebar .titlebar-right .window-controls-container { width: calc(100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px)); height: env(titlebar-area-height, 35px); @@ -311,29 +316,31 @@ order: 1; } -/* Desktop Windows/Linux Window Controls*/ -.monaco-workbench:not(.web):not(.mac) .part.titlebar .window-controls-container.primary { +/* Window Controls Container Desktop: apply zoom friendly size */ +.monaco-workbench:not(.web):not(.mac) .part.titlebar .window-controls-container { width: calc(138px / var(--zoom-factor, 1)); } -.monaco-workbench:not(.web):not(.mac) .part.titlebar .titlebar-container.counter-zoom .window-controls-container.primary { +.monaco-workbench:not(.web):not(.mac) .part.titlebar .titlebar-container.counter-zoom .window-controls-container { width: 138px; } +.monaco-workbench.linux:not(.web) .part.titlebar .window-controls-container.wco-enabled { + width: calc(100px / var(--zoom-factor, 1)); /* TODO@bpasero TODO@benibenj this should not be hardcoded (https://github.com/microsoft/vscode/issues/226804) */ +} + +.monaco-workbench.linux:not(.web) .part.titlebar .titlebar-container.counter-zoom .window-controls-container.wco-enabled { + width: 100px; /* TODO@bpasero TODO@benibenj this should not be hardcoded (https://github.com/microsoft/vscode/issues/226804) */ +} + .monaco-workbench:not(.web):not(.mac) .part.titlebar .titlebar-container:not(.counter-zoom) .window-controls-container * { zoom: calc(1 / var(--zoom-factor, 1)); } -/* Desktop macOS Window Controls */ -.monaco-workbench:not(.web).mac .part.titlebar .window-controls-container.primary { +.monaco-workbench:not(.web).mac .part.titlebar .window-controls-container { width: 70px; } -.monaco-workbench.fullscreen .part.titlebar .window-controls-container { - display: none; - background-color: transparent; -} - /* Window Control Icons */ .monaco-workbench .part.titlebar .window-controls-container > .window-icon { display: flex; @@ -342,6 +349,11 @@ height: 100%; width: 46px; font-size: 16px; + color: var(--vscode-titleBar-activeForeground); +} + +.monaco-workbench .part.titlebar.inactive .window-controls-container > .window-icon { + color: var(--vscode-titleBar-inactiveForeground); } .monaco-workbench .part.titlebar .window-controls-container > .window-icon::before { @@ -455,11 +467,3 @@ border-radius: 16px; text-align: center; } - -.monaco-workbench .part.titlebar .window-controls-container .window-icon { - color: var(--vscode-titleBar-activeForeground); -} - -.monaco-workbench .part.titlebar.inactive .window-controls-container .window-icon { - color: var(--vscode-titleBar-inactiveForeground); -} diff --git a/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 9dd7d5550bf..169a4c588cf 100644 --- a/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/code/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -249,7 +249,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { //#endregion protected rootContainer!: HTMLElement; - protected primaryWindowControls: HTMLElement | undefined; + protected windowControlsContainer: HTMLElement | undefined; protected dragRegion: HTMLElement | undefined; private title!: HTMLElement; @@ -476,21 +476,31 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.createActionToolBarMenus(); } - let primaryControlLocation = isMacintosh ? 'left' : 'right'; - if (isMacintosh && isNative) { + // Window Controls Container + if (!hasNativeTitlebar(this.configurationService, this.titleBarStyle)) { + let windowControlsLocation = isMacintosh ? 'left' : 'right'; + if (isMacintosh && isNative) { - // Check if the locale is RTL, macOS will move traffic lights in RTL locales - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/textInfo + // Check if the locale is RTL, macOS will move traffic lights in RTL locales + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/textInfo - const localeInfo = new Intl.Locale(platformLocale) as any; - if (localeInfo?.textInfo?.direction === 'rtl') { - primaryControlLocation = 'right'; + const localeInfo = new Intl.Locale(platformLocale) as any; + if (localeInfo?.textInfo?.direction === 'rtl') { + windowControlsLocation = 'right'; + } } - } - if (!hasNativeTitlebar(this.configurationService, this.titleBarStyle)) { - this.primaryWindowControls = append(primaryControlLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container.primary')); - append(primaryControlLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container.secondary')); + if (isMacintosh && isNative && windowControlsLocation === 'left') { + // macOS native: controls are on the left and the container is not needed to make room + // for something, except for web where a custom menu being supported). not putting the + // container helps with allowing to move the window when clicking very close to the + // window control buttons. + } else { + this.windowControlsContainer = append(windowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container')); + if (isWCOEnabled()) { + this.windowControlsContainer.classList.add('wco-enabled'); + } + } } // Context menu over title bar: depending on the OS and the location of the click this will either be diff --git a/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d5cf3c355e0..be6c4955294 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -36,7 +36,7 @@ import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; -import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; +import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; @@ -261,9 +261,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlas Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); - -// Disabled until https://github.com/microsoft/vscode/issues/218646 is fixed -// registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); registerChatActions(); registerChatCopyActions(); diff --git a/code/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/code/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 09a83daf318..2bf50f4a8cf 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -3,17 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { localize, localize2 } from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService } from 'vs/platform/log/common/log'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -21,7 +20,9 @@ import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer import { CHAT_VIEW_ID } from 'vs/workbench/contrib/chat/browser/chat'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { ChatAgentLocation, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_EXTENSION_INVALID, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IRawChatParticipantContribution } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; +import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -160,35 +161,6 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi }, }); -export class ChatCompatibilityNotifier implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.chatCompatNotifier'; - - constructor( - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, - @INotificationService notificationService: INotificationService, - @ICommandService commandService: ICommandService - ) { - // It may be better to have some generic UI for this, for any extension that is incompatible, - // but this is only enabled for Copilot Chat now and it needs to be obvious. - extensionsWorkbenchService.queryLocal().then(exts => { - const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat'); - if (chat?.local?.validations.some(v => v[0] === Severity.Error)) { - notificationService.notify({ - severity: Severity.Error, - message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), - actions: { - primary: [ - new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { - return commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); - }) - ] - } - }); - } - }); - } -} - export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; @@ -198,9 +170,10 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { constructor( @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @ILogService private readonly logService: ILogService, + @ILogService private readonly logService: ILogService ) { this._viewContainer = this.registerViewContainer(); + this.registerDefaultParticipantView(); this.handleAndRegisterChatExtensions(); } @@ -239,11 +212,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - const store = new DisposableStore(); - if (providerDescriptor.isDefault && (!providerDescriptor.locations || providerDescriptor.locations?.includes(ChatAgentLocation.Panel))) { - store.add(this.registerDefaultParticipantView(providerDescriptor)); - } - const participantsAndCommandsDisambiguation: { categoryName: string; description: string; @@ -260,6 +228,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { } } + const store = new DisposableStore(); store.add(this._chatAgentService.registerAgent( providerDescriptor.id, { @@ -318,15 +287,9 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { return viewContainer; } - private hasRegisteredDefaultParticipantView = false; - private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable { - if (this.hasRegisteredDefaultParticipantView) { - this.logService.warn(`Tried to register a second default chat participant view for "${defaultParticipantDescriptor.id}"`); - return Disposable.None; - } - - // Register View - const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name; + private registerDefaultParticipantView(): IDisposable { + // Register View. Name must be hardcoded because we want to show it even when the extension fails to load due to an API version incompatibility. + const name = 'GitHub Copilot'; const viewDescriptor: IViewDescriptor[] = [{ id: CHAT_VIEW_ID, containerIcon: this._viewContainer.icon, @@ -336,12 +299,11 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(ChatViewPane), + when: ContextKeyExpr.or(CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED, CONTEXT_CHAT_EXTENSION_INVALID) }]; - this.hasRegisteredDefaultParticipantView = true; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); return toDisposable(() => { - this.hasRegisteredDefaultParticipantView = false; Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); }); } @@ -350,3 +312,39 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { return `${extensionId.value}_${participantName}`; } + +export class ChatCompatibilityNotifier implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.chatCompatNotifier'; + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService, + ) { + // It may be better to have some generic UI for this, for any extension that is incompatible, + // but this is only enabled for Copilot Chat now and it needs to be obvious. + + const showExtensionLabel = localize('showExtension', "Show Extension"); + const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); + viewsRegistry.registerViewWelcomeContent(CHAT_VIEW_ID, { + content: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date.") + `\n\n[${showExtensionLabel}](command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([['GitHub.copilot-chat']]))})`, + when: CONTEXT_CHAT_EXTENSION_INVALID, + }); + + const isInvalid = CONTEXT_CHAT_EXTENSION_INVALID.bindTo(contextKeyService); + extensionsWorkbenchService.queryLocal().then(exts => { + const chat = exts.find(ext => ext.identifier.id === 'github.copilot-chat'); + if (chat?.local?.validations.some(v => v[0] === Severity.Error)) { + // This catches vscode starting up with the invalid extension, but the extension may still get updated by vscode after this. + isInvalid.set(true); + } + }); + + const listener = chatAgentService.onDidChangeAgents(() => { + if (chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { + isInvalid.set(false); + listener.dispose(); + } + }); + } +} diff --git a/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 0cb92990dff..aa2655cba87 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -88,8 +88,9 @@ export class ChatViewPane extends ViewPane { } else if (this._widget?.viewModel?.initState === ChatModelInitState.Initialized) { // Model is initialized, and the default agent disappeared, so show welcome view this.didUnregisterProvider = true; - this._onDidChangeViewWelcomeState.fire(); } + + this._onDidChangeViewWelcomeState.fire(); })); } @@ -114,6 +115,10 @@ export class ChatViewPane extends ViewPane { } override shouldShowWelcome(): boolean { + if (!this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { + return true; + } + const noPersistedSessions = !this.chatService.hasSessions(); return this.didUnregisterProvider || !this._widget?.viewModel && (noPersistedSessions || this.didProviderRegistrationFail); } diff --git a/code/src/vs/workbench/contrib/chat/common/chatAgents.ts b/code/src/vs/workbench/contrib/chat/common/chatAgents.ts index 5cc12623309..f565b5b9266 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -24,7 +24,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { asJson, IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; @@ -233,11 +233,13 @@ export class ChatAgentService implements IChatAgentService { readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; private readonly _hasDefaultAgent: IContextKey; + private readonly _defaultAgentRegistered: IContextKey; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService); + this._defaultAgentRegistered = CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED.bindTo(this.contextKeyService); } registerAgent(id: string, data: IChatAgentData): IDisposable { @@ -246,6 +248,10 @@ export class ChatAgentService implements IChatAgentService { throw new Error(`Agent already registered: ${JSON.stringify(id)}`); } + if (data.isDefault) { + this._defaultAgentRegistered.set(true); + } + const that = this; const commands = data.slashCommands; data = { @@ -258,6 +264,10 @@ export class ChatAgentService implements IChatAgentService { this._agents.set(id, entry); return toDisposable(() => { this._agents.delete(id); + if (data.isDefault) { + this._defaultAgentRegistered.set(false); + } + this._onDidChangeAgents.fire(undefined); }); } @@ -445,7 +455,7 @@ export class ChatAgentService implements IChatAgentService { const participants = this.getAgents().reduce((acc, a) => { acc.push({ participant: a.id, disambiguation: a.disambiguation ?? [] }); for (const command of a.slashCommands) { - acc.push({ participant: a.id, command: command.name, disambiguation: [] }); + acc.push({ participant: a.id, command: command.name, disambiguation: command.disambiguation ?? [] }); } return acc; }, []); diff --git a/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 5930a97566c..f651a114a44 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -25,7 +25,9 @@ export const CONTEXT_CHAT_INPUT_HAS_FOCUS = new RawContextKey('chatInpu export const CONTEXT_IN_CHAT_INPUT = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const CONTEXT_IN_CHAT_SESSION = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); -export const CONTEXT_CHAT_ENABLED = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is registered.") }); +export const CONTEXT_CHAT_ENABLED = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); +export const CONTEXT_CHAT_PANEL_PARTICIPANT_REGISTERED = new RawContextKey('chatPanelParticipantRegistered', false, { type: 'boolean', description: localize('chatParticipantRegistered', "True when a default chat participant is registered for the panel.") }); +export const CONTEXT_CHAT_EXTENSION_INVALID = new RawContextKey('chatExtensionInvalid', false, { type: 'boolean', description: localize('chatExtensionInvalid', "True when the installed chat extension is invalid and needs to be updated.") }); export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey('chatCursorAtTop', false); export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey('chatInputHasAgent', false); export const CONTEXT_CHAT_LOCATION = new RawContextKey('chatLocation', undefined); diff --git a/code/src/vs/workbench/contrib/chat/common/chatModel.ts b/code/src/vs/workbench/contrib/chat/common/chatModel.ts index 14a94c046d7..b9578baed08 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -616,6 +616,8 @@ export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChat * TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too. */ export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData { + normalizeOldFields(raw); + if (!('version' in raw)) { return { version: 3, @@ -636,6 +638,30 @@ export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISe return raw; } +function normalizeOldFields(raw: ISerializableChatDataIn): void { + // Fill in fields that very old chat data may be missing + if (!raw.sessionId) { + raw.sessionId = generateUuid(); + } + + if (!raw.creationDate) { + raw.creationDate = getLastYearDate(); + } + + if ('version' in raw && (raw.version === 2 || raw.version === 3)) { + if (!raw.lastMessageDate) { + // A bug led to not porting creationDate properly, and that was copied to lastMessageDate, so fix that up if missing. + raw.lastMessageDate = getLastYearDate(); + } + } +} + +function getLastYearDate(): number { + const lastYearDate = new Date(); + lastYearDate.setFullYear(lastYearDate.getFullYear() - 1); + return lastYearDate.getTime(); +} + export function isExportableSessionData(obj: unknown): obj is IExportableChatData { const data = obj as IExportableChatData; return typeof data === 'object' && diff --git a/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 3ca4524e80e..2b6cfac741c 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -481,9 +481,8 @@ export class ChatService extends Disposable implements IChatService { } async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { - this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); - if (!request.trim()) { + if (!request.trim() && !options?.slashCommand && !options?.agentId) { this.trace('sendRequest', 'Rejected empty message'); return; } @@ -546,6 +545,7 @@ export class ChatService extends Disposable implements IChatService { const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); + const requests = [...model.getRequests()]; let gotProgress = false; const requestType = commandPart ? 'slashCommand' : 'string'; @@ -639,7 +639,7 @@ export class ChatService extends Disposable implements IChatService { if (this.configurationService.getValue('chat.experimental.detectParticipant.enabled') !== false && this.chatAgentService.hasChatParticipantDetectionProviders() && !agentPart && !commandPart && enableCommandDetection) { // We have no agent or command to scope history with, pass the full history to the participant detection provider - const defaultAgentHistory = this.getHistoryEntriesFromModel(model, location, defaultAgent.id); + const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id); // Prepare the request object that we will send to the participant detection provider const chatAgentRequest = await prepareChatAgentRequest(defaultAgent, agentSlashCommandPart?.command, enableCommandDetection, undefined, false); @@ -658,7 +658,7 @@ export class ChatService extends Disposable implements IChatService { await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); // Recompute history in case the agent or command changed - const history = this.getHistoryEntriesFromModel(model, location, agent.id); + const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id); const requestProps = await prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent); const pendingRequest = this._pendingRequests.get(sessionId); if (pendingRequest && !pendingRequest.requestId) { @@ -668,7 +668,7 @@ export class ChatService extends Disposable implements IChatService { const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); rawResult = agentResult; agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); - chatTitlePromise = model.getRequests().length === 1 && !model.customTitle ? this.chatAgentService.getChatTitle(defaultAgent.id, this.getHistoryEntriesFromModel(model, location, agent.id), CancellationToken.None) : undefined; + chatTitlePromise = model.getRequests().length === 1 && !model.customTitle ? this.chatAgentService.getChatTitle(defaultAgent.id, this.getHistoryEntriesFromModel(model.getRequests(), model.sessionId, location, agent.id), CancellationToken.None) : undefined; } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { request = model.addRequest(parsedRequest, { variables: [] }, attempt); completeResponseCreated(); @@ -775,9 +775,9 @@ export class ChatService extends Disposable implements IChatService { }; } - private getHistoryEntriesFromModel(model: IChatModel, location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] { + private getHistoryEntriesFromModel(requests: IChatRequestModel[], sessionId: string, location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] { const history: IChatAgentHistoryEntry[] = []; - for (const request of model.getRequests()) { + for (const request of requests) { if (!request.response) { continue; } @@ -791,7 +791,7 @@ export class ChatService extends Disposable implements IChatService { const promptTextResult = getPromptText(request.message); const historyRequest: IChatAgentRequest = { - sessionId: model.sessionId, + sessionId: sessionId, requestId: request.id, agentId: request.response.agent?.id ?? '', message: promptTextResult.message, diff --git a/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index bffc94d794e..b0455f490d4 100644 --- a/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/code/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -941,12 +941,6 @@ export class StopReadAloud extends Action2 { when: ScopedChatSynthesisInProgress, group: 'navigation', order: -1 - }, - { - id: MENU_INLINE_CHAT_WIDGET_SECONDARY, - when: ScopedChatSynthesisInProgress, - group: 'navigation', - order: -1 } ] }); @@ -980,6 +974,15 @@ export class StopReadChatItemAloud extends Action2 { CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered ), group: 'navigation' + }, + { + id: MENU_INLINE_CHAT_WIDGET_SECONDARY, + when: ContextKeyExpr.and( + ScopedChatSynthesisInProgress, // only when in progress + CONTEXT_RESPONSE, // only for responses + CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered + ), + group: 'navigation' } ] }); diff --git a/code/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/code/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index 4be9380fe47..c37b05e8a89 100644 --- a/code/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/code/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -17,7 +17,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, ISerializableChatData1, ISerializableChatData2, normalizeSerializableChatData, Response } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, normalizeSerializableChatData, Response } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestTextPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -230,4 +230,53 @@ suite('normalizeSerializableChatData', () => { assert.strictEqual(newData.lastMessageDate, v2Data.lastMessageDate); assert.strictEqual(newData.customTitle, v2Data.computedTitle); }); + + test('old bad data', () => { + const v1Data: ISerializableChatData1 = { + // Testing the scenario where these are missing + sessionId: undefined!, + creationDate: undefined!, + + initialLocation: undefined, + isImported: false, + requesterAvatarIconUri: undefined, + requesterUsername: 'me', + requests: [], + responderAvatarIconUri: undefined, + responderUsername: 'bot', + welcomeMessage: [] + }; + + const newData = normalizeSerializableChatData(v1Data); + assert.strictEqual(newData.version, 3); + assert.ok(newData.creationDate > 0); + assert.ok(newData.lastMessageDate > 0); + assert.ok(newData.sessionId); + }); + + test('v3 with bug', () => { + const v3Data: ISerializableChatData3 = { + // Test case where old data was wrongly normalized and these fields were missing + creationDate: undefined!, + lastMessageDate: undefined!, + + version: 3, + initialLocation: undefined, + isImported: false, + requesterAvatarIconUri: undefined, + requesterUsername: 'me', + requests: [], + responderAvatarIconUri: undefined, + responderUsername: 'bot', + sessionId: 'session1', + welcomeMessage: [], + customTitle: 'computed title' + }; + + const newData = normalizeSerializableChatData(v3Data); + assert.strictEqual(newData.version, 3); + assert.ok(newData.creationDate > 0); + assert.ok(newData.lastMessageDate > 0); + assert.ok(newData.sessionId); + }); }); diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsView.ts b/code/src/vs/workbench/contrib/comments/browser/commentsView.ts index 253f7023f72..75deafec5d3 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -172,7 +172,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this.filters = this._register(new CommentsFilters({ showResolved: this.viewState['showResolved'] !== false, showUnresolved: this.viewState['showUnresolved'] !== false, - sortBy: this.viewState['sortBy'], + sortBy: this.viewState['sortBy'] ?? CommentsSortOrder.ResourceAscending, }, this.contextKeyService)); this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved)); diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/code/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index 8fedca8e845..e2494534a05 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -7,7 +7,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { CommentsViewFilterFocusContextKey, ICommentsView } from 'vs/workbench/contrib/comments/browser/comments'; import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -74,9 +74,9 @@ export class CommentsFilters extends Disposable { } } - private _sortBy = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService); + private _sortBy: IContextKey = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService); get sortBy(): CommentsSortOrder { - return this._sortBy.get()!; + return this._sortBy.get() ?? CommentsSortOrder.ResourceAscending; } set sortBy(sortBy: CommentsSortOrder) { if (this._sortBy.get() !== sortBy) { @@ -208,7 +208,7 @@ registerAction2(class extends ViewAction { icon: Codicon.history, viewId: COMMENTS_VIEW_ID, toggled: { - condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.UpdatedAtDescending), + condition: ContextKeyExpr.equals(CONTEXT_KEY_SORT_BY.key, CommentsSortOrder.UpdatedAtDescending), title: localize('sorting by updated at', "Updated Time"), }, menu: { @@ -229,13 +229,13 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleSortByResource`, - title: localize('toggle sorting by resource', "File"), + title: localize('toggle sorting by resource', "Position in File"), category: localize('comments', "Comments"), icon: Codicon.history, viewId: COMMENTS_VIEW_ID, toggled: { - condition: ContextKeyExpr.equals('commentsView.sortBy', CommentsSortOrder.ResourceAscending), - title: localize('sorting by file', "File"), + condition: ContextKeyExpr.equals(CONTEXT_KEY_SORT_BY.key, CommentsSortOrder.ResourceAscending), + title: localize('sorting by position in file', "Position in File"), }, menu: { id: commentSortSubmenu, diff --git a/code/src/vs/workbench/contrib/debug/browser/callStackWidget.ts b/code/src/vs/workbench/contrib/debug/browser/callStackWidget.ts index 50cfbf0f3a5..0b6b0e6bc28 100644 --- a/code/src/vs/workbench/contrib/debug/browser/callStackWidget.ts +++ b/code/src/vs/workbench/contrib/debug/browser/callStackWidget.ts @@ -12,20 +12,24 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue } from 'vs/base/common/observable'; +import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; import { Constants } from 'vs/base/common/uint'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import 'vs/css!./media/callStackWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorContributionCtor, EditorContributionInstantiation, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; +import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { Location } from 'vs/editor/common/languages'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ClickLinkGesture, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture'; import { localize, localize2 } from 'vs/nls'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -38,7 +42,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { makeStackFrameColumnDecoration, TOP_STACK_FRAME_DECORATION } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; export class CallStackFrame { @@ -97,6 +101,9 @@ class WrappedCustomStackFrame implements IFrameLikeItem { constructor(public readonly original: CustomStackFrame) { } } +const isFrameLike = (item: unknown): item is IFrameLikeItem => + item instanceof WrappedCallStackFrame || item instanceof WrappedCustomStackFrame; + type ListItem = WrappedCallStackFrame | SkippedCallFrames | WrappedCustomStackFrame; const WIDGET_CLASS_NAME = 'multiCallStackWidget'; @@ -157,6 +164,17 @@ export class CallStackWidget extends Disposable { this.layoutEmitter.fire(); } + public collapseAll() { + transaction(tx => { + for (let i = 0; i < this.list.length; i++) { + const frame = this.list.element(i); + if (isFrameLike(frame)) { + frame.collapsed.set(true, tx); + } + } + }); + } + private async loadFrame(replacing: SkippedCallFrames): Promise { if (!this.cts) { return; @@ -356,9 +374,9 @@ abstract class AbstractFrameRenderer { - item.collapsed.set(!item.collapsed.get(), undefined); - })); + const toggleCollapse = () => item.collapsed.set(!item.collapsed.get(), undefined); + elementStore.add(collapse.onDidClick(toggleCollapse)); + elementStore.add(dom.addDisposableListener(elements.title, 'click', toggleCollapse)); } disposeElement(element: ListItem, index: number, templateData: T, height: number | undefined): void { @@ -382,26 +400,33 @@ class FrameCodeRenderer extends AbstractFrameRenderer { private readonly containingEditor: ICodeEditor | undefined, private readonly onLayout: Event, @ITextModelService private readonly modelService: ITextModelService, - @ICodeEditorService private readonly editorService: ICodeEditorService, @IInstantiationService instantiationService: IInstantiationService, ) { super(instantiationService); } protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IStackTemplateData { + // override default e.g. language contributions, only allow users to click + // on code in the call stack to go to its source location + const contributions: IEditorContributionDescription[] = [{ + id: ClickToLocationContribution.ID, + instantiation: EditorContributionInstantiation.BeforeFirstInteraction, + ctor: ClickToLocationContribution as EditorContributionCtor, + }]; + const editor = this.containingEditor ? this.instantiationService.createInstance( EmbeddedCodeEditorWidget, data.elements.editor, editorOptions, - { isSimpleWidget: true }, + { isSimpleWidget: true, contributions }, this.containingEditor, ) : this.instantiationService.createInstance( CodeEditorWidget, data.elements.editor, editorOptions, - { isSimpleWidget: true }, + { isSimpleWidget: true, contributions }, ); data.templateStore.add(editor); @@ -423,20 +448,6 @@ class FrameCodeRenderer extends AbstractFrameRenderer { const uri = item.source!; template.label.element.setFile(uri); - template.elements.title.role = 'link'; - elementStore.add(dom.addDisposableListener(template.elements.title, 'click', e => { - this.editorService.openCodeEditor({ - resource: uri, - options: { - selection: Range.fromPositions({ - column: item.column ?? 1, - lineNumber: item.line ?? 1, - }), - selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, - }, - }, this.containingEditor || null, e.ctrlKey || e.metaKey); - })); - const cts = new CancellationTokenSource(); elementStore.add(toDisposable(() => cts.dispose(true))); this.modelService.createModelReference(uri).then(reference => { @@ -632,6 +643,73 @@ class SkippedRenderer implements IListRenderer { } } +/** A simple contribution that makes all data in the editor clickable to go to the location */ +class ClickToLocationContribution extends Disposable implements IEditorContribution { + public static readonly ID = 'clickToLocation'; + private readonly linkDecorations: IEditorDecorationsCollection; + private current: { line: number; word: IWordAtPosition } | undefined; + + constructor( + private readonly editor: ICodeEditor, + @IEditorService editorService: IEditorService, + ) { + super(); + this.linkDecorations = editor.createDecorationsCollection(); + this._register(toDisposable(() => this.linkDecorations.clear())); + + const clickLinkGesture = this._register(new ClickLinkGesture(editor)); + + this._register(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => { + this.onMove(mouseEvent); + })); + this._register(clickLinkGesture.onExecute((e) => { + const model = this.editor.getModel(); + if (!this.current || !model) { + return; + } + + editorService.openEditor({ + resource: model.uri, + options: { + selection: Range.fromPositions(new Position(this.current.line, this.current.word.startColumn)), + selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, + }, + }, e.hasSideBySideModifier ? SIDE_GROUP : undefined); + })); + } + + private onMove(mouseEvent: ClickLinkMouseEvent) { + if (!mouseEvent.hasTriggerModifier) { + return this.clear(); + } + + const position = mouseEvent.target.position; + const word = position && this.editor.getModel()?.getWordAtPosition(position); + if (!word) { + return this.clear(); + } + + const prev = this.current?.word; + if (prev && prev.startColumn === word.startColumn && prev.endColumn === word.endColumn && prev.word === word.word) { + return; + } + + this.current = { word, line: position.lineNumber }; + this.linkDecorations.set([{ + range: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn), + options: { + description: 'call-stack-go-to-file-link', + inlineClassName: 'call-stack-go-to-file-link', + }, + }]); + } + + private clear() { + this.linkDecorations.clear(); + this.current = undefined; + } +} + registerAction2(class extends Action2 { constructor() { super({ diff --git a/code/src/vs/workbench/contrib/debug/browser/media/callStackWidget.css b/code/src/vs/workbench/contrib/debug/browser/media/callStackWidget.css index 60f5493087f..e4c0a1a61b1 100644 --- a/code/src/vs/workbench/contrib/debug/browser/media/callStackWidget.css +++ b/code/src/vs/workbench/contrib/debug/browser/media/callStackWidget.css @@ -59,3 +59,9 @@ line-height: inherit !important; } } + +.monaco-editor .call-stack-go-to-file-link { + text-decoration: underline; + cursor: pointer; + color: var(--vscode-editorLink-activeForeground) !important; +} diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 8119cdd08fa..22d6a83fa5d 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, MENU_INLINE_CHAT_CONTENT_STATUS, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, MENU_INLINE_CHAT_CONTENT_STATUS, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; import { Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -287,7 +287,7 @@ export class DiscardHunkAction extends AbstractInlineChatAction { constructor() { super({ - id: 'inlineChat.discardHunkChange', + id: ACTION_DISCARD_CHANGES, title: localize('discard', 'Discard'), icon: Codicon.chromeClose, precondition: CTX_INLINE_CHAT_VISIBLE, diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 29b4dee9719..c6d9ae577fb 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -39,7 +39,7 @@ import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; -import { HunkInformation, Session, StashedSession } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { HunkInformation, HunkState, Session, StashedSession } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatError } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; import { EditModeStrategy, HunkAction, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; @@ -622,6 +622,7 @@ export class InlineChatController implements IEditorContribution { return; } if (e.kind === 'move') { + assertType(this._session); const log: typeof this._log = (msg: string, ...args: any[]) => this._log('state=_showRequest) moving inline chat', msg, ...args); log('move was requested', e.target, e.range); @@ -636,13 +637,13 @@ export class InlineChatController implements IEditorContribution { } const newEditor = editorPane.getControl(); - if (!newEditor || !isCodeEditor(newEditor) || !newEditor.hasModel()) { + if (!isCodeEditor(newEditor) || !newEditor.hasModel()) { log('new editor is either missing or not a code editor or does not have a model'); return; } - if (!this._session) { - log('controller does not have a session'); + if (this._inlineChatSessionService.getSession(newEditor, e.target)) { + log('new editor ALREADY has a session'); return; } @@ -741,7 +742,7 @@ export class InlineChatController implements IEditorContribution { await responsePromise.p; await progressiveEditsQueue.whenIdle(); - if (response.isCanceled) { + if (response.result?.errorDetails) { await this._session.undoChangesUntil(response.requestId); } @@ -758,7 +759,6 @@ export class InlineChatController implements IEditorContribution { if (response.result?.errorDetails) { // - await this._session.undoChangesUntil(response.requestId); } else if (response.response.value.length === 0) { // empty -> show message @@ -990,8 +990,21 @@ export class InlineChatController implements IEditorContribution { // ---- controller API showSaveHint(): void { - const status = localize('savehint', "Accept or discard changes to continue saving"); + if (!this._session) { + return; + } + + const status = localize('savehint', "Accept or discard changes to continue saving."); this._ui.value.zone.widget.updateStatus(status, { classes: ['warn'] }); + + if (this._ui.value.zone.position) { + this._editor.revealLineInCenterIfOutsideViewport(this._ui.value.zone.position.lineNumber); + } else { + const hunk = this._session.hunkData.getInfo().find(info => info.getState() === HunkState.Pending); + if (hunk) { + this._editor.revealLineInCenterIfOutsideViewport(hunk.getRangesN()[0].startLineNumber); + } + } } acceptInput() { diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index a3a6a25a86a..82c395d21e2 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -360,27 +360,30 @@ export class HunkData { // mirror textModelN changes to textModel0 execept for those that // overlap with a hunk - type HunkRangePair = { rangeN: Range; range0: Range }; + type HunkRangePair = { rangeN: Range; range0: Range; markAccepted: () => void }; const hunkRanges: HunkRangePair[] = []; const ranges0: Range[] = []; - for (const { textModelNDecorations, textModel0Decorations, state } of this._data.values()) { + for (const entry of this._data.values()) { - if (state === HunkState.Pending) { + if (entry.state === HunkState.Pending) { // pending means the hunk's changes aren't "sync'd" yet - for (let i = 1; i < textModelNDecorations.length; i++) { - const rangeN = this._textModelN.getDecorationRange(textModelNDecorations[i]); - const range0 = this._textModel0.getDecorationRange(textModel0Decorations[i]); + for (let i = 1; i < entry.textModelNDecorations.length; i++) { + const rangeN = this._textModelN.getDecorationRange(entry.textModelNDecorations[i]); + const range0 = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]); if (rangeN && range0) { - hunkRanges.push({ rangeN, range0 }); + hunkRanges.push({ + rangeN, range0, + markAccepted: () => entry.state = HunkState.Accepted + }); } } - } else if (state === HunkState.Accepted) { + } else if (entry.state === HunkState.Accepted) { // accepted means the hunk's changes are also in textModel0 - for (let i = 1; i < textModel0Decorations.length; i++) { - const range = this._textModel0.getDecorationRange(textModel0Decorations[i]); + for (let i = 1; i < entry.textModel0Decorations.length; i++) { + const range = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]); if (range) { ranges0.push(range); } @@ -399,16 +402,20 @@ export class HunkData { let pendingChangesLen = 0; - for (const { rangeN, range0 } of hunkRanges) { - if (rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) { + for (const entry of hunkRanges) { + if (entry.rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) { // pending hunk _before_ this change. When projecting into textModel0 we need to // subtract that. Because diffing is relaxed it might include changes that are not // actual insertions/deletions. Therefore we need to take the length of the original // range into account. - pendingChangesLen += this._textModelN.getValueLengthInRange(rangeN); - pendingChangesLen -= this._textModel0.getValueLengthInRange(range0); - - } else if (Range.areIntersectingOrTouching(rangeN, change.range)) { + pendingChangesLen += this._textModelN.getValueLengthInRange(entry.rangeN); + pendingChangesLen -= this._textModel0.getValueLengthInRange(entry.range0); + + } else if (Range.areIntersectingOrTouching(entry.rangeN, change.range)) { + // an edit overlaps with a (pending) hunk. We take this as a signal + // to mark the hunk as accepted and to ignore the edit. The range of the hunk + // will be up-to-date because of decorations created for them + entry.markAccepted(); isOverlapping = true; break; @@ -447,24 +454,23 @@ export class HunkData { diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); - if (!diff || diff.changes.length === 0) { - // return new HunkData([], session); - return; - } - - // merge changes neighboring changes - const mergedChanges = [diff.changes[0]]; - for (let i = 1; i < diff.changes.length; i++) { - const lastChange = mergedChanges[mergedChanges.length - 1]; - const thisChange = diff.changes[i]; - if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) { - mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping( - lastChange.original.join(thisChange.original), - lastChange.modified.join(thisChange.modified), - (lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? []) - ); - } else { - mergedChanges.push(thisChange); + let mergedChanges: DetailedLineRangeMapping[] = []; + + if (diff && diff.changes.length > 0) { + // merge changes neighboring changes + mergedChanges = [diff.changes[0]]; + for (let i = 1; i < diff.changes.length; i++) { + const lastChange = mergedChanges[mergedChanges.length - 1]; + const thisChange = diff.changes[i]; + if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) { + mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping( + lastChange.original.join(thisChange.original), + lastChange.modified.join(thisChange.modified), + (lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? []) + ); + } else { + mergedChanges.push(thisChange); + } } } diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index c44f6e6ee7e..01df414ec2a 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -8,7 +8,7 @@ import { coalesceInPlace } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { themeColorFromId } from 'vs/base/common/themables'; +import { themeColorFromId, ThemeIcon } from 'vs/base/common/themables'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines'; @@ -27,7 +27,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { HunkInformation, Session, HunkState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; -import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { assertType } from 'vs/base/common/types'; import { IModelService } from 'vs/editor/common/services/model'; import { performAsyncTextEdit, asProgressiveEdit } from './utils'; @@ -43,6 +43,9 @@ import { generateUuid } from 'vs/base/common/uuid'; import { MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Iterable } from 'vs/base/common/iterator'; +import { ConflictActionsFactory, IContentWidgetAction } from 'vs/workbench/contrib/mergeEditor/browser/view/conflictActions'; +import { observableValue } from 'vs/base/common/observable'; +import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; export interface IEditObserver { start(): void; @@ -216,8 +219,10 @@ type HunkDisplayData = { decorationIds: string[]; - viewZoneId: string | undefined; - viewZone: IViewZone; + diffViewZoneId: string | undefined; + diffViewZone: IViewZone; + + lensActionsViewZoneIds?: string[]; distance: number; position: Position; @@ -257,6 +262,7 @@ export class LiveStrategy extends EditModeStrategy { private readonly _ctxCurrentChangeShowsDiff: IContextKey; private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; + private readonly _lensActionsFactory: ConflictActionsFactory; private _editCount: number = 0; constructor( @@ -268,6 +274,8 @@ export class LiveStrategy extends EditModeStrategy { @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configService: IConfigurationService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextService: IContextKeyService, @ITextFileService textFileService: ITextFileService, @IInstantiationService instaService: IInstantiationService ) { @@ -276,6 +284,7 @@ export class LiveStrategy extends EditModeStrategy { this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService); this._progressiveEditingDecorations = this._editor.createDecorationsCollection(); + this._lensActionsFactory = this._store.add(new ConflictActionsFactory(this._editor)); } @@ -487,45 +496,95 @@ export class LiveStrategy extends EditModeStrategy { afterLineNumber: -1, heightInLines: result.heightInLines, domNode, - ordinal: 50000 + 1 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 + ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 }; const toggleDiff = () => { const scrollState = StableEditorScrollState.capture(this._editor); changeDecorationsAndViewZones(this._editor, (_decorationsAccessor, viewZoneAccessor) => { assertType(data); - if (!data.viewZoneId) { + if (!data.diffViewZoneId) { const [hunkRange] = hunkData.getRangesN(); viewZoneData.afterLineNumber = hunkRange.startLineNumber - 1; - data.viewZoneId = viewZoneAccessor.addZone(viewZoneData); + data.diffViewZoneId = viewZoneAccessor.addZone(viewZoneData); overlay?.updateExtraTop(result.heightInLines); } else { - viewZoneAccessor.removeZone(data.viewZoneId!); + viewZoneAccessor.removeZone(data.diffViewZoneId!); overlay?.updateExtraTop(0); - data.viewZoneId = undefined; + data.diffViewZoneId = undefined; } }); - this._ctxCurrentChangeShowsDiff.set(typeof data?.viewZoneId === 'string'); + this._ctxCurrentChangeShowsDiff.set(typeof data?.diffViewZoneId === 'string'); scrollState.restore(this._editor); }; - const overlay = this._showOverlayToolbar + const overlay = this._showOverlayToolbar && false ? this._instaService.createInstance(InlineChangeOverlay, this._editor, hunkData) : undefined; + + let lensActions: DisposableStore | undefined; + const lensActionsViewZoneIds: string[] = []; + + if (this._showOverlayToolbar && hunkData.getState() === HunkState.Pending) { + + lensActions = new DisposableStore(); + + const menu = this._menuService.createMenu(MENU_INLINE_CHAT_ZONE, this._contextService); + const makeActions = () => { + const actions: IContentWidgetAction[] = []; + const tuples = menu.getActions(); + for (const [, group] of tuples) { + for (const item of group) { + if (item instanceof MenuItemAction) { + + let text = item.label; + + if (item.id === ACTION_TOGGLE_DIFF) { + text = item.checked ? 'Hide Changes' : 'Show Changes'; + } else if (ThemeIcon.isThemeIcon(item.item.icon)) { + text = `$(${item.item.icon.id}) ${text}`; + } + + actions.push({ + text, + tooltip: item.tooltip, + action: async () => item.run(), + }); + } + } + } + return actions; + }; + + const obs = observableValue(this, makeActions()); + lensActions.add(menu.onDidChange(() => obs.set(makeActions(), undefined))); + lensActions.add(menu); + + lensActions.add(this._lensActionsFactory.createWidget(viewZoneAccessor, + hunkRanges[0].startLineNumber - 1, + obs, + lensActionsViewZoneIds + )); + } + const remove = () => { changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { assertType(data); for (const decorationId of data.decorationIds) { decorationsAccessor.removeDecoration(decorationId); } - if (data.viewZoneId) { - viewZoneAccessor.removeZone(data.viewZoneId); + if (data.diffViewZoneId) { + viewZoneAccessor.removeZone(data.diffViewZoneId); } data.decorationIds = []; - data.viewZoneId = undefined; + data.diffViewZoneId = undefined; + + data.lensActionsViewZoneIds?.forEach(viewZoneAccessor.removeZone); + data.lensActionsViewZoneIds = undefined; }); + lensActions?.dispose(); overlay?.dispose(); }; @@ -548,8 +607,9 @@ export class LiveStrategy extends EditModeStrategy { data = { hunk: hunkData, decorationIds, - viewZoneId: '', - viewZone: viewZoneData, + diffViewZoneId: '', + diffViewZone: viewZoneData, + lensActionsViewZoneIds, distance: myDistance, position: hunkRanges[0].getStartPosition().delta(-1), acceptHunk, diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 252534e6643..91a572c2ee6 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -385,7 +385,7 @@ export class InlineChatWidget { } protected _getExtraHeight(): number { - return 4 /* padding */ + 2 /*border*/ + 4 /*shadow*/; + return 2 /*border*/ + 4 /*shadow*/; } get value(): string { diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 1ce7b490450..42612f493f8 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { addDisposableListener, Dimension } from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { toDisposable } from 'vs/base/common/lifecycle'; +import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -29,6 +29,7 @@ export class InlineChatZoneWidget extends ZoneWidget { readonly widget: EditorBasedInlineChatWidget; + private readonly _scrollUp = this._disposables.add(new ScrollUpState(this.editor)); private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; private _dimension?: Dimension; @@ -165,6 +166,7 @@ export class InlineChatZoneWidget extends ZoneWidget { this.widget.focus(); revealZone(); + this._scrollUp.enable(); } override updatePositionAndHeight(position: Position): void { @@ -186,14 +188,15 @@ export class InlineChatZoneWidget extends ZoneWidget { return isResponseVM(candidate) && candidate.response.value.length > 0; }); - if (hasResponse && zoneTop < scrollTop) { + if (hasResponse && zoneTop < scrollTop || this._scrollUp.didScrollUp) { // don't reveal the zone if it is already out of view (unless we are still getting ready) - return () => { + // or if an outside scroll-up happened (e.g the user scrolled up to see the new content) + return this._scrollUp.runIgnored(() => { scrollState.restore(this.editor); - }; + }); } - return () => { + return this._scrollUp.runIgnored(() => { scrollState.restore(this.editor); const scrollTop = this.editor.getScrollTop(); @@ -216,7 +219,7 @@ export class InlineChatZoneWidget extends ZoneWidget { this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); } - }; + }); } protected override revealRange(range: Range, isLastLine: boolean): void { @@ -229,6 +232,7 @@ export class InlineChatZoneWidget extends ZoneWidget { override hide(): void { const scrollState = StableEditorBottomScrollState.capture(this.editor); + this._scrollUp.disable(); this._ctxCursorPosition.reset(); this.widget.reset(); this.widget.chatWidget.setVisible(false); @@ -237,3 +241,54 @@ export class InlineChatZoneWidget extends ZoneWidget { scrollState.restore(this.editor); } } + +class ScrollUpState { + + private _lastScrollTop: number = this._editor.getScrollTop(); + private _didScrollUp?: boolean; + private _ignoreEvents = false; + + private readonly _listener = new MutableDisposable(); + + constructor(private readonly _editor: ICodeEditor) { } + + dispose(): void { + this._listener.dispose(); + } + + enable(): void { + this._didScrollUp = undefined; + this._listener.value = this._editor.onDidScrollChange(e => { + if (!e.scrollTopChanged || this._ignoreEvents) { + return; + } + const currentScrollTop = e.scrollTop; + if (currentScrollTop > this._lastScrollTop) { + this._listener.clear(); + this._didScrollUp = true; + } + this._lastScrollTop = currentScrollTop; + }); + } + + disable(): void { + this._listener.clear(); + this._didScrollUp = undefined; + } + + runIgnored(callback: () => void): () => void { + return () => { + this._ignoreEvents = true; + try { + return callback(); + } finally { + this._ignoreEvents = false; + } + }; + } + + get didScrollUp(): boolean | undefined { + return this._didScrollUp; + } + +} diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/code/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index f209158f8fe..f79ae5a6a9f 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/code/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -20,7 +20,7 @@ } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part { - padding: 4px 6px 0 6px; + padding: 2px 6px 0 6px; } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-execute-toolbar { @@ -32,6 +32,12 @@ border-radius: 2px; } + +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-input-followups .interactive-session-followups { + margin: 2px 0 0 4px; +} + + .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list { padding: 4px 0 0 0; } @@ -70,7 +76,15 @@ display: flex; justify-content: space-between; align-items: center; - padding: 6px 6px 0 6px + padding-left: 6px; + padding-right: 6px; +} + +.monaco-workbench .inline-chat > .status { + .label, + .actions { + padding-top: 6px; + } } .monaco-workbench .inline-chat .status .actions.hidden { @@ -92,7 +106,8 @@ .monaco-workbench .inline-chat .status .label.status { margin-left: auto; - padding: 0 6px; + padding-right: 6px; + padding-left: 6px; } .monaco-workbench .inline-chat .status .label.hidden, diff --git a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index d656e6c3f25..f4c87af0d22 100644 --- a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -116,6 +116,7 @@ export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey() { + override get id() { return 'one'; } + }; + session.markModelVersion(fakeRequest); + + assert.strictEqual(editor.getModel().getLineCount(), 15); + + await makeEditAsAi([EditOperation.replace(new Range(7, 1, 7, Number.MAX_SAFE_INTEGER), `error = error.replace( + /See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/, + 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information' + );`)]); + + assert.strictEqual(editor.getModel().getLineCount(), 18); + + // called when a response errors out + await session.undoChangesUntil(fakeRequest.id); + await session.hunkData.recompute({ applied: 0, sha1: 'fakeSha1' }, undefined); + + assert.strictEqual(editor.getModel().getValue(), origValue); + + session.hunkData.discardAll(); // called when dimissing the session + assert.strictEqual(editor.getModel().getValue(), origValue); + }); + + test('Apply Code\'s preview should be easier to undo/esc #7537', async function () { + model.setValue(`export function fib(n) { + if (n <= 0) return 0; + if (n === 1) return 0; + if (n === 2) return 1; + return fib(n - 1) + fib(n - 2); +}`); + const session = await inlineChatSessionService.createSession(editor, { editMode: EditMode.Live }, CancellationToken.None); + assertType(session); + + await makeEditAsAi([EditOperation.replace(new Range(5, 1, 6, Number.MAX_SAFE_INTEGER), ` + let a = 0, b = 1, c; + for (let i = 3; i <= n; i++) { + c = a + b; + a = b; + b = c; + } + return b; +}`)]); + + assert.strictEqual(session.hunkData.size, 1); + assert.strictEqual(session.hunkData.pending, 1); + assert.ok(session.hunkData.getInfo().every(d => d.getState() === HunkState.Pending)); + + await assertSnapshot(editor.getModel().getValue(), { name: '1' }); + + await model.undo(); + await assertSnapshot(editor.getModel().getValue(), { name: '2' }); + + // overlapping edits (even UNDO) mark edits as accepted + assert.strictEqual(session.hunkData.size, 1); + assert.strictEqual(session.hunkData.pending, 0); + assert.ok(session.hunkData.getInfo().every(d => d.getState() === HunkState.Accepted)); + + // no further change when discarding + session.hunkData.discardAll(); // CANCEL + await assertSnapshot(editor.getModel().getValue(), { name: '2' }); + }); + }); diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts index 8d88a891a5b..3290793d532 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts @@ -54,8 +54,8 @@ export class ConflictActionsFactory extends Disposable { newStyle += `${this._styleClassName} { font-family: var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}}`; } this._styleElement.textContent = newStyle; - this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily ?? 'inherit'); - this._editor.getContainerDomNode().style.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings); + this._editor.getContainerDomNode().style?.setProperty(fontFamilyVar, fontFamily ?? 'inherit'); + this._editor.getContainerDomNode().style?.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings); } private _getLayoutInfo() { diff --git a/code/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts b/code/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts index 3175a1ca927..27a48ee27b9 100644 --- a/code/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts +++ b/code/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts @@ -33,6 +33,7 @@ export abstract class FixedZoneWidget extends Disposable { domNode: document.createElement('div'), afterLineNumber: afterLineNumber, heightInPx: height, + ordinal: 50000 + 1, onComputedHeight: (height) => { this.widgetDomNode.style.height = `${height}px`; }, diff --git a/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index ba24fdfaf6d..69060d9dd4c 100644 --- a/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/code/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -314,7 +314,7 @@ class HistoryItemRenderer implements ITreeRenderer labels.includes(l.title)); - if (historyItemLabels) { + if (historyItemLabels.length > 0) { const historyItemGroupLocalColor = colorTheme.getColor(historyItemGroupLocal); const historyItemGroupRemoteColor = colorTheme.getColor(historyItemGroupRemote); const historyItemGroupBaseColor = colorTheme.getColor(historyItemGroupBase); diff --git a/code/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts b/code/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts index 4ef1cdae76e..5265cf2c785 100644 --- a/code/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts +++ b/code/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts @@ -117,22 +117,21 @@ export class NotebookSearchService implements INotebookSearchService { } private async doesFileExist(includes: string[], folderQueries: IFolderQuery[], token: CancellationToken): Promise { - const promises: Promise[] = includes.map(async includePattern => { + const promises: Promise[] = includes.map(async includePattern => { const query = this.queryBuilder.file(folderQueries.map(e => e.folder), { includePattern: includePattern.startsWith('/') ? includePattern : '**/' + includePattern, // todo: find cleaner way to ensure that globs match all appropriate filetypes - exists: true + exists: true, + onlyFileScheme: true, }); return this.searchService.fileSearch( query, token ).then((ret) => { - if (!ret.limitHit) { - throw Error('File not found'); - } + return !!ret.limitHit; }); }); - return Promise.any(promises).then(() => true).catch(() => false); + return Promise.any(promises); } private async getClosedNotebookResults(textQuery: ITextQuery, scannedFiles: ResourceSet, token: CancellationToken): Promise { diff --git a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index c624a5f0036..e3a7a149174 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/code/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -388,9 +388,9 @@ function Send-Completions { if ($completions.CompletionMatches.Count -gt 0 -and $completions.CompletionMatches.Where({ $_.ResultType -eq 3 -or $_.ResultType -eq 4 })) { # Add `../ relative to the top completion $firstCompletion = $completions.CompletionMatches[0] - if ($firstCompletion.CompletionText.StartsWith('../')) { - if ($completionPrefix -match '(\.\.\/)+') { - $parentDir = "$($matches[0])../" + if ($firstCompletion.CompletionText.StartsWith("..$([System.IO.Path]::DirectorySeparatorChar)")) { + if ($completionPrefix -match "(\.\.$([System.IO.Path]::DirectorySeparatorChar))+") { + $parentDir = "$($matches[0])..$([System.IO.Path]::DirectorySeparatorChar)" $currentPath = Split-Path -Parent $firstCompletion.ToolTip try { $parentDirPath = Split-Path -Parent $currentPath diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 541b5b7ccf9..67e4e4af62f 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -2317,7 +2317,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { async handleMouseEvent(event: MouseEvent, contextMenu: IMenu): Promise<{ cancelContextMenu: boolean } | void> { // Don't handle mouse event if it was on the scroll bar - if (dom.isHTMLElement(event.target) && event.target.classList.contains('scrollbar')) { + if (dom.isHTMLElement(event.target) && (event.target.classList.contains('scrollbar') || event.target.classList.contains('slider'))) { return { cancelContextMenu: true }; } diff --git a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 9ef44f9bcea..433de4eedc7 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -9,6 +9,7 @@ import { AutoOpenBarrier } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { isWindows } from 'vs/base/common/platform'; import { localize2 } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService, IReadableSet } from 'vs/platform/contextkey/common/contextkey'; @@ -170,16 +171,20 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo // If completions are requested, pause and queue input events until completions are // received. This fixing some problems in PowerShell, particularly enter not executing - // when typing quickly and some characters being printed twice. - let barrier: AutoOpenBarrier | undefined; - this.add(addon.onDidRequestCompletions(() => { - barrier = new AutoOpenBarrier(2000); - this._instance.pauseInputEvents(barrier); - })); - this.add(addon.onDidReceiveCompletions(() => { - barrier?.open(); - barrier = undefined; - })); + // when typing quickly and some characters being printed twice. On Windows this isn't + // needed because inputs are _not_ echoed when not handled immediately. + // TODO: This should be based on the OS of the pty host, not the client + if (!isWindows) { + let barrier: AutoOpenBarrier | undefined; + this.add(addon.onDidRequestCompletions(() => { + barrier = new AutoOpenBarrier(2000); + this._instance.pauseInputEvents(barrier); + })); + this.add(addon.onDidReceiveCompletions(() => { + barrier?.open(); + barrier = undefined; + })); + } } } } diff --git a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts index cb133b4b20d..c6c396e5840 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts @@ -30,6 +30,10 @@ export class TestResultStackWidget extends Disposable { )); } + public collapseAll() { + this.widget.collapseAll(); + } + public update(messageFrame: AnyStackFrame, stack: ITestMessageStackFrame[]) { this.widget.setFrames([messageFrame, ...stack.map(frame => new CallStackFrame( frame.label, diff --git a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts index 9fa863304ff..e61463bcdc0 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts @@ -22,6 +22,9 @@ interface ISubjectCommon { controllerId: string; } +export const inspectSubjectHasStack = (subject: InspectSubject | undefined) => + subject instanceof MessageSubject && !!subject.stack?.length; + export class MessageSubject implements ISubjectCommon { public readonly test: ITestItem; public readonly message: ITestMessage; diff --git a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts index a65a1057df5..6be1cd3c43f 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts @@ -838,22 +838,21 @@ class TreeActionsProvider { } if (element instanceof TestMessageElement) { + id = MenuId.TestMessageContext; + contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]); + primary.push(new Action( - 'testing.outputPeek.goToFile', - localize('testing.goToFile', "Go to Source"), + 'testing.outputPeek.goToTest', + localize('testing.goToTest', "Go to Test"), ThemeIcon.asClassName(Codicon.goToFile), undefined, () => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId), )); - } - if (element instanceof TestMessageElement) { - id = MenuId.TestMessageContext; - contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]); if (this.showRevealLocationOnMessages && element.location) { primary.push(new Action( 'testing.outputPeek.goToError', - localize('testing.goToError', "Go to Source"), + localize('testing.goToError', "Go to Error"), ThemeIcon.asClassName(Codicon.goToFile), undefined, () => this.editorService.openEditor({ diff --git a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts index db80e2c6ed8..67f9f05bfc7 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts @@ -322,6 +322,13 @@ export class TestResultsViewContent extends Disposable { }); } + /** + * Collapses all displayed stack frames. + */ + public collapseStack() { + this.callStackWidget.collapseAll(); + } + private getCallFrames(subject: InspectSubject) { if (!(subject instanceof MessageSubject)) { return undefined; diff --git a/code/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/code/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index e422256e519..8caac4a2527 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -25,7 +25,7 @@ import { testingResultsIcon, testingViewIcon } from 'vs/workbench/contrib/testin import { TestCoverageView } from 'vs/workbench/contrib/testing/browser/testCoverageView'; import { TestingDecorationService, TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations'; import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; -import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; +import { CloseTestPeek, CollapsePeekStack, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { TestingProgressTrigger } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; @@ -136,6 +136,7 @@ registerAction2(GoToPreviousMessageAction); registerAction2(GoToNextMessageAction); registerAction2(CloseTestPeek); registerAction2(ToggleTestingPeekHistory); +registerAction2(CollapsePeekStack); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingPeekOpener, LifecyclePhase.Eventually); diff --git a/code/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/code/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 7884f77acb1..0ff1cc43644 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { equals } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -456,11 +457,24 @@ export class TestingDecorations extends Disposable implements IEditorContributio decorations.syncDecorations(this._currentUri); } })); - this._register(this.editor.onKeyDown(e => { - if (e.keyCode === KeyCode.Alt && this._currentUri) { - decorations.updateDecorationsAlternateAction(this._currentUri!, true); + + const win = dom.getWindow(editor.getDomNode()); + this._register(dom.addDisposableListener(win, 'keydown', e => { + if (new StandardKeyboardEvent(e).keyCode === KeyCode.Alt && this._currentUri) { + decorations.updateDecorationsAlternateAction(this._currentUri, true); + } + })); + this._register(dom.addDisposableListener(win, 'keyup', e => { + if (new StandardKeyboardEvent(e).keyCode === KeyCode.Alt && this._currentUri) { + decorations.updateDecorationsAlternateAction(this._currentUri, false); } })); + this._register(dom.addDisposableListener(win, 'blur', () => { + if (this._currentUri) { + decorations.updateDecorationsAlternateAction(this._currentUri, false); + } + })); + this._register(this.editor.onKeyUp(e => { if (e.keyCode === KeyCode.Alt && this._currentUri) { decorations.updateDecorationsAlternateAction(this._currentUri!, false); diff --git a/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 2bac0cf08ee..3acce2ece81 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -14,6 +14,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { observableValue } from 'vs/base/common/observable'; import { count } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -43,6 +44,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -52,7 +54,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { renderTestMessageAsText } from 'vs/workbench/contrib/testing/browser/testMessageColorizer'; -import { InspectSubject, MessageSubject, TaskSubject, TestOutputSubject, mapFindTestMessage } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject'; +import { InspectSubject, MessageSubject, TaskSubject, TestOutputSubject, inspectSubjectHasStack, mapFindTestMessage } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject'; import { TestResultsViewContent } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent'; import { testingMessagePeekBorder, testingPeekBorder, testingPeekHeaderBackground, testingPeekMessageHeaderBackground } from 'vs/workbench/contrib/testing/browser/theme'; import { AutoOpenPeekViewWhen, TestingConfigKeys, getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; @@ -498,6 +500,13 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo this.peek.clear(); } + /** + * Collapses all displayed stack frames. + */ + public collapseStack() { + this.peek.value?.collapseStack(); + } + /** * Shows the next message in the peek, if possible. */ @@ -645,10 +654,14 @@ class TestResultsPeek extends PeekViewWidget { private static lastHeightInLines?: number; private readonly visibilityChange = this._disposables.add(new Emitter()); + private readonly _current = observableValue('testPeekCurrent', undefined); private content!: TestResultsViewContent; private scopedContextKeyService!: IContextKeyService; private dimension?: dom.Dimension; - public current?: InspectSubject; + + public get current() { + return this._current.get(); + } constructor( editor: ICodeEditor, @@ -702,7 +715,14 @@ class TestResultsPeek extends PeekViewWidget { protected override _fillHead(container: HTMLElement): void { super._fillHead(container); - const menu = this.menuService.createMenu(MenuId.TestPeekTitle, this.contextKeyService); + const menuContextKeyService = this._disposables.add(this.contextKeyService.createScoped(container)); + this._disposables.add(bindContextKey( + TestingContextKeys.peekHasStack, + menuContextKeyService, + reader => inspectSubjectHasStack(this._current.read(reader)), + )); + + const menu = this.menuService.createMenu(MenuId.TestPeekTitle, menuContextKeyService); const actionBar = this._actionbarWidget!; this._disposables.add(menu.onDidChange(() => { actions.length = 0; @@ -732,7 +752,7 @@ class TestResultsPeek extends PeekViewWidget { */ public setModel(subject: InspectSubject): Promise { if (subject instanceof TaskSubject || subject instanceof TestOutputSubject) { - this.current = subject; + this._current.set(subject, undefined); return this.showInPlace(subject); } @@ -743,14 +763,14 @@ class TestResultsPeek extends PeekViewWidget { return Promise.resolve(); } - this.current = subject; + this._current.set(subject, undefined); if (!revealLocation) { return this.showInPlace(subject); } // If there is a stack we want to display, ensure the default size is large-ish const peekLines = TestResultsPeek.lastHeightInLines || Math.max( - subject instanceof MessageSubject && subject.stack?.length ? Math.ceil(this.getVisibleEditorLines() / 2) : 0, + inspectSubjectHasStack(subject) ? Math.ceil(this.getVisibleEditorLines() / 2) : 0, hintMessagePeekHeight(message) ); @@ -760,6 +780,13 @@ class TestResultsPeek extends PeekViewWidget { return this.showInPlace(subject); } + /** + * Collapses all displayed stack frames. + */ + public collapseStack() { + this.content.collapseStack(); + } + private getVisibleEditorLines() { // note that we don't use the view ranges because we don't want to get // thrown off by large wrapping lines. Being approximate here is okay. @@ -1025,6 +1052,31 @@ export class GoToPreviousMessageAction extends Action2 { } } +export class CollapsePeekStack extends Action2 { + public static readonly ID = 'testing.collapsePeekStack'; + constructor() { + super({ + id: CollapsePeekStack.ID, + title: localize2('testing.collapsePeekStack', 'Collapse Stack Frames'), + icon: Codicon.collapseAll, + category: Categories.Test, + menu: [{ + id: MenuId.TestPeekTitle, + when: TestingContextKeys.peekHasStack, + group: 'navigation', + order: 4, + }], + }); + } + + public override run(accessor: ServicesAccessor) { + const editor = getPeekedEditorFromFocus(accessor.get(ICodeEditorService)); + if (editor) { + TestingOutputPeekController.get(editor)?.collapseStack(); + } + } +} + export class OpenMessageInEditorAction extends Action2 { public static readonly ID = 'testing.openMessageInEditor'; constructor() { diff --git a/code/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/code/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index ac303b4886f..1cfd764dc23 100644 --- a/code/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/code/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -29,6 +29,7 @@ export namespace TestingContextKeys { export const inlineCoverageEnabled = new RawContextKey('testing.inlineCoverageEnabled', false, { type: 'boolean', description: localize('testing.inlineCoverageEnabled', 'Indicates whether inline coverage is shown') }); export const canGoToRelatedCode = new RawContextKey('testing.canGoToRelatedCode', false, { type: 'boolean', description: localize('testing.canGoToRelatedCode', 'Whether a controller implements a capability to find code related to a test') }); export const canGoToRelatedTest = new RawContextKey('testing.canGoToRelatedTest', false, { type: 'boolean', description: localize('testing.canGoToRelatedTest', 'Whether a controller implements a capability to find tests related to code') }); + export const peekHasStack = new RawContextKey('testing.peekHasStack', false, { type: 'boolean', description: localize('testing.peekHasStack', 'Whether the message shown in a peek view has a stack trace') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, diff --git a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index c471d251303..17ddb9cb36d 100644 --- a/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/code/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -92,6 +92,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi private profileWidget: ProfileWidget | undefined; private model: UserDataProfilesEditorModel | undefined; + private templates: readonly IProfileTemplateInfo[] = []; constructor( group: IEditorGroup, @@ -207,7 +208,7 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi actions: { getActions: () => { const actions: IAction[] = []; - if (this.model?.templates.length) { + if (this.templates.length) { actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), this.getCreateFromTemplateActions())); actions.push(new Separator()); } @@ -225,15 +226,13 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi } private getCreateFromTemplateActions(): IAction[] { - return this.model - ? this.model.templates.map(template => - new Action( - `template:${template.url}`, - template.name, - undefined, - true, - () => this.createNewProfile(URI.parse(template.url)))) - : []; + return this.templates.map(template => + new Action( + `template:${template.url}`, + template.name, + undefined, + true, + () => this.createNewProfile(URI.parse(template.url)))); } private registerListeners(): void { @@ -343,9 +342,12 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi override async setInput(input: UserDataProfilesEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); this.model = await input.resolve(); - if (this.profileWidget) { - this.profileWidget.templates = this.model.templates; - } + this.model.getTemplates().then(templates => { + this.templates = templates; + if (this.profileWidget) { + this.profileWidget.templates = templates; + } + }); this.updateProfilesList(); this._register(this.model.onDidChange(element => this.updateProfilesList(element))); @@ -710,7 +712,6 @@ class ProfileTreeDataSource implements IAsyncDataSource()); readonly onDidChange = this._onDidChange.event; - private _templates: IProfileTemplateInfo[] | undefined; - get templates(): readonly IProfileTemplateInfo[] { return this._templates ?? []; } + private templates: Promise | undefined; constructor( @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @@ -761,9 +760,11 @@ export class UserDataProfilesEditorModel extends EditorModel { } } - override async resolve(): Promise { - await super.resolve(); - this._templates = await this.userDataProfileManagementService.getBuiltinProfileTemplates(); + getTemplates(): Promise { + if (!this.templates) { + this.templates = this.userDataProfileManagementService.getBuiltinProfileTemplates(); + } + return this.templates; } private createProfileElement(profile: IUserDataProfile): [UserDataProfileElement, DisposableStore] { @@ -771,7 +772,7 @@ export class UserDataProfilesEditorModel extends EditorModel { const activateAction = disposables.add(new Action( 'userDataProfile.activate', - localize('active', "Use for Current Window"), + localize('active', "Use this Profile for Current Window"), ThemeIcon.asClassName(Codicon.check), true, () => this.userDataProfileManagementService.switchProfile(profileElement.profile) @@ -808,25 +809,16 @@ export class UserDataProfilesEditorModel extends EditorModel { () => this.openWindow(profileElement.profile) )); - const useAsNewWindowProfileAction = disposables.add(new Action( - 'userDataProfile.useAsNewWindowProfile', - localize('use as new window', "Use for New Windows"), - undefined, - true, - () => profileElement.toggleNewWindowProfile() - )); - const primaryActions: IAction[] = []; + primaryActions.push(activateAction); primaryActions.push(newWindowAction); - if (!profile.isDefault) { - primaryActions.push(deleteAction); - } const secondaryActions: IAction[] = []; - secondaryActions.push(activateAction); - secondaryActions.push(useAsNewWindowProfileAction); - secondaryActions.push(new Separator()); secondaryActions.push(copyFromProfileAction); secondaryActions.push(exportAction); + if (!profile.isDefault) { + secondaryActions.push(new Separator()); + secondaryActions.push(deleteAction); + } const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement, profile, @@ -834,16 +826,9 @@ export class UserDataProfilesEditorModel extends EditorModel { [primaryActions, secondaryActions] )); - activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id; + activateAction.enabled = this.userDataProfileService.currentProfile.id !== profileElement.profile.id; disposables.add(this.userDataProfileService.onDidChangeCurrentProfile(() => - activateAction.checked = this.userDataProfileService.currentProfile.id === profileElement.profile.id)); - - useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile; - disposables.add(profileElement.onDidChange(e => { - if (e.newWindowProfile) { - useAsNewWindowProfileAction.checked = profileElement.isNewWindowProfile; - } - })); + activateAction.enabled = this.userDataProfileService.currentProfile.id !== profileElement.profile.id)); return [profileElement, disposables]; } diff --git a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 0b826034b3b..3ca45752928 100644 --- a/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/code/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -226,6 +226,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ 'onSettingChanged:workbench.colorTheme', 'onCommand:workbench.action.selectTheme' ], + when: '!accessibilityModeEnabled', media: { type: 'markdown', path: 'theme_picker', } }, { @@ -399,7 +400,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ isFeatured: true, icon: setupIcon, when: CONTEXT_ACCESSIBILITY_MODE_ENABLED.key, - next: 'SetupScreenReaderExtended', + next: 'Setup', content: { type: 'steps', steps: [ @@ -470,90 +471,6 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ ] } }, - { - id: 'SetupScreenReaderExtended', - title: localize('gettingStarted.setupScreenReaderExtended.title', "Learn more about using VS Code with a Screen Reader"), - description: localize('gettingStarted.setupScreenReaderExtended.description', "Customize your editor, learn the basics, and start coding"), - isFeatured: true, - icon: setupIcon, - when: `!isWeb && ${CONTEXT_ACCESSIBILITY_MODE_ENABLED.key}`, - content: { - type: 'steps', - steps: [ - { - id: 'extensionsWeb', - title: localize('gettingStarted.extensions.title', "Code with extensions"), - description: localize('gettingStarted.extensionsWeb.description.interpolated', "Extensions are VS Code's power-ups. A growing number are becoming available in the web.\n{0}", Button(localize('browsePopularWeb', "Browse Popular Web Extensions"), 'command:workbench.extensions.action.showPopularExtensions')), - when: 'workspacePlatform == \'webworker\'', - media: { - type: 'markdown', path: 'empty' - } - }, - { - id: 'findLanguageExtensions', - title: localize('gettingStarted.findLanguageExts.title', "Rich support for all your languages"), - description: localize('gettingStarted.findLanguageExts.description.interpolated', "Code smarter with syntax highlighting, code completion, linting and debugging. While many languages are built-in, many more can be added as extensions.\n{0}", Button(localize('browseLangExts', "Browse Language Extensions"), 'command:workbench.extensions.action.showLanguageExtensions')), - when: 'workspacePlatform != \'webworker\'', - media: { - type: 'markdown', path: 'empty' - } - }, - { - id: 'settings', - title: localize('gettingStarted.settings.title', "Tune your settings"), - description: localize('gettingStarted.settings.description.interpolated', "Customize every aspect of VS Code and your extensions to your liking. Commonly used settings are listed first to get you started.\n{0}", Button(localize('tweakSettings', "Open Settings"), 'command:toSide:workbench.action.openSettings')), - media: { - type: 'markdown', path: 'empty' - } - }, - { - id: 'settingsSync', - title: localize('gettingStarted.settingsSync.title', "Sync settings across devices"), - description: localize('gettingStarted.settingsSync.description.interpolated', "Keep your essential customizations backed up and updated across all your devices.\n{0}", Button(localize('enableSync', "Backup and Sync Settings"), 'command:workbench.userDataSync.actions.turnOn')), - when: 'syncStatus != uninitialized', - completionEvents: ['onEvent:sync-enabled'], - media: { - type: 'markdown', path: 'empty' - } - }, - { - id: 'commandPaletteTask', - title: localize('gettingStarted.commandPalette.title', "Unlock productivity with the Command Palette "), - description: localize('gettingStarted.commandPalette.description.interpolated', "Run commands without reaching for your mouse to accomplish any task in VS Code.\n{0}", Button(localize('commandPalette', "Open Command Palette"), 'command:workbench.action.showCommands')), - media: { - type: 'markdown', path: 'empty' - } - }, - { - id: 'pickAFolderTask-Mac', - title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), - description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFileFolder')), - when: 'isMac && workspaceFolderCount == 0', - media: { - type: 'markdown', path: 'empty' - } - }, - { - id: 'pickAFolderTask-Other', - title: localize('gettingStarted.setup.OpenFolder.title', "Open up your code"), - description: localize('gettingStarted.setup.OpenFolder.description.interpolated', "You're all set to start coding. Open a project folder to get your files into VS Code.\n{0}", Button(localize('pickFolder', "Pick a Folder"), 'command:workbench.action.files.openFolder')), - when: '!isMac && workspaceFolderCount == 0', - media: { - type: 'markdown', path: 'empty' - } - }, - { - id: 'quickOpen', - title: localize('gettingStarted.quickOpen.title', "Quickly navigate between your files"), - description: localize('gettingStarted.quickOpen.description.interpolated', "Navigate between files in an instant with one keystroke. Tip: Open multiple files by pressing the right arrow key.\n{0}", Button(localize('quickOpen', "Quick Open a File"), 'command:toSide:workbench.action.quickOpen')), - when: 'workspaceFolderCount != 0', - media: { - type: 'markdown', path: 'empty' - } - }, - ] - } - }, { id: 'Beginner', isFeatured: false, diff --git a/code/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/code/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 12609c51916..2fa6f984217 100644 --- a/code/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/code/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -156,17 +156,17 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { }))); } - // Window Controls (Native Windows/Linux) - if (!isMacintosh && !hasNativeTitlebar(this.configurationService) && !isWCOEnabled() && this.primaryWindowControls) { + // Window Controls (Native Linux when WCO is disabled) + if (isLinux && !hasNativeTitlebar(this.configurationService) && !isWCOEnabled() && this.windowControlsContainer) { // Minimize - const minimizeIcon = append(this.primaryWindowControls, $('div.window-icon.window-minimize' + ThemeIcon.asCSSSelector(Codicon.chromeMinimize))); + const minimizeIcon = append(this.windowControlsContainer, $('div.window-icon.window-minimize' + ThemeIcon.asCSSSelector(Codicon.chromeMinimize))); this._register(addDisposableListener(minimizeIcon, EventType.CLICK, () => { this.nativeHostService.minimizeWindow({ targetWindowId }); })); // Restore - this.maxRestoreControl = append(this.primaryWindowControls, $('div.window-icon.window-max-restore')); + this.maxRestoreControl = append(this.windowControlsContainer, $('div.window-icon.window-max-restore')); this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, async () => { const maximized = await this.nativeHostService.isMaximized({ targetWindowId }); if (maximized) { @@ -177,7 +177,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { })); // Close - const closeIcon = append(this.primaryWindowControls, $('div.window-icon.window-close' + ThemeIcon.asCSSSelector(Codicon.chromeClose))); + const closeIcon = append(this.windowControlsContainer, $('div.window-icon.window-close' + ThemeIcon.asCSSSelector(Codicon.chromeClose))); this._register(addDisposableListener(closeIcon, EventType.CLICK, () => { this.nativeHostService.closeWindow({ targetWindowId }); })); diff --git a/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index bbbd1d0b177..ec37cfccaa5 100644 --- a/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/code/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -41,7 +41,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench public readonly onEnablementChanged: Event = this._onEnablementChanged.event; protected readonly extensionsManager: ExtensionsManager; - private readonly storageManger: StorageManager; + private readonly storageManager: StorageManager; constructor( @IStorageService storageService: IStorageService, @@ -63,7 +63,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IInstantiationService instantiationService: IInstantiationService, ) { super(); - this.storageManger = this._register(new StorageManager(storageService)); + this.storageManager = this._register(new StorageManager(storageService)); const uninstallDisposable = this._register(Event.filter(extensionManagementService.onDidUninstallExtension, e => !e.error)(({ identifier }) => this._reset(identifier))); let isDisposed = false; @@ -610,11 +610,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (!this.hasWorkspace) { return []; } - return this.storageManger.get(storageId, StorageScope.WORKSPACE); + return this.storageManager.get(storageId, StorageScope.WORKSPACE); } private _setExtensions(storageId: string, extensions: IExtensionIdentifier[]): void { - this.storageManger.set(storageId, extensions, StorageScope.WORKSPACE); + this.storageManager.set(storageId, extensions, StorageScope.WORKSPACE); } private async _onDidChangeGloballyDisabledExtensions(extensionIdentifiers: ReadonlyArray, source?: string): Promise { diff --git a/code/src/vs/workbench/services/search/common/fileSearchManager.ts b/code/src/vs/workbench/services/search/common/fileSearchManager.ts index 659217efd5e..73505b7a1fb 100644 --- a/code/src/vs/workbench/services/search/common/fileSearchManager.ts +++ b/code/src/vs/workbench/services/search/common/fileSearchManager.ts @@ -13,6 +13,8 @@ import { URI } from 'vs/base/common/uri'; import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider, hasSiblingFn, excludeToGlobPattern, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search'; import { FileSearchProviderFolderOptions, FileSearchProviderNew, FileSearchProviderOptions } from 'vs/workbench/services/search/common/searchExtTypes'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { OldFileSearchProviderConverter } from 'vs/workbench/services/search/common/searchExtConversionTypes'; interface IInternalFileMatch { base: URI; @@ -53,7 +55,7 @@ class FileSearchEngine { private globalExcludePattern?: glob.ParsedExpression; - constructor(private config: IFileQuery, private provider: FileSearchProviderNew, private sessionToken?: unknown) { + constructor(private config: IFileQuery, private provider: FileSearchProviderNew, private sessionLifecycle?: SessionLifecycle) { this.filePattern = config.filePattern; this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || undefined; @@ -116,10 +118,11 @@ class FileSearchEngine { private async doSearch(fqs: IFolderQuery[], onResult: (match: IInternalFileMatch) => void): Promise { const cancellation = new CancellationTokenSource(); const folderOptions = fqs.map(fq => this.getSearchOptionsForFolder(fq)); + const session = this.provider instanceof OldFileSearchProviderConverter ? this.sessionLifecycle?.tokenSource.token : this.sessionLifecycle?.obj; const options: FileSearchProviderOptions = { folderOptions, maxResults: this.config.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, - session: this.sessionToken + session }; @@ -301,11 +304,30 @@ interface IInternalSearchComplete { stats?: IFileSearchProviderStats; } +/** + * For backwards compatibility, store both a cancellation token and a session object. The session object is the new implementation, where + */ +class SessionLifecycle extends Disposable { + public readonly obj: object; + public readonly tokenSource: CancellationTokenSource; + + constructor() { + super(); + this.obj = new Object(); + this.tokenSource = new CancellationTokenSource(); + } + + public override dispose(): void { + this.tokenSource.cancel(); + super.dispose(); + } +} + export class FileSearchManager { private static readonly BATCH_SIZE = 512; - private readonly sessions = new Map(); + private readonly sessions = new Map(); fileSearch(config: IFileQuery, provider: FileSearchProviderNew, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { const sessionTokenSource = this.getSessionTokenSource(config.cacheKey); @@ -333,17 +355,19 @@ export class FileSearchManager { } clearCache(cacheKey: string): void { + // cancel the token + this.sessions.get(cacheKey)?.dispose(); // with no reference to this, it will be removed from WeakMaps this.sessions.delete(cacheKey); } - private getSessionTokenSource(cacheKey: string | undefined): unknown { + private getSessionTokenSource(cacheKey: string | undefined): SessionLifecycle | undefined { if (!cacheKey) { return undefined; } if (!this.sessions.has(cacheKey)) { - this.sessions.set(cacheKey, new Object()); + this.sessions.set(cacheKey, new SessionLifecycle()); } return this.sessions.get(cacheKey); diff --git a/code/src/vs/workbench/services/search/common/queryBuilder.ts b/code/src/vs/workbench/services/search/common/queryBuilder.ts index ed1ab661245..53b37e5f17d 100644 --- a/code/src/vs/workbench/services/search/common/queryBuilder.ts +++ b/code/src/vs/workbench/services/search/common/queryBuilder.ts @@ -92,6 +92,7 @@ interface ICommonQueryBuilderOptions { disregardSearchExcludeSettings?: boolean; ignoreSymlinks?: boolean; onlyOpenEditors?: boolean; + onlyFileScheme?: boolean; } export interface IFileQueryBuilderOptions extends ICommonQueryBuilderOptions { @@ -260,7 +261,8 @@ export class QueryBuilder { excludePattern: excludeSearchPathsInfo.pattern, includePattern: includeSearchPathsInfo.pattern, onlyOpenEditors: options.onlyOpenEditors, - maxResults: options.maxResults + maxResults: options.maxResults, + onlyFileScheme: options.onlyFileScheme }; if (options.onlyOpenEditors) { diff --git a/code/src/vs/workbench/services/search/common/search.ts b/code/src/vs/workbench/services/search/common/search.ts index 5f6283c3346..79b0e06ee8b 100644 --- a/code/src/vs/workbench/services/search/common/search.ts +++ b/code/src/vs/workbench/services/search/common/search.ts @@ -100,6 +100,7 @@ export interface ICommonQueryProps { maxResults?: number; usingSearchPaths?: boolean; + onlyFileScheme?: boolean; } export interface IFileQueryProps extends ICommonQueryProps { diff --git a/code/src/vs/workbench/services/search/common/searchService.ts b/code/src/vs/workbench/services/search/common/searchService.ts index f4e35c525e1..b58dc567e87 100644 --- a/code/src/vs/workbench/services/search/common/searchService.ts +++ b/code/src/vs/workbench/services/search/common/searchService.ts @@ -271,6 +271,9 @@ export class SearchService extends Disposable implements ISearchService { return []; } await Promise.all([...fqs.keys()].map(async scheme => { + if (query.onlyFileScheme && scheme !== Schemas.file) { + return; + } const schemeFQs = fqs.get(scheme)!; let provider = this.getSearchProvider(query.type).get(scheme); diff --git a/code/src/vscode-dts/vscode.d.ts b/code/src/vscode-dts/vscode.d.ts index 09ce5a92296..b84653d1505 100644 --- a/code/src/vscode-dts/vscode.d.ts +++ b/code/src/vscode-dts/vscode.d.ts @@ -7505,7 +7505,7 @@ declare module 'vscode' { * @example * // Execute a command in a terminal immediately after being created * const myTerm = window.createTerminal(); - * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => { + * window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration }) => { * if (terminal === myTerm) { * const command = shellIntegration.executeCommand({ * command: 'echo', diff --git a/code/src/vscode-dts/vscode.proposed.fileSearchProviderNew.d.ts b/code/src/vscode-dts/vscode.proposed.fileSearchProviderNew.d.ts index d31f5db77f0..36b0b482313 100644 --- a/code/src/vscode-dts/vscode.proposed.fileSearchProviderNew.d.ts +++ b/code/src/vscode-dts/vscode.proposed.fileSearchProviderNew.d.ts @@ -54,9 +54,10 @@ declare module 'vscode' { /** * An object with a lifespan that matches the session's lifespan. If the provider chooses to, this object can be used as the key for a cache, - * and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared. + * and searches with the same session object can search the same cache. When the object is garbage-collected, the session is complete and the cache can be cleared. + * Please do not store any references to the session object, except via a weak reference (e.g. `WeakRef` or `WeakMap`). */ - session: unknown; + session: object; /** * The maximum number of results to be returned.