diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 3f2b9136159..04996c83c73 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1230,6 +1230,10 @@ export function asCSSUrl(uri: URI): string { return `url('${FileAccess.asBrowserUri(uri).toString(true).replace(/'/g, '%27')}')`; } +export function asCSSPropertyValue(value: string) { + return `'${value.replace(/'/g, '%27')}'`; +} + export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void { // If the data is provided as Buffer, we create a diff --git a/src/vs/base/browser/ui/iconLabel/iconLabels.ts b/src/vs/base/browser/ui/iconLabel/iconLabels.ts index 1460c8e1595..103bca257a5 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabels.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabels.ts @@ -6,8 +6,7 @@ import * as dom from 'vs/base/browser/dom'; import { CSSIcon } from 'vs/base/common/codicons'; -const labelWithIconsRegex = /(\\)?\$\(([a-z\-]+(?:~[a-z\-]+)?)\)/gi; - +const labelWithIconsRegex = new RegExp(`(\\\\)?\\$\\((${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?)\\)`, 'g'); export function renderLabelWithIcons(text: string): Array { const elements = new Array(); let match: RegExpMatchArray | null; diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index aa85f54e866..d111029aab0 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -71,21 +71,25 @@ export interface CSSIcon { readonly id: string; } + export namespace CSSIcon { - export const iconIdRegex = /^(codicon\/)?([a-z\-]+)(?:~([a-z\-]+))?$/i; + export const iconNameExpression = '[A-Za-z0-9\\-]+'; + export const iconModifierExpression = '~[A-Za-z]+'; + + const cssIconIdRegex = new RegExp(`^(${iconNameExpression})(${iconModifierExpression})?$`); export function asClassNameArray(icon: CSSIcon): string[] { if (icon instanceof Codicon) { return ['codicon', 'codicon-' + icon.id]; } - const match = iconIdRegex.exec(icon.id); + const match = cssIconIdRegex.exec(icon.id); if (!match) { return asClassNameArray(Codicon.error); } - let [, , id, modifier] = match; + let [, id, modifier] = match; const classNames = ['codicon', 'codicon-' + id]; if (modifier) { - classNames.push('codicon-modifier-' + modifier); + classNames.push('codicon-modifier-' + modifier.substr(1)); } return classNames; } diff --git a/src/vs/base/common/iconLabels.ts b/src/vs/base/common/iconLabels.ts index 8ad65bee767..222e1bb94f2 100644 --- a/src/vs/base/common/iconLabels.ts +++ b/src/vs/base/common/iconLabels.ts @@ -3,28 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CSSIcon } from 'vs/base/common/codicons'; import { matchesFuzzy, IMatch } from 'vs/base/common/filters'; import { ltrim } from 'vs/base/common/strings'; export const iconStartMarker = '$('; -const escapeIconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; +const iconsRegex = new RegExp(`\\$\\(${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?\\)`, 'g'); // no capturing groups + +const escapeIconsRegex = new RegExp(`(\\\\)?${iconsRegex.source}`, 'g'); export function escapeIcons(text: string): string { return text.replace(escapeIconsRegex, (match, escaped) => escaped ? match : `\\${match}`); } -const markdownEscapedIconsRegex = /\\\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; +const markdownEscapedIconsRegex = new RegExp(`\\\\${iconsRegex.source}`, 'g'); export function markdownEscapeEscapedIcons(text: string): string { // Need to add an extra \ for escaping in markdown return text.replace(markdownEscapedIconsRegex, match => `\\${match}`); } -const markdownUnescapeIconsRegex = /(\\)?\$\\\(([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?)\\\)/gi; -export function markdownUnescapeIcons(text: string): string { - return text.replace(markdownUnescapeIconsRegex, (match, escaped, iconId) => escaped ? match : `$(${iconId})`); -} - -const stripIconsRegex = /(\s)?(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)(\s)?/gi; +const stripIconsRegex = new RegExp(`(\\s)?(\\\\)?${iconsRegex.source}(\\s)?`, 'g'); export function stripIcons(text: string): string { if (text.indexOf(iconStartMarker) === -1) { return text; diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 4b66374e5ea..7d622c76c53 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -71,6 +71,14 @@ export namespace Iterable { } } + export function reduce(iterable: Iterable, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R { + let value = initialValue; + for (const element of iterable) { + value = reducer(value, element); + } + return value; + } + /** * Returns an iterable slice of the array, with the same semantics as `array.slice()`. */ diff --git a/src/vs/base/test/common/iconLabels.test.ts b/src/vs/base/test/common/iconLabels.test.ts index 4b05b5b8af8..ab17675ceae 100644 --- a/src/vs/base/test/common/iconLabels.test.ts +++ b/src/vs/base/test/common/iconLabels.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { IMatch } from 'vs/base/common/filters'; -import { matchesFuzzyIconAware, parseLabelWithIcons, IParsedLabelWithIcons, stripIcons } from 'vs/base/common/iconLabels'; +import { matchesFuzzyIconAware, parseLabelWithIcons, IParsedLabelWithIcons, stripIcons, escapeIcons, markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels'; export interface IIconFilter { // Returns null if word doesn't match. @@ -71,4 +71,18 @@ suite('Icon Labels', () => { assert.strictEqual(stripIcons('$(Hello) World'), ' World'); assert.strictEqual(stripIcons('$(Hello) W$(oi)rld'), ' Wrld'); }); + + + test('escapeIcons', () => { + assert.strictEqual(escapeIcons('Hello World'), 'Hello World'); + assert.strictEqual(escapeIcons('$(Hello World'), '$(Hello World'); + assert.strictEqual(escapeIcons('$(Hello) World'), '\\$(Hello) World'); + assert.strictEqual(escapeIcons('\\$(Hello) W$(oi)rld'), '\\$(Hello) W\\$(oi)rld'); + }); + + test('markdownEscapeEscapedIcons', () => { + assert.strictEqual(markdownEscapeEscapedIcons('Hello World'), 'Hello World'); + assert.strictEqual(markdownEscapeEscapedIcons('$(Hello) World'), '$(Hello) World'); + assert.strictEqual(markdownEscapeEscapedIcons('\\$(Hello) World'), '\\\\$(Hello) World'); + }); }); diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index f61c9de8586..c83016ed920 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -16,7 +16,7 @@ import { ColorIdentifier, Extensions, IColorRegistry } from 'vs/platform/theme/c import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IThemingRegistry, ITokenStyle } from 'vs/platform/theme/common/themeService'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; +import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; const VS_THEME_NAME = 'vs'; const VS_DARK_THEME_NAME = 'vs-dark'; @@ -214,9 +214,9 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME)); this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME)); - const iconRegistry = getIconRegistry(); + const iconsStyleSheet = getIconsStyleSheet(); - this._codiconCSS = iconRegistry.getCSS(); + this._codiconCSS = iconsStyleSheet.getCSS(); this._themeCSS = ''; this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`; this._globalStyleElement = null; @@ -224,8 +224,8 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon this._colorMapOverride = null; this.setTheme(VS_THEME_NAME); - iconRegistry.onDidChange(() => { - this._codiconCSS = iconRegistry.getCSS(); + iconsStyleSheet.onDidChange(() => { + this._codiconCSS = iconsStyleSheet.getCSS(); this._updateCSS(); }); } diff --git a/src/vs/platform/theme/browser/iconsStyleSheet.ts b/src/vs/platform/theme/browser/iconsStyleSheet.ts new file mode 100644 index 00000000000..3e10a2d0053 --- /dev/null +++ b/src/vs/platform/theme/browser/iconsStyleSheet.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { getIconRegistry, IconContribution, IconFontContribution } from 'vs/platform/theme/common/iconRegistry'; +import { asCSSPropertyValue, asCSSUrl } from 'vs/base/browser/dom'; +import { Event, Emitter } from 'vs/base/common/event'; + + +export interface IIconsStyleSheet { + getCSS(): string; + readonly onDidChange: Event; +} + +export function getIconsStyleSheet(): IIconsStyleSheet { + const onDidChangeEmmiter = new Emitter(); + const iconRegistry = getIconRegistry(); + iconRegistry.onDidChange(() => onDidChangeEmmiter.fire()); + + return { + onDidChange: onDidChangeEmmiter.event, + getCSS() { + const usedFontIds: { [id: string]: IconFontContribution } = {}; + const formatIconRule = (contribution: IconContribution): string | undefined => { + let definition = contribution.defaults; + while (ThemeIcon.isThemeIcon(definition)) { + const c = iconRegistry.getIcon(definition.id); + if (!c) { + return undefined; + } + definition = c.defaults; + } + const fontId = definition.fontId; + if (fontId) { + const fontContribution = iconRegistry.getIconFont(fontId); + if (fontContribution) { + usedFontIds[fontId] = fontContribution; + return `.codicon-${contribution.id}:before { content: '${definition.character}'; font-family: ${asCSSPropertyValue(fontId)}; }`; + } + } + return `.codicon-${contribution.id}:before { content: '${definition.character}'; }`; + }; + + const rules = []; + for (let contribution of iconRegistry.getIcons()) { + const rule = formatIconRule(contribution); + if (rule) { + rules.push(rule); + } + } + for (let id in usedFontIds) { + const fontContribution = usedFontIds[id]; + const src = fontContribution.definition.src.map(l => `${asCSSUrl(l.location)} format('${l.format}')`).join(', '); + rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(id)}; }`); + } + return rules.join('\n'); + } + }; +} diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index c2f5d6b8e04..15432878dc7 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -11,11 +11,11 @@ import { localize } from 'vs/nls'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as Codicons from 'vs/base/common/codicons'; +import { URI } from 'vs/base/common/uri'; // ------ API types - -// color registry +// icon registry export const Extensions = { IconContribution: 'base.contributions.icons' }; @@ -34,6 +34,15 @@ export interface IconContribution { defaults: IconDefaults; } +export interface IconFontContribution { + id: string; + definition: IconFontDefinition; +} + +export interface IconFontDefinition { + src: { location: URI, format: string; }[] +} + export interface IIconRegistry { readonly onDidChange: Event; @@ -42,12 +51,12 @@ export interface IIconRegistry { * Register a icon to the registry. * @param id The icon id * @param defaults The default values - * @description the description + * @param description The description */ registerIcon(id: string, defaults: IconDefaults, description?: string): ThemeIcon; /** - * Register a icon to the registry. + * Deregister a icon from the registry. */ deregisterIcon(id: string): void; @@ -62,7 +71,7 @@ export interface IIconRegistry { getIcon(id: string): IconContribution | undefined; /** - * JSON schema for an object to assign icon values to one of the color contributions. + * JSON schema for an object to assign icon values to one of the icon contributions. */ getIconSchema(): IJSONSchema; @@ -72,10 +81,26 @@ export interface IIconRegistry { getIconReferenceSchema(): IJSONSchema; /** - * The CSS for all icons + * Register a icon font to the registry. + * @param id The icon font id + * @param definition The iocn font definition */ - getCSS(): string; + registerIconFont(id: string, definition: IconFontDefinition): IconFontContribution; + /** + * Deregister an icon font to the registry. + */ + deregisterIconFont(id: string): void; + + /** + * Get all icon font contributions + */ + getIconFonts(): IconFontContribution[]; + + /** + * Get the icon font for the given id + */ + getIconFont(id: string): IconFontContribution | undefined; } class IconRegistry implements IIconRegistry { @@ -99,10 +124,13 @@ class IconRegistry implements IIconRegistry { type: 'object', properties: {} }; - private iconReferenceSchema: IJSONSchema & { enum: string[], enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; + private iconReferenceSchema: IJSONSchema & { enum: string[], enumDescriptions: string[] } = { type: 'string', pattern: `^${Codicons.CSSIcon.iconNameExpression}$`, enum: [], enumDescriptions: [] }; + + private iconFontsById: { [key: string]: IconFontContribution }; constructor() { this.iconsById = {}; + this.iconFontsById = {}; } public registerIcon(id: string, defaults: IconDefaults, description?: string, deprecationMessage?: string): ThemeIcon { @@ -164,27 +192,27 @@ class IconRegistry implements IIconRegistry { return this.iconReferenceSchema; } - public getCSS() { - const rules = []; - for (let id in this.iconsById) { - const rule = this.formatRule(id); - if (rule) { - rules.push(rule); - } + public registerIconFont(id: string, definition: IconFontDefinition): IconFontContribution { + const existing = this.iconFontsById[id]; + if (existing) { + return existing; } - return rules.join('\n'); + let iconFontContribution: IconFontContribution = { id, definition }; + this.iconFontsById[id] = iconFontContribution; + this._onDidChange.fire(); + return iconFontContribution; } - private formatRule(id: string): string | undefined { - let definition = this.iconsById[id].defaults; - while (ThemeIcon.isThemeIcon(definition)) { - const c = this.iconsById[definition.id]; - if (!c) { - return undefined; - } - definition = c.defaults; - } - return `.codicon-${id}:before { content: '${definition.character}'; }`; + public deregisterIconFont(id: string): void { + delete this.iconFontsById[id]; + } + + public getIconFonts(): IconFontContribution[] { + return Object.keys(this.iconFontsById).map(id => this.iconFontsById[id]); + } + + public getIconFont(id: string): IconFontContribution | undefined { + return this.iconFontsById[id]; } public toString() { diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index 1096c6be5e8..4032bab91fc 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -40,18 +40,15 @@ export namespace ThemeIcon { return obj && typeof obj === 'object' && typeof (obj).id === 'string' && (typeof (obj).color === 'undefined' || ThemeColor.isThemeColor((obj).color)); } - const _regexFromString = /^\$\(([a-z.]+\/)?([a-z-~]+)\)$/i; + const _regexFromString = new RegExp(`^\\$\\((${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?)\\)$`); export function fromString(str: string): ThemeIcon | undefined { const match = _regexFromString.exec(str); if (!match) { return undefined; } - let [, owner, name] = match; - if (!owner || owner === 'codicon/') { - return { id: name }; - } - return { id: owner + name }; + let [, name] = match; + return { id: name }; } export function modify(icon: ThemeIcon, modifier: 'disabled' | 'spin' | undefined): ThemeIcon { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index f48e12c11c3..ce6e303298d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1486,9 +1486,8 @@ declare module 'vscode' { export class NotebookCellOutput { readonly outputs: NotebookCellOutputItem[]; - readonly metadata?: Record; - constructor(outputs: NotebookCellOutputItem[], metadata?: Record); + constructor(outputs: NotebookCellOutputItem[]); //TODO@jrieken HACK to workaround dependency issues... toJSON(): any; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 1e3169fd5e0..583887ec6ad 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -11,6 +11,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle // --- other interested parties import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValidationExtensionPoint'; import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint'; +import { IconExtensionPoint, IconFontExtensionPoint } from 'vs/workbench/services/themes/common/iconExtensionPoint'; import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes/common/tokenClassificationExtensionPoint'; import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint'; @@ -79,6 +80,8 @@ export class ExtensionPoints implements IWorkbenchContribution { // Classes that handle extension points... this.instantiationService.createInstance(JSONValidationExtensionPoint); this.instantiationService.createInstance(ColorExtensionPoint); + this.instantiationService.createInstance(IconExtensionPoint); + this.instantiationService.createInstance(IconFontExtensionPoint); this.instantiationService.createInstance(TokenClassificationExtensionPoints); this.instantiationService.createInstance(LanguageConfigurationFileHandler); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 9c08ed54186..6b4af0a13f6 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2826,10 +2826,7 @@ export class NotebookCellOutput { return obj instanceof NotebookCellOutput; } - constructor( - readonly outputs: NotebookCellOutputItem[], - readonly metadata?: Record - ) { } + constructor(readonly outputs: NotebookCellOutputItem[]) { } toJSON(): IDisplayOutput { let data: { [key: string]: unknown; } = {}; diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index 20accba7677..01831676a97 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -214,7 +214,9 @@ function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Part } function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorService: IEditorService, next: boolean): void { - const editorFocused = editorService.activeEditorPane?.hasFocus(); + // Need to ask if the active editor has focus since the layoutService is not aware of some custom editor focus behavior(notebooks) + // Also need to ask the layoutService for the case if no editor is opened + const editorFocused = editorService.activeEditorPane?.hasFocus() || layoutService.hasFocus(Parts.EDITOR_PART); const currentlyFocusedPart = editorFocused ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART : layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART : layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined; let partToFocus = Parts.EDITOR_PART; diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index aedb2a6f604..29c56c5202c 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -342,10 +342,11 @@ export class Workbench extends Layout { // Create Parts [ { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, - { id: Parts.ACTIVITYBAR_PART, role: 'navigation', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, - { id: Parts.SIDEBAR_PART, role: 'complementary', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, + // Use role 'none' for some parts to make screen readers less chatty #114892 + { id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, + { id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } }, - { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', positionToString(this.state.panel.position)] }, + { id: Parts.PANEL_PART, role: 'none', classes: ['panel', positionToString(this.state.panel.position)] }, { id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] } ].forEach(({ id, role, classes, options }) => { const partContainer = this.createPart(id, role, classes); diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 2c8425de65e..ad8b61dc520 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -10,7 +10,7 @@ import { Event } from 'vs/base/common/event'; import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -30,7 +30,6 @@ export class SCMStatusController implements IWorkbenchContribution { private statusBarDisposable: IDisposable = Disposable.None; private focusDisposable: IDisposable = Disposable.None; private focusedRepository: ISCMRepository | undefined = undefined; - private focusedProviderContextKey: IContextKey; private readonly badgeDisposable = new MutableDisposable(); private disposables = new DisposableStore(); private repositoryDisposables = new Set(); @@ -45,7 +44,6 @@ export class SCMStatusController implements IWorkbenchContribution { @IConfigurationService private readonly configurationService: IConfigurationService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { - this.focusedProviderContextKey = contextKeyService.createKey('scmProvider', undefined); this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); @@ -126,7 +124,6 @@ export class SCMStatusController implements IWorkbenchContribution { this.focusDisposable.dispose(); this.focusedRepository = repository; - this.focusedProviderContextKey.set(repository && repository.provider.id); if (repository && repository.provider.onDidChangeStatusBarCommands) { this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository)); diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index 7945dc95a48..b7655429425 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -13,7 +13,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IAction, IActionViewItem } from 'vs/base/common/actions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -22,7 +21,7 @@ import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; -import { collectContextMenuActions, StatusBarAction, StatusBarActionViewItem } from 'vs/workbench/contrib/scm/browser/util'; +import { collectContextMenuActions, getStatusBarActionViewItem } from 'vs/workbench/contrib/scm/browser/util'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; class ListDelegate implements IListVirtualDelegate { @@ -63,7 +62,7 @@ export class SCMRepositoriesViewPane extends ViewPane { const listContainer = append(container, $('.scm-view.scm-repositories-view')); const delegate = new ListDelegate(); - const renderer = this.instantiationService.createInstance(RepositoryRenderer, a => this.getActionViewItem(a),); + const renderer = this.instantiationService.createInstance(RepositoryRenderer, getStatusBarActionViewItem); const identityProvider = { getId: (r: ISCMRepository) => r.provider.id }; this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Main`, listContainer, delegate, [renderer], { @@ -192,12 +191,4 @@ export class SCMRepositoriesViewPane extends ViewPane { this.list.setFocus([selection[0]]); } } - - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action instanceof StatusBarAction) { - return new StatusBarActionViewItem(action); - } - - return super.getActionViewItem(action); - } } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index f25046fbe24..0364638e5be 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -7,23 +7,23 @@ import 'vs/css!./media/scm'; import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; +import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { MenuItemAction, IMenuService } from 'vs/platform/actions/common/actions'; -import { IAction, IActionViewItem, ActionRunner, Action, RadioGroup, Separator, SubmenuAction, IActionViewItemProvider } from 'vs/base/common/actions'; +import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2 } from 'vs/platform/actions/common/actions'; +import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IThemeService, registerThemingParticipant, IFileIconTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, StatusBarAction, StatusBarActionViewItem, getRepositoryVisibilityActions } from './util'; +import { IThemeService, registerThemingParticipant, IFileIconTheme } from 'vs/platform/theme/common/themeService'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getStatusBarActionViewItem } from './util'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { WorkbenchCompressibleObjectTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; @@ -79,6 +79,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { LabelFuzzyScore } from 'vs/base/browser/ui/tree/abstractTree'; import { Selection } from 'vs/editor/common/core/selection'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode | ISCMResource; @@ -760,9 +761,149 @@ const enum ViewModelMode { } const enum ViewModelSortKey { - Path, - Name, - Status + Path = 'path', + Name = 'name', + Status = 'status' +} + +const Menus = { + ViewSort: new MenuId('SCMViewSort'), + Repositories: new MenuId('SCMRepositories'), +}; + +const ContextKeys = { + ViewModelMode: new RawContextKey('scmViewModelMode', ViewModelMode.List), + ViewModelSortKey: new RawContextKey('scmViewModelSortKey', ViewModelSortKey.Path), + ViewModelAreAllRepositoriesCollapsed: new RawContextKey('scmViewModelAreAllRepositoriesCollapsed', false), + ViewModelIsAnyRepositoryCollapsible: new RawContextKey('scmViewModelIsAnyRepositoryCollapsible', false), + SCMProvider: new RawContextKey('scmProvider', undefined), + SCMProviderRootUri: new RawContextKey('scmProviderRootUri', undefined), + SCMProviderHasRootUri: new RawContextKey('scmProviderHasRootUri', undefined), + RepositoryCount: new RawContextKey('scmRepositoryCount', 0), + RepositoryVisibilityCount: new RawContextKey('scmRepositoryVisibleCount', 0), + RepositoryVisibility(repository: ISCMRepository) { + return new RawContextKey(`scmRepositoryVisible:${repository.provider.id}`, false); + } +}; + +MenuRegistry.appendMenuItem(MenuId.SCMTitle, { + title: localize('sortAction', "View & Sort"), + submenu: Menus.ViewSort, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0)), + group: '0_view&sort' +}); + +MenuRegistry.appendMenuItem(Menus.ViewSort, { + title: localize('repositories', "Repositories"), + submenu: Menus.Repositories, + group: '0_repositories' +}); + +class RepositoryVisibilityAction extends Action2 { + + private repository: ISCMRepository; + + constructor(repository: ISCMRepository) { + const title = repository.provider.rootUri ? basename(repository.provider.rootUri) : repository.provider.label; + super({ + id: `workbench.scm.action.toggleRepositoryVisibility.${repository.provider.id}`, + title, + f1: false, + precondition: ContextKeyExpr.or(ContextKeys.RepositoryVisibilityCount.notEqualsTo(1), ContextKeys.RepositoryVisibility(repository).isEqualTo(false)), + toggled: ContextKeys.RepositoryVisibility(repository).isEqualTo(true), + menu: { id: Menus.Repositories } + }); + this.repository = repository; + } + + run(accessor: ServicesAccessor) { + const scmViewService = accessor.get(ISCMViewService); + scmViewService.toggleVisibility(this.repository); + } +} + +interface RepositoryVisibilityItem { + readonly contextKey: IContextKey; + dispose(): void; +} + +class RepositoryVisibilityActionController { + + private items = new Map(); + private repositoryCountContextKey: IContextKey; + private repositoryVisibilityCountContextKey: IContextKey; + private disposables = new DisposableStore(); + + constructor( + @ISCMViewService private scmViewService: ISCMViewService, + @ISCMService scmService: ISCMService, + @IContextKeyService private contextKeyService: IContextKeyService + ) { + this.repositoryCountContextKey = ContextKeys.RepositoryCount.bindTo(contextKeyService); + this.repositoryVisibilityCountContextKey = ContextKeys.RepositoryVisibilityCount.bindTo(contextKeyService); + + scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.disposables); + scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + + for (const repository of scmService.repositories) { + this.onDidAddRepository(repository); + } + } + + private onDidAddRepository(repository: ISCMRepository): void { + const action = registerAction2(class extends RepositoryVisibilityAction { + constructor() { + super(repository); + } + }); + + const contextKey = ContextKeys.RepositoryVisibility(repository).bindTo(this.contextKeyService); + contextKey.set(this.scmViewService.isVisible(repository)); + + this.items.set(repository, { + contextKey, + dispose() { + contextKey.reset(); + action.dispose(); + } + }); + + this.updateRepositoriesCounts(); + } + + private onDidRemoveRepository(repository: ISCMRepository): void { + this.items.get(repository)?.dispose(); + this.items.delete(repository); + this.updateRepositoriesCounts(); + } + + private onDidChangeVisibleRepositories(): void { + let count = 0; + + for (const [repository, item] of this.items) { + const isVisible = this.scmViewService.isVisible(repository); + item.contextKey.set(isVisible); + + if (isVisible) { + count++; + } + } + + this.repositoryCountContextKey.set(this.items.size); + this.repositoryVisibilityCountContextKey.set(count); + } + + private updateRepositoriesCounts(): void { + this.repositoryCountContextKey.set(this.items.size); + this.repositoryVisibilityCountContextKey.set(Iterable.reduce(this.items.keys(), (r, repository) => r + (this.scmViewService.isVisible(repository) ? 1 : 0), 0)); + } + + dispose(): void { + this.disposables.dispose(); + dispose(this.items.values()); + this.items.clear(); + } } class ViewModel { @@ -770,12 +911,14 @@ class ViewModel { private readonly _onDidChangeMode = new Emitter(); readonly onDidChangeMode = this._onDidChangeMode.event; - private _onDidChangeRepositoryCollapseState = new Emitter(); - readonly onDidChangeRepositoryCollapseState: Event; private visible: boolean = false; get mode(): ViewModelMode { return this._mode; } set mode(mode: ViewModelMode) { + if (this._mode === mode) { + return; + } + this._mode = mode; for (const [, item] of this.items) { @@ -792,14 +935,17 @@ class ViewModel { this.refresh(); this._onDidChangeMode.fire(mode); + this.modeContextKey.set(mode); } + private _sortKey: ViewModelSortKey = ViewModelSortKey.Path; get sortKey(): ViewModelSortKey { return this._sortKey; } set sortKey(sortKey: ViewModelSortKey) { if (sortKey !== this._sortKey) { this._sortKey = sortKey; this.refresh(); } + this.sortKeyContextKey.set(sortKey); } private _treeViewStateIsStale = false; @@ -817,29 +963,44 @@ class ViewModel { private scrollTop: number | undefined; private alwaysShowRepositories = false; private firstVisible = true; - private viewSubMenuAction: SCMViewSubMenuAction | undefined; private disposables = new DisposableStore(); + private modeContextKey: IContextKey; + private sortKeyContextKey: IContextKey; + private areAllRepositoriesCollapsedContextKey: IContextKey; + private isAnyRepositoryCollapsibleContextKey: IContextKey; + private scmProviderContextKey: IContextKey; + private scmProviderRootUriContextKey: IContextKey; + private scmProviderHasRootUriContextKey: IContextKey; + constructor( private tree: WorkbenchCompressibleObjectTree, private inputRenderer: InputRenderer, private _mode: ViewModelMode, - private _sortKey: ViewModelSortKey, private _treeViewState: ITreeViewState | undefined, @IInstantiationService protected instantiationService: IInstantiationService, @IEditorService protected editorService: IEditorService, @IConfigurationService protected configurationService: IConfigurationService, @ISCMViewService private scmViewService: ISCMViewService, - @IUriIdentityService private uriIdentityService: IUriIdentityService + @IUriIdentityService private uriIdentityService: IUriIdentityService, + @IContextKeyService contextKeyService: IContextKeyService ) { - this.onDidChangeRepositoryCollapseState = Event.any( - this._onDidChangeRepositoryCollapseState.event, - Event.signal(Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element))) - ); + this.modeContextKey = ContextKeys.ViewModelMode.bindTo(contextKeyService); + this.modeContextKey.set(_mode); + this.sortKeyContextKey = ContextKeys.ViewModelSortKey.bindTo(contextKeyService); + this.sortKeyContextKey.set(this._sortKey); + this.areAllRepositoriesCollapsedContextKey = ContextKeys.ViewModelAreAllRepositoriesCollapsed.bindTo(contextKeyService); + this.isAnyRepositoryCollapsibleContextKey = ContextKeys.ViewModelIsAnyRepositoryCollapsible.bindTo(contextKeyService); + this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); + this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); + this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); this.onDidChangeConfiguration(); + Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element)) + (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); + this.disposables.add(this.tree.onDidChangeCollapseState(() => this._treeViewStateIsStale = true)); } @@ -950,10 +1111,21 @@ class ViewModel { } this.visible = visible; - this._onDidChangeRepositoryCollapseState.fire(); + this.updateRepositoryCollapseAllContextKeys(); } private refresh(item?: IRepositoryItem | IGroupItem): void { + if (!this.alwaysShowRepositories && this.items.size === 1) { + const provider = Iterable.first(this.items.values())!.element.provider; + this.scmProviderContextKey.set(provider.contextValue); + this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); + this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); + } else { + this.scmProviderContextKey.set(undefined); + this.scmProviderRootUriContextKey.set(undefined); + this.scmProviderHasRootUriContextKey.set(false); + } + if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { const item = Iterable.first(this.items.values())!; this.tree.setChildren(null, this.render(item, this.treeViewState).children); @@ -964,7 +1136,7 @@ class ViewModel { this.tree.setChildren(null, items.map(item => this.render(item, this.treeViewState))); } - this._onDidChangeRepositoryCollapseState.fire(); + this.updateRepositoryCollapseAllContextKeys(); } private render(item: IRepositoryItem | IGroupItem, treeViewState?: ITreeViewState): ICompressedTreeElement { @@ -1067,56 +1239,18 @@ class ViewModel { this.tree.domFocus(); } - getViewActions(): IAction[] { - if (this.scmViewService.visibleRepositories.length === 0) { - return this.scmViewService.menus.titleMenu.actions; + private updateRepositoryCollapseAllContextKeys(): void { + if (!this.visible || this.scmViewService.visibleRepositories.length === 1) { + this.isAnyRepositoryCollapsibleContextKey.set(false); + this.areAllRepositoriesCollapsedContextKey.set(false); + return; } - if (this.alwaysShowRepositories || this.scmViewService.visibleRepositories.length !== 1) { - return []; - } - - const menus = this.scmViewService.menus.getRepositoryMenus(this.scmViewService.visibleRepositories[0].provider); - return menus.titleMenu.actions; + this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); + this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); } - getViewSecondaryActions(): IAction[] { - if (this.scmViewService.visibleRepositories.length === 0) { - return this.scmViewService.menus.titleMenu.secondaryActions; - } - - if (!this.viewSubMenuAction) { - this.viewSubMenuAction = this.instantiationService.createInstance(SCMViewSubMenuAction, this); - this.disposables.add(this.viewSubMenuAction); - } - - if (this.alwaysShowRepositories || this.scmViewService.visibleRepositories.length !== 1) { - return this.viewSubMenuAction.actions.slice(0); - } - - const menus = this.scmViewService.menus.getRepositoryMenus(this.scmViewService.visibleRepositories[0].provider); - const secondaryActions = menus.titleMenu.secondaryActions; - - if (secondaryActions.length === 0) { - return [this.viewSubMenuAction]; - } - - return [this.viewSubMenuAction, new Separator(), ...secondaryActions]; - } - - getViewActionsContext(): any { - if (this.scmViewService.visibleRepositories.length === 0) { - return []; - } - - if (this.alwaysShowRepositories || this.scmViewService.visibleRepositories.length !== 1) { - return undefined; - } - - return this.scmViewService.visibleRepositories[0].provider; - } - - collapseAllProviders(): void { + collapseAllRepositories(): void { for (const repository of this.scmViewService.visibleRepositories) { if (this.tree.isCollapsible(repository)) { this.tree.collapse(repository); @@ -1124,7 +1258,7 @@ class ViewModel { } } - expandAllProviders(): void { + expandAllRepositories(): void { for (const repository of this.scmViewService.visibleRepositories) { if (this.tree.isCollapsible(repository)) { this.tree.expand(repository); @@ -1132,22 +1266,6 @@ class ViewModel { } } - isAnyProviderCollapsible(): boolean { - if (!this.visible || this.scmViewService.visibleRepositories.length === 1) { - return false; - } - - return this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r)); - } - - areAllProvidersCollapsed(): boolean { - if (!this.visible || this.scmViewService.visibleRepositories.length === 1) { - return false; - } - - return this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r))); - } - dispose(): void { this.visibilityDisposables.dispose(); this.disposables.dispose(); @@ -1156,145 +1274,155 @@ class ViewModel { } } -class SCMViewRepositoriesSubMenuAction extends SubmenuAction { - - get actions(): IAction[] { - return getRepositoryVisibilityActions(this.scmService, this.scmViewService); +class SetListViewModeAction extends ViewAction { + constructor(menu: Partial = {}) { + super({ + id: 'workbench.scm.action.setListViewMode', + title: localize('setListViewMode', "View as List"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.listFlat, + toggled: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List), + menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } + }); } - constructor( - @ISCMService private readonly scmService: ISCMService, - @ISCMViewService private readonly scmViewService: ISCMViewService, - ) { - super('scm.repositories', localize('repositories', "Repositories"), []); + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.viewModel.mode = ViewModelMode.List; } } -class SCMViewSubMenuAction extends SubmenuAction implements IDisposable { - - private disposable: IDisposable; - - constructor( - viewModel: ViewModel, - @IInstantiationService instantiationService: IInstantiationService - ) { - const listAction = new SCMViewModeListAction(viewModel); - const treeAction = new SCMViewModeTreeAction(viewModel); - const sortByNameAction = new SCMSortByNameAction(viewModel); - const sortByPathAction = new SCMSortByPathAction(viewModel); - const sortByStatusAction = new SCMSortByStatusAction(viewModel); - const actions = [ - instantiationService.createInstance(SCMViewRepositoriesSubMenuAction), - new Separator(), - ...new RadioGroup([listAction, treeAction]).actions, - new Separator(), - ...new RadioGroup([sortByNameAction, sortByPathAction, sortByStatusAction]).actions - ]; - - super( - 'scm.viewsort', - localize('sortAction', "View & Sort"), - actions - ); - - this.disposable = combinedDisposable(listAction, treeAction, sortByNameAction, sortByPathAction, sortByStatusAction); - } - - dispose(): void { - this.disposable.dispose(); +class SetListViewModeNavigationAction extends SetListViewModeAction { + constructor() { + super({ + id: MenuId.SCMTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.Tree)), + group: 'navigation', + order: -1000 + }); } } -export class ToggleViewModeAction extends Action { - - static readonly ID = 'workbench.scm.action.toggleViewMode'; - static readonly LABEL = localize('toggleViewMode', "Toggle View Mode"); - - constructor(id: string = ToggleViewModeAction.ID, label: string = ToggleViewModeAction.LABEL, private viewModel: ViewModel, private mode?: ViewModelMode) { - super(id, label); - this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this)); - this.onDidChangeMode(this.viewModel.mode); +class SetTreeViewModeAction extends ViewAction { + constructor(menu: Partial = {}) { + super({ + id: 'workbench.scm.action.setTreeViewMode', + title: localize('setTreeViewMode', "View as Tree"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.listTree, + toggled: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.Tree), + menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } + }); } - async run(): Promise { - if (typeof this.mode === 'undefined') { - this.viewModel.mode = this.viewModel.mode === ViewModelMode.List ? ViewModelMode.Tree : ViewModelMode.List; - } else { - this.viewModel.mode = this.mode; - } - } - - private onDidChangeMode(mode: ViewModelMode): void { - const iconClass = ThemeIcon.asClassName(mode === ViewModelMode.List ? Codicon.listTree : Codicon.listFlat); - this.class = `scm-action toggle-view-mode ${iconClass}`; - this.checked = this.viewModel.mode === this.mode; + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.viewModel.mode = ViewModelMode.Tree; } } -class SCMViewModeListAction extends ToggleViewModeAction { - constructor(viewModel: ViewModel) { - super('workbench.scm.action.viewModeList', localize('viewModeList', "View as List"), viewModel, ViewModelMode.List); +class SetTreeViewModeNavigationAction extends SetTreeViewModeAction { + constructor() { + super({ + id: MenuId.SCMTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List)), + group: 'navigation', + order: -1000 + }); } } -class SCMViewModeTreeAction extends ToggleViewModeAction { - constructor(viewModel: ViewModel) { - super('workbench.scm.action.viewModeTree', localize('viewModeTree', "View as Tree"), viewModel, ViewModelMode.Tree); +registerAction2(SetListViewModeAction); +registerAction2(SetTreeViewModeAction); +registerAction2(SetListViewModeNavigationAction); +registerAction2(SetTreeViewModeNavigationAction); + +abstract class SetSortKeyAction extends ViewAction { + constructor(private sortKey: ViewModelSortKey, title: string) { + super({ + id: `workbench.scm.action.setSortKey.${sortKey}`, + title: title, + viewId: VIEW_PANE_ID, + f1: false, + toggled: ContextKeys.ViewModelSortKey.isEqualTo(sortKey), + menu: { id: Menus.ViewSort, group: '2_sort' } + }); + } + + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.viewModel.sortKey = this.sortKey; } } -abstract class SCMSortAction extends Action { - - private readonly _listener: IDisposable; - - constructor(id: string, label: string, private viewModel: ViewModel, private sortKey: ViewModelSortKey) { - super(id, label); - - this.checked = this.sortKey === ViewModelSortKey.Path; - this.enabled = this.viewModel?.mode === ViewModelMode.List ?? false; - this._listener = viewModel?.onDidChangeMode(e => this.enabled = e === ViewModelMode.List); - } - - async run(): Promise { - if (this.sortKey !== this.viewModel.sortKey) { - this.checked = !this.checked; - this.viewModel.sortKey = this.sortKey; - } - } - - dispose(): void { - this._listener.dispose(); - super.dispose(); +class SetSortByNameAction extends SetSortKeyAction { + constructor() { + super(ViewModelSortKey.Name, localize('sortByName', "Sort by Name")); } } -class SCMSortByNameAction extends SCMSortAction { - static readonly ID = 'workbench.scm.action.sortByName'; - static readonly LABEL = localize('sortByName', "Sort by Name"); - - constructor(viewModel: ViewModel) { - super(SCMSortByNameAction.ID, SCMSortByNameAction.LABEL, viewModel, ViewModelSortKey.Name); +class SetSortByPathAction extends SetSortKeyAction { + constructor() { + super(ViewModelSortKey.Path, localize('sortByPath', "Sort by Path")); } } -class SCMSortByPathAction extends SCMSortAction { - static readonly ID = 'workbench.scm.action.sortByPath'; - static readonly LABEL = localize('sortByPath', "Sort by Path"); - - constructor(viewModel: ViewModel) { - super(SCMSortByPathAction.ID, SCMSortByPathAction.LABEL, viewModel, ViewModelSortKey.Path); +class SetSortByStatusAction extends SetSortKeyAction { + constructor() { + super(ViewModelSortKey.Status, localize('sortByStatus', "Sort by Status")); } } -class SCMSortByStatusAction extends SCMSortAction { - static readonly ID = 'workbench.scm.action.sortByStatus'; - static readonly LABEL = localize('sortByStatus', "Sort by Status"); +registerAction2(SetSortByNameAction); +registerAction2(SetSortByPathAction); +registerAction2(SetSortByStatusAction); - constructor(viewModel: ViewModel) { - super(SCMSortByStatusAction.ID, SCMSortByStatusAction.LABEL, viewModel, ViewModelSortKey.Status); +class CollapseAllRepositoriesAction extends ViewAction { + + constructor() { + super({ + id: `workbench.scm.action.collapseAllRepositories`, + title: localize('collapse all', "Collapse All Repositories"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.SCMTitle, + group: 'navigation', + when: ContextKeyExpr.and(ContextKeys.ViewModelIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.ViewModelAreAllRepositoriesCollapsed.isEqualTo(false)) + } + }); + } + + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.viewModel.collapseAllRepositories(); } } +class ExpandAllRepositoriesAction extends ViewAction { + + constructor() { + super({ + id: `workbench.scm.action.expandAllRepositories`, + title: localize('expand all', "Expand All Repositories"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.expandAll, + menu: { + id: MenuId.SCMTitle, + group: 'navigation', + when: ContextKeyExpr.and(ContextKeys.ViewModelIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.ViewModelAreAllRepositoriesCollapsed.isEqualTo(true)) + } + }); + } + + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.viewModel.expandAllRepositories(); + } +} + +registerAction2(CollapseAllRepositoriesAction); +registerAction2(ExpandAllRepositoriesAction); + class SCMInputWidget extends Disposable { private readonly defaultInputFontFamily = DEFAULT_FONT_FAMILY; @@ -1630,73 +1758,65 @@ class SCMInputWidget extends Disposable { } } -class SCMCollapseAction extends Action { - - private allCollapsed = false; - - constructor(private viewModel: ViewModel) { - super('scm.collapse', undefined, undefined, true); - this._register(viewModel.onDidChangeRepositoryCollapseState(this.update, this)); - this.update(); - } - - async run(): Promise { - if (this.allCollapsed) { - this.viewModel.expandAllProviders(); - } else { - this.viewModel.collapseAllProviders(); - } - } - - private update(): void { - const isAnyProviderCollapsible = this.viewModel.isAnyProviderCollapsible(); - - this.enabled = isAnyProviderCollapsible; - this.allCollapsed = isAnyProviderCollapsible && this.viewModel.areAllProvidersCollapsed(); - this.label = this.allCollapsed ? localize('expand all', "Expand All Repositories") : localize('collapse all', "Collapse All Repositories"); - this.class = ThemeIcon.asClassName(this.allCollapsed ? Codicon.expandAll : Codicon.collapseAll); - } -} - export class SCMViewPane extends ViewPane { - private _onDidLayout = new Emitter(); - private layoutCache: ISCMLayout = { - height: undefined, - width: undefined, - onDidChange: this._onDidLayout.event - }; + private _onDidLayout: Emitter; + private layoutCache: ISCMLayout; private listContainer!: HTMLElement; private tree!: WorkbenchCompressibleObjectTree; - private viewModel!: ViewModel; + private _viewModel!: ViewModel; + get viewModel(): ViewModel { return this._viewModel; } private listLabels!: ResourceLabels; private inputRenderer!: InputRenderer; - private toggleViewModelModeAction: ToggleViewModeAction | undefined; + + private scmService: ISCMService; + private scmViewService: ISCMViewService; + private storageService: IStorageService; + private commandService: ICommandService; + private editorService: IEditorService; + private menuService: IMenuService; constructor( options: IViewPaneOptions, - @ISCMService private scmService: ISCMService, - @ISCMViewService private scmViewService: ISCMViewService, - @IKeybindingService protected keybindingService: IKeybindingService, - @IThemeService protected themeService: IThemeService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IContextViewService protected contextViewService: IContextViewService, - @ICommandService protected commandService: ICommandService, - @IEditorService protected editorService: IEditorService, - @IInstantiationService protected instantiationService: IInstantiationService, + @ISCMService scmService: ISCMService, + @ISCMViewService scmViewService: ISCMViewService, + @IKeybindingService keybindingService: IKeybindingService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @ICommandService commandService: ICommandService, + @IEditorService editorService: IEditorService, + @IInstantiationService _instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IConfigurationService protected configurationService: IConfigurationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService protected menuService: IMenuService, - @IStorageService private storageService: IStorageService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService _contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, + @IStorageService storageService: IStorageService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); + const contextKeyService = _contextKeyService.createScoped(); + const services = new ServiceCollection([IContextKeyService, contextKeyService]); + const instantiationService = _instantiationService.createChild(services); - this._register(this.scmViewService.menus.titleMenu.onDidChangeTitle(this.updateActions, this)); + super({ ...options, titleMenuId: MenuId.SCMTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + + this.scmService = scmService; + this.scmViewService = scmViewService; + this.storageService = storageService; + this.commandService = commandService; + this.editorService = editorService; + this.menuService = menuService; + + this._onDidLayout = new Emitter(); + this.layoutCache = { + height: undefined, + width: undefined, + onDidChange: this._onDidLayout.event + }; + + this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); + // this._register(this.scmViewService.menus.titleMenu.onDidChangeTitle(this.updateActions, this)); } protected renderBody(container: HTMLElement): void { @@ -1719,13 +1839,9 @@ export class SCMViewPane extends ViewPane { this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'))(updateProviderCountVisibility)); updateProviderCountVisibility(); - this._register(this.scmViewService.onDidChangeVisibleRepositories(() => this.updateActions())); - this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => this.tree.updateElementHeight(input, height)); const delegate = new ListDelegate(this.inputRenderer); - const actionViewItemProvider = (action: IAction) => this.getActionViewItem(action); - this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this._register(this.listLabels); @@ -1734,15 +1850,15 @@ export class SCMViewPane extends ViewPane { this._register(actionRunner.onBeforeRun(() => this.tree.domFocus())); const renderers: ICompressibleTreeRenderer[] = [ - this.instantiationService.createInstance(RepositoryRenderer, actionViewItemProvider), + this.instantiationService.createInstance(RepositoryRenderer, getStatusBarActionViewItem), this.inputRenderer, - this.instantiationService.createInstance(ResourceGroupRenderer, actionViewItemProvider), - this.instantiationService.createInstance(ResourceRenderer, () => this.viewModel, this.listLabels, actionViewItemProvider, actionRunner) + this.instantiationService.createInstance(ResourceGroupRenderer, getStatusBarActionViewItem), + this.instantiationService.createInstance(ResourceRenderer, () => this._viewModel, this.listLabels, getStatusBarActionViewItem, actionRunner) ]; const filter = new SCMTreeFilter(); - const sorter = new SCMTreeSorter(() => this.viewModel); - const keyboardNavigationLabelProvider = this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this.viewModel); + const sorter = new SCMTreeSorter(() => this._viewModel); + const keyboardNavigationLabelProvider = this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this._viewModel); const identityProvider = new SCMResourceIdentityProvider(); this.tree = this.instantiationService.createInstance( @@ -1789,41 +1905,40 @@ export class SCMViewPane extends ViewPane { } catch {/* noop */ } } - this.viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer, viewMode, ViewModelSortKey.Path, viewState); - this._register(this.viewModel); + this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); + + this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer, viewMode, viewState); + this._register(this._viewModel); this.listContainer.classList.add('file-icon-themable-tree'); this.listContainer.classList.add('show-file-icons'); this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); - this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this)); + this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - this.toggleViewModelModeAction = new ToggleViewModeAction(ToggleViewModeAction.ID, ToggleViewModeAction.LABEL, this.viewModel); - this._register(this.toggleViewModelModeAction); - - this._register(this.onDidChangeBodyVisibility(this.viewModel.setVisible, this.viewModel)); + this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'))(this.updateActions, this)); this.updateActions(); this._register(this.storageService.onWillSaveState(e => { if (e.reason === WillSaveStateReason.SHUTDOWN) { - this.storageService.store(`scm.viewState`, JSON.stringify(this.viewModel.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); + this.storageService.store(`scm.viewState`, JSON.stringify(this._viewModel.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); } })); } private updateIndentStyles(theme: IFileIconTheme): void { - this.listContainer.classList.toggle('list-view-mode', this.viewModel.mode === ViewModelMode.List); - this.listContainer.classList.toggle('tree-view-mode', this.viewModel.mode === ViewModelMode.Tree); - this.listContainer.classList.toggle('align-icons-and-twisties', (this.viewModel.mode === ViewModelMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); - this.listContainer.classList.toggle('hide-arrows', this.viewModel.mode === ViewModelMode.Tree && theme.hidesExplorerArrows === true); + this.listContainer.classList.toggle('list-view-mode', this._viewModel.mode === ViewModelMode.List); + this.listContainer.classList.toggle('tree-view-mode', this._viewModel.mode === ViewModelMode.Tree); + this.listContainer.classList.toggle('align-icons-and-twisties', (this._viewModel.mode === ViewModelMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); + this.listContainer.classList.toggle('hide-arrows', this._viewModel.mode === ViewModelMode.Tree && theme.hidesExplorerArrows === true); } private onDidChangeMode(): void { this.updateIndentStyles(this.themeService.getFileIconTheme()); - this.storageService.store(`scm.viewMode`, this.viewModel.mode, StorageScope.WORKSPACE, StorageTarget.USER); + this.storageService.store(`scm.viewMode`, this._viewModel.mode, StorageScope.WORKSPACE, StorageTarget.USER); } layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { @@ -1847,56 +1962,10 @@ export class SCMViewPane extends ViewPane { super.focus(); if (this.isExpanded()) { - this.viewModel.focus(); + this._viewModel.focus(); } } - getActions(): IAction[] { - const result = []; - - if (this.toggleViewModelModeAction) { - result.push(this.toggleViewModelModeAction); - } - - if (!this.viewModel) { - return result; - } - - if (this.scmViewService.visibleRepositories.length < 2) { - return [...result, ...this.viewModel.getViewActions()]; - } - - return [ - ...result, - new SCMCollapseAction(this.viewModel), - ...this.viewModel.getViewActions() - ]; - } - - getSecondaryActions(): IAction[] { - if (!this.viewModel) { - return []; - } - - return this.viewModel.getViewSecondaryActions(); - } - - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action instanceof StatusBarAction) { - return new StatusBarActionViewItem(action); - } - - return super.getActionViewItem(action); - } - - getActionsContext(): any { - if (!this.viewModel) { - return []; - } - - return this.viewModel.getViewActionsContext(); - } - private async open(e: IOpenEvent): Promise { if (!e.element) { return; @@ -1960,9 +2029,17 @@ export class SCMViewPane extends ViewPane { private onListContextMenu(e: ITreeContextMenuEvent): void { if (!e.element) { + const menu = this.menuService.createMenu(Menus.ViewSort, this.contextKeyService); + const actions: IAction[] = []; + const disposable = createAndFillInContextMenuActions(menu, undefined, actions); + return this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => getRepositoryVisibilityActions(this.scmService, this.scmViewService) + getActions: () => actions, + onHide: () => { + disposable.dispose(); + menu.dispose(); + } }); } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPaneContainer.ts b/src/vs/workbench/contrib/scm/browser/scmViewPaneContainer.ts index 48d52e3cbe1..1e014cf01f2 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPaneContainer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPaneContainer.ts @@ -17,7 +17,6 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { SCMViewPane } from 'vs/workbench/contrib/scm/browser/scmViewPane'; export class SCMViewPaneContainer extends ViewPaneContainer { @@ -41,18 +40,6 @@ export class SCMViewPaneContainer extends ViewPaneContainer { parent.classList.add('scm-viewlet'); } - getActionsContext(): unknown { - if (this.views.length === 1) { - const view = this.views[0]; - - if (view instanceof SCMViewPane) { - return view.getActionsContext(); - } - } - - return undefined; - } - getOptimalWidth(): number { return 400; } diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 664dea50728..fc27ff6b731 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -3,19 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMService, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IDisposable, Disposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Action, IAction } from 'vs/base/common/actions'; +import { Action, IAction, IActionViewItem } from 'vs/base/common/actions'; import { createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { equals } from 'vs/base/common/arrays'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Command } from 'vs/editor/common/modes'; -import { basename } from 'vs/base/common/resources'; -import { Iterable } from 'vs/base/common/iterator'; import { reset } from 'vs/base/browser/dom'; export function isSCMRepository(element: any): element is ISCMRepository { @@ -96,7 +94,7 @@ export class StatusBarAction extends Action { } } -export class StatusBarActionViewItem extends ActionViewItem { +class StatusBarActionViewItem extends ActionViewItem { constructor(action: StatusBarAction) { super(null, action, {}); @@ -109,25 +107,10 @@ export class StatusBarActionViewItem extends ActionViewItem { } } -export function getRepositoryVisibilityActions(scmService: ISCMService, scmViewService: ISCMViewService): IAction[] { - const visible = new Set(); - const actions = scmService.repositories.map(repository => { - const label = repository.provider.rootUri ? basename(repository.provider.rootUri) : repository.provider.label; - const action = new Action('scm.repository.toggleVisibility', label, undefined, true, async () => { - scmViewService.toggleVisibility(repository); - }); - - if (scmViewService.isVisible(repository)) { - action.checked = true; - visible.add(action); - } - - return action; - }); - - if (visible.size === 1) { - Iterable.first(visible.values())!.enabled = false; +export function getStatusBarActionViewItem(action: IAction): IActionViewItem | undefined { + if (action instanceof StatusBarAction) { + return new StatusBarActionViewItem(action); } - return actions; + return undefined; } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 407c508439f..f9e77fdb961 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -145,7 +145,6 @@ export interface ISCMRepositoryMenus { } export interface ISCMMenus { - readonly titleMenu: ISCMTitleMenu; getRepositoryMenus(provider: ISCMProvider): ISCMRepositoryMenus; } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index ba20c4d6d90..1bd5631f70f 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -570,7 +570,8 @@ export class SimpleFileDialog { return UpdateResult.InvalidPath; } else { const inputUriDirname = resources.dirname(valueUri); - if (!resources.extUriIgnorePathCase.isEqual(resources.removeTrailingPathSeparator(this.currentFolder), inputUriDirname)) { + if (!resources.extUriIgnorePathCase.isEqual(resources.removeTrailingPathSeparator(this.currentFolder), inputUriDirname) + && (!/^[a-zA-Z]:$/.test(this.filePickBox.value) || !equalsIgnoreCase(this.pathFromUri(this.currentFolder).substring(0, this.filePickBox.value.length), this.filePickBox.value))) { let statWithoutTrailing: IFileStat | undefined; try { statWithoutTrailing = await this.fileService.resolve(inputUriDirname); diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 65f25cd6475..5611ff53613 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -38,7 +38,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IHostColorSchemeService } from 'vs/workbench/services/themes/common/hostColorSchemeService'; import { RunOnceScheduler, Sequencer } from 'vs/base/common/async'; import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; -import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; +import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; // implementation @@ -183,13 +183,13 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { const codiconStyleSheet = createStyleSheet(); codiconStyleSheet.id = 'codiconStyles'; - const iconRegistry = getIconRegistry(); + const iconsStyleSheet = getIconsStyleSheet(); function updateAll() { - codiconStyleSheet.textContent = iconRegistry.getCSS(); + codiconStyleSheet.textContent = iconsStyleSheet.getCSS(); } const delayer = new RunOnceScheduler(updateAll, 0); - iconRegistry.onDidChange(() => delayer.schedule()); + iconsStyleSheet.onDidChange(() => delayer.schedule()); delayer.schedule(); } diff --git a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts new file mode 100644 index 00000000000..80eda1e3554 --- /dev/null +++ b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { IIconRegistry, Extensions as IconRegistryExtensions, IconFontDefinition } from 'vs/platform/theme/common/iconRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { CSSIcon } from 'vs/base/common/codicons'; +import { fontIdRegex } from 'vs/workbench/services/themes/common/productIconThemeSchema'; +import * as resources from 'vs/base/common/resources'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; + +interface IIconExtensionPoint { + id: string; + description: string; + default: { iconFontId: string; character: string; } | string; +} + +interface IIconFontExtensionPoint { + id: string; + src: { + path: string; + format: string; + }[]; +} + +const iconRegistry: IIconRegistry = Registry.as(IconRegistryExtensions.IconContribution); + +const iconReferenceSchema = iconRegistry.getIconReferenceSchema(); +const iconIdPattern = `^${CSSIcon.iconNameExpression}$`; + +const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'icons', + jsonSchema: { + description: nls.localize('contributes.icons', 'Contributes extension defined themable icons'), + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: nls.localize('contributes.icon.id', 'The identifier of the themable icon'), + pattern: iconIdPattern, + patternErrorMessage: nls.localize('contributes.icon.id.format', 'Identifiers must only contain letters, digits and minus.'), + }, + description: { + type: 'string', + description: nls.localize('contributes.icon.description', 'The description of the themable icon'), + }, + default: { + anyOf: [ + iconReferenceSchema, + { + type: 'object', + properties: { + iconFontId: { + description: nls.localize('contributes.icon.default.iconFontId', 'The id of the icon font that defines the icon.'), + type: 'string' + }, + character: { + description: nls.localize('contributes.icon.default.character', 'The character for the icon in the icon font.'), + type: 'string' + } + }, + defaultSnippets: [{ body: { iconFontId: '${1:myIconFont}', character: '${2:\\\\E001}' } }] + } + ], + description: nls.localize('contributes.icon.default', 'The default of the icon. Either a reference to an extisting ThemeIcon or an icon in an icon font.'), + } + } + } + } +}); + +const iconFontConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'iconFonts', + jsonSchema: { + description: nls.localize('contributes.iconFonts', 'Contributes icon fonts to be used by icon contributions.'), + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: nls.localize('contributes.iconFonts.id', 'The ID of the font.'), + pattern: fontIdRegex, + patternErrorMessage: nls.localize('contributes.iconFonts.id.formatError', 'The ID must only contain letters, numbers, underscore and minus.') + }, + src: { + type: 'array', + description: nls.localize('contributes.iconFonts.src', 'The location of the font.'), + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: nls.localize('contributes.iconFonts.src.path', 'The font path, relative to the current extension location.'), + }, + format: { + type: 'string', + description: nls.localize('contributes.iconFonts.src.format', 'The format of the font.'), + enum: ['woff', 'woff2', 'truetype', 'opentype', 'embedded-opentype', 'svg'] + } + }, + required: [ + 'path', + 'format' + ] + } + } + } + } + } +}); + +export class IconExtensionPoint { + + constructor() { + iconConfigurationExtPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + const extensionValue = extension.value; + const collector = extension.collector; + + if (!extension.description.enableProposedApi) { + collector.error(nls.localize('invalid.icons.proposedAPI', "'configuration.icons is a proposed contribution point and only available when running out of dev or with the following command line switch: --enable-proposed-api {0}", extension.description.identifier.value)); + return; + } + + if (!extensionValue || !Array.isArray(extensionValue)) { + collector.error(nls.localize('invalid.icons.configuration', "'configuration.icons' must be a array")); + return; + } + + for (const iconContribution of extensionValue) { + if (typeof iconContribution.id !== 'string' || iconContribution.id.length === 0) { + collector.error(nls.localize('invalid.icons.id', "'configuration.icons.id' must be defined and can not be empty")); + return; + } + if (!iconContribution.id.match(iconIdPattern)) { + collector.error(nls.localize('invalid.icons.id.format', "'configuration.icons.id' must only contain letters, digits and minuses")); + return; + } + if (typeof iconContribution.description !== 'string' || iconContribution.id.length === 0) { + collector.error(nls.localize('invalid.icons.description', "'configuration.icons.description' must be defined and can not be empty")); + return; + } + let defaultIcon = iconContribution.default; + if (typeof defaultIcon === 'string') { + iconRegistry.registerIcon(iconContribution.id, { id: defaultIcon }, iconContribution.description); + } else if (typeof defaultIcon === 'object' && typeof defaultIcon.iconFontId === 'string' && typeof defaultIcon.character === 'string') { + iconRegistry.registerIcon(iconContribution.id, { + fontId: getFontId(extension.description, defaultIcon.iconFontId), + character: defaultIcon.character, + }, iconContribution.description); + } else { + collector.error(nls.localize('invalid.icons.default', "'configuration.icons.default' must be either a reference to the id of an other theme icon (string) or a icon definition (object)")); + } + } + } + for (const extension of delta.removed) { + const extensionValue = extension.value; + for (const iconContribution of extensionValue) { + iconRegistry.deregisterIcon(iconContribution.id); + } + } + }); + } +} + +export class IconFontExtensionPoint { + + constructor() { + iconFontConfigurationExtPoint.setHandler((_extensions, delta) => { + for (const extension of delta.added) { + const extensionValue = extension.value; + const collector = extension.collector; + + if (!extension.description.enableProposedApi) { + collector.error(nls.localize('invalid.iconFonts.proposedAPI', "'configuration.iconFonts is a proposed contribution point and only available when running out of dev or with the following command line switch: --enable-proposed-api {0}", extension.description.identifier.value)); + return; + } + + if (!extensionValue || !Array.isArray(extensionValue)) { + collector.error(nls.localize('invalid.iconFonts.configuration', "'configuration.iconFonts' must be a array")); + return; + } + + for (const iconFontContribution of extensionValue) { + if (typeof iconFontContribution.id !== 'string' || iconFontContribution.id.length === 0) { + collector.error(nls.localize('invalid.iconFonts.id', "'configuration.iconFonts.id' must be defined and can not be empty")); + return; + } + if (!iconFontContribution.id.match(fontIdRegex)) { + collector.error(nls.localize('invalid.iconFonts.id.format', "'configuration.iconFonts.id' must only contain letters, numbers, underscore and minus.")); + return; + } + if (!Array.isArray(iconFontContribution.src) || !iconFontContribution.src.length) { + collector.error(nls.localize('invalid.iconFonts.src', "'configuration.iconFonts.src' must be an array with locations of the icon font.")); + return; + } + const def: IconFontDefinition = { src: [] }; + for (const src of iconFontContribution.src) { + if (typeof src === 'object' && typeof src.path === 'string' && typeof src.format === 'string') { + const extensionLocation = extension.description.extensionLocation; + const iconFontLocation = resources.joinPath(extensionLocation, src.path); + if (!resources.isEqualOrParent(iconFontLocation, extensionLocation)) { + collector.warn(nls.localize('invalid.iconFonts.src.path', "Expected `contributes.iconFonts.src.path` ({0}) to be included inside extension's folder ({0}). This might make the extension non-portable.", iconFontLocation.path, extensionLocation.path)); + } + def.src.push({ + location: iconFontLocation, + format: src.format, + }); + } else { + collector.error(nls.localize('invalid.iconFonts.src.item', "Items of 'configuration.iconFonts.src' must be objects with properties 'path' and 'format'")); + } + } + iconRegistry.registerIconFont(getFontId(extension.description, iconFontContribution.id), def); + } + } + for (const extension of delta.removed) { + const extensionValue = extension.value; + for (const iconFontContribution of extensionValue) { + iconRegistry.deregisterIconFont(getFontId(extension.description, iconFontContribution.id)); + } + } + }); + } +} + +function getFontId(description: IExtensionDescription, iconFontId: string) { + return `${description.identifier.value}/${iconFontId}`; +}