Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICodeEditor, isCodeEditor, isDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServiceImpl';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { TextEditorOptions } from 'vs/workbench/common/editor';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { isEqual } from 'vs/base/common/resources';
export class CodeEditorService extends CodeEditorServiceImpl {
constructor(
@IEditorService private readonly editorService: IEditorService,
@IThemeService themeService: IThemeService
) {
super(themeService);
}
getActiveCodeEditor(): ICodeEditor | null {
const activeTextEditorControl = this.editorService.activeTextEditorControl;
if (isCodeEditor(activeTextEditorControl)) {
return activeTextEditorControl;
}
if (isDiffEditor(activeTextEditorControl)) {
return activeTextEditorControl.getModifiedEditor();
}
const activeControl = this.editorService.activeEditorPane?.getControl();
if (isCompositeEditor(activeControl) && isCodeEditor(activeControl.activeCodeEditor)) {
return activeControl.activeCodeEditor;
}
return null;
}
async openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
// Special case: If the active editor is a diff editor and the request to open originates and
// targets the modified side of it, we just apply the request there to prevent opening the modified
// side as separate editor.
const activeTextEditorControl = this.editorService.activeTextEditorControl;
if (
!sideBySide && // we need the current active group to be the taret
isDiffEditor(activeTextEditorControl) && // we only support this for active text diff editors
input.options && // we need options to apply
input.resource && // we need a request resource to compare with
activeTextEditorControl.getModel() && // we need a target model to compare with
source === activeTextEditorControl.getModifiedEditor() && // we need the source of this request to be the modified side of the diff editor
isEqual(input.resource, activeTextEditorControl.getModel()!.modified.uri) // we need the input resources to match with modified side
) {
const targetEditor = activeTextEditorControl.getModifiedEditor();
const textOptions = TextEditorOptions.create(input.options);
textOptions.apply(targetEditor, ScrollType.Smooth);
return targetEditor;
}
// Open using our normal editor service
return this.doOpenCodeEditor(input, source, sideBySide);
}
private async doOpenCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
const control = await this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
if (control) {
const widget = control.getControl();
if (isCodeEditor(widget)) {
return widget;
}
if (isCompositeEditor(widget) && isCodeEditor(widget.activeCodeEditor)) {
return widget.activeCodeEditor;
}
}
return null;
}
}
registerSingleton(ICodeEditorService, CodeEditorService, true);

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IEditorDropTargetDelegate } from 'vs/workbench/browser/parts/editor/editorDropTarget';
export const IEditorDropService = createDecorator<IEditorDropService>('editorDropService');
export interface IEditorDropService {
readonly _serviceBrand: undefined;
/**
* Allows to register a drag and drop target for editors.
*/
createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,597 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent } from 'vs/workbench/common/editor';
import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDimension } from 'vs/editor/common/editorCommon';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
export const IEditorGroupsService = createDecorator<IEditorGroupsService>('editorGroupsService');
export const enum GroupDirection {
UP,
DOWN,
LEFT,
RIGHT
}
export function preferredSideBySideGroupDirection(configurationService: IConfigurationService): GroupDirection.DOWN | GroupDirection.RIGHT {
const openSideBySideDirection = configurationService.getValue<'right' | 'down'>('workbench.editor.openSideBySideDirection');
if (openSideBySideDirection === 'down') {
return GroupDirection.DOWN;
}
return GroupDirection.RIGHT;
}
export const enum GroupOrientation {
HORIZONTAL,
VERTICAL
}
export const enum GroupLocation {
FIRST,
LAST,
NEXT,
PREVIOUS
}
export interface IFindGroupScope {
direction?: GroupDirection;
location?: GroupLocation;
}
export const enum GroupsArrangement {
/**
* Make the current active group consume the maximum
* amount of space possible.
*/
MINIMIZE_OTHERS,
/**
* Size all groups evenly.
*/
EVEN,
/**
* Will behave like MINIMIZE_OTHERS if the active
* group is not already maximized and EVEN otherwise
*/
TOGGLE
}
export interface GroupLayoutArgument {
size?: number;
groups?: GroupLayoutArgument[];
}
export interface EditorGroupLayout {
orientation: GroupOrientation;
groups: GroupLayoutArgument[];
}
export interface IMoveEditorOptions {
index?: number;
inactive?: boolean;
preserveFocus?: boolean;
}
export interface ICopyEditorOptions extends IMoveEditorOptions { }
export interface IAddGroupOptions {
activate?: boolean;
}
export const enum MergeGroupMode {
COPY_EDITORS,
MOVE_EDITORS
}
export interface IMergeGroupOptions {
mode?: MergeGroupMode;
index?: number;
}
export interface ICloseEditorOptions {
preserveFocus?: boolean;
}
export type ICloseEditorsFilter = {
except?: IEditorInput,
direction?: CloseDirection,
savedOnly?: boolean,
excludeSticky?: boolean
};
export interface ICloseAllEditorsOptions {
excludeSticky?: boolean;
}
export interface IEditorReplacement {
editor: IEditorInput;
replacement: IEditorInput;
options?: IEditorOptions | ITextEditorOptions;
}
export const enum GroupsOrder {
/**
* Groups sorted by creation order (oldest one first)
*/
CREATION_TIME,
/**
* Groups sorted by most recent activity (most recent active first)
*/
MOST_RECENTLY_ACTIVE,
/**
* Groups sorted by grid widget order
*/
GRID_APPEARANCE
}
export interface IEditorGroupsService {
readonly _serviceBrand: undefined;
/**
* An event for when the active editor group changes. The active editor
* group is the default location for new editors to open.
*/
readonly onDidActiveGroupChange: Event<IEditorGroup>;
/**
* An event for when a new group was added.
*/
readonly onDidAddGroup: Event<IEditorGroup>;
/**
* An event for when a group was removed.
*/
readonly onDidRemoveGroup: Event<IEditorGroup>;
/**
* An event for when a group was moved.
*/
readonly onDidMoveGroup: Event<IEditorGroup>;
/**
* An event for when a group gets activated.
*/
readonly onDidActivateGroup: Event<IEditorGroup>;
/**
* An event for when the group container is layed out.
*/
readonly onDidLayout: Event<IDimension>;
/**
* An event for when the index of a group changes.
*/
readonly onDidGroupIndexChange: Event<IEditorGroup>;
/**
* The size of the editor groups area.
*/
readonly contentDimension: IDimension;
/**
* An active group is the default location for new editors to open.
*/
readonly activeGroup: IEditorGroup;
/**
* All groups that are currently visible in the editor area in the
* order of their creation (oldest first).
*/
readonly groups: ReadonlyArray<IEditorGroup>;
/**
* The number of editor groups that are currently opened.
*/
readonly count: number;
/**
* The current layout orientation of the root group.
*/
readonly orientation: GroupOrientation;
/**
* A promise that resolves when groups have been restored.
*/
readonly whenRestored: Promise<void>;
/**
* Find out if the editor group service has editors to restore from a previous session.
*/
readonly willRestoreEditors: boolean;
/**
* Get all groups that are currently visible in the editor area.
*
* @param order the order of the editors to use
*/
getGroups(order: GroupsOrder): ReadonlyArray<IEditorGroup>;
/**
* Allows to convert a group identifier to a group.
*/
getGroup(identifier: GroupIdentifier): IEditorGroup | undefined;
/**
* Set a group as active. An active group is the default location for new editors to open.
*/
activateGroup(group: IEditorGroup | GroupIdentifier): IEditorGroup;
/**
* Returns the size of a group.
*/
getSize(group: IEditorGroup | GroupIdentifier): { width: number, height: number };
/**
* Sets the size of a group.
*/
setSize(group: IEditorGroup | GroupIdentifier, size: { width: number, height: number }): void;
/**
* Arrange all groups according to the provided arrangement.
*/
arrangeGroups(arrangement: GroupsArrangement): void;
/**
* Applies the provided layout by either moving existing groups or creating new groups.
*/
applyLayout(layout: EditorGroupLayout): void;
/**
* Enable or disable centered editor layout.
*/
centerLayout(active: boolean): void;
/**
* Find out if the editor layout is currently centered.
*/
isLayoutCentered(): boolean;
/**
* Sets the orientation of the root group to be either vertical or horizontal.
*/
setGroupOrientation(orientation: GroupOrientation): void;
/**
* Find a groupd in a specific scope:
* * `GroupLocation.FIRST`: the first group
* * `GroupLocation.LAST`: the last group
* * `GroupLocation.NEXT`: the next group from either the active one or `source`
* * `GroupLocation.PREVIOUS`: the previous group from either the active one or `source`
* * `GroupDirection.UP`: the next group above the active one or `source`
* * `GroupDirection.DOWN`: the next group below the active one or `source`
* * `GroupDirection.LEFT`: the next group to the left of the active one or `source`
* * `GroupDirection.RIGHT`: the next group to the right of the active one or `source`
*
* @param scope the scope of the group to search in
* @param source optional source to search from
* @param wrap optionally wrap around if reaching the edge of groups
*/
findGroup(scope: IFindGroupScope, source?: IEditorGroup | GroupIdentifier, wrap?: boolean): IEditorGroup;
/**
* Add a new group to the editor area. A new group is added by splitting a provided one in
* one of the four directions.
*
* @param location the group from which to split to add a new group
* @param direction the direction of where to split to
* @param options configure the newly group with options
*/
addGroup(location: IEditorGroup | GroupIdentifier, direction: GroupDirection, options?: IAddGroupOptions): IEditorGroup;
/**
* Remove a group from the editor area.
*/
removeGroup(group: IEditorGroup | GroupIdentifier): void;
/**
* Move a group to a new group in the editor area.
*
* @param group the group to move
* @param location the group from which to split to add the moved group
* @param direction the direction of where to split to
*/
moveGroup(group: IEditorGroup | GroupIdentifier, location: IEditorGroup | GroupIdentifier, direction: GroupDirection): IEditorGroup;
/**
* Merge the editors of a group into a target group. By default, all editors will
* move and the source group will close. This behaviour can be configured via the
* `IMergeGroupOptions` options.
*
* @param group the group to merge
* @param target the target group to merge into
* @param options controls how the merge should be performed. by default all editors
* will be moved over to the target and the source group will close. Configure to
* `MOVE_EDITORS_KEEP_GROUP` to prevent the source group from closing. Set to
* `COPY_EDITORS` to copy the editors into the target instead of moding them.
*/
mergeGroup(group: IEditorGroup | GroupIdentifier, target: IEditorGroup | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroup;
/**
* Copy a group to a new group in the editor area.
*
* @param group the group to copy
* @param location the group from which to split to add the copied group
* @param direction the direction of where to split to
*/
copyGroup(group: IEditorGroup | GroupIdentifier, location: IEditorGroup | GroupIdentifier, direction: GroupDirection): IEditorGroup;
/**
* Access the options of the editor part.
*/
readonly partOptions: IEditorPartOptions;
/**
* An event that notifies when editor part options change.
*/
readonly onDidEditorPartOptionsChange: Event<IEditorPartOptionsChangeEvent>;
/**
* Enforce editor part options temporarily.
*/
enforcePartOptions(options: IEditorPartOptions): IDisposable;
}
export const enum GroupChangeKind {
/* Group Changes */
GROUP_ACTIVE,
GROUP_INDEX,
/* Editor Changes */
EDITOR_OPEN,
EDITOR_CLOSE,
EDITOR_MOVE,
EDITOR_ACTIVE,
EDITOR_LABEL,
EDITOR_PIN,
EDITOR_STICKY,
EDITOR_DIRTY
}
export interface IGroupChangeEvent {
kind: GroupChangeKind;
editor?: IEditorInput;
editorIndex?: number;
}
export const enum OpenEditorContext {
NEW_EDITOR = 1,
MOVE_EDITOR = 2,
COPY_EDITOR = 3
}
export interface IEditorGroup {
/**
* An aggregated event for when the group changes in any way.
*/
readonly onDidGroupChange: Event<IGroupChangeEvent>;
/**
* An event that is fired when the group gets disposed.
*/
readonly onWillDispose: Event<void>;
/**
* An event that is fired when an editor is about to close.
*/
readonly onWillCloseEditor: Event<IEditorCloseEvent>;
/**
* A unique identifier of this group that remains identical even if the
* group is moved to different locations.
*/
readonly id: GroupIdentifier;
/**
* A number that indicates the position of this group in the visual
* order of groups from left to right and top to bottom. The lowest
* index will likely be top-left while the largest index in most
* cases should be bottom-right, but that depends on the grid.
*/
readonly index: number;
/**
* A human readable label for the group. This label can change depending
* on the layout of all editor groups. Clients should listen on the
* `onDidGroupChange` event to react to that.
*/
readonly label: string;
/**
* A human readable label for the group to be used by screen readers.
*/
readonly ariaLabel: string;
/**
* The active editor pane is the currently visible editor pane of the group.
*/
readonly activeEditorPane: IVisibleEditorPane | undefined;
/**
* The active editor is the currently visible editor of the group
* within the current active editor pane.
*/
readonly activeEditor: IEditorInput | null;
/**
* The editor in the group that is in preview mode if any. There can
* only ever be one editor in preview mode.
*/
readonly previewEditor: IEditorInput | null;
/**
* The number of opened editors in this group.
*/
readonly count: number;
/**
* The number of sticky editors in this group.
*/
readonly stickyCount: number;
/**
* All opened editors in the group in sequential order of their appearance.
*/
readonly editors: ReadonlyArray<IEditorInput>;
/**
* The scoped context key service for this group.
*/
readonly scopedContextKeyService: IContextKeyService;
/**
* Get all editors that are currently opened in the group.
*
* @param order the order of the editors to use
* @param options options to select only specific editors as instructed
*/
getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): ReadonlyArray<IEditorInput>;
/**
* Returns the editor at a specific index of the group.
*/
getEditorByIndex(index: number): IEditorInput | undefined;
/**
* Returns the index of the editor in the group or -1 if not opened.
*/
getIndexOfEditor(editor: IEditorInput): number;
/**
* Open an editor in this group.
*
* @returns a promise that resolves around an IEditor instance unless
* the call failed, or the editor was not opened as active editor.
*/
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, context?: OpenEditorContext): Promise<IEditorPane | null>;
/**
* Opens editors in this group.
*
* @returns a promise that resolves around an IEditor instance unless
* the call failed, or the editor was not opened as active editor. Since
* a group can only ever have one active editor, even if many editors are
* opened, the result will only be one editor.
*/
openEditors(editors: IEditorInputWithOptions[]): Promise<IEditorPane | null>;
/**
* Find out if the provided editor is opened in the group.
*
* Note: An editor can be opened but not actively visible.
*/
isOpened(editor: IEditorInput): boolean;
/**
* Find out if the provided editor is pinned in the group.
*/
isPinned(editor: IEditorInput): boolean;
/**
* Find out if the provided editor or index of editor is sticky in the group.
*/
isSticky(editorOrIndex: IEditorInput | number): boolean;
/**
* Find out if the provided editor is active in the group.
*/
isActive(editor: IEditorInput): boolean;
/**
* Move an editor from this group either within this group or to another group.
*/
moveEditor(editor: IEditorInput, target: IEditorGroup, options?: IMoveEditorOptions): void;
/**
* Copy an editor from this group to another group.
*
* Note: It is currently not supported to show the same editor more than once in the same group.
*/
copyEditor(editor: IEditorInput, target: IEditorGroup, options?: ICopyEditorOptions): void;
/**
* Close an editor from the group. This may trigger a confirmation dialog if
* the editor is dirty and thus returns a promise as value.
*
* @param editor the editor to close, or the currently active editor
* if unspecified.
*
* @returns a promise when the editor is closed.
*/
closeEditor(editor?: IEditorInput, options?: ICloseEditorOptions): Promise<void>;
/**
* Closes specific editors in this group. This may trigger a confirmation dialog if
* there are dirty editors and thus returns a promise as value.
*
* @returns a promise when all editors are closed.
*/
closeEditors(editors: IEditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<void>;
/**
* Closes all editors from the group. This may trigger a confirmation dialog if
* there are dirty editors and thus returns a promise as value.
*
* @returns a promise when all editors are closed.
*/
closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void>;
/**
* Replaces editors in this group with the provided replacement.
*
* @param editors the editors to replace
*
* @returns a promise that is resolved when the replaced active
* editor (if any) has finished loading.
*/
replaceEditors(editors: IEditorReplacement[]): Promise<void>;
/**
* Set an editor to be pinned. A pinned editor is not replaced
* when another editor opens at the same location.
*
* @param editor the editor to pin, or the currently active editor
* if unspecified.
*/
pinEditor(editor?: IEditorInput): void;
/**
* Set an editor to be sticky. A sticky editor is showing in the beginning
* of the tab stripe and will not be impacted by close operations.
*
* @param editor the editor to make sticky, or the currently active editor
* if unspecified.
*/
stickEditor(editor?: IEditorInput): void;
/**
* Set an editor to be non-sticky and thus moves back to a location after
* sticky editors and can be closed normally.
*
* @param editor the editor to make unsticky, or the currently active editor
* if unspecified.
*/
unstickEditor(editor?: IEditorInput): void;
/**
* Move keyboard focus into the group.
*/
focus(): void;
}

