Merge branch 'master' into sandy081/smoketests/remote

This commit is contained in:
Sandeep Somavarapu 2021-02-04 16:22:43 +01:00
commit 3bd244bcf2
24 changed files with 818 additions and 434 deletions

View file

@ -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

View file

@ -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<HTMLSpanElement | string> {
const elements = new Array<HTMLSpanElement | string>();
let match: RegExpMatchArray | null;

View file

@ -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;
}

View file

@ -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;

View file

@ -71,6 +71,14 @@ export namespace Iterable {
}
}
export function reduce<T, R>(iterable: Iterable<T>, 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()`.
*/

View file

@ -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');
});
});

View file

@ -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();
});
}

View file

@ -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<void>;
}
export function getIconsStyleSheet(): IIconsStyleSheet {
const onDidChangeEmmiter = new Emitter<void>();
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');
}
};
}

View file

@ -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<void>;
@ -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() {

View file

@ -40,18 +40,15 @@ export namespace ThemeIcon {
return obj && typeof obj === 'object' && typeof (<ThemeIcon>obj).id === 'string' && (typeof (<ThemeIcon>obj).color === 'undefined' || ThemeColor.isThemeColor((<ThemeIcon>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 {

View file

@ -1486,9 +1486,8 @@ declare module 'vscode' {
export class NotebookCellOutput {
readonly outputs: NotebookCellOutputItem[];
readonly metadata?: Record<string, string | number | boolean>;
constructor(outputs: NotebookCellOutputItem[], metadata?: Record<string, string | number | boolean>);
constructor(outputs: NotebookCellOutputItem[]);
//TODO@jrieken HACK to workaround dependency issues...
toJSON(): any;

View file

@ -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);
}

View file

@ -2826,10 +2826,7 @@ export class NotebookCellOutput {
return obj instanceof NotebookCellOutput;
}
constructor(
readonly outputs: NotebookCellOutputItem[],
readonly metadata?: Record<string, string | number | boolean>
) { }
constructor(readonly outputs: NotebookCellOutputItem[]) { }
toJSON(): IDisplayOutput {
let data: { [key: string]: unknown; } = {};

View file

@ -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;

View file

@ -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);

View file

@ -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<string | undefined>;
private readonly badgeDisposable = new MutableDisposable<IDisposable>();
private disposables = new DisposableStore();
private repositoryDisposables = new Set<IDisposable>();
@ -45,7 +44,6 @@ export class SCMStatusController implements IWorkbenchContribution {
@IConfigurationService private readonly configurationService: IConfigurationService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
) {
this.focusedProviderContextKey = contextKeyService.createKey<string | undefined>('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));

View file

@ -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<ISCMRepository> {
@ -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);
}
}

View file

@ -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, ISCMResourceGroup> | 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<ViewModelMode>('scmViewModelMode', ViewModelMode.List),
ViewModelSortKey: new RawContextKey<ViewModelSortKey>('scmViewModelSortKey', ViewModelSortKey.Path),
ViewModelAreAllRepositoriesCollapsed: new RawContextKey<boolean>('scmViewModelAreAllRepositoriesCollapsed', false),
ViewModelIsAnyRepositoryCollapsible: new RawContextKey<boolean>('scmViewModelIsAnyRepositoryCollapsible', false),
SCMProvider: new RawContextKey<string | undefined>('scmProvider', undefined),
SCMProviderRootUri: new RawContextKey<string | undefined>('scmProviderRootUri', undefined),
SCMProviderHasRootUri: new RawContextKey<boolean>('scmProviderHasRootUri', undefined),
RepositoryCount: new RawContextKey<number>('scmRepositoryCount', 0),
RepositoryVisibilityCount: new RawContextKey<number>('scmRepositoryVisibleCount', 0),
RepositoryVisibility(repository: ISCMRepository) {
return new RawContextKey<boolean>(`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<boolean>;
dispose(): void;
}
class RepositoryVisibilityActionController {
private items = new Map<ISCMRepository, RepositoryVisibilityItem>();
private repositoryCountContextKey: IContextKey<number>;
private repositoryVisibilityCountContextKey: IContextKey<number>;
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<ViewModelMode>();
readonly onDidChangeMode = this._onDidChangeMode.event;
private _onDidChangeRepositoryCollapseState = new Emitter<void>();
readonly onDidChangeRepositoryCollapseState: Event<void>;
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<ViewModelMode>;
private sortKeyContextKey: IContextKey<ViewModelSortKey>;
private areAllRepositoriesCollapsedContextKey: IContextKey<boolean>;
private isAnyRepositoryCollapsibleContextKey: IContextKey<boolean>;
private scmProviderContextKey: IContextKey<string | undefined>;
private scmProviderRootUriContextKey: IContextKey<string | undefined>;
private scmProviderHasRootUriContextKey: IContextKey<boolean>;
constructor(
private tree: WorkbenchCompressibleObjectTree<TreeElement, FuzzyScore>,
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<TreeElement> {
@ -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<SCMViewPane> {
constructor(menu: Partial<IAction2Options['menu']> = {}) {
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<void> {
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<SCMViewPane> {
constructor(menu: Partial<IAction2Options['menu']> = {}) {
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<void> {
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<void> {
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<SCMViewPane> {
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<void> {
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<void> {
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<SCMViewPane> {
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<void> {
view.viewModel.collapseAllRepositories();
}
}
class ExpandAllRepositoriesAction extends ViewAction<SCMViewPane> {
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<void> {
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<void> {
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<void>();
private layoutCache: ISCMLayout = {
height: undefined,
width: undefined,
onDidChange: this._onDidLayout.event
};
private _onDidLayout: Emitter<void>;
private layoutCache: ISCMLayout;
private listContainer!: HTMLElement;
private tree!: WorkbenchCompressibleObjectTree<TreeElement, FuzzyScore>;
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<void>();
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<any, any, any>[] = [
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<TreeElement | undefined>): Promise<void> {
if (!e.element) {
return;
@ -1960,9 +2029,17 @@ export class SCMViewPane extends ViewPane {
private onListContextMenu(e: ITreeContextMenuEvent<TreeElement | null>): 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();
}
});
}

View file

@ -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;
}

View file

@ -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<IAction>();
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;
}

View file

@ -145,7 +145,6 @@ export interface ISCMRepositoryMenus {
}
export interface ISCMMenus {
readonly titleMenu: ISCMTitleMenu;
getRepositoryMenus(provider: ISCMProvider): ISCMRepositoryMenus;
}

View file

@ -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);

View file

@ -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();
}

View file

@ -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<IIconRegistry>(IconRegistryExtensions.IconContribution);
const iconReferenceSchema = iconRegistry.getIconReferenceSchema();
const iconIdPattern = `^${CSSIcon.iconNameExpression}$`;
const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IIconExtensionPoint[]>({
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<IIconFontExtensionPoint[]>({
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 = <IIconExtensionPoint[]>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 = <IIconExtensionPoint[]>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 = <IIconFontExtensionPoint[]>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 = <IIconFontExtensionPoint[]>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}`;
}