From 7e746b4fd66093d9c790f857e2b2ea3e3a7389b2 Mon Sep 17 00:00:00 2001 From: helmisek <6172587+Helmisek@users.noreply.github.com> Date: Thu, 19 Sep 2024 01:58:28 +0200 Subject: [PATCH 01/11] fix: add codelens imports --- .../contrib/codelens/browser/codeLensCache.ts | 22 +++++----- .../contrib/codelens/browser/codelens.ts | 22 +++++----- .../codelens/browser/codelensController.ts | 40 +++++++++---------- .../codelens/browser/codelensWidget.ts | 20 +++++----- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts index 5f479695..f7666b8c 100644 --- a/src/vs/editor/contrib/codelens/browser/codeLensCache.ts +++ b/src/vs/editor/contrib/codelens/browser/codeLensCache.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from '../../../../base/common/event.js'; -import { LRUCache } from '../../../../base/common/map.js'; -import { Range } from '../../../common/core/range.js'; -import { ITextModel } from '../../../common/model.js'; -import { CodeLens, CodeLensList, CodeLensProvider } from '../../../common/languages.js'; -import { CodeLensModel } from './codelens.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from '../../../../platform/storage/common/storage.js'; -import { mainWindow } from '../../../../base/browser/window.js'; -import { runWhenWindowIdle } from '../../../../base/browser/dom.js'; +import { Event } from 'vs/base/common/event'; +import { LRUCache } from 'vs/base/common/map'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { CodeLens, CodeLensList, CodeLensProvider } from 'vs/editor/common/languages'; +import { CodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { mainWindow } from 'vs/base/browser/window'; +import { runWhenWindowIdle } from 'vs/base/browser/dom'; export const ICodeLensCache = createDecorator('ICodeLensCache'); diff --git a/src/vs/editor/contrib/codelens/browser/codelens.ts b/src/vs/editor/contrib/codelens/browser/codelens.ts index 0a777339..68253150 100644 --- a/src/vs/editor/contrib/codelens/browser/codelens.ts +++ b/src/vs/editor/contrib/codelens/browser/codelens.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { illegalArgument, onUnexpectedExternalError } from '../../../../base/common/errors.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { assertType } from '../../../../base/common/types.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ITextModel } from '../../../common/model.js'; -import { CodeLens, CodeLensList, CodeLensProvider } from '../../../common/languages.js'; -import { IModelService } from '../../../common/services/model.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js'; -import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { CodeLens, CodeLensList, CodeLensProvider } from 'vs/editor/common/languages'; +import { IModelService } from 'vs/editor/common/services/model'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; export interface CodeLensItem { symbol: CodeLens; diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index 6eca903f..49433647 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -4,26 +4,26 @@ *--------------------------------------------------------------------------------------------*/ -import { CancelablePromise, createCancelablePromise, disposableTimeout, RunOnceScheduler } from '../../../../base/common/async.js'; -import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; -import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js'; -import { IActiveCodeEditor, ICodeEditor, IViewZoneChangeAccessor, MouseTargetType } from '../../../browser/editorBrowser.js'; -import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js'; -import { EditorOption, EDITOR_FONT_DEFAULTS } from '../../../common/config/editorOptions.js'; -import { IEditorContribution } from '../../../common/editorCommon.js'; -import { EditorContextKeys } from '../../../common/editorContextKeys.js'; -import { IModelDecorationsChangeAccessor } from '../../../common/model.js'; -import { CodeLens, Command } from '../../../common/languages.js'; -import { CodeLensItem, CodeLensModel, getCodeLensModel } from './codelens.js'; -import { ICodeLensCache } from './codeLensCache.js'; -import { CodeLensHelper, CodeLensWidget } from './codelensWidget.js'; -import { localize } from '../../../../nls.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; -import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; +import { CancelablePromise, createCancelablePromise, disposableTimeout, RunOnceScheduler } from 'vs/base/common/async'; +import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; +import { IActiveCodeEditor, ICodeEditor, IViewZoneChangeAccessor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; +import { CodeLens, Command } from 'vs/editor/common/languages'; +import { CodeLensItem, CodeLensModel, getCodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens'; +import { ICodeLensCache } from 'vs/editor/contrib/codelens/browser/codeLensCache'; +import { CodeLensHelper, CodeLensWidget } from 'vs/editor/contrib/codelens/browser/codelensWidget'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; export class CodeLensContribution implements IEditorContribution { diff --git a/src/vs/editor/contrib/codelens/browser/codelensWidget.ts b/src/vs/editor/contrib/codelens/browser/codelensWidget.ts index 394c333b..c89fc7fd 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensWidget.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from '../../../../base/browser/dom.js'; -import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { Constants } from '../../../../base/common/uint.js'; -import './codelensWidget.css'; -import { ContentWidgetPositionPreference, IActiveCodeEditor, IContentWidget, IContentWidgetPosition, IViewZone, IViewZoneChangeAccessor } from '../../../browser/editorBrowser.js'; -import { Range } from '../../../common/core/range.js'; -import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from '../../../common/model.js'; -import { ModelDecorationOptions } from '../../../common/model/textModel.js'; -import { CodeLens, Command } from '../../../common/languages.js'; -import { CodeLensItem } from './codelens.js'; +import * as dom from 'vs/base/browser/dom'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Constants } from 'vs/base/common/uint'; +import 'vs/css!./codelensWidget'; +import { ContentWidgetPositionPreference, IActiveCodeEditor, IContentWidget, IContentWidgetPosition, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { CodeLens, Command } from 'vs/editor/common/languages'; +import { CodeLensItem } from 'vs/editor/contrib/codelens/browser/codelens'; class CodeLensViewZone implements IViewZone { From b07fbd627ae2c7c979e1ea55a3b9fafabbe5f5ae Mon Sep 17 00:00:00 2001 From: helmisek <6172587+Helmisek@users.noreply.github.com> Date: Thu, 19 Sep 2024 02:02:05 +0200 Subject: [PATCH 02/11] fix: add more changes --- .../services/hoverService/hoverWidget.ts | 2 +- src/vs/editor/common/languages.ts | 6 ++--- .../browser/codeActionController.ts | 6 +++++ .../codeAction/browser/lightBulbWidget.ts | 24 ++++++++++--------- .../contrib/rename/browser/renameWidget.ts | 5 ++-- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/src/vs/editor/browser/services/hoverService/hoverWidget.ts index ed929bc9..2d8e9ae9 100644 --- a/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/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/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 32b937b6..fbb24ec6 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -2019,9 +2019,9 @@ export interface CommentThreadChangedEvent { } export interface CodeLens { - range: IRange; // the range of code - id?: string; // no idea what this is for - command?: Command; // command to run when they click the codeLens + range: IRange; + id?: string; + command?: Command; } export interface CodeLensList { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index dbbe9a91..862459df 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/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/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 0395f421..bc71291b 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -75,6 +75,14 @@ export class LightBulbWidget extends Disposable implements IContentWidget { private _gutterState: LightBulbState.State = LightBulbState.Hidden; private _iconClasses: string[] = []; + private readonly lightbulbClasses = [ + 'codicon-' + GUTTER_LIGHTBULB_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AUTO_FIX_ICON.id, + 'codicon-' + GUTTER_LIGHTBULB_AIFIX_ICON.id, + 'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id + ]; + private _preferredKbLabel?: string; private _quickFixKbLabel?: string; @@ -148,15 +156,8 @@ export class LightBulbWidget extends Disposable implements IContentWidget { })); this._register(this._editor.onMouseDown(async (e: IEditorMouseEvent) => { - const lightbulbClasses = [ - 'codicon-' + GUTTER_LIGHTBULB_ICON.id, - 'codicon-' + GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON.id, - 'codicon-' + GUTTER_LIGHTBULB_AUTO_FIX_ICON.id, - 'codicon-' + GUTTER_LIGHTBULB_AIFIX_ICON.id, - 'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id - ]; - if (!e.target.element || !lightbulbClasses.some(cls => e.target.element && e.target.element.classList.contains(cls))) { + if (!e.target.element || !this.lightbulbClasses.some(cls => e.target.element && e.target.element.classList.contains(cls))) { return; } @@ -247,7 +248,9 @@ export class LightBulbWidget extends Disposable implements IContentWidget { let hasDecoration = false; if (currLineDecorations) { for (const decoration of currLineDecorations) { - if (decoration.options.glyphMarginClassName) { + const glyphClass = decoration.options.glyphMarginClassName; + + if (glyphClass && !this.lightbulbClasses.some(className => glyphClass.includes(className))) { hasDecoration = true; break; } @@ -271,7 +274,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 +282,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/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index b284cd51..1f6fcc14 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/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); } From b638e890d52c2994d64f686aeb3072ae043a95bd Mon Sep 17 00:00:00 2001 From: helmisek <6172587+Helmisek@users.noreply.github.com> Date: Thu, 19 Sep 2024 02:03:31 +0200 Subject: [PATCH 03/11] fix: add more changes for base --- src/vs/base/browser/browser.ts | 4 ++-- src/vs/base/browser/ui/radio/radio.ts | 4 ++-- .../base/parts/sandbox/common/electronTypes.ts | 18 ------------------ 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts index 87db1c57..f3e6b43c 100644 --- a/src/vs/base/browser/browser.ts +++ b/src/vs/base/browser/browser.ts @@ -136,6 +136,6 @@ export function isWCOEnabled(): boolean { // Returns the bounding rect of the titlebar area if it is supported and defined // See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect -export function getWCOBoundingRect(): DOMRect | undefined { - return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect(); +export function getWCOTitlebarAreaRect(targetWindow: Window): DOMRect | undefined { + return (targetWindow.navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect(); } diff --git a/src/vs/base/browser/ui/radio/radio.ts b/src/vs/base/browser/ui/radio/radio.ts index 2f57e4ac..5ab1edc4 100644 --- a/src/vs/base/browser/ui/radio/radio.ts +++ b/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/src/vs/base/parts/sandbox/common/electronTypes.ts b/src/vs/base/parts/sandbox/common/electronTypes.ts index 43fa7507..ef8c1026 100644 --- a/src/vs/base/parts/sandbox/common/electronTypes.ts +++ b/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -217,24 +217,6 @@ export interface FileFilter { name: string; } -export interface OpenDevToolsOptions { - /** - * Opens the devtools with specified dock state, can be `left`, `right`, `bottom`, - * `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's - * possible to dock back. In `detach` mode it's not. - */ - mode: ('left' | 'right' | 'bottom' | 'undocked' | 'detach'); - /** - * Whether to bring the opened devtools window to the foreground. The default is - * `true`. - */ - activate?: boolean; - /** - * A title for the DevTools window (only in `undocked` or `detach` mode). - */ - title?: string; -} - interface InputEvent { // Docs: https://electronjs.org/docs/api/structures/input-event From efc3022b26dad1423a535af599bde70b0f34b7db Mon Sep 17 00:00:00 2001 From: helmisek <6172587+Helmisek@users.noreply.github.com> Date: Thu, 19 Sep 2024 02:05:27 +0200 Subject: [PATCH 04/11] fix: add more changes for platform --- .../actionWidget/browser/actionList.ts | 4 ++-- .../actionWidget/browser/actionWidget.css | 5 ++-- .../common/extensionEnablementService.ts | 10 ++++---- src/vs/platform/native/common/native.ts | 4 ++-- .../electron-main/nativeHostMainService.ts | 24 +++++++++++++++---- src/vs/platform/theme/common/colorUtils.ts | 3 ++- src/vs/platform/window/common/window.ts | 3 --- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 98403a2a..8eeeee2d 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/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/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index b9ebc031..d205c7ab 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/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/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 61c8816e..7637899a 100644 --- a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts +++ b/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/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 344d4539..a27a17ad 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -6,7 +6,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'vs/base/parts/sandbox/common/electronTypes'; +import { MessageBoxOptions, MessageBoxReturnValue, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'vs/base/parts/sandbox/common/electronTypes'; import { ISerializableCommandAction } from 'vs/platform/action/common/action'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -178,7 +178,7 @@ export interface ICommonNativeHostService { exit(code: number): Promise; // Development - openDevTools(options?: Partial & INativeHostOptions): Promise; + openDevTools(options?: INativeHostOptions): Promise; toggleDevTools(options?: INativeHostOptions): Promise; // Perf Introspection diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 6c49504e..4ccaa6b1 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import { exec } from 'child_process'; -import { app, BrowserWindow, clipboard, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, SaveDialogOptions, SaveDialogReturnValue, screen, shell, webContents } from 'electron'; +import { app, BrowserWindow, clipboard, Display, Menu, MessageBoxOptions, MessageBoxReturnValue, OpenDialogOptions, OpenDialogReturnValue, powerMonitor, SaveDialogOptions, SaveDialogReturnValue, screen, shell, webContents } from 'electron'; import { arch, cpus, freemem, loadavg, platform, release, totalmem, type } from 'os'; import { promisify } from 'util'; import { memoize } from 'vs/base/common/decorators'; @@ -33,7 +33,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { ICodeWindow } from 'vs/platform/window/electron-main/window'; -import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable } from 'vs/platform/window/common/window'; +import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IPoint, IRectangle, IWindowOpenable, useWindowControlsOverlay } from 'vs/platform/window/common/window'; import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; @@ -855,14 +855,28 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Development - async openDevTools(windowId: number | undefined, options?: Partial & INativeHostOptions): Promise { + async openDevTools(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); - window?.win?.webContents.openDevTools(options?.mode ? { mode: options.mode, activate: options.activate } : undefined); + + let mode: 'bottom' | undefined = undefined; + if (isLinux && useWindowControlsOverlay(this.configurationService)) { + mode = 'bottom'; // TODO@bpasero WCO and devtools collide with default option 'right' + } + window?.win?.webContents.openDevTools(mode ? { mode } : undefined); } async toggleDevTools(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); - window?.win?.webContents.toggleDevTools(); + const webContents = window?.win?.webContents; + if (!webContents) { + return; + } + + if (isLinux && useWindowControlsOverlay(this.configurationService) && !webContents.isDevToolsOpened()) { + webContents.openDevTools({ mode: 'bottom' }); // TODO@bpasero WCO and devtools collide with default option 'right' + } else { + webContents.toggleDevTools(); + } } //#endregion diff --git a/src/vs/platform/theme/common/colorUtils.ts b/src/vs/platform/theme/common/colorUtils.ts index 14ceea88..a237166f 100644 --- a/src/vs/platform/theme/common/colorUtils.ts +++ b/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/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 40b1ebf0..dc5de9f2 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -14,7 +14,6 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { FileType } from 'vs/platform/files/common/files'; import { ILoggerResource, LogLevel } from 'vs/platform/log/common/log'; import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy'; -import product from 'vs/platform/product/common/product'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IAnyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; @@ -240,8 +239,6 @@ export function useWindowControlsOverlay(configurationService: IConfigurationSer if (typeof setting === 'boolean') { return setting; } - - return product.quality !== 'stable'; // disable by default in stable for now (TODO@bpasero TODO@benibenj flip when custom title is default) } // Default to true. From 9505e6eb695d76495028d413fe0b78135665efa6 Mon Sep 17 00:00:00 2001 From: helmisek <6172587+Helmisek@users.noreply.github.com> Date: Thu, 19 Sep 2024 02:06:35 +0200 Subject: [PATCH 05/11] fix: add more changes for workbench --- .../api/browser/mainThreadWorkspace.ts | 5 +- .../workbench/api/common/extHost.api.impl.ts | 8 -- .../api/common/extHostApiCommands.ts | 11 -- .../api/common/extHostLanguageFeatures.ts | 6 +- .../workbench/api/common/extHostWorkspace.ts | 7 +- .../parts/titlebar/media/titlebarpart.css | 51 +++---- .../browser/parts/titlebar/titlebarPart.ts | 62 ++++++--- .../chat/browser/actions/chatActions.ts | 2 +- .../contrib/chat/browser/chat.contribution.ts | 6 +- .../contrib/chat/browser/chatInputPart.ts | 6 +- .../browser/chatParticipantContributions.ts | 98 +++++++------- .../contrib/chat/browser/chatViewPane.ts | 7 +- .../contrib/chat/browser/codeBlockPart.ts | 7 +- .../contrib/chat/common/chatAgents.ts | 15 ++- .../contrib/chat/common/chatContextKeys.ts | 4 +- .../contrib/chat/common/chatModel.ts | 26 ++++ .../contrib/chat/common/chatServiceImpl.ts | 16 +-- .../actions/voiceChatActions.ts | 15 ++- .../chat/test/common/chatModel.test.ts | 51 ++++++- .../contrib/comments/browser/commentsView.ts | 2 +- .../comments/browser/commentsViewActions.ts | 14 +- .../contrib/debug/browser/callStackWidget.ts | 125 ++++++++++++++---- .../debug/browser/media/callStackWidget.css | 12 +- .../extensions/browser/extensionsViews.ts | 1 - .../inlineChat/browser/inlineChatActions.ts | 4 +- .../browser/inlineChatController.ts | 27 +++- .../inlineChat/browser/inlineChatSession.ts | 72 +++++----- .../browser/inlineChatStrategies.ts | 92 ++++++++++--- .../inlineChat/browser/inlineChatWidget.ts | 6 +- .../browser/inlineChatZoneWidget.ts | 67 +++++++++- .../inlineChat/browser/media/inlineChat.css | 31 ++++- .../contrib/inlineChat/common/inlineChat.ts | 1 + .../test/browser/inlineChatController.test.ts | 4 +- .../test/browser/inlineChatSession.test.ts | 88 ++++++++++++ .../browser/view/conflictActions.ts | 4 +- .../browser/view/fixedZoneWidget.ts | 1 + .../notebook/browser/diff/diffComponents.ts | 10 +- .../browser/diff/diffElementViewModel.ts | 2 +- .../browser/diff/notebookDiffViewModel.ts | 10 +- .../common/model/notebookCellTextModel.ts | 4 + .../contrib/scm/browser/scmHistoryViewPane.ts | 2 +- .../notebookSearch/notebookSearchService.ts | 11 +- .../browser/media/shellIntegration-bash.sh | 2 +- .../browser/media/shellIntegration.ps1 | 14 +- .../terminal/browser/terminalInstance.ts | 2 +- .../browser/terminal.suggest.contribution.ts | 25 ++-- .../testResultsView/testMessageStack.ts | 4 + .../testResultsView/testResultsSubject.ts | 3 + .../testResultsView/testResultsTree.ts | 13 +- .../testResultsView/testResultsViewContent.ts | 7 + .../testing/browser/testing.contribution.ts | 3 +- .../testing/browser/testingDecorations.ts | 20 ++- .../testing/browser/testingOutputPeek.ts | 64 ++++++++- .../testing/common/testingContextKeys.ts | 1 + .../browser/userDataProfilesEditor.ts | 29 ++-- .../browser/userDataProfilesEditorModel.ts | 43 ++---- .../common/gettingStartedContent.ts | 87 +----------- .../electron-sandbox/desktop.contribution.ts | 3 +- .../parts/titlebar/titlebarPart.ts | 10 +- .../browser/extensionEnablementService.ts | 8 +- .../search/common/fileSearchManager.ts | 34 ++++- .../services/search/common/queryBuilder.ts | 4 +- .../services/search/common/search.ts | 1 + .../services/search/common/searchService.ts | 3 + .../electron-sandbox/workbenchTestServices.ts | 2 +- src/vs/workbench/workbench.common.main.ts | 6 - 66 files changed, 917 insertions(+), 464 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 12441286..880a6026 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/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/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 6da7aadc..a9c8d66c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -550,14 +550,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable { return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider); }, - - - // VOID added this (I think will need to add this back when add ctrl+K) - // registerVoidCtrlKProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable { - // return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider); - // }, - - registerDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable { return extHostLanguageFeatures.registerDefinitionProvider(extension, checkSelector(selector), provider); }, diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 5e25c778..6384178b 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -304,17 +304,6 @@ const newCommands: ApiCommand[] = [ })(value); }) ), - // // --- Void code lens - // new ApiCommand( - // 'vscode.executeVoidCodeLensProvider', '_executeVoidCodeLensProvider', 'Execute Void code lens provider.', - // [ApiCommandArgument.Uri, ApiCommandArgument.Number.with('itemResolveCount', 'Number of lenses that should be resolved and returned. Will only return resolved lenses, will impact performance)').optional()], - // new ApiCommandResult('A promise that resolves to an array of VoidCodeLens-instances.', (value, _args, converter) => { - // return tryMapWith(item => { - // return new types.CodeLens(typeConverters.Range.to(item.range), item.command && converter.fromInternal(item.command)); - // })(value); - // }) - // ), - // --- code actions new ApiCommand( 'vscode.executeCodeActionProvider', '_executeCodeActionProvider', 'Execute code action provider.', diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 50ab3ea6..5bb629a6 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/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/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 33e3bafe..951c87d9 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/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/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 15afa9df..9a66a127 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/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(var(--title-wco-width, 138px)); +} + +.monaco-workbench.linux:not(.web) .part.titlebar .titlebar-container.counter-zoom .window-controls-container.wco-enabled { + width: var(--title-wco-width, 138px); +} + .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 { @@ -376,7 +388,6 @@ z-index: 2500; -webkit-app-region: no-drag; height: 100%; - min-width: 28px; } .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-right > .action-toolbar-container { @@ -455,11 +466,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/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 9dd7d555..ff858e53 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/titlebarpart'; import { localize, localize2 } from 'vs/nls'; import { MultiWindowParts, Part } from 'vs/workbench/browser/part'; import { ITitleService } from 'vs/workbench/services/title/browser/titleService'; -import { getWCOBoundingRect, getZoomFactor, isWCOEnabled } from 'vs/base/browser/browser'; +import { getWCOTitlebarAreaRect, getZoomFactor, isWCOEnabled, onDidChangeZoomLevel } from 'vs/base/browser/browser'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, TitlebarStyle, hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT } from 'vs/platform/window/common/window'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -228,7 +228,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { const wcoEnabled = isWeb && isWCOEnabled(); let value = this.isCommandCenterVisible || wcoEnabled ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : 30; if (wcoEnabled) { - value = Math.max(value, getWCOBoundingRect()?.height ?? 0); + value = Math.max(value, getWCOTitlebarAreaRect(getWindow(this.element))?.height ?? 0); } return value / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1); @@ -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,49 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this.createActionToolBarMenus(); } - let primaryControlLocation = 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 - - const localeInfo = new Intl.Locale(platformLocale) as any; - if (localeInfo?.textInfo?.direction === 'rtl') { - primaryControlLocation = 'right'; - } - } - + // Window Controls Container 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')); + let primaryWindowControlsLocation = 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 + + const localeInfo = new Intl.Locale(platformLocale) as any; + if (localeInfo?.textInfo?.direction === 'rtl') { + primaryWindowControlsLocation = 'right'; + } + } + + if (isMacintosh && isNative && primaryWindowControlsLocation === '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(primaryWindowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container')); + if (isWeb) { + // Web: its possible to have control overlays on both sides, for example on macOS + // with window controls on the left and PWA controls on the right. + append(primaryWindowControlsLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container')); + } + + if (isWCOEnabled()) { + this.windowControlsContainer.classList.add('wco-enabled'); + + const updateWCOWidthVariable = () => { + const targetWindow = getWindow(this.element); + const wcoTitlebarAreaRect = getWCOTitlebarAreaRect(targetWindow); + if (wcoTitlebarAreaRect) { + const wcoWidth = targetWindow.innerWidth - wcoTitlebarAreaRect.width - wcoTitlebarAreaRect.x; + this.windowControlsContainer?.style.setProperty('--title-wco-width', `${wcoWidth}px`); + } + }; + updateWCOWidthVariable(); + + this._register(onDidChangeZoomLevel(() => setTimeout(() => updateWCOWidthVariable(), 5))); // Somehow it does not get the right size without this timeout :-/ + } + } } // Context menu over title bar: depending on the OS and the location of the click this will either be diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 28e759fb..d2e60118 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -152,7 +152,7 @@ class ChatHistoryAction extends Action2 { let lastDate: string | undefined = undefined; const picks = items.flatMap((i): [IQuickPickSeparator | undefined, IChatPickerItem] => { - const timeAgoStr = fromNowByDay(i.lastMessageDate, true); + const timeAgoStr = fromNowByDay(i.lastMessageDate, true, true); const separator: IQuickPickSeparator | undefined = timeAgoStr !== lastDate ? { type: 'separator', label: timeAgoStr, } : undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d5cf3c35..be6c4955 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/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/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 8f910bc2..52d7d73b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -516,7 +516,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge onDidChangeCursorPosition(); } - private initAttachedContext(container: HTMLElement) { + private initAttachedContext(container: HTMLElement, isLayout = false) { const oldHeight = container.offsetHeight; dom.clearNode(container); this.attachedContextDisposables.clear(); @@ -578,7 +578,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachedContextDisposables.add(disp); }); - if (oldHeight !== container.offsetHeight) { + if (oldHeight !== container.offsetHeight && !isLayout) { this._onDidChangeHeight.fire(); } } @@ -609,7 +609,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private previousInputEditorDimension: IDimension | undefined; private _layout(height: number, width: number, allowRecurse = true): void { - this.initAttachedContext(this.attachedContextContainer); + this.initAttachedContext(this.attachedContextContainer, true); const data = this.getLayoutData(); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 09a83daf..2bf50f4a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/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/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 0cb92990..68fb88bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/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.getContributedDefaultAgent(ChatAgentLocation.Panel)) { + return true; + } + const noPersistedSessions = !this.chatService.hasSessions(); return this.didUnregisterProvider || !this._widget?.viewModel && (noPersistedSessions || this.didProviderRegistrationFail); } diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index 1b53c429..5db94d51 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -770,15 +770,11 @@ export class CodeCompareBlockPart extends Disposable { }); dom.reset(this.messageElement, message); - } const diffData = await data.diffData; - if (!diffData) { - return; - } - if (!isEditApplied) { + if (!isEditApplied && diffData) { const viewModel = this.diffEditor.createViewModel({ original: diffData.original, modified: diffData.modified @@ -801,6 +797,7 @@ export class CodeCompareBlockPart extends Disposable { } else { this.diffEditor.setModel(null); this._lastDiffEditorViewModel.value = undefined; + this._onDidChangeContentHeight.fire(); } this.toolbar.context = { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 5cc12623..5725b2b0 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/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 = { @@ -256,8 +262,13 @@ export class ChatAgentService implements IChatAgentService { }; const entry = { data }; this._agents.set(id, entry); + this._onDidChangeAgents.fire(undefined); return toDisposable(() => { this._agents.delete(id); + if (data.isDefault) { + this._defaultAgentRegistered.set(false); + } + this._onDidChangeAgents.fire(undefined); }); } @@ -445,7 +456,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/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 5930a975..f651a114 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/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/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 14a94c04..b9578bae 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/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/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 3ca4524e..2b6cfac7 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/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/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index bffc94d7..b0455f49 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/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/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index 4be9380f..c37b05e8 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/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/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 253f7023..75deafec 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/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/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index 8fedca8e..e2494534 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/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/src/vs/workbench/contrib/debug/browser/callStackWidget.ts b/src/vs/workbench/contrib/debug/browser/callStackWidget.ts index 50cfbf0f..d02de4fb 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackWidget.ts +++ b/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'; @@ -137,6 +144,7 @@ export class CallStackWidget extends Disposable { multipleSelectionSupport: false, mouseSupport: false, keyboardSupport: false, + setRowLineHeight: false, accessibilityProvider: instantiationService.createInstance(StackAccessibilityProvider), } ) as WorkbenchList); @@ -157,6 +165,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 +375,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 +401,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 +449,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 +644,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/src/vs/workbench/contrib/debug/browser/media/callStackWidget.css b/src/vs/workbench/contrib/debug/browser/media/callStackWidget.css index 60f54930..e5e0d56e 100644 --- a/src/vs/workbench/contrib/debug/browser/media/callStackWidget.css +++ b/src/vs/workbench/contrib/debug/browser/media/callStackWidget.css @@ -24,6 +24,10 @@ &[role="link"] { cursor: pointer; } + + .monaco-icon-label::before { + height: auto; + } } &.collapsed { @@ -39,6 +43,7 @@ .collapse-button { width: 16px; min-height: 1px; /* show even if empty */ + line-height: 0; a { cursor: pointer; @@ -56,6 +61,11 @@ .multiCallStackWidget { .multiCallStackFrameContainer { background: none !important; - 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/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 1e8ef29a..f2eea7c6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -79,7 +79,6 @@ class ExtensionsViewState extends Disposable implements IExtensionsViewState { } } - export interface ExtensionsListViewOptions { server?: IExtensionManagementServer; flexibleHeight?: boolean; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 8119cdd0..22d6a83f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/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/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 29b4dee9..c6d9ae57 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/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/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index a3a6a25a..f5b111e5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -263,7 +263,7 @@ export class StashedSession { // keep session for a little bit, only release when user continues to work (type, move cursor, etc.) this._session = session; this._ctxHasStashedSession.set(true); - this._listener = Event.once(Event.any(editor.onDidChangeCursorSelection, editor.onDidChangeModelContent, editor.onDidChangeModel))(() => { + this._listener = Event.once(Event.any(editor.onDidChangeCursorSelection, editor.onDidChangeModelContent, editor.onDidChangeModel, editor.onDidBlurEditorWidget))(() => { this._session = undefined; this._sessionService.releaseSession(session); this._ctxHasStashedSession.reset(); @@ -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); + pendingChangesLen += this._textModelN.getValueLengthInRange(entry.rangeN); + pendingChangesLen -= this._textModel0.getValueLengthInRange(entry.range0); - } else if (Range.areIntersectingOrTouching(rangeN, change.range)) { + } 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; - } + let mergedChanges: DetailedLineRangeMapping[] = []; - // 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); + 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/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index c44f6e6e..01df414e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/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/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 252534e6..e0423850 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -96,9 +96,9 @@ export class InlineChatWidget { h('div.accessibleViewer@accessibleViewer'), h('div.status@status', [ h('div.label.info.hidden@infoLabel'), - h('div.actions.button-style.hidden@toolbar1'), + h('div.actions.hidden@toolbar1'), h('div.label.status.hidden@statusLabel'), - h('div.actions.button-style.hidden@toolbar2'), + h('div.actions.secondary.hidden@toolbar2'), ]), ] ); @@ -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/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 1ce7b490..42612f49 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/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/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index f209158f..d8fac566 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/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, @@ -156,6 +171,16 @@ gap: 4px; } +.monaco-workbench .inline-chat .status .actions.secondary { + display: none; +} + +.monaco-workbench .inline-chat .status:hover .actions.secondary, +.monaco-workbench .inline-chat:focus .status .actions.secondary, +.monaco-workbench .inline-chat .status:focus-within .actions.secondary { + display: inherit; +} + .monaco-workbench .inline-chat-diff-overlay { .monaco-button { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index d656e6c3..f4c87af0 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/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/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts index 8d88a891..3290793d 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts +++ b/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/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts index 3175a1ca..27a48ee2 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/fixedZoneWidget.ts +++ b/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/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index a2adaf3b..a8f2c46c 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -1074,7 +1074,7 @@ export class DeletedElement extends SingleSideDiffElement { layout(state: IDiffElementLayoutState) { DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => { - if (state.editorHeight || state.outerWidth) { + if ((state.editorHeight || state.outerWidth) && this._editor) { this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; this._editor.layout({ width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), @@ -1254,7 +1254,7 @@ export class InsertElement extends SingleSideDiffElement { layout(state: IDiffElementLayoutState) { DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._diffEditorContainer), () => { - if (state.editorHeight || state.outerWidth) { + if ((state.editorHeight || state.outerWidth) && this._editor) { this._editorContainer.style.height = `${this.cell.layoutInfo.editorHeight}px`; this._editor.layout({ width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), @@ -1644,7 +1644,7 @@ export class ModifiedElement extends AbstractElementRenderer { { updateInfoRendering: () => renderSourceEditor(), checkIfModified: (cell) => { - return cell.modified?.textModel.getValue() !== cell.original?.textModel.getValue() ? { reason: undefined } : false; + return cell.modified?.textModel.getTextBufferHash() !== cell.original?.textModel.getTextBufferHash() ? { reason: undefined } : false; }, getFoldingState: (cell) => cell.cellFoldingState, updateFoldingState: (cell, state) => cell.cellFoldingState = state, @@ -1660,7 +1660,7 @@ export class ModifiedElement extends AbstractElementRenderer { const scopedContextKeyService = this.contextKeyService.createScoped(this.templateData.inputToolbarContainer); this._register(scopedContextKeyService); const inputChanged = NOTEBOOK_DIFF_CELL_INPUT.bindTo(scopedContextKeyService); - inputChanged.set(this.cell.modified.textModel.getValue() !== this.cell.original.textModel.getValue()); + inputChanged.set(this.cell.modified.textModel.getTextBufferHash() !== this.cell.original.textModel.getTextBufferHash()); const ignoreWhitespace = NOTEBOOK_DIFF_CELL_IGNORE_WHITESPACE.bindTo(scopedContextKeyService); const ignore = this.textConfigurationService.getValue(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace'); @@ -1675,7 +1675,7 @@ export class ModifiedElement extends AbstractElementRenderer { const refreshToolbar = () => { const ignore = this.textConfigurationService.getValue(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace'); ignoreWhitespace.set(ignore); - const hasChanges = this.cell.modified.textModel.getValue() !== this.cell.original.textModel.getValue(); + const hasChanges = this.cell.modified.textModel.getTextBufferHash() !== this.cell.original.textModel.getTextBufferHash(); inputChanged.set(hasChanges); if (hasChanges) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index 9d0cf11b..ad8e1461 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -222,7 +222,7 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB layoutState: CellLayoutState.Uninitialized }; - this.cellFoldingState = modified?.textModel?.getValue() !== original?.textModel?.getValue() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed; + this.cellFoldingState = modified?.getTextBufferHash() !== original?.getTextBufferHash() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed; this.metadataFoldingState = PropertyFoldingState.Collapsed; this.outputFoldingState = PropertyFoldingState.Collapsed; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts index 0c82062b..dd2b3f12 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts @@ -441,16 +441,12 @@ function createDiffViewModels(instantiationService: IInstantiationService, confi ); } case 'unchanged': { - const originalCell = originalModel.cells[diff.originalCellIndex]; - const modifiedCell = modifiedModel.cells[diff.modifiedCellIndex]; - const type = (originalCell.textModel?.getValue() !== modifiedCell.textModel?.getValue()) ? 'modified' : 'unchanged'; return new SideBySideDiffElementViewModel( model.modified.notebook, model.original.notebook, - originalCell, - modifiedCell, - type, - eventDispatcher, + originalModel.cells[diff.originalCellIndex], + modifiedModel.cells[diff.modifiedCellIndex], + 'unchanged', eventDispatcher, initData, notebookService ); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 3d7310c6..e83261cf 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -419,6 +419,10 @@ export class NotebookCellTextModel extends Disposable implements ICell { return false; } + if (this.outputs.length !== b.outputs.length) { + return false; + } + if (this.getTextLength() !== b.getTextLength()) { return false; } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index ba24fdfa..69060d9d 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/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/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts index 4ef1cdae..5265cf2c 100644 --- a/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts +++ b/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/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 92c0e07f..fbeaa22f 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -328,10 +328,10 @@ __vsc_restore_exit_code() { __vsc_prompt_cmd_original() { __vsc_status="$?" + builtin local cmd __vsc_restore_exit_code "${__vsc_status}" # Evaluate the original PROMPT_COMMAND similarly to how bash would normally # See https://unix.stackexchange.com/a/672843 for technique - builtin local cmd for cmd in "${__vsc_original_prompt_command[@]}"; do eval "${cmd:-}" done diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index c624a5f0..444df384 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -172,9 +172,9 @@ function Set-MappedKeyHandler { function Get-KeywordCompletionResult( $Keyword, - $Description = $null + $Description = $Keyword ) { - [System.Management.Automation.CompletionResult]::new($Keyword, $Keyword, [System.Management.Automation.CompletionResultType]::Keyword, $null -ne $Description ? $Description : $Keyword) + [System.Management.Automation.CompletionResult]::new($Keyword, $Keyword, [System.Management.Automation.CompletionResultType]::Keyword, $Description) } function Set-MappedKeyHandlers { @@ -360,7 +360,7 @@ function Send-Completions { # completions are consistent regardless of where it was requested elseif ($lastWord -match '[/\\]') { $lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\')) - if ($lastSlashIndex -ne -1 && $lastSlashIndex -lt $cursorIndex) { + if ($lastSlashIndex -ne -1 -and $lastSlashIndex -lt $cursorIndex) { $newCursorIndex = $lastSlashIndex + 1 $completionPrefix = $completionPrefix.Substring(0, $newCursorIndex) $prefixCursorDelta = $cursorIndex - $newCursorIndex @@ -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 @@ -430,7 +430,7 @@ function Send-Completions { # completions are consistent regardless of where it was requested if ($completionPrefix -match '[/\\]') { $lastSlashIndex = $completionPrefix.LastIndexOfAny(@('/', '\')) - if ($lastSlashIndex -ne -1 && $lastSlashIndex -lt $cursorIndex) { + if ($lastSlashIndex -ne -1 -and $lastSlashIndex -lt $cursorIndex) { $newCursorIndex = $lastSlashIndex + 1 $completionPrefix = $completionPrefix.Substring(0, $newCursorIndex) $prefixCursorDelta = $cursorIndex - $newCursorIndex diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index f38b9bf8..ff8607f6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -2305,7 +2305,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/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 9ef44f9b..433de4ee 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts index cb133b4b..c6c396e5 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts index 9fa86330..e61463bc 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts index a65a1057..6be1cd3c 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts index db80e2c6..67f9f05b 100644 --- a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index e422256e..8caac4a2 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 7884f77a..0ff1cc43 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/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/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 2bac0cf0..3acce2ec 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/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/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index ac303b48..1cfd764d 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/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/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index c471d251..17ddb9cb 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/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/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 0b826034..3ca45752 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/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/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index c51e033d..69b98abf 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -28,7 +28,6 @@ import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; import { ModifierKeyEmitter } from 'vs/base/browser/dom'; import { applicationConfigurationNodeBase, securityConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sandbox/window'; -import product from 'vs/platform/product/common/product'; // Actions (function registerActions(): void { @@ -240,7 +239,7 @@ import product from 'vs/platform/product/common/product'; 'type': 'boolean', 'included': isLinux, 'markdownDescription': localize('window.experimentalControlOverlay', "Show the native window controls when {0} is set to `custom` (Linux only).", '`#window.titleBarStyle#`'), - 'default': product.quality !== 'stable', // TODO@bpasero disable by default in stable for now (TODO@bpasero TODO@benibenj flip when custom title is default) + 'default': true }, 'window.customTitleBarVisibility': { 'type': 'string', diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 12609c51..2fa6f984 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/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/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index bbbd1d0b..ec37cfcc 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/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/src/vs/workbench/services/search/common/fileSearchManager.ts b/src/vs/workbench/services/search/common/fileSearchManager.ts index 659217ef..73505b7a 100644 --- a/src/vs/workbench/services/search/common/fileSearchManager.ts +++ b/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/src/vs/workbench/services/search/common/queryBuilder.ts b/src/vs/workbench/services/search/common/queryBuilder.ts index ed1ab661..53b37e5f 100644 --- a/src/vs/workbench/services/search/common/queryBuilder.ts +++ b/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/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 5f6283c3..79b0e06e 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/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/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index f4e35c52..b58dc567 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/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/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 7bcb272f..c19ad7c8 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -143,7 +143,7 @@ export class TestNativeHostService implements INativeHostService { async closeWindow(): Promise { } async quit(): Promise { } async exit(code: number): Promise { } - async openDevTools(options?: Partial & INativeHostOptions | undefined): Promise { } + async openDevTools(): Promise { } async toggleDevTools(): Promise { } async resolveProxy(url: string): Promise { return undefined; } async lookupAuthorization(authInfo: AuthInfo): Promise { return undefined; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 461caaf8..9e01f175 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -3,11 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -//#region --- void -import 'vs/workbench/contrib/void/browser/void.contribution' -//#endregion - - //#region --- editor/workbench core import 'vs/editor/editor.all'; @@ -15,7 +10,6 @@ import 'vs/editor/editor.all'; import 'vs/workbench/api/browser/extensionHost.contribution'; import 'vs/workbench/browser/workbench.contribution'; - //#endregion From 02834de9999ce2177b880bc4f748e57373cbc75b Mon Sep 17 00:00:00 2001 From: Mathew Pareles <51329589+mathewpareles@users.noreply.github.com> Date: Thu, 19 Sep 2024 08:18:15 -0700 Subject: [PATCH 06/11] Update CONTRIBUTING.md --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d81ad16d..04e9a6ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ npm run build This will start a new instance of VS Code with the extension enabled. If this does not work, you can press Ctrl+Shift+P, select "Debug: Start Debugging", and select "VS Code Extension Development". -If you would like to use AI features, you need to provide an API key. You can do that by going to Settings (Ctrl+,) and modifying `void > "Anthropic Api Key"`. The "Which API" environment variable controls the provider and defaults to "anthropic". +If you would like to use AI features, you need to provide an API key. You can do that by going to Settings (Ctrl+,) typing in "void", and adding the API key you want to use (eg. the `"Anthropic Api Key"` environment variable). The "Which API" environment variable controls the provider and defaults to "anthropic". Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page! @@ -56,6 +56,7 @@ Now that you're set up, feel free to check out our [Issues](https://github.com/v Beyond the extension, we very occasionally edit the IDE when we need to access more functionality. If you want to work on the full IDE, please follow the steps below, or see VS Code's full [how to contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. +Before starting, make sure you've built the extension (by running `cd .\extensions\void\` and `npm run build`). 1. Install all dependencies. Make sure you have yarn installed (`npm install -g yarn`) ``` From 5e17c1201e644dd0ceb7355739858b042350688b Mon Sep 17 00:00:00 2001 From: Mathew Pareles <51329589+mathewpareles@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:04:06 -0700 Subject: [PATCH 07/11] Update CONTRIBUTING.md --- CONTRIBUTING.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04e9a6ca..bf20791e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,11 +5,10 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a There are two main ways to contribute: -- Suggest New Features (discord) -- Build New Features (roadmap) +- Suggest New Features ([here](https://github.com/voideditor/void/issues/new)) +- Build New Features ([here](https://github.com/orgs/voideditor/projects/2/views/3)) - -See the [GitHub Issues](https://github.com/orgs/voideditor/projects/2/views/3) list for a list of the most important features to build, or feel free to create new issues. +For a complete list of issues to work on, see [here](https://github.com/voideditor/void/issues/). We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes). From c7932ca641dc732142c2b513af30aadc19bc38f9 Mon Sep 17 00:00:00 2001 From: Mathew Pareles <51329589+mathewpareles@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:06:11 -0700 Subject: [PATCH 08/11] Update CONTRIBUTING.md --- CONTRIBUTING.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf20791e..a002dee6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,10 +5,8 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a There are two main ways to contribute: -- Suggest New Features ([here](https://github.com/voideditor/void/issues/new)) -- Build New Features ([here](https://github.com/orgs/voideditor/projects/2/views/3)) - -For a complete list of issues to work on, see [here](https://github.com/voideditor/void/issues/). +- Suggest New Features ([discord](https://discord.gg/4GAxHVAD) or [create an issue](https://github.com/voideditor/void/issues/new)) +- Build New Features ([important issues](https://github.com/orgs/voideditor/projects/2/views/3)) We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes). From d219a22b6ebb2386c38129cc8c929e73d5ff1906 Mon Sep 17 00:00:00 2001 From: Mathew Pareles <51329589+mathewpareles@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:06:45 -0700 Subject: [PATCH 09/11] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a002dee6..9d76fc72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a There are two main ways to contribute: - Suggest New Features ([discord](https://discord.gg/4GAxHVAD) or [create an issue](https://github.com/voideditor/void/issues/new)) -- Build New Features ([important issues](https://github.com/orgs/voideditor/projects/2/views/3)) +- Build New Features (see [here](https://github.com/orgs/voideditor/projects/2/views/3)) We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes). From 069f7db03267342599d6c869c013b0fdd710cf19 Mon Sep 17 00:00:00 2001 From: Mathew Pareles <51329589+mathewpareles@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:06:56 -0700 Subject: [PATCH 10/11] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d76fc72..0d4dc412 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a There are two main ways to contribute: -- Suggest New Features ([discord](https://discord.gg/4GAxHVAD) or [create an issue](https://github.com/voideditor/void/issues/new)) +- Suggest New Features ([discord](https://discord.gg/4GAxHVAD)) - Build New Features (see [here](https://github.com/orgs/voideditor/projects/2/views/3)) We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes). From 1f1f4013b9cd554f5cf75ba43c74d9eaf8442f48 Mon Sep 17 00:00:00 2001 From: Mathew Pareles <51329589+mathewpareles@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:07:15 -0700 Subject: [PATCH 11/11] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d4dc412..822de5d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Welcome! 👋 This is a guide on how to contribute to Void. We want to make it a There are two main ways to contribute: - Suggest New Features ([discord](https://discord.gg/4GAxHVAD)) -- Build New Features (see [here](https://github.com/orgs/voideditor/projects/2/views/3)) +- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3)) We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).