View File

@@ -0,0 +1,229 @@
/*---------------------------------------------------------------------------------------------
* 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 { IJSONSchema } from 'vs/base/common/jsonSchema';
import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
import { Registry } from 'vs/platform/registry/common/platform';
import { ICustomEditorInfo, IEditorService, IOpenEditorOverrideHandler, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService';
import { IEditorInput, IEditorPane, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorResourceAccessor } from 'vs/workbench/common/editor';
import { ITextEditorOptions, IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorGroup, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { URI } from 'vs/base/common/uri';
import { extname, basename, isEqual } from 'vs/base/common/resources';
/**
* Id of the default editor for open with.
*/
export const DEFAULT_EDITOR_ID = 'default';
/**
* Try to open an resource with a given editor.
*
* @param input Resource to open.
* @param id Id of the editor to use. If not provided, the user is prompted for which editor to use.
*/
export async function openEditorWith(
input: IEditorInput,
id: string | undefined,
options: IEditorOptions | ITextEditorOptions | undefined,
group: IEditorGroup,
editorService: IEditorService,
configurationService: IConfigurationService,
quickInputService: IQuickInputService,
): Promise<IEditorPane | undefined> {
const resource = input.resource;
if (!resource) {
return;
}
const overrideOptions = { ...options, override: id };
const allEditorOverrides = getAllAvailableEditors(resource, id, overrideOptions, group, editorService);
if (!allEditorOverrides.length) {
return;
}
let overrideToUse;
if (typeof id === 'string') {
overrideToUse = allEditorOverrides.find(([_, entry]) => entry.id === id);
} else if (allEditorOverrides.length === 1) {
overrideToUse = allEditorOverrides[0];
}
if (overrideToUse) {
return overrideToUse[0].open(input, overrideOptions, group, OpenEditorContext.NEW_EDITOR)?.override;
}
// Prompt
const originalResource = EditorResourceAccessor.getOriginalUri(input) || resource;
const resourceExt = extname(originalResource);
const items: (IQuickPickItem & { handler: IOpenEditorOverrideHandler })[] = allEditorOverrides.map(([handler, entry]) => {
return {
handler: handler,
id: entry.id,
label: entry.label,
description: entry.active ? nls.localize('promptOpenWith.currentlyActive', 'Currently Active') : undefined,
detail: entry.detail,
buttons: resourceExt ? [{
iconClass: 'codicon-settings-gear',
tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt)
}] : undefined
};
});
const picker = quickInputService.createQuickPick<(IQuickPickItem & { handler: IOpenEditorOverrideHandler })>();
picker.items = items;
if (items.length) {
picker.selectedItems = [items[0]];
}
picker.placeholder = nls.localize('promptOpenWith.placeHolder', "Select editor for '{0}'", basename(originalResource));
const pickedItem = await new Promise<(IQuickPickItem & { handler: IOpenEditorOverrideHandler }) | undefined>(resolve => {
picker.onDidAccept(() => {
resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0] : undefined);
picker.dispose();
});
picker.onDidTriggerItemButton(e => {
const pick = e.item;
const id = pick.id;
resolve(pick); // open the view
picker.dispose();
// And persist the setting
if (pick && id) {
const newAssociation: CustomEditorAssociation = { viewType: id, filenamePattern: '*' + resourceExt };
const currentAssociations = [...configurationService.getValue<CustomEditorsAssociations>(customEditorsAssociationsSettingId)];
// First try updating existing association
for (let i = 0; i < currentAssociations.length; ++i) {
const existing = currentAssociations[i];
if (existing.filenamePattern === newAssociation.filenamePattern) {
currentAssociations.splice(i, 1, newAssociation);
configurationService.updateValue(customEditorsAssociationsSettingId, currentAssociations);
return;
}
}
// Otherwise, create a new one
currentAssociations.unshift(newAssociation);
configurationService.updateValue(customEditorsAssociationsSettingId, currentAssociations);
}
});
picker.show();
});
return pickedItem?.handler.open(input, { ...options, override: pickedItem.id }, group, OpenEditorContext.NEW_EDITOR)?.override;
}
const builtinProviderDisplayName = nls.localize('builtinProviderDisplayName', "Built-in");
export const defaultEditorOverrideEntry = Object.freeze({
id: DEFAULT_EDITOR_ID,
label: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"),
detail: builtinProviderDisplayName
});
/**
* Get a list of all available editors, including the default text editor.
*/
export function getAllAvailableEditors(
resource: URI,
id: string | undefined,
options: IEditorOptions | ITextEditorOptions | undefined,
group: IEditorGroup,
editorService: IEditorService
): Array<[IOpenEditorOverrideHandler, IOpenEditorOverrideEntry]> {
const fileEditorInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileEditorInputFactory();
const overrides = editorService.getEditorOverrides(resource, options, group);
if (!overrides.some(([_, entry]) => entry.id === DEFAULT_EDITOR_ID)) {
overrides.unshift([
{
open: (input: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => {
const resource = EditorResourceAccessor.getOriginalUri(input);
if (!resource) {
return;
}
const fileEditorInput = editorService.createEditorInput({ resource, forceFile: true });
const textOptions: IEditorOptions | ITextEditorOptions = options ? { ...options, override: false } : { override: false };
return { override: editorService.openEditor(fileEditorInput, textOptions, group) };
}
},
{
...defaultEditorOverrideEntry,
active: fileEditorInputFactory.isFileEditorInput(editorService.activeEditor) && isEqual(editorService.activeEditor.resource, resource),
}]);
}
return overrides;
}
export const customEditorsAssociationsSettingId = 'workbench.editorAssociations';
export const viewTypeSchamaAddition: IJSONSchema = {
type: 'string',
enum: []
};
export type CustomEditorAssociation = {
readonly viewType: string;
readonly filenamePattern?: string;
};
export type CustomEditorsAssociations = readonly CustomEditorAssociation[];
export const editorAssociationsConfigurationNode: IConfigurationNode = {
...workbenchConfigurationNodeBase,
properties: {
[customEditorsAssociationsSettingId]: {
type: 'array',
markdownDescription: nls.localize('editor.editorAssociations', "Configure which editor to use for specific file types."),
items: {
type: 'object',
defaultSnippets: [{
body: {
'viewType': '$1',
'filenamePattern': '$2'
}
}],
properties: {
'viewType': {
anyOf: [
{
type: 'string',
description: nls.localize('editor.editorAssociations.viewType', "The unique id of the editor to use."),
},
viewTypeSchamaAddition
]
},
'filenamePattern': {
type: 'string',
description: nls.localize('editor.editorAssociations.filenamePattern', "Glob pattern specifying which files the editor should be used for."),
}
}
}
}
}
};
export const DEFAULT_CUSTOM_EDITOR: ICustomEditorInfo = {
id: 'default',
displayName: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"),
providerDisplayName: builtinProviderDisplayName
};
export function updateViewTypeSchema(enumValues: string[], enumDescriptions: string[]): void {
viewTypeSchamaAddition.enum = enumValues;
viewTypeSchamaAddition.enumDescriptions = enumDescriptions;
Registry.as<IConfigurationRegistry>(Extensions.Configuration)
.notifyConfigurationSchemaUpdated(editorAssociationsConfigurationNode);
}

View File

@@ -0,0 +1,296 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IResourceEditorInput, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextEditorPane, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent } from 'vs/workbench/common/editor';
import { Event } from 'vs/base/common/event';
import { IEditor, IDiffEditor } from 'vs/editor/common/editorCommon';
import { IEditorGroup, IEditorReplacement, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
export const IEditorService = createDecorator<IEditorService>('editorService');
export type IResourceEditorInputType = IResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput;
export interface IResourceEditorReplacement {
readonly editor: IResourceEditorInputType;
readonly replacement: IResourceEditorInputType;
}
export const ACTIVE_GROUP = -1;
export type ACTIVE_GROUP_TYPE = typeof ACTIVE_GROUP;
export const SIDE_GROUP = -2;
export type SIDE_GROUP_TYPE = typeof SIDE_GROUP;
export interface IOpenEditorOverrideEntry {
id: string;
label: string;
active: boolean;
detail?: string;
}
export interface IOpenEditorOverrideHandler {
open(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, context: OpenEditorContext): IOpenEditorOverride | undefined;
getEditorOverrides?(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): IOpenEditorOverrideEntry[];
}
export interface IOpenEditorOverride {
/**
* If defined, will prevent the opening of an editor and replace the resulting
* promise with the provided promise for the openEditor() call.
*/
override?: Promise<IEditorPane | undefined>;
}
export interface ISaveEditorsOptions extends ISaveOptions {
/**
* If true, will ask for a location of the editor to save to.
*/
readonly saveAs?: boolean;
}
export interface IBaseSaveRevertAllEditorOptions {
/**
* Whether to include untitled editors as well.
*/
readonly includeUntitled?: boolean;
/**
* Whether to exclude sticky editors.
*/
readonly excludeSticky?: boolean;
}
export interface ISaveAllEditorsOptions extends ISaveEditorsOptions, IBaseSaveRevertAllEditorOptions { }
export interface IRevertAllEditorsOptions extends IRevertOptions, IBaseSaveRevertAllEditorOptions { }
export interface ICustomEditorInfo {
readonly id: string;
readonly displayName: string;
readonly providerDisplayName: string;
}
export interface ICustomEditorViewTypesHandler {
readonly onDidChangeViewTypes: Event<void>;
getViewTypes(): ICustomEditorInfo[];
}
export interface IEditorService {
readonly _serviceBrand: undefined;
/**
* Emitted when the currently active editor changes.
*
* @see `IEditorService.activeEditorPane`
*/
readonly onDidActiveEditorChange: Event<void>;
/**
* Emitted when any of the current visible editors changes.
*
* @see `IEditorService.visibleEditorPanes`
*/
readonly onDidVisibleEditorsChange: Event<void>;
/**
* Emitted when an editor is closed.
*/
readonly onDidCloseEditor: Event<IEditorCloseEvent>;
/**
* The currently active editor pane or `undefined` if none. The editor pane is
* the workbench container for editors of any kind.
*
* @see `IEditorService.activeEditor` for access to the active editor input
*/
readonly activeEditorPane: IVisibleEditorPane | undefined;
/**
* The currently active editor or `undefined` if none. An editor is active when it is
* located in the currently active editor group. It will be `undefined` if the active
* editor group has no editors open.
*/
readonly activeEditor: IEditorInput | undefined;
/**
* The currently active text editor control or `undefined` if there is currently no active
* editor or the active editor widget is neither a text nor a diff editor.
*
* @see `IEditorService.activeEditor`
*/
readonly activeTextEditorControl: IEditor | IDiffEditor | undefined;
/**
* The currently active text editor mode or `undefined` if there is currently no active
* editor or the active editor control is neither a text nor a diff editor. If the active
* editor is a diff editor, the modified side's mode will be taken.
*/
readonly activeTextEditorMode: string | undefined;
/**
* All editor panes that are currently visible across all editor groups.
*
* @see `IEditorService.visibleEditors` for access to the visible editor inputs
*/
readonly visibleEditorPanes: ReadonlyArray<IVisibleEditorPane>;
/**
* All editors that are currently visible. An editor is visible when it is opened in an
* editor group and active in that group. Multiple editor groups can be opened at the same time.
*/
readonly visibleEditors: ReadonlyArray<IEditorInput>;
/**
* All text editor widgets that are currently visible across all editor groups. A text editor
* widget is either a text or a diff editor.
*/
readonly visibleTextEditorControls: ReadonlyArray<IEditor | IDiffEditor>;
/**
* All editors that are opened across all editor groups in sequential order
* of appearance.
*
* This includes active as well as inactive editors in each editor group.
*/
readonly editors: ReadonlyArray<IEditorInput>;
/**
* The total number of editors that are opened either inactive or active.
*/
readonly count: number;
/**
* All editors that are opened across all editor groups with their group
* identifier.
*
* @param order the order of the editors to use
* @param options wether to exclude sticky editors or not
*/
getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): ReadonlyArray<IEditorIdentifier>;
/**
* Open an editor in an editor group.
*
* @param editor the editor to open
* @param options the options to use for the editor
* @param group the target group. If unspecified, the editor will open in the currently
* active group. Use `SIDE_GROUP_TYPE` to open the editor in a new editor group to the side
* of the currently active group.
*
* @returns the editor that opened or `undefined` if the operation failed or the editor was not
* opened to be active.
*/
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditorPane | undefined>;
openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextEditorPane | undefined>;
openEditor(editor: IResourceDiffEditorInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextDiffEditorPane | undefined>;
/**
* Open editors in an editor group.
*
* @param editors the editors to open with associated options
* @param group the target group. If unspecified, the editor will open in the currently
* active group. Use `SIDE_GROUP_TYPE` to open the editor in a new editor group to the side
* of the currently active group.
*
* @returns the editors that opened. The array can be empty or have less elements for editors
* that failed to open or were instructed to open as inactive.
*/
openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ReadonlyArray<IEditorPane>>;
openEditors(editors: IResourceEditorInputType[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ReadonlyArray<IEditorPane>>;
/**
* Replaces editors in an editor group with the provided replacement.
*
* @param editors the editors to replace
*
* @returns a promise that is resolved when the replaced active
* editor (if any) has finished loading.
*/
replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
/**
* Find out if the provided editor is opened in any editor group.
*
* Note: An editor can be opened but not actively visible.
*
* @param editor the editor to check for being opened. If a
* `IResourceEditorInput` is passed in, the resource is checked on
* all opened editors. In case of a side by side editor, the
* right hand side resource is considered only.
*/
isOpen(editor: IResourceEditorInput): boolean;
isOpen(editor: IEditorInput): boolean;
/**
* Get all available editor overrides for the editor input.
*/
getEditorOverrides(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][];
/**
* Allows to override the opening of editors by installing a handler that will
* be called each time an editor is about to open allowing to override the
* operation to open a different editor.
*/
overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable;
/**
* Register handlers for custom editor view types.
* The handler will provide all available custom editors registered
* and also notify the editor service when a custom editor view type is registered/unregistered.
*/
registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable;
/**
* Converts a lightweight input to a workbench editor input.
*/
createEditorInput(input: IResourceEditorInputType): IEditorInput;
/**
* Save the provided list of editors.
*
* @returns `true` if all editors saved and `false` otherwise.
*/
save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean>;
/**
* Save all editors.
*
* @returns `true` if all editors saved and `false` otherwise.
*/
saveAll(options?: ISaveAllEditorsOptions): Promise<boolean>;
/**
* Reverts the provided list of editors.
*
* @returns `true` if all editors reverted and `false` otherwise.
*/
revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean>;
/**
* Reverts all editors.
*
* @returns `true` if all editors reverted and `false` otherwise.
*/
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean>;
/**
* Track the provided editors until all have been closed.
*
* @param options use `waitForSaved: true` to wait for the resources
* being saved. If auto-save is enabled, it may be possible to close
* an editor while the save continues in the background.
*/
whenClosed(editors: IResourceEditorInput[], options?: { waitForSaved: boolean }): Promise<void>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,572 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { EditorOptions, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
import { URI } from 'vs/base/common/uri';
import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, TestEditorPart } from 'vs/workbench/test/browser/workbenchTestServices';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { GroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
import { EditorActivation } from 'vs/platform/editor/common/editor';
import { WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver';
import { timeout } from 'vs/base/common/async';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { isWeb } from 'vs/base/common/platform';
const TEST_EDITOR_ID = 'MyTestEditorForEditorsObserver';
const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorsObserver';
const TEST_SERIALIZABLE_EDITOR_INPUT_ID = 'testSerializableEditorInputForEditorsObserver';
suite('EditorsObserver', function () {
let disposables: IDisposable[] = [];
setup(() => {
disposables.push(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput)], TEST_SERIALIZABLE_EDITOR_INPUT_ID));
});
teardown(() => {
dispose(disposables);
disposables = [];
});
async function createPart(): Promise<TestEditorPart> {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(TestEditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
return part;
}
async function createEditorObserver(): Promise<[EditorPart, EditorsObserver]> {
const part = await createPart();
const observer = new EditorsObserver(part, new TestStorageService());
return [part, observer];
}
test('basics (single group)', async () => {
const [part, observer] = await createEditorObserver();
let onDidMostRecentlyActiveEditorsChangeCalled = false;
const listener = observer.onDidMostRecentlyActiveEditorsChange(() => {
onDidMostRecentlyActiveEditorsChangeCalled = true;
});
let currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 0);
assert.equal(onDidMostRecentlyActiveEditorsChangeCalled, false);
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 1);
assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[0].editor, input1);
assert.equal(onDidMostRecentlyActiveEditorsChangeCalled, true);
assert.equal(observer.hasEditor(input1.resource), true);
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 3);
assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[0].editor, input3);
assert.equal(currentEditorsMRU[1].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[1].editor, input2);
assert.equal(currentEditorsMRU[2].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[2].editor, input1);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 3);
assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[0].editor, input2);
assert.equal(currentEditorsMRU[1].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[1].editor, input3);
assert.equal(currentEditorsMRU[2].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[2].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
onDidMostRecentlyActiveEditorsChangeCalled = false;
await part.activeGroup.closeEditor(input1);
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 2);
assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[0].editor, input2);
assert.equal(currentEditorsMRU[1].groupId, part.activeGroup.id);
assert.equal(currentEditorsMRU[1].editor, input3);
assert.equal(onDidMostRecentlyActiveEditorsChangeCalled, true);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
await part.activeGroup.closeAllEditors();
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 0);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), false);
assert.equal(observer.hasEditor(input3.resource), false);
part.dispose();
listener.dispose();
});
test('basics (multi group)', async () => {
const [part, observer] = await createEditorObserver();
const rootGroup = part.activeGroup;
let currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 0);
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 2);
assert.equal(currentEditorsMRU[0].groupId, sideGroup.id);
assert.equal(currentEditorsMRU[0].editor, input1);
assert.equal(currentEditorsMRU[1].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[1].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 2);
assert.equal(currentEditorsMRU[0].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[0].editor, input1);
assert.equal(currentEditorsMRU[1].groupId, sideGroup.id);
assert.equal(currentEditorsMRU[1].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
// Opening an editor inactive should not change
// the most recent editor, but rather put it behind
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
await rootGroup.openEditor(input2, EditorOptions.create({ inactive: true }));
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 3);
assert.equal(currentEditorsMRU[0].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[0].editor, input1);
assert.equal(currentEditorsMRU[1].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[1].editor, input2);
assert.equal(currentEditorsMRU[2].groupId, sideGroup.id);
assert.equal(currentEditorsMRU[2].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), true);
await rootGroup.closeAllEditors();
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 1);
assert.equal(currentEditorsMRU[0].groupId, sideGroup.id);
assert.equal(currentEditorsMRU[0].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), false);
await sideGroup.closeAllEditors();
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 0);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), false);
part.dispose();
});
test('copy group', async function () {
if (isWeb) {
this.skip();
}
const [part, observer] = await createEditorObserver();
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
const rootGroup = part.activeGroup;
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
let currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 3);
assert.equal(currentEditorsMRU[0].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[0].editor, input3);
assert.equal(currentEditorsMRU[1].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[1].editor, input2);
assert.equal(currentEditorsMRU[2].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[2].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
const copiedGroup = part.copyGroup(rootGroup, rootGroup, GroupDirection.RIGHT);
copiedGroup.setActive(true);
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 6);
assert.equal(currentEditorsMRU[0].groupId, copiedGroup.id);
assert.equal(currentEditorsMRU[0].editor, input3);
assert.equal(currentEditorsMRU[1].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[1].editor, input3);
assert.equal(currentEditorsMRU[2].groupId, copiedGroup.id);
assert.equal(currentEditorsMRU[2].editor, input2);
assert.equal(currentEditorsMRU[3].groupId, copiedGroup.id);
assert.equal(currentEditorsMRU[3].editor, input1);
assert.equal(currentEditorsMRU[4].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[4].editor, input2);
assert.equal(currentEditorsMRU[5].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[5].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
await rootGroup.closeAllEditors();
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
await copiedGroup.closeAllEditors();
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), false);
assert.equal(observer.hasEditor(input3.resource), false);
part.dispose();
});
test('initial editors are part of observer and state is persisted & restored (single group)', async () => {
const part = await createPart();
const rootGroup = part.activeGroup;
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
await part.whenRestored;
let currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 3);
assert.equal(currentEditorsMRU[0].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[0].editor, input3);
assert.equal(currentEditorsMRU[1].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[1].editor, input2);
assert.equal(currentEditorsMRU[2].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[2].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredObserver = new EditorsObserver(part, storage);
await part.whenRestored;
currentEditorsMRU = restoredObserver.editors;
assert.equal(currentEditorsMRU.length, 3);
assert.equal(currentEditorsMRU[0].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[0].editor, input3);
assert.equal(currentEditorsMRU[1].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[1].editor, input2);
assert.equal(currentEditorsMRU[2].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[2].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
part.clearState();
part.dispose();
});
test('initial editors are part of observer (multi group)', async () => {
const part = await createPart();
const rootGroup = part.activeGroup;
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
await part.whenRestored;
let currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 3);
assert.equal(currentEditorsMRU[0].groupId, sideGroup.id);
assert.equal(currentEditorsMRU[0].editor, input3);
assert.equal(currentEditorsMRU[1].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[1].editor, input2);
assert.equal(currentEditorsMRU[2].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[2].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredObserver = new EditorsObserver(part, storage);
await part.whenRestored;
currentEditorsMRU = restoredObserver.editors;
assert.equal(currentEditorsMRU.length, 3);
assert.equal(currentEditorsMRU[0].groupId, sideGroup.id);
assert.equal(currentEditorsMRU[0].editor, input3);
assert.equal(currentEditorsMRU[1].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[1].editor, input2);
assert.equal(currentEditorsMRU[2].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[2].editor, input1);
assert.equal(restoredObserver.hasEditor(input1.resource), true);
assert.equal(restoredObserver.hasEditor(input2.resource), true);
assert.equal(restoredObserver.hasEditor(input3.resource), true);
part.clearState();
part.dispose();
});
test('observer does not restore editors that cannot be serialized', async () => {
const part = await createPart();
const rootGroup = part.activeGroup;
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
await part.whenRestored;
let currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 1);
assert.equal(currentEditorsMRU[0].groupId, rootGroup.id);
assert.equal(currentEditorsMRU[0].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredObserver = new EditorsObserver(part, storage);
await part.whenRestored;
currentEditorsMRU = restoredObserver.editors;
assert.equal(currentEditorsMRU.length, 0);
assert.equal(restoredObserver.hasEditor(input1.resource), false);
part.clearState();
part.dispose();
});
test('observer closes editors when limit reached (across all groups)', async () => {
const part = await createPart();
part.enforcePartOptions({ limit: { enabled: true, value: 3 } });
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
const rootGroup = part.activeGroup;
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID);
const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true }));
assert.equal(rootGroup.count, 3);
assert.equal(rootGroup.isOpened(input1), false);
assert.equal(rootGroup.isOpened(input2), true);
assert.equal(rootGroup.isOpened(input3), true);
assert.equal(rootGroup.isOpened(input4), true);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
assert.equal(observer.hasEditor(input4.resource), true);
input2.setDirty();
part.enforcePartOptions({ limit: { enabled: true, value: 1 } });
await timeout(0);
assert.equal(rootGroup.count, 2);
assert.equal(rootGroup.isOpened(input1), false);
assert.equal(rootGroup.isOpened(input2), true); // dirty
assert.equal(rootGroup.isOpened(input3), false);
assert.equal(rootGroup.isOpened(input4), true);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), false);
assert.equal(observer.hasEditor(input4.resource), true);
const input5 = new TestFileEditorInput(URI.parse('foo://bar5'), TEST_EDITOR_INPUT_ID);
await sideGroup.openEditor(input5, EditorOptions.create({ pinned: true }));
assert.equal(rootGroup.count, 1);
assert.equal(rootGroup.isOpened(input1), false);
assert.equal(rootGroup.isOpened(input2), true); // dirty
assert.equal(rootGroup.isOpened(input3), false);
assert.equal(rootGroup.isOpened(input4), false);
assert.equal(sideGroup.isOpened(input5), true);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), false);
assert.equal(observer.hasEditor(input4.resource), false);
assert.equal(observer.hasEditor(input5.resource), true);
observer.dispose();
part.dispose();
});
test('observer closes editors when limit reached (in group)', async () => {
const part = await createPart();
part.enforcePartOptions({ limit: { enabled: true, value: 3, perEditorGroup: true } });
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
const rootGroup = part.activeGroup;
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID);
const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true }));
assert.equal(rootGroup.count, 3); // 1 editor got closed due to our limit!
assert.equal(rootGroup.isOpened(input1), false);
assert.equal(rootGroup.isOpened(input2), true);
assert.equal(rootGroup.isOpened(input3), true);
assert.equal(rootGroup.isOpened(input4), true);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
assert.equal(observer.hasEditor(input4.resource), true);
await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
await sideGroup.openEditor(input4, EditorOptions.create({ pinned: true }));
assert.equal(sideGroup.count, 3);
assert.equal(sideGroup.isOpened(input1), false);
assert.equal(sideGroup.isOpened(input2), true);
assert.equal(sideGroup.isOpened(input3), true);
assert.equal(sideGroup.isOpened(input4), true);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
assert.equal(observer.hasEditor(input4.resource), true);
part.enforcePartOptions({ limit: { enabled: true, value: 1, perEditorGroup: true } });
await timeout(10);
assert.equal(rootGroup.count, 1);
assert.equal(rootGroup.isOpened(input1), false);
assert.equal(rootGroup.isOpened(input2), false);
assert.equal(rootGroup.isOpened(input3), false);
assert.equal(rootGroup.isOpened(input4), true);
assert.equal(sideGroup.count, 1);
assert.equal(sideGroup.isOpened(input1), false);
assert.equal(sideGroup.isOpened(input2), false);
assert.equal(sideGroup.isOpened(input3), false);
assert.equal(sideGroup.isOpened(input4), true);
assert.equal(observer.hasEditor(input1.resource), false);
assert.equal(observer.hasEditor(input2.resource), false);
assert.equal(observer.hasEditor(input3.resource), false);
assert.equal(observer.hasEditor(input4.resource), true);
observer.dispose();
part.dispose();
});
test('observer does not close sticky', async () => {
const part = await createPart();
part.enforcePartOptions({ limit: { enabled: true, value: 3 } });
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
const rootGroup = part.activeGroup;
const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID);
const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, sticky: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true }));
assert.equal(rootGroup.count, 3);
assert.equal(rootGroup.isOpened(input1), true);
assert.equal(rootGroup.isOpened(input2), false);
assert.equal(rootGroup.isOpened(input3), true);
assert.equal(rootGroup.isOpened(input4), true);
assert.equal(observer.hasEditor(input1.resource), true);
assert.equal(observer.hasEditor(input2.resource), false);
assert.equal(observer.hasEditor(input3.resource), true);
assert.equal(observer.hasEditor(input4.resource), true);
observer.dispose();
part.dispose();
});
});