diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 1f9525d2024..268e1e6657c 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -7,22 +7,22 @@ import * as nls from 'vs/nls'; export namespace AccessibilityHelpNLS { export const accessibilityHelpTitle = nls.localize('accessibilityHelpTitle', "Accessibility Help"); - export const openingDocs = nls.localize("openingDocs", "Now opening the Accessibility documentation page."); + export const openingDocs = nls.localize("openingDocs", "Opening the Accessibility documentation page."); export const readonlyDiffEditor = nls.localize("readonlyDiffEditor", "You are in a read-only pane of a diff editor."); export const editableDiffEditor = nls.localize("editableDiffEditor", "You are in a pane of a diff editor."); export const readonlyEditor = nls.localize("readonlyEditor", "You are in a read-only code editor."); export const editableEditor = nls.localize("editableEditor", "You are in a code editor."); - export const changeConfigToOnMac = nls.localize("changeConfigToOnMac", "To configure the application to be optimized for usage with a Screen Reader press Command+E now."); - export const changeConfigToOnWinLinux = nls.localize("changeConfigToOnWinLinux", "To configure the application to be optimized for usage with a Screen Reader press Control+E now."); + export const changeConfigToOnMac = nls.localize("changeConfigToOnMac", "Configure the application to be optimized for usage with a Screen Reader (Command+E)."); + export const changeConfigToOnWinLinux = nls.localize("changeConfigToOnWinLinux", "Configure the application to be optimized for usage with a Screen Reader (Control+E)."); export const auto_on = nls.localize("auto_on", "The application is configured to be optimized for usage with a Screen Reader."); export const auto_off = nls.localize("auto_off", "The application is configured to never be optimized for usage with a Screen Reader."); export const screenReaderModeEnabled = nls.localize("screenReaderModeEnabled", "Screen Reader Optimized Mode enabled."); export const screenReaderModeDisabled = nls.localize("screenReaderModeDisabled", "Screen Reader Optimized Mode disabled."); - export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior by pressing {0}."); + export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior {0}."); export const tabFocusModeOnMsgNoKb = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding."); export const stickScrollKb = nls.localize("stickScrollKb", "Run the command: Focus Sticky Scroll ({0}) to focus the currently nested scopes."); export const stickScrollNoKb = nls.localize("stickScrollNoKb", "Run the command: Focus Sticky Scroll to focus the currently nested scopes. It is currently not triggerable by a keybinding."); - export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}."); + export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior {0}."); export const tabFocusModeOffMsgNoKb = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding."); export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help"); } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts index 0c245548ad0..cbb74ee49b9 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts @@ -102,11 +102,14 @@ class AccessibilityHelpProvider implements IAccessibleContentProvider { const editorContext = this._contextKeyService.getContext(this._editor.getDomNode()!); if (editorContext.getValue(CommentContextKeys.activeEditorHasCommentingRange.key)) { - content.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.NextThread, CommentAccessibilityHelpNLS.nextCommentThreadKb, CommentAccessibilityHelpNLS.nextCommentThreadNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.PreviousThread, CommentAccessibilityHelpNLS.previousCommentThreadKb, CommentAccessibilityHelpNLS.previousCommentThreadNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); + const commentCommandInfo = []; + commentCommandInfo.push(CommentAccessibilityHelpNLS.intro); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.NextThread, CommentAccessibilityHelpNLS.nextCommentThreadKb, CommentAccessibilityHelpNLS.nextCommentThreadNoKb)); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.PreviousThread, CommentAccessibilityHelpNLS.previousCommentThreadKb, CommentAccessibilityHelpNLS.previousCommentThreadNoKb)); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); + commentCommandInfo.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); + content.push(commentCommandInfo.join('\n')); } if (options.get(EditorOption.stickyScroll).enabled) { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 568d0719d21..e93c7388477 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -363,7 +363,7 @@ export class AccessibleView extends Disposable { this._accessibleViewCurrentProviderId.set(provider.verbositySettingKey.replaceAll('accessibility.verbosity.', '')); } const value = this._configurationService.getValue(provider.verbositySettingKey); - const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nPress H now to open a browser window with more information related to accessibility.\n\n") : ''; + const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility (H).") : ''; let disableHelpHint = ''; if (provider.options.type === AccessibleViewType.Help && !!value) { disableHelpHint = this._getDisableVerbosityHint(provider.verbositySettingKey); @@ -384,7 +384,8 @@ export class AccessibleView extends Disposable { message += '\n'; } } - this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint; + const exitThisDialogHint = localize('exit', '\n\nExit this dialog (Escape).'); + this._currentContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; this._updateContextKeys(provider, true); this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment: this._currentContent })).then((model) => { @@ -402,7 +403,7 @@ export class AccessibleView extends Disposable { const verbose = this._configurationService.getValue(provider.verbositySettingKey); const hasActions = this._accessibleViewSupportsNavigation.get() || this._accessibleViewVerbosityEnabled.get() || this._accessibleViewGoToSymbolSupported.get() || this._currentProvider?.actions; if (verbose && !showAccessibleViewHelp && hasActions) { - actionsHint = localize('ariaAccessibleViewActions', "Use Shift+Tab to explore actions such as disabling this hint."); + actionsHint = localize('ariaAccessibleViewActions', 'Explore actions such as disabling this hint (Shift+Tab).'); } let ariaLabel = provider.options.type === AccessibleViewType.Help ? localize('accessibility-help', "Accessibility Help") : localize('accessible-view', "Accessible View"); this._title.textContent = ariaLabel; @@ -520,7 +521,7 @@ export class AccessibleView extends Disposable { private _getAccessibleViewHelpDialogContent(providerHasSymbols?: boolean): string { const navigationHint = this._getNavigationHint(); const goToSymbolHint = this._getGoToSymbolHint(providerHasSymbols); - const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab))"); + const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab))."); let hint = localize('intro', "In the accessible view, you can:\n"); if (navigationHint) { @@ -540,9 +541,9 @@ export class AccessibleView extends Disposable { const nextKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowNext)?.getAriaLabel(); const previousKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowPrevious)?.getAriaLabel(); if (nextKeybinding && previousKeybinding) { - hint = localize('accessibleViewNextPreviousHint', "Show the next ({0}) or previous ({1}) item", nextKeybinding, previousKeybinding); + hint = localize('accessibleViewNextPreviousHint', "Show the next ({0}) or previous ({1}) item.", nextKeybinding, previousKeybinding); } else { - hint = localize('chatAccessibleViewNextPreviousHintNoKb', "Show the next or previous item by configuring keybindings for the Show Next & Previous in Accessible View commands"); + hint = localize('chatAccessibleViewNextPreviousHintNoKb', "Show the next or previous item by configuring keybindings for the Show Next & Previous in Accessible View commands."); } return hint; } @@ -553,9 +554,9 @@ export class AccessibleView extends Disposable { let hint = ''; const disableKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.DisableVerbosityHint, this._contextKeyService)?.getAriaLabel(); if (disableKeybinding) { - hint = localize('acessibleViewDisableHint', "Disable accessibility verbosity for this feature ({0}). This will disable the hint to open the accessible view for example.\n", disableKeybinding); + hint = localize('acessibleViewDisableHint', "\n\nDisable accessibility verbosity for this feature ({0}).", disableKeybinding); } else { - hint = localize('accessibleViewDisableHintNoKb', "Add a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.\n"); + hint = localize('accessibleViewDisableHintNoKb', "\n\nAdd a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.s"); } return hint; } diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index a7e9ee09995..d447e6d5355 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -68,19 +68,21 @@ registerSingleton(ICommentService, CommentService, InstantiationType.Delayed); export namespace CommentAccessibilityHelpNLS { - export const escape = nls.localize('escape', "Dismiss the comment widget via Escape."); - export const nextRange = nls.localize('next', "Navigate to the next commenting range via ({0})."); - export const nextRangeNoKb = nls.localize('nextNoKb', "Run the command: Go to Next Commenting Range, which is currently not triggerable via keybinding."); - export const previousRange = nls.localize('previous', "Navigate to the previous comment range via ({0})."); + export const intro = nls.localize('intro', "The editor contains a commentable range. Some useful commands include:"); + export const introWidget = nls.localize('introWidget', "Some useful comment commands include:"); + export const escape = nls.localize('escape', "- Dismiss Comment (Escape)"); + export const nextRange = nls.localize('next', "- Navigate to the next commenting range ({0})"); + export const nextRangeNoKb = nls.localize('nextNoKb', "- Go to Next Commenting Range, which is currently not triggerable via keybinding."); + export const previousRange = nls.localize('previous', "- Navigate to the previous commenting range ({0})"); export const previousRangeNoKb = nls.localize('previousNoKb', "Run the command: Go to Previous Commenting Range, which is currently not triggerable via keybinding."); - export const nextCommentThreadKb = nls.localize('nextCommentThreadKb', "Navigate to the next comment thread via ({0})."); - export const nextCommentThreadNoKb = nls.localize('nextCommentThreadNoKb', "Run the command: Go to Next Comment Thread, which is currently not triggerable via keybinding."); - export const previousCommentThreadKb = nls.localize('previousCommentThreadKb', "Navigate to the previous comment thread via ({0})."); - export const previousCommentThreadNoKb = nls.localize('previousCommentThreadNoKb', "Run the command: Go to Previous Comment Thread, which is currently not triggerable via keybinding."); - export const addComment = nls.localize('addComment', "Add a comment via ({0})."); - export const addCommentNoKb = nls.localize('addCommentNoKb', "Add a comment via the command: Add Comment on Current Selection, which is currently not triggerable via keybinding."); - export const submitComment = nls.localize('submitComment', "Submit the comment via ({0})."); - export const submitCommentNoKb = nls.localize('submitCommentNoKb', "Submit the comment by navigating with tab to the button, as it's currently not triggerable via keybinding."); + export const nextCommentThreadKb = nls.localize('nextCommentThreadKb', "- Navigate to the next comment thread ({0})"); + export const nextCommentThreadNoKb = nls.localize('nextCommentThreadNoKb', "- Run the command: Go to Next Comment Thread, which is currently not triggerable via keybinding."); + export const previousCommentThreadKb = nls.localize('previousCommentThreadKb', "- Navigate to the previous comment thread ({0})"); + export const previousCommentThreadNoKb = nls.localize('previousCommentThreadNoKb', "- Run the command: Go to Previous Comment Thread, which is currently not triggerable via keybinding."); + export const addComment = nls.localize('addComment', "- Add Comment ({0})"); + export const addCommentNoKb = nls.localize('addCommentNoKb', "- Add Comment on Current Selection, which is currently not triggerable via keybinding."); + export const submitComment = nls.localize('submitComment', "- Submit Comment ({0})"); + export const submitCommentNoKb = nls.localize('submitCommentNoKb', "- Submit Comment, accessible via tabbing, as it's currently not triggerable with a keybinding."); } export class CommentsAccessibilityHelpContribution extends Disposable { @@ -114,12 +116,13 @@ export class CommentsAccessibilityHelpProvider implements IAccessibleContentProv provideContent(): string { this._element = document.activeElement as HTMLElement; const content: string[] = []; + content.push(CommentAccessibilityHelpNLS.introWidget); content.push(CommentAccessibilityHelpNLS.escape); content.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); + content.push(this._descriptionForCommand(CommentCommandId.Submit, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.submitCommentNoKb)); content.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); content.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.Submit, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.submitCommentNoKb)); - return content.join('\n\n'); + return content.join('\n'); } onClose(): void { this._element?.focus(); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts index ead402ce540..5c9682417a9 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { isWeb } from 'vs/base/common/platform'; import { mock } from 'vs/base/test/common/mock'; import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -20,8 +19,7 @@ import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; - -(isWeb ? suite.skip : suite)('NotebookEditorStickyScroll', () => { +suite('NotebookEditorStickyScroll', () => { let disposables: DisposableStore; let instantiationService: TestInstantiationService; diff --git a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts index 5e0db441a80..43d3c804d40 100644 --- a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts @@ -14,23 +14,23 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IOpenEvent, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewPaneOptions, ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; import { ActionButtonRenderer } from 'vs/workbench/contrib/scm/browser/scmViewPane'; import { getActionViewItemProvider, isSCMActionButton, isSCMRepository, isSCMRepositoryArray } from 'vs/workbench/contrib/scm/browser/util'; -import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { comparePaths } from 'vs/base/common/comparers'; import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history'; import { localize } from 'vs/nls'; @@ -41,8 +41,18 @@ import { basename, dirname } from 'vs/base/common/resources'; import { ILabelService } from 'vs/platform/label/common/label'; import { stripIcons } from 'vs/base/common/iconLabels'; import { FileKind } from 'vs/platform/files/common/files'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Emitter } from 'vs/base/common/event'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { IResourceNode, ResourceTree } from 'vs/base/common/resourceTree'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement; +type SCMHistoryItemChangeResourceTreeNode = IResourceNode; +type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement | SCMHistoryItemChangeResourceTreeNode; function isSCMHistoryItemGroupTreeElement(obj: any): obj is SCMHistoryItemGroupTreeElement { return (obj as SCMHistoryItemGroupTreeElement).type === 'historyItemGroup'; @@ -86,11 +96,25 @@ function getSCMResourceId(element: TreeElement): string { const historyItemGroup = historyItem.historyItemGroup; const provider = historyItemGroup.repository.provider; return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`; + } else if (ResourceTree.isResourceNode(element)) { + const historyItem = element.context; + const historyItemGroup = historyItem.historyItemGroup; + const provider = historyItemGroup.repository.provider; + return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; } else { throw new Error('Invalid tree element'); } } +const enum ViewMode { + List = 'list', + Tree = 'tree' +} + +const ContextKeys = { + ViewMode: new RawContextKey('scmSyncViewMode', ViewMode.List), +}; + interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { readonly description?: string; readonly ancestor?: string; @@ -130,12 +154,26 @@ class ListDelegate implements IListVirtualDelegate { return HistoryItemRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemChangeTreeElement(element)) { return HistoryItemChangeRenderer.TEMPLATE_ID; + } else if (ResourceTree.isResourceNode(element)) { + return HistoryItemChangeRenderer.TEMPLATE_ID; } else { throw new Error('Invalid tree element'); } } } +class CompressionDelegate implements ITreeCompressionDelegate { + + isIncompressible(element: TreeElement): boolean { + if (ResourceTree.isResourceNode(element)) { + return element.childrenCount === 0 || !element.parent || !element.parent.parent; + } + + return true; + } + +} + interface HistoryItemGroupTemplate { readonly label: IconLabel; readonly count: CountBadge; @@ -236,12 +274,15 @@ interface HistoryItemChangeTemplate { readonly disposables: IDisposable; } -class HistoryItemChangeRenderer implements ITreeRenderer { +class HistoryItemChangeRenderer implements ICompressibleTreeRenderer { static readonly TEMPLATE_ID = 'historyItemChange'; get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; } - constructor(private labels: ResourceLabels) { } + constructor( + private readonly viewMode: () => ViewMode, + private readonly labels: ResourceLabels, + @ILabelService private labelService: ILabelService) { } renderTemplate(container: HTMLElement): HistoryItemChangeTemplate { const element = append(container, $('.change')); @@ -252,11 +293,27 @@ class HistoryItemChangeRenderer implements ITreeRenderer, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + renderElement(node: ITreeNode, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + const historyItemChangeOrFolder = node.element; + const fileKind = ResourceTree.isResourceNode(historyItemChangeOrFolder) && historyItemChangeOrFolder.childrenCount > 0 ? FileKind.FOLDER : FileKind.FILE; + templateData.fileLabel.setFile(node.element.uri, { fileDecorations: { colors: false, badges: true }, - fileKind: FileKind.FILE, - hidePath: false, + fileKind, + hidePath: this.viewMode() === ViewMode.Tree, + }); + } + + renderCompressedElements(node: ITreeNode, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + const compressed = node.element as ICompressedTreeNode; + + const folder = compressed.elements[compressed.elements.length - 1]; + const label = compressed.elements.map(e => e.name); + + templateData.fileLabel.setResource({ resource: folder.uri, name: label }, { + fileDecorations: { colors: false, badges: true }, + fileKind: FileKind.FOLDER, + separator: this.labelService.getSeparator(folder.uri.scheme) }); } @@ -292,15 +349,7 @@ class SCMSyncViewPaneAccessibilityProvider implements IListAccessibilityProvider } else if (isSCMHistoryItemTreeElement(element)) { return `${stripIcons(element.label).trim()}${element.description ? `, ${element.description}` : ''}`; } else if (isSCMHistoryItemChangeTreeElement(element)) { - const result: string[] = []; - - result.push(basename(element.uri)); - - // TODO - add decoration - // if (element.decorations.tooltip) { - // result.push(element.decorations.tooltip); - // } - + const result = [basename(element.uri)]; const path = this.labelService.getUriLabel(dirname(element.uri), { relative: true, noPrefix: true }); if (path) { @@ -375,7 +424,7 @@ export class SCMSyncViewPane extends ViewPane { private listLabels!: ResourceLabels; private treeContainer!: HTMLElement; - private _tree!: WorkbenchAsyncDataTree; + private _tree!: WorkbenchCompressibleAsyncDataTree; private _viewModel!: SCMSyncPaneViewModel; get viewModel(): SCMSyncPaneViewModel { return this._viewModel; } @@ -407,24 +456,28 @@ export class SCMSyncViewPane extends ViewPane { this._register(this.listLabels); this._tree = this.instantiationService.createInstance( - WorkbenchAsyncDataTree, + WorkbenchCompressibleAsyncDataTree, 'SCM Sync View', this.treeContainer, new ListDelegate(), + new CompressionDelegate(), [ this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(ActionButtonRenderer), this.instantiationService.createInstance(HistoryItemGroupRenderer), this.instantiationService.createInstance(HistoryItemRenderer), - this.instantiationService.createInstance(HistoryItemChangeRenderer, this.listLabels), + this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewModel.mode, this.listLabels), ], - this.instantiationService.createInstance(SCMSyncDataSource), + this.instantiationService.createInstance(SCMSyncDataSource, () => this.viewModel.mode), { + compressionEnabled: true, horizontalScrolling: false, + autoExpandSingleChildren: true, + collapseByDefault: (e) => !ResourceTree.isResourceNode(e), accessibilityProvider: this.instantiationService.createInstance(SCMSyncViewPaneAccessibilityProvider), identityProvider: this.instantiationService.createInstance(SCMSyncViewPaneTreeIdentityProvider), sorter: this.instantiationService.createInstance(SCMSyncViewPaneTreeSorter), - }) as WorkbenchAsyncDataTree; + }) as WorkbenchCompressibleAsyncDataTree; this._register(this._tree); this._register(this._tree.onDidOpen(this.onDidOpen, this)); @@ -436,6 +489,7 @@ export class SCMSyncViewPane extends ViewPane { this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); + this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); } protected override layoutBody(height: number, width: number): void { @@ -443,18 +497,30 @@ export class SCMSyncViewPane extends ViewPane { this._tree.layout(height, width); } + private onDidChangeMode(): void { + this.updateIndentStyles(this.themeService.getFileIconTheme()); + } + private async onDidOpen(e: IOpenEvent): Promise { if (!e.element) { return; - } else if (isSCMHistoryItemChangeTreeElement(e.element)) { + } + + if (isSCMHistoryItemChangeTreeElement(e.element)) { if (e.element.originalUri && e.element.modifiedUri) { await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e); } + } else if (ResourceTree.isResourceNode(e.element) && e.element.childrenCount === 0) { + if (e.element.element?.originalUri && e.element.element?.modifiedUri) { + await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.element.uri, e.element.element.originalUri, e.element.element.modifiedUri), e); + } } } private updateIndentStyles(theme: any): void { - this.treeContainer.classList.toggle('align-icons-and-twisties', theme.hasFileIcons || (theme.hasFileIcons && !theme.hasFolderIcons)); + this.treeContainer.classList.toggle('list-view-mode', this._viewModel.mode === ViewMode.List); + this.treeContainer.classList.toggle('tree-view-mode', this._viewModel.mode === ViewMode.Tree); + this.treeContainer.classList.toggle('align-icons-and-twisties', (this._viewModel.mode === ViewMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); } override dispose(): void { @@ -465,6 +531,26 @@ export class SCMSyncViewPane extends ViewPane { class SCMSyncPaneViewModel { + private readonly _onDidChangeMode = new Emitter(); + readonly onDidChangeMode = this._onDidChangeMode.event; + + private _mode: ViewMode; + get mode(): ViewMode { return this._mode; } + set mode(mode: ViewMode) { + if (this._mode === mode) { + return; + } + + this._mode = mode; + + this.refresh(); + this.modeContextKey.set(mode); + this._onDidChangeMode.fire(mode); + + this.storageService.store(`scm.syncViewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); + } + + private modeContextKey: IContextKey; private repositories = new Map(); private historyProviders = new Map(); @@ -473,19 +559,25 @@ class SCMSyncPaneViewModel { private readonly disposables = new DisposableStore(); constructor( - private readonly tree: WorkbenchAsyncDataTree, + private readonly tree: WorkbenchCompressibleAsyncDataTree, + @IContextKeyService contextKeyService: IContextKeyService, @ISCMViewService scmViewService: ISCMViewService, @IConfigurationService private readonly configurationService: IConfigurationService, - + @IStorageService private readonly storageService: IStorageService, ) { - configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); - this.onDidChangeConfiguration(); + configurationService.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this.disposables); + this._onDidChangeConfiguration(); scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.disposables); this._onDidChangeVisibleRepositories({ added: scmViewService.visibleRepositories, removed: [] }); + + this._mode = this.getViewMode(); + + this.modeContextKey = ContextKeys.ViewMode.bindTo(contextKeyService); + this.modeContextKey.set(this._mode); } - private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { + private _onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) { this.alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); this.refresh(); @@ -524,6 +616,16 @@ class SCMSyncPaneViewModel { } } + private getViewMode(): ViewMode { + let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewMode.List : ViewMode.Tree; + const storageMode = this.storageService.get(`scm.syncViewMode`, StorageScope.WORKSPACE) as ViewMode; + if (typeof storageMode === 'string') { + mode = storageMode; + } + + return mode; + } + private async refresh(repository?: ISCMRepository): Promise { if (this.repositories.size === 0) { return; @@ -548,6 +650,10 @@ class SCMSyncPaneViewModel { class SCMSyncDataSource implements IAsyncDataSource { + constructor( + private readonly viewMode: () => ViewMode, + @IUriIdentityService private uriIdentityService: IUriIdentityService) { } + hasChildren(element: TreeElement): boolean { if (isSCMRepositoryArray(element)) { return true; @@ -561,6 +667,8 @@ class SCMSyncDataSource implements IAsyncDataSource { return true; } else if (isSCMHistoryItemChangeTreeElement(element)) { return false; + } else if (ResourceTree.isResourceNode(element)) { + return element.childrenCount > 0; } else { throw new Error('hasChildren not implemented.'); } @@ -651,14 +759,35 @@ class SCMSyncDataSource implements IAsyncDataSource { // History Item Changes const changes = await historyProvider.provideHistoryItemChanges(element.id) ?? []; - children.push(...changes.map(change => ({ - uri: change.uri, - originalUri: change.originalUri, - modifiedUri: change.modifiedUri, - renameUri: change.renameUri, - historyItem: element, - type: 'historyItemChange' - } as SCMHistoryItemChangeTreeElement))); + + if (this.viewMode() === ViewMode.List) { + // List + children.push(...changes.map(change => ({ + uri: change.uri, + originalUri: change.originalUri, + modifiedUri: change.modifiedUri, + renameUri: change.renameUri, + historyItem: element, + type: 'historyItemChange' + } as SCMHistoryItemChangeTreeElement))); + } else { + // Tree + const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); + for (const change of changes) { + tree.add(change.uri, { + uri: change.uri, + originalUri: change.originalUri, + modifiedUri: change.modifiedUri, + renameUri: change.renameUri, + historyItem: element, + type: 'historyItemChange' + } as SCMHistoryItemChangeTreeElement); + } + + children.push(...tree.root.children); + } + } else if (ResourceTree.isResourceNode(element)) { + children.push(...element.children); } else { throw new Error('getChildren Method not implemented.'); } @@ -666,3 +795,50 @@ class SCMSyncDataSource implements IAsyncDataSource { return children; } } + +class SetListViewModeAction extends ViewAction { + + constructor() { + super({ + id: 'workbench.scm.sync.action.setListViewMode', + title: localize('setListViewMode', "View as List"), + viewId: SYNC_VIEW_PANE_ID, + f1: false, + icon: Codicon.listTree, + toggled: ContextKeys.ViewMode.isEqualTo(ViewMode.List), + menu: { + id: MenuId.ViewTitle, + group: '1_viewmode' + } + }); + } + + async runInView(_: ServicesAccessor, view: SCMSyncViewPane): Promise { + view.viewModel.mode = ViewMode.List; + } +} + +class SetTreeViewModeAction extends ViewAction { + + constructor() { + super({ + id: 'workbench.scm.sync.action.setTreeViewMode', + title: localize('setTreeViewMode', "View as Tree"), + viewId: SYNC_VIEW_PANE_ID, + f1: false, + icon: Codicon.listFlat, + toggled: ContextKeys.ViewMode.isEqualTo(ViewMode.Tree), + menu: { + id: MenuId.ViewTitle, + group: '1_viewmode' + } + }); + } + + async runInView(_: ServicesAccessor, view: SCMSyncViewPane): Promise { + view.viewModel.mode = ViewMode.Tree; + } +} + +registerAction2(SetListViewModeAction); +registerAction2(SetTreeViewModeAction); diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 5d19c5e5205..2260a1d41ad 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -132,13 +132,15 @@ async function runTestsInBrowser(testModules, browserType) { const page = await context.newPage(); const target = url.pathToFileURL(path.join(__dirname, 'renderer.html')); if (argv.build) { - if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { - target.search = `?build=true&ci=true`; - } else { - target.search = `?build=true`; - } - } else if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { - target.search = `?ci=true`; + target.searchParams.set('build', 'true'); + } + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + target.searchParams.set('ci', 'true'); + } + + // see comment on warmupExposedMethods in renderer.html for what's going on + if (browserType === 'webkit') { + target.searchParams.set('ioWarmup', __dirname); } const emitter = new events.EventEmitter(); diff --git a/test/unit/browser/renderer.html b/test/unit/browser/renderer.html index fe62a749a68..c8a25ecf84d 100644 --- a/test/unit/browser/renderer.html +++ b/test/unit/browser/renderer.html @@ -129,8 +129,25 @@ } } + /** + * There is some bug in WebKit on macOS in CI only that causes the first + * invokation of the file functions to (sometimes) take an inordinately + * long period of time to run. Get around this by invoking them here. + */ + async function doIoWarmup() { + const dir = url.searchParams.get('ioWarmup'); + if (!dir) { + return; + } + + // these are the only two functions actually used in CI presently: + await __readFileInTests(dir + '/' + 'renderer.html'); + await __readDirInTests(dir); + } + window.loadAndRun = async function loadAndRun({ modules, grep }, manual = false) { // load + await doIoWarmup(); await loadModules(modules); // await new Promise((resolve, reject) => { // require(modules, resolve, err => { @@ -152,7 +169,8 @@ }); } - const modules = new URL(window.location.href).searchParams.getAll('m'); + const url = new URL(window.location.href); + const modules = url.searchParams.getAll('m'); if (Array.isArray(modules) && modules.length > 0) { console.log('MANUALLY running tests', modules);