Merge from vscode 4f85c3c94c15457e1d4c9e67da6800630394ea54 (#8757)

* Merge from vscode 4f85c3c94c15457e1d4c9e67da6800630394ea54

* disable failing tests
This commit is contained in:
Anthony Dresser
2019-12-19 23:41:55 -08:00
committed by GitHub
parent 778a34a9d2
commit 74caccdfe6
40 changed files with 2058 additions and 1299 deletions

View File

@@ -6,7 +6,7 @@ labels: ''
assignees: ''
---
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- Please search existing issues to avoid creating duplicates. -->
<!-- Also please test using the latest insiders build to make sure your issue has not already been fixed. -->

View File

@@ -5,7 +5,7 @@
@font-face {
font-family: "codicon";
src: url("./codicon.ttf?072cd8445a025297c265f9d008123381") format("truetype");
src: url("./codicon.ttf?17db7f5e5f31fd546e62218bb0823c0c") format("truetype");
}
.codicon[class*='codicon-'] {

View File

@@ -32,6 +32,7 @@ import { InitializingRangeProvider, ID_INIT_PROVIDER } from 'vs/editor/contrib/f
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { onUnexpectedError } from 'vs/base/common/errors';
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
const CONTEXT_FOLDING_ENABLED = new RawContextKey<boolean>('foldingEnabled', false);
@@ -421,8 +422,12 @@ export class FoldingController extends Disposable implements IEditorContribution
if (region && region.startLineNumber === lineNumber) {
let isCollapsed = region.isCollapsed;
if (iconClicked || isCollapsed) {
let toToggle = [region];
if (e.event.middleButton || e.event.shiftKey) {
let toToggle = [];
let considerRegionsInside = this.shouldConsiderRegionsInside(e.event);
if (isCollapsed || (!isCollapsed && !considerRegionsInside)) {
toToggle.push(region);
}
if (considerRegionsInside) {
toToggle.push(...foldingModel.getRegionsInside(region, (r: FoldingRegion) => r.isCollapsed === isCollapsed));
}
foldingModel.toggleCollapseState(toToggle);
@@ -433,6 +438,10 @@ export class FoldingController extends Disposable implements IEditorContribution
}).then(undefined, onUnexpectedError);
}
private shouldConsiderRegionsInside(event: IMouseEvent): boolean {
return event.middleButton || event.shiftKey;
}
public reveal(position: IPosition): void {
this.editor.revealPositionInCenterIfOutsideViewport(position, ScrollType.Smooth);
}

View File

@@ -13,6 +13,8 @@ export class FoldingDecorationProvider implements IDecorationProvider {
private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
afterContentClassName: 'inline-folded',
className: 'folded-background',
isWholeLine: true,
linesDecorationsClassName: 'codicon codicon-chevron-right'
});

View File

@@ -6,6 +6,9 @@
import { ITextModel, IModelDecorationOptions, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
import { Event, Emitter } from 'vs/base/common/event';
import { FoldingRegions, ILineRange, FoldingRegion } from './foldingRanges';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { registerColor, editorSelectionBackground, darken, lighten } from 'vs/platform/theme/common/colorRegistry';
import * as nls from 'vs/nls';
export interface IDecorationProvider {
getDecorationOption(isCollapsed: boolean): IModelDecorationOptions;
@@ -90,7 +93,6 @@ export class FoldingModel {
};
newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed) });
};
let i = 0;
let nextCollapsed = () => {
while (i < this._regions.length) {
@@ -355,3 +357,12 @@ export function setCollapseStateForType(foldingModel: FoldingModel, type: string
}
foldingModel.toggleCollapseState(toToggle);
}
export const foldBackgroundBackground = registerColor('editor.foldBackground', { light: lighten(editorSelectionBackground, 0.5), dark: darken(editorSelectionBackground, 0.5), hc: null }, nls.localize('editorSelectionBackground', "Color of the editor selection."));
registerThemingParticipant((theme, collector) => {
const foldBackground = theme.getColor(foldBackgroundBackground);
if (foldBackground) {
collector.addRule(`.monaco-editor .folded-background { background-color: ${foldBackground}; }`);
}
});

View File

@@ -47,7 +47,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
if (this.userDataSyncStoreService.userDataSyncStore) {
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
this._register(authTokenService.onDidChangeStatus(() => this.onDidChangeAuthTokenStatus()));
}
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal));
@@ -118,11 +117,4 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
return null;
}
private onDidChangeAuthTokenStatus(): void {
if (this.authTokenService.status === AuthTokenStatus.SignedOut) {
this.stop();
}
}
}

View File

@@ -1019,10 +1019,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
restoreWindows = 'all'; // always reopen all windows when an update was applied
} else {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
restoreWindows = windowConfig?.restoreWindows || 'one';
restoreWindows = windowConfig?.restoreWindows || 'all'; // by default restore all windows
if (['all', 'folders', 'one', 'none'].indexOf(restoreWindows) === -1) {
restoreWindows = 'one';
restoreWindows = 'all'; // by default restore all windows
}
}

View File

@@ -1474,4 +1474,40 @@ declare module 'vscode' {
}
//#endregion
//#region color theme access
/**
* Represents a color theme kind.
*/
export enum ColorThemeKind {
Light = 1,
Dark = 2,
HighContrast = 3
}
/**
* Represents a color theme.
*/
export interface ColorTheme {
/**
* The kind of this color theme: light, dark or high contrast.
*/
readonly kind: ColorThemeKind;
}
export namespace window {
/**
* The currently active color theme as configured in the settings. The active
* theme can be changed via the `workbench.colorTheme` setting.
*/
export let activeColorTheme: ColorTheme;
/**
* An [event](#Event) which fires when the active theme changes or one of it's colors chnage.
*/
export const onDidChangeActiveColorTheme: Event<ColorTheme>;
}
}

View File

@@ -48,6 +48,7 @@ import './mainThreadStatusBar';
import './mainThreadStorage';
import './mainThreadTelemetry';
import './mainThreadTerminalService';
import './mainThreadTheming';
import './mainThreadTreeViews';
import './mainThreadDownloadService';
import './mainThreadUrls';

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MainContext, IExtHostContext, ExtHostThemingShape, ExtHostContext, MainThreadThemingShape } from '../common/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@extHostNamedCustomer(MainContext.MainThreadTheming)
export class MainThreadTheming implements MainThreadThemingShape {
private readonly _themeService: IThemeService;
private readonly _proxy: ExtHostThemingShape;
private readonly _themeChangeListener: IDisposable;
constructor(
extHostContext: IExtHostContext,
@IThemeService themeService: IThemeService
) {
this._themeService = themeService;
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTheming);
this._themeChangeListener = this._themeService.onThemeChange(e => {
this._proxy.$onColorThemeChange(this._themeService.getTheme().type);
});
}
dispose(): void {
this._themeChangeListener.dispose();
}
}

View File

@@ -68,6 +68,7 @@ import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransf
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { find } from 'vs/base/common/arrays';
import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming';
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
export interface IExtensionApiFactory {
@@ -126,7 +127,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments));
const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol));
const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress)));
const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol));
const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol));
const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol));
// Check that no named customers are missing
// {{SQL CARBON EDIT}} filter out the services we don't expose
@@ -554,6 +556,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
},
createInputBox(): vscode.InputBox {
return extHostQuickOpen.createInputBox(extension.identifier);
},
get activeColorTheme(): vscode.ColorTheme {
checkProposedApiEnabled(extension);
return extHostTheming.activeColorTheme;
},
onDidChangeActiveColorTheme(listener, thisArg?, disposables?) {
checkProposedApiEnabled(extension);
return extHostTheming.onDidChangeActiveColorTheme(listener, thisArg, disposables);
}
};
@@ -940,7 +950,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
DebugConsoleMode: extHostTypes.DebugConsoleMode,
Decoration: extHostTypes.Decoration,
WebviewContentState: extHostTypes.WebviewContentState,
UIKind: UIKind
UIKind: UIKind,
ColorThemeKind: extHostTypes.ColorThemeKind
};
};
}

View File

@@ -1398,6 +1398,12 @@ export interface ExtHostStorageShape {
$acceptValue(shared: boolean, key: string, value: object | undefined): void;
}
export interface ExtHostThemingShape {
$onColorThemeChange(themeType: string): void;
}
export interface MainThreadThemingShape extends IDisposable {
}
export interface ExtHostTunnelServiceShape {
$findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]>;
@@ -1446,6 +1452,7 @@ export const MainContext = {
MainThreadTask: createMainId<MainThreadTaskShape>('MainThreadTask'),
MainThreadWindow: createMainId<MainThreadWindowShape>('MainThreadWindow'),
MainThreadLabelService: createMainId<MainThreadLabelServiceShape>('MainThreadLabelService'),
MainThreadTheming: createMainId<MainThreadThemingShape>('MainThreadTheming'),
MainThreadTunnelService: createMainId<MainThreadTunnelServiceShape>('MainThreadTunnelService')
};
@@ -1480,6 +1487,7 @@ export const ExtHostContext = {
ExtHostStorage: createMainId<ExtHostStorageShape>('ExtHostStorage'),
ExtHostUrls: createExtId<ExtHostUrlsShape>('ExtHostUrls'),
ExtHostOutputService: createMainId<ExtHostOutputServiceShape>('ExtHostOutputService'),
ExtHosLabelService: createMainId<ExtHostLabelServiceShape>('ExtHostLabelService'),
ExtHostLabelService: createMainId<ExtHostLabelServiceShape>('ExtHostLabelService'),
ExtHostTheming: createMainId<ExtHostThemingShape>('ExtHostTheming'),
ExtHostTunnelService: createMainId<ExtHostTunnelServiceShape>('ExtHostTunnelService')
};

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ColorTheme, ColorThemeKind } from './extHostTypes';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { ExtHostThemingShape } from 'vs/workbench/api/common/extHost.protocol';
import { Emitter, Event } from 'vs/base/common/event';
export class ExtHostTheming implements ExtHostThemingShape {
readonly _serviceBrand: undefined;
private _actual: ColorTheme;
private _onDidChangeActiveColorTheme: Emitter<ColorTheme>;
constructor(
@IExtHostRpcService _extHostRpc: IExtHostRpcService
) {
this._actual = new ColorTheme(ColorThemeKind.Dark);
this._onDidChangeActiveColorTheme = new Emitter<ColorTheme>();
}
public get activeColorTheme(): ColorTheme {
return this._actual;
}
$onColorThemeChange(type: string): void {
let kind = type === 'light' ? ColorThemeKind.Light : type === 'dark' ? ColorThemeKind.Dark : ColorThemeKind.HighContrast;
this._actual = new ColorTheme(kind);
this._onDidChangeActiveColorTheme.fire(this._actual);
}
public get onDidChangeActiveColorTheme(): Event<ColorTheme> {
return this._onDidChangeActiveColorTheme.event;
}
}

View File

@@ -2508,3 +2508,20 @@ export enum WebviewContentState {
Unchanged = 2,
Dirty = 3,
}
//#region Theming
@es5ClassCompat
export class ColorTheme implements vscode.ColorTheme {
constructor(public readonly kind: ColorThemeKind) {
}
}
export enum ColorThemeKind {
Light = 1,
Dark = 2,
HighContrast = 3
}
//#endregion Theming

View File

@@ -178,7 +178,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
const address = row.local_address.split(':');
return {
socket: parseInt(row.inode, 10),
ip: address[0],
ip: this.parseIpAddress(address[0]),
port: parseInt(address[1], 16)
};
}).map(port => [port.port, port])
@@ -186,6 +186,17 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
];
}
private parseIpAddress(hex: string): string {
let result = '';
for (let i = hex.length - 2; (i >= 0); i -= 2) {
result += parseInt(hex.substr(i, 2), 16);
if (i !== 0) {
result += '.';
}
}
return result;
}
private loadConnectionTable(stdout: string): Record<string, string>[] {
const lines = stdout.trim().split('\n');
const names = lines.shift()!.trim().split(/\s+/)

View File

@@ -151,4 +151,14 @@ export interface EditorServiceImpl extends IEditorService {
* Emitted when an editor failed to open.
*/
readonly onDidOpenEditorFail: Event<IEditorIdentifier>;
/**
* Emitted when the list of most recently active editors change.
*/
readonly onDidMostRecentlyActiveEditorsChange: Event<void>;
/**
* Access to the list of most recently active editors.
*/
readonly mostRecentlyActiveEditors: ReadonlyArray<IEditorIdentifier>;
}

View File

@@ -0,0 +1,396 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, Extensions, IEditorPartOptionsChangeEvent } from 'vs/workbench/common/editor';
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
import { Event, Emitter } from 'vs/base/common/event';
import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { coalesce } from 'vs/base/common/arrays';
import { LinkedMap, Touch } from 'vs/base/common/map';
import { equals } from 'vs/base/common/objects';
interface ISerializedEditorsList {
entries: ISerializedEditorIdentifier[];
}
interface ISerializedEditorIdentifier {
groupId: GroupIdentifier;
index: number;
}
/**
* A observer of opened editors across all editor groups by most recently used.
* Rules:
* - the last editor in the list is the one most recently activated
* - the first editor in the list is the one that was activated the longest time ago
* - an editor that opens inactive will be placed behind the currently active editor
*
* The observer may start to close editors based on the workbench.editor.limit setting.
*/
export class EditorsObserver extends Disposable {
private static readonly STORAGE_KEY = 'editors.mru';
private readonly keyMap = new Map<GroupIdentifier, Map<IEditorInput, IEditorIdentifier>>();
private readonly mostRecentEditorsMap = new LinkedMap<IEditorIdentifier, IEditorIdentifier>();
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
get editors(): IEditorIdentifier[] {
return this.mostRecentEditorsMap.values();
}
constructor(
@IEditorGroupsService private editorGroupsService: IEditorGroupsService,
@IStorageService private readonly storageService: IStorageService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.storageService.onWillSaveState(() => this.saveState()));
this._register(this.editorGroupsService.onDidAddGroup(group => this.onGroupAdded(group)));
this._register(this.editorGroupsService.onDidEditorPartOptionsChange(e => this.onDidEditorPartOptionsChange(e)));
this.editorGroupsService.whenRestored.then(() => this.loadState());
}
private onGroupAdded(group: IEditorGroup): void {
// Make sure to add any already existing editor
// of the new group into our list in LRU order
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */);
}
// Make sure that active editor is put as first if group is active
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
// Group Listeners
this.registerGroupListeners(group);
}
private registerGroupListeners(group: IEditorGroup): void {
const groupDisposables = new DisposableStore();
groupDisposables.add(group.onDidGroupChange(e => {
switch (e.kind) {
// Group gets active: put active editor as most recent
case GroupChangeKind.GROUP_ACTIVE: {
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
break;
}
// Editor gets active: put active editor as most recent
// if group is active, otherwise second most recent
case GroupChangeKind.EDITOR_ACTIVE: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group);
}
break;
}
// Editor opens: put it as second most recent
//
// Also check for maximum allowed number of editors and
// start to close oldest ones if needed.
case GroupChangeKind.EDITOR_OPEN: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, false /* is not active */);
this.ensureOpenedEditorsLimit({ groupId: group.id, editor: e.editor }, group.id);
}
break;
}
// Editor closes: remove from recently opened
case GroupChangeKind.EDITOR_CLOSE: {
if (e.editor) {
this.removeMostRecentEditor(group, e.editor);
}
break;
}
}
}));
// Make sure to cleanup on dispose
Event.once(group.onWillDispose)(() => dispose(groupDisposables));
}
private onDidEditorPartOptionsChange(event: IEditorPartOptionsChangeEvent): void {
if (!equals(event.newPartOptions.limit, event.oldPartOptions.limit)) {
const activeGroup = this.editorGroupsService.activeGroup;
let exclude: IEditorIdentifier | undefined = undefined;
if (activeGroup.activeEditor) {
exclude = { editor: activeGroup.activeEditor, groupId: activeGroup.id };
}
this.ensureOpenedEditorsLimit(exclude);
}
}
private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void {
const key = this.ensureKey(group, editor);
const mostRecentEditor = this.mostRecentEditorsMap.first;
// Active or first entry: add to end of map
if (isActive || !mostRecentEditor) {
this.mostRecentEditorsMap.set(key, key, mostRecentEditor ? Touch.AsOld /* make first */ : undefined);
}
// Otherwise: insert before most recent
else {
// we have most recent editors. as such we
// put this newly opened editor right before
// the current most recent one because it cannot
// be the most recently active one unless
// it becomes active. but it is still more
// active then any other editor in the list.
this.mostRecentEditorsMap.set(key, key, Touch.AsOld /* make first */);
this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */);
}
// Event
this._onDidChange.fire();
}
private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void {
const key = this.findKey(group, editor);
if (key) {
// Remove from most recent editors
this.mostRecentEditorsMap.delete(key);
// Remove from key map
const map = this.keyMap.get(group.id);
if (map && map.delete(key.editor) && map.size === 0) {
this.keyMap.delete(group.id);
}
// Event
this._onDidChange.fire();
}
}
private findKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier | undefined {
const groupMap = this.keyMap.get(group.id);
if (!groupMap) {
return undefined;
}
return groupMap.get(editor);
}
private ensureKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier {
let groupMap = this.keyMap.get(group.id);
if (!groupMap) {
groupMap = new Map();
this.keyMap.set(group.id, groupMap);
}
let key = groupMap.get(editor);
if (!key) {
key = { groupId: group.id, editor };
groupMap.set(editor, key);
}
return key;
}
private async ensureOpenedEditorsLimit(exclude: IEditorIdentifier | undefined, groupId?: GroupIdentifier): Promise<void> {
if (
!this.editorGroupsService.partOptions.limit?.enabled ||
typeof this.editorGroupsService.partOptions.limit.value !== 'number' ||
this.editorGroupsService.partOptions.limit.value <= 0
) {
return; // return early if not enabled or invalid
}
const limit = this.editorGroupsService.partOptions.limit.value;
// In editor group
if (this.editorGroupsService.partOptions.limit?.perEditorGroup) {
// For specific editor groups
if (typeof groupId === 'number') {
const group = this.editorGroupsService.getGroup(groupId);
if (group) {
this.doEnsureOpenedEditorsLimit(limit, group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).map(editor => ({ editor, groupId })), exclude);
}
}
// For all editor groups
else {
for (const group of this.editorGroupsService.groups) {
await this.ensureOpenedEditorsLimit(exclude, group.id);
}
}
}
// Across all editor groups
else {
this.doEnsureOpenedEditorsLimit(limit, this.mostRecentEditorsMap.values(), exclude);
}
}
private async doEnsureOpenedEditorsLimit(limit: number, mostRecentEditors: IEditorIdentifier[], exclude?: IEditorIdentifier): Promise<void> {
if (limit >= mostRecentEditors.length) {
return; // only if opened editors exceed setting and is valid and enabled
}
// Extract least recently used editors that can be closed
const leastRecentlyClosableEditors = mostRecentEditors.reverse().filter(({ editor, groupId }) => {
if (editor.isDirty()) {
return false; // not dirty editors
}
if (exclude && editor === exclude.editor && groupId === exclude.groupId) {
return false; // never the editor that should be excluded
}
return true;
});
// Close editors until we reached the limit again
let editorsToCloseCount = mostRecentEditors.length - limit;
const mapGroupToEditorsToClose = new Map<GroupIdentifier, IEditorInput[]>();
for (const { groupId, editor } of leastRecentlyClosableEditors) {
let editorsInGroupToClose = mapGroupToEditorsToClose.get(groupId);
if (!editorsInGroupToClose) {
editorsInGroupToClose = [];
mapGroupToEditorsToClose.set(groupId, editorsInGroupToClose);
}
editorsInGroupToClose.push(editor);
editorsToCloseCount--;
if (editorsToCloseCount === 0) {
break; // limit reached
}
}
for (const [groupId, editors] of mapGroupToEditorsToClose) {
const group = this.editorGroupsService.getGroup(groupId);
if (group) {
await group.closeEditors(editors, { preserveFocus: true });
}
}
}
private saveState(): void {
if (this.mostRecentEditorsMap.isEmpty()) {
this.storageService.remove(EditorsObserver.STORAGE_KEY, StorageScope.WORKSPACE);
} else {
this.storageService.store(EditorsObserver.STORAGE_KEY, JSON.stringify(this.serialize()), StorageScope.WORKSPACE);
}
}
private serialize(): ISerializedEditorsList {
const registry = Registry.as<IEditorInputFactoryRegistry>(Extensions.EditorInputFactories);
const entries = this.mostRecentEditorsMap.values();
const mapGroupToSerializableEditorsOfGroup = new Map<IEditorGroup, IEditorInput[]>();
return {
entries: coalesce(entries.map(({ editor, groupId }) => {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
return undefined;
}
// Find serializable editors of group
let serializableEditorsOfGroup = mapGroupToSerializableEditorsOfGroup.get(group);
if (!serializableEditorsOfGroup) {
serializableEditorsOfGroup = group.getEditors(EditorsOrder.SEQUENTIAL).filter(editor => {
const factory = registry.getEditorInputFactory(editor.getTypeId());
return factory?.canSerialize(editor);
});
mapGroupToSerializableEditorsOfGroup.set(group, serializableEditorsOfGroup);
}
// Only store the index of the editor of that group
// which can be undefined if the editor is not serializable
const index = serializableEditorsOfGroup.indexOf(editor);
if (index === -1) {
return undefined;
}
return { groupId, index };
}))
};
}
private loadState(): void {
const serialized = this.storageService.get(EditorsObserver.STORAGE_KEY, StorageScope.WORKSPACE);
// Previous state:
if (serialized) {
// Load editors map from persisted state
this.deserialize(JSON.parse(serialized));
}
// No previous state: best we can do is add each editor
// from oldest to most recently used editor group
else {
const groups = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groups.length - 1; i >= 0; i--) {
const group = groups[i];
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */);
}
}
}
// Ensure we listen on group changes for those that exist on startup
for (const group of this.editorGroupsService.groups) {
this.registerGroupListeners(group);
}
}
private deserialize(serialized: ISerializedEditorsList): void {
const mapValues: [IEditorIdentifier, IEditorIdentifier][] = [];
for (const { groupId, index } of serialized.entries) {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
continue;
}
// Find editor for entry
const editor = group.getEditorByIndex(index);
if (!editor) {
continue;
}
// Make sure key is registered as well
const editorIdentifier = this.ensureKey(group, editor);
mapValues.push([editorIdentifier, editorIdentifier]);
}
// Fill map with deserialized values
this.mostRecentEditorsMap.fromJSON(mapValues);
}
}

View File

@@ -44,6 +44,7 @@
line-height: 22px;
height: 100%;
vertical-align: top;
max-width: 40vw;
}
.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak {
@@ -94,7 +95,9 @@
height: 100%;
padding: 0 5px 0 5px;
white-space: pre; /* gives some degree of styling */
align-items: center
align-items: center;
text-overflow: ellipsis;
overflow: hidden;
}
.monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover {

View File

@@ -348,7 +348,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
getTitle(): string {
if (this.isViewMergedWithContainer()) {
const paneItemTitle = this.paneItems[0].pane.title;
if (this.options.donotShowContainerTitleWhenMergedWithContainer) {
if (this.options.donotShowContainerTitleWhenMergedWithContainer || this.viewContainer.name === paneItemTitle) {
return this.paneItems[0].pane.title;
}
return paneItemTitle ? `${this.viewContainer.name}: ${paneItemTitle}` : this.viewContainer.name;

View File

@@ -131,6 +131,22 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio
'default': true,
'description': nls.localize('centeredLayoutAutoResize', "Controls if the centered layout should automatically resize to maximum width when more than one group is open. Once only one group is open it will resize back to the original centered width.")
},
'workbench.editor.limit.enabled': {
'type': 'boolean',
'default': false,
'description': nls.localize('limitEditorsEnablement', "Controls if the number of opened editors should be limited or not. When enabled, less recently used editors that are not dirty will close to make space for newly opening editors.")
},
'workbench.editor.limit.value': {
'type': 'number',
'default': 10,
'exclusiveMinimum': 0,
'description': nls.localize('limitEditorsMaximum', "Controls the maximum number of opened editors. Use the `workbench.editor.limit.perEditorGroup` setting to control this limit per editor group or across all groups.")
},
'workbench.editor.limit.perEditorGroup': {
'type': 'boolean',
'default': false,
'description': nls.localize('perEditorGroup', "Controls if the limit of maximum opened editors should apply per editor group or across all editor groups.")
},
'workbench.commandPalette.history': {
'type': 'number',
'description': nls.localize('commandHistory', "Controls the number of recently used commands to keep in history for the command palette. Set to 0 to disable command history."),

View File

@@ -1171,6 +1171,11 @@ interface IEditorPartConfiguration {
labelFormat?: 'default' | 'short' | 'medium' | 'long';
restoreViewState?: boolean;
splitSizing?: 'split' | 'distribute';
limit?: {
enabled?: boolean;
value?: number;
perEditorGroup?: boolean;
};
}
export interface IEditorPartOptions extends IEditorPartConfiguration {

View File

@@ -34,7 +34,8 @@ export function getSimpleEditorOptions(): IEditorOptions {
acceptSuggestionOnEnter: 'smart',
minimap: {
enabled: false
}
},
renderIndentGuides: false
};
}

View File

@@ -68,8 +68,8 @@ export class ConfigurationManager implements IConfigurationManager {
@ICommandService private readonly commandService: ICommandService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionService private readonly extensionService: IExtensionService,
@IContextKeyService contextKeyService: IContextKeyService,
@IHistoryService historyService: IHistoryService
@IHistoryService private readonly historyService: IHistoryService,
@IContextKeyService contextKeyService: IContextKeyService
) {
this.configProviders = [];
this.adapterDescriptorFactories = [];
@@ -83,13 +83,7 @@ export class ConfigurationManager implements IConfigurationManager {
if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) {
this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE));
} else if (this.launches.length > 0) {
const rootUri = historyService.getLastActiveWorkspaceRoot();
let launch = this.getLaunch(rootUri);
if (!launch || launch.getConfigurationNames().length === 0) {
launch = first(this.launches, l => !!(l && l.getConfigurationNames().length), launch) || this.launches[0];
}
this.selectConfiguration(launch);
this.selectConfiguration(undefined);
}
}
@@ -286,13 +280,13 @@ export class ConfigurationManager implements IConfigurationManager {
this.toDispose.push(Event.any<IWorkspaceFoldersChangeEvent | WorkbenchState>(this.contextService.onDidChangeWorkspaceFolders, this.contextService.onDidChangeWorkbenchState)(() => {
this.initLaunches();
const toSelect = this.selectedLaunch || (this.launches.length > 0 ? this.launches[0] : undefined);
this.selectConfiguration(toSelect);
this.selectConfiguration(undefined);
this.setCompoundSchemaValues();
}));
this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('launch')) {
this.selectConfiguration(this.selectedLaunch);
// A change happen in the launch.json. If there is already a launch configuration selected, do not change the selection.
this.selectConfiguration(undefined);
this.setCompoundSchemaValues();
}
}));
@@ -306,7 +300,7 @@ export class ConfigurationManager implements IConfigurationManager {
this.launches.push(this.instantiationService.createInstance(UserLaunch));
if (this.selectedLaunch && this.launches.indexOf(this.selectedLaunch) === -1) {
this.setSelectedLaunch(undefined);
this.selectConfiguration(undefined);
}
}
@@ -355,10 +349,23 @@ export class ConfigurationManager implements IConfigurationManager {
}
selectConfiguration(launch: ILaunch | undefined, name?: string): void {
if (typeof launch === 'undefined') {
const rootUri = this.historyService.getLastActiveWorkspaceRoot();
launch = this.getLaunch(rootUri);
if (!launch || launch.getConfigurationNames().length === 0) {
launch = first(this.launches, l => !!(l && l.getConfigurationNames().length), launch) || this.launches[0];
}
}
const previousLaunch = this.selectedLaunch;
const previousName = this.selectedName;
this.selectedLaunch = launch;
this.setSelectedLaunch(launch);
if (this.selectedLaunch) {
this.storageService.store(DEBUG_SELECTED_ROOT, this.selectedLaunch.uri.toString(), StorageScope.WORKSPACE);
} else {
this.storageService.remove(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE);
}
const names = launch ? launch.getConfigurationNames() : [];
if (name && names.indexOf(name) >= 0) {
this.setSelectedLaunchName(name);
@@ -467,16 +474,6 @@ export class ConfigurationManager implements IConfigurationManager {
}
}
private setSelectedLaunch(selectedLaunch: ILaunch | undefined): void {
this.selectedLaunch = selectedLaunch;
if (this.selectedLaunch) {
this.storageService.store(DEBUG_SELECTED_ROOT, this.selectedLaunch.uri.toString(), StorageScope.WORKSPACE);
} else {
this.storageService.remove(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE);
}
}
dispose(): void {
this.toDispose = dispose(this.toDispose);
}

View File

@@ -4,15 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!vs/workbench/contrib/debug/browser/media/repl';
import * as nls from 'vs/nls';
import { URI as uri } from 'vs/base/common/uri';
import * as errors from 'vs/base/common/errors';
import { IAction, IActionViewItem, Action } from 'vs/base/common/actions';
import * as dom from 'vs/base/browser/dom';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { CancellationToken } from 'vs/base/common/cancellation';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import severity from 'vs/base/common/severity';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { ITextModel } from 'vs/editor/common/model';
import { Position } from 'vs/editor/common/core/position';
@@ -30,7 +27,7 @@ import { memoize } from 'vs/base/common/decorators';
import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { IDebugService, REPL_ID, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IExpressionContainer, IExpression, IReplElementSource, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug';
import { IDebugService, REPL_ID, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug';
import { HistoryNavigator } from 'vs/base/common/history';
import { IHistoryNavigationWidget } from 'vs/base/browser/history';
import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/platform/browser/contextScopedHistoryWidget';
@@ -43,27 +40,21 @@ import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/d
import { CompletionContext, CompletionList, CompletionProviderRegistry } from 'vs/editor/common/modes';
import { first } from 'vs/base/common/arrays';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { Variable } from 'vs/workbench/contrib/debug/common/debugModel';
import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel';
import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ITreeRenderer, ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
import { ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { renderExpressionValue, AbstractExpressionsRenderer, IExpressionTemplateData, renderVariable, IInputBoxOptions } from 'vs/workbench/contrib/debug/browser/baseDebugView';
import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling';
import { ILabelService } from 'vs/platform/label/common/label';
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { RunOnceScheduler } from 'vs/base/common/async';
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { FuzzyScore } from 'vs/base/common/filters';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { PANEL_BACKGROUND } from 'vs/workbench/common/theme';
import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider } from 'vs/workbench/contrib/debug/browser/replViewer';
import { localize } from 'vs/nls';
const $ = dom.$;
@@ -401,17 +392,17 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati
@memoize
private get refreshScheduler(): RunOnceScheduler {
return new RunOnceScheduler(() => {
return new RunOnceScheduler(async () => {
if (!this.tree.getInput()) {
return;
}
const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight;
this.tree.updateChildren().then(() => {
if (lastElementVisible) {
// Only scroll if we were scrolled all the way down before tree refreshed #10486
revealLastElement(this.tree);
}
}, errors.onUnexpectedError);
await this.tree.updateChildren();
if (lastElementVisible) {
// Only scroll if we were scrolled all the way down before tree refreshed #10486
revealLastElement(this.tree);
}
}, Repl.REFRESH_DELAY);
}
@@ -442,7 +433,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati
// https://github.com/microsoft/TypeScript/issues/32526
new ReplDataSource() as IAsyncDataSource<IDebugSession, IReplElement>,
{
ariaLabel: nls.localize('replAriaLabel', "Read Eval Print Loop Panel"),
ariaLabel: localize('replAriaLabel', "Read Eval Print Loop Panel"),
accessibilityProvider: new ReplAccessibilityProvider(),
identityProvider: { getId: (element: IReplElement) => element.getId() },
mouseSupport: false,
@@ -482,7 +473,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati
[IContextKeyService, scopedContextKeyService], [IPrivateReplService, this]));
const options = getSimpleEditorOptions();
options.readOnly = true;
options.ariaLabel = nls.localize('debugConsole', "Debug Console");
options.ariaLabel = localize('debugConsole', "Debug Console");
this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions());
@@ -505,18 +496,18 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati
private onContextMenu(e: ITreeContextMenuEvent<IReplElement>): void {
const actions: IAction[] = [];
actions.push(new Action('debug.replCopy', nls.localize('copy', "Copy"), undefined, true, async () => {
actions.push(new Action('debug.replCopy', localize('copy', "Copy"), undefined, true, async () => {
const nativeSelection = window.getSelection();
if (nativeSelection) {
await this.clipboardService.writeText(nativeSelection.toString());
}
return Promise.resolve();
}));
actions.push(new Action('workbench.debug.action.copyAll', nls.localize('copyAll', "Copy All"), undefined, true, async () => {
actions.push(new Action('workbench.debug.action.copyAll', localize('copyAll', "Copy All"), undefined, true, async () => {
await this.clipboardService.writeText(this.getVisibleContent());
return Promise.resolve();
}));
actions.push(new Action('debug.collapseRepl', nls.localize('collapse', "Collapse All"), undefined, true, () => {
actions.push(new Action('debug.collapseRepl', localize('collapse', "Collapse All"), undefined, true, () => {
this.tree.collapseAll();
this.replInput.focus();
return Promise.resolve();
@@ -561,7 +552,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati
},
renderOptions: {
after: {
contentText: nls.localize('startDebugFirst', "Please start a debug session to evaluate expressions"),
contentText: localize('startDebugFirst', "Please start a debug session to evaluate expressions"),
color: transparentForeground ? transparentForeground.toString() : undefined
}
}
@@ -588,338 +579,11 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati
this.replElementsChangeListener.dispose();
}
this.refreshScheduler.dispose();
this.modelChangeListener.dispose();
super.dispose();
}
}
// Repl tree
interface IReplEvaluationInputTemplateData {
label: HighlightedLabel;
}
interface IReplEvaluationResultTemplateData {
value: HTMLElement;
annotation: HTMLElement;
}
interface ISimpleReplElementTemplateData {
container: HTMLElement;
value: HTMLElement;
source: HTMLElement;
getReplElementSource(): IReplElementSource | undefined;
toDispose: IDisposable[];
}
interface IRawObjectReplTemplateData {
container: HTMLElement;
expression: HTMLElement;
name: HTMLElement;
value: HTMLElement;
annotation: HTMLElement;
label: HighlightedLabel;
}
class ReplEvaluationInputsRenderer implements ITreeRenderer<ReplEvaluationInput, FuzzyScore, IReplEvaluationInputTemplateData> {
static readonly ID = 'replEvaluationInput';
get templateId(): string {
return ReplEvaluationInputsRenderer.ID;
}
renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData {
dom.append(container, $('span.arrow.codicon.codicon-chevron-right'));
const input = dom.append(container, $('.expression'));
const label = new HighlightedLabel(input, false);
return { label };
}
renderElement(element: ITreeNode<ReplEvaluationInput, FuzzyScore>, index: number, templateData: IReplEvaluationInputTemplateData): void {
const evaluation = element.element;
templateData.label.set(evaluation.value, createMatches(element.filterData));
}
disposeTemplate(templateData: IReplEvaluationInputTemplateData): void {
// noop
}
}
class ReplEvaluationResultsRenderer implements ITreeRenderer<ReplEvaluationResult, FuzzyScore, IReplEvaluationResultTemplateData> {
static readonly ID = 'replEvaluationResult';
get templateId(): string {
return ReplEvaluationResultsRenderer.ID;
}
constructor(private readonly linkDetector: LinkDetector) { }
renderTemplate(container: HTMLElement): IReplEvaluationResultTemplateData {
dom.append(container, $('span.arrow.codicon.codicon-chevron-left'));
const output = dom.append(container, $('.evaluation-result.expression'));
const value = dom.append(output, $('span.value'));
const annotation = dom.append(output, $('span'));
return { value, annotation };
}
renderElement(element: ITreeNode<ReplEvaluationResult, FuzzyScore>, index: number, templateData: IReplEvaluationResultTemplateData): void {
const expression = element.element;
renderExpressionValue(expression, templateData.value, {
preserveWhitespace: !expression.hasChildren,
showHover: false,
colorize: true,
linkDetector: this.linkDetector
});
if (expression.hasChildren) {
templateData.annotation.className = 'annotation codicon codicon-info';
templateData.annotation.title = nls.localize('stateCapture', "Object state is captured from first evaluation");
}
}
disposeTemplate(templateData: IReplEvaluationResultTemplateData): void {
// noop
}
}
class ReplSimpleElementsRenderer implements ITreeRenderer<SimpleReplElement, FuzzyScore, ISimpleReplElementTemplateData> {
static readonly ID = 'simpleReplElement';
constructor(
private readonly linkDetector: LinkDetector,
@IEditorService private readonly editorService: IEditorService,
@ILabelService private readonly labelService: ILabelService,
@IThemeService private readonly themeService: IThemeService
) { }
get templateId(): string {
return ReplSimpleElementsRenderer.ID;
}
renderTemplate(container: HTMLElement): ISimpleReplElementTemplateData {
const data: ISimpleReplElementTemplateData = Object.create(null);
dom.addClass(container, 'output');
const expression = dom.append(container, $('.output.expression.value-and-source'));
data.container = container;
data.value = dom.append(expression, $('span.value'));
data.source = dom.append(expression, $('.source'));
data.toDispose = [];
data.toDispose.push(dom.addDisposableListener(data.source, 'click', e => {
e.preventDefault();
e.stopPropagation();
const source = data.getReplElementSource();
if (source) {
source.source.openInEditor(this.editorService, {
startLineNumber: source.lineNumber,
startColumn: source.column,
endLineNumber: source.lineNumber,
endColumn: source.column
});
}
}));
return data;
}
renderElement({ element }: ITreeNode<SimpleReplElement, FuzzyScore>, index: number, templateData: ISimpleReplElementTemplateData): void {
// value
dom.clearNode(templateData.value);
// Reset classes to clear ansi decorations since templates are reused
templateData.value.className = 'value';
const result = handleANSIOutput(element.value, this.linkDetector, this.themeService, element.session);
templateData.value.appendChild(result);
dom.addClass(templateData.value, (element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info');
templateData.source.textContent = element.sourceData ? `${element.sourceData.source.name}:${element.sourceData.lineNumber}` : '';
templateData.source.title = element.sourceData ? this.labelService.getUriLabel(element.sourceData.source.uri) : '';
templateData.getReplElementSource = () => element.sourceData;
}
disposeTemplate(templateData: ISimpleReplElementTemplateData): void {
dispose(templateData.toDispose);
}
}
export class ReplVariablesRenderer extends AbstractExpressionsRenderer {
static readonly ID = 'replVariable';
get templateId(): string {
return ReplVariablesRenderer.ID;
}
constructor(
private readonly linkDetector: LinkDetector,
@IDebugService debugService: IDebugService,
@IContextViewService contextViewService: IContextViewService,
@IThemeService themeService: IThemeService,
) {
super(debugService, contextViewService, themeService);
}
protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
renderVariable(expression as Variable, data, true, highlights, this.linkDetector);
}
protected getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {
return undefined;
}
}
class ReplRawObjectsRenderer implements ITreeRenderer<RawObjectReplElement, FuzzyScore, IRawObjectReplTemplateData> {
static readonly ID = 'rawObject';
constructor(private readonly linkDetector: LinkDetector) { }
get templateId(): string {
return ReplRawObjectsRenderer.ID;
}
renderTemplate(container: HTMLElement): IRawObjectReplTemplateData {
dom.addClass(container, 'output');
const expression = dom.append(container, $('.output.expression'));
const name = dom.append(expression, $('span.name'));
const label = new HighlightedLabel(name, false);
const value = dom.append(expression, $('span.value'));
const annotation = dom.append(expression, $('span'));
return { container, expression, name, label, value, annotation };
}
renderElement(node: ITreeNode<RawObjectReplElement, FuzzyScore>, index: number, templateData: IRawObjectReplTemplateData): void {
// key
const element = node.element;
templateData.label.set(element.name ? `${element.name}:` : '', createMatches(node.filterData));
if (element.name) {
templateData.name.textContent = `${element.name}:`;
} else {
templateData.name.textContent = '';
}
// value
renderExpressionValue(element.value, templateData.value, {
preserveWhitespace: true,
showHover: false,
linkDetector: this.linkDetector
});
// annotation if any
if (element.annotation) {
templateData.annotation.className = 'annotation codicon codicon-info';
templateData.annotation.title = element.annotation;
} else {
templateData.annotation.className = '';
templateData.annotation.title = '';
}
}
disposeTemplate(templateData: IRawObjectReplTemplateData): void {
// noop
}
}
class ReplDelegate extends CachedListVirtualDelegate<IReplElement> {
constructor(private configurationService: IConfigurationService) {
super();
}
getHeight(element: IReplElement): number {
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
if (!config.console.wordWrap) {
return this.estimateHeight(element, true);
}
return super.getHeight(element);
}
protected estimateHeight(element: IReplElement, ignoreValueLength = false): number {
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
const rowHeight = Math.ceil(1.4 * config.console.fontSize);
const countNumberOfLines = (str: string) => Math.max(1, (str && str.match(/\r\n|\n/g) || []).length);
const hasValue = (e: any): e is { value: string } => typeof e.value === 'string';
// Calculate a rough overestimation for the height
// For every 30 characters increase the number of lines needed
if (hasValue(element)) {
let value = element.value;
let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 30));
return valueRows * rowHeight;
}
return rowHeight;
}
getTemplateId(element: IReplElement): string {
if (element instanceof Variable && element.name) {
return ReplVariablesRenderer.ID;
}
if (element instanceof ReplEvaluationResult) {
return ReplEvaluationResultsRenderer.ID;
}
if (element instanceof ReplEvaluationInput) {
return ReplEvaluationInputsRenderer.ID;
}
if (element instanceof SimpleReplElement || (element instanceof Variable && !element.name)) {
// Variable with no name is a top level variable which should be rendered like a repl element #17404
return ReplSimpleElementsRenderer.ID;
}
return ReplRawObjectsRenderer.ID;
}
hasDynamicHeight(element: IReplElement): boolean {
// Empty elements should not have dynamic height since they will be invisible
return element.toString().length > 0;
}
}
function isDebugSession(obj: any): obj is IDebugSession {
return typeof obj.getReplElements === 'function';
}
class ReplDataSource implements IAsyncDataSource<IDebugSession, IReplElement> {
hasChildren(element: IReplElement | IDebugSession): boolean {
if (isDebugSession(element)) {
return true;
}
return !!(<IExpressionContainer>element).hasChildren;
}
getChildren(element: IReplElement | IDebugSession): Promise<IReplElement[]> {
if (isDebugSession(element)) {
return Promise.resolve(element.getReplElements());
}
if (element instanceof RawObjectReplElement) {
return element.getChildren();
}
return (<IExpression>element).getChildren();
}
}
class ReplAccessibilityProvider implements IAccessibilityProvider<IReplElement> {
getAriaLabel(element: IReplElement): string {
if (element instanceof Variable) {
return nls.localize('replVariableAriaLabel', "Variable {0} has value {1}, read eval print loop, debug", element.name, element.value);
}
if (element instanceof SimpleReplElement || element instanceof ReplEvaluationInput || element instanceof ReplEvaluationResult) {
return nls.localize('replValueOutputAriaLabel', "{0}, read eval print loop, debug", element.value);
}
if (element instanceof RawObjectReplElement) {
return nls.localize('replRawObjectAriaLabel', "Repl variable {0} has value {1}, read eval print loop, debug", element.name, element.value);
}
return '';
}
}
// Repl actions and commands
class AcceptReplInputAction extends EditorAction {
@@ -927,7 +591,7 @@ class AcceptReplInputAction extends EditorAction {
constructor() {
super({
id: 'repl.action.acceptInput',
label: nls.localize({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "REPL Accept Input"),
label: localize({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "REPL Accept Input"),
alias: 'REPL Accept Input',
precondition: CONTEXT_IN_DEBUG_REPL,
kbOpts: {
@@ -949,7 +613,7 @@ class FilterReplAction extends EditorAction {
constructor() {
super({
id: 'repl.action.filter',
label: nls.localize('repl.action.filter', "REPL Focus Content to Filter"),
label: localize('repl.action.filter', "REPL Focus Content to Filter"),
alias: 'REPL Filter',
precondition: CONTEXT_IN_DEBUG_REPL,
kbOpts: {
@@ -971,7 +635,7 @@ class ReplCopyAllAction extends EditorAction {
constructor() {
super({
id: 'repl.action.copyAll',
label: nls.localize('actions.repl.copyAll', "Debug: Console Copy All"),
label: localize('actions.repl.copyAll', "Debug: Console Copy All"),
alias: 'Debug Console Copy All',
precondition: CONTEXT_IN_DEBUG_REPL,
});
@@ -1004,7 +668,7 @@ class SelectReplActionViewItem extends FocusSessionActionViewItem {
class SelectReplAction extends Action {
static readonly ID = 'workbench.action.debug.selectRepl';
static readonly LABEL = nls.localize('selectRepl', "Select Debug Console");
static readonly LABEL = localize('selectRepl', "Select Debug Console");
constructor(id: string, label: string,
@IDebugService private readonly debugService: IDebugService,
@@ -1027,7 +691,7 @@ class SelectReplAction extends Action {
export class ClearReplAction extends Action {
static readonly ID = 'workbench.debug.panel.action.clearReplAction';
static readonly LABEL = nls.localize('clearRepl', "Clear Console");
static readonly LABEL = localize('clearRepl', "Clear Console");
constructor(id: string, label: string,
@IPanelService private readonly panelService: IPanelService
@@ -1038,6 +702,6 @@ export class ClearReplAction extends Action {
async run(): Promise<any> {
const repl = <Repl>this.panelService.openPanel(REPL_ID);
await repl.clearRepl();
aria.status(nls.localize('debugConsoleCleared', "Debug console was cleared"));
aria.status(localize('debugConsoleCleared', "Debug console was cleared"));
}
}

View File

@@ -0,0 +1,352 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import severity from 'vs/base/common/severity';
import * as dom from 'vs/base/browser/dom';
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { Variable } from 'vs/workbench/contrib/debug/common/debugModel';
import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel';
import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { renderExpressionValue, AbstractExpressionsRenderer, IExpressionTemplateData, renderVariable, IInputBoxOptions } from 'vs/workbench/contrib/debug/browser/baseDebugView';
import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling';
import { ILabelService } from 'vs/platform/label/common/label';
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { IReplElementSource, IDebugService, IExpression, IReplElement, IDebugConfiguration, IDebugSession, IExpressionContainer } from 'vs/workbench/contrib/debug/common/debug';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { localize } from 'vs/nls';
const $ = dom.$;
interface IReplEvaluationInputTemplateData {
label: HighlightedLabel;
}
interface IReplEvaluationResultTemplateData {
value: HTMLElement;
annotation: HTMLElement;
}
interface ISimpleReplElementTemplateData {
container: HTMLElement;
value: HTMLElement;
source: HTMLElement;
getReplElementSource(): IReplElementSource | undefined;
toDispose: IDisposable[];
}
interface IRawObjectReplTemplateData {
container: HTMLElement;
expression: HTMLElement;
name: HTMLElement;
value: HTMLElement;
annotation: HTMLElement;
label: HighlightedLabel;
}
export class ReplEvaluationInputsRenderer implements ITreeRenderer<ReplEvaluationInput, FuzzyScore, IReplEvaluationInputTemplateData> {
static readonly ID = 'replEvaluationInput';
get templateId(): string {
return ReplEvaluationInputsRenderer.ID;
}
renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData {
dom.append(container, $('span.arrow.codicon.codicon-chevron-right'));
const input = dom.append(container, $('.expression'));
const label = new HighlightedLabel(input, false);
return { label };
}
renderElement(element: ITreeNode<ReplEvaluationInput, FuzzyScore>, index: number, templateData: IReplEvaluationInputTemplateData): void {
const evaluation = element.element;
templateData.label.set(evaluation.value, createMatches(element.filterData));
}
disposeTemplate(templateData: IReplEvaluationInputTemplateData): void {
// noop
}
}
export class ReplEvaluationResultsRenderer implements ITreeRenderer<ReplEvaluationResult, FuzzyScore, IReplEvaluationResultTemplateData> {
static readonly ID = 'replEvaluationResult';
get templateId(): string {
return ReplEvaluationResultsRenderer.ID;
}
constructor(private readonly linkDetector: LinkDetector) { }
renderTemplate(container: HTMLElement): IReplEvaluationResultTemplateData {
dom.append(container, $('span.arrow.codicon.codicon-chevron-left'));
const output = dom.append(container, $('.evaluation-result.expression'));
const value = dom.append(output, $('span.value'));
const annotation = dom.append(output, $('span'));
return { value, annotation };
}
renderElement(element: ITreeNode<ReplEvaluationResult, FuzzyScore>, index: number, templateData: IReplEvaluationResultTemplateData): void {
const expression = element.element;
renderExpressionValue(expression, templateData.value, {
preserveWhitespace: !expression.hasChildren,
showHover: false,
colorize: true,
linkDetector: this.linkDetector
});
if (expression.hasChildren) {
templateData.annotation.className = 'annotation codicon codicon-info';
templateData.annotation.title = localize('stateCapture', "Object state is captured from first evaluation");
}
}
disposeTemplate(templateData: IReplEvaluationResultTemplateData): void {
// noop
}
}
export class ReplSimpleElementsRenderer implements ITreeRenderer<SimpleReplElement, FuzzyScore, ISimpleReplElementTemplateData> {
static readonly ID = 'simpleReplElement';
constructor(
private readonly linkDetector: LinkDetector,
@IEditorService private readonly editorService: IEditorService,
@ILabelService private readonly labelService: ILabelService,
@IThemeService private readonly themeService: IThemeService
) { }
get templateId(): string {
return ReplSimpleElementsRenderer.ID;
}
renderTemplate(container: HTMLElement): ISimpleReplElementTemplateData {
const data: ISimpleReplElementTemplateData = Object.create(null);
dom.addClass(container, 'output');
const expression = dom.append(container, $('.output.expression.value-and-source'));
data.container = container;
data.value = dom.append(expression, $('span.value'));
data.source = dom.append(expression, $('.source'));
data.toDispose = [];
data.toDispose.push(dom.addDisposableListener(data.source, 'click', e => {
e.preventDefault();
e.stopPropagation();
const source = data.getReplElementSource();
if (source) {
source.source.openInEditor(this.editorService, {
startLineNumber: source.lineNumber,
startColumn: source.column,
endLineNumber: source.lineNumber,
endColumn: source.column
});
}
}));
return data;
}
renderElement({ element }: ITreeNode<SimpleReplElement, FuzzyScore>, index: number, templateData: ISimpleReplElementTemplateData): void {
// value
dom.clearNode(templateData.value);
// Reset classes to clear ansi decorations since templates are reused
templateData.value.className = 'value';
const result = handleANSIOutput(element.value, this.linkDetector, this.themeService, element.session);
templateData.value.appendChild(result);
dom.addClass(templateData.value, (element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info');
templateData.source.textContent = element.sourceData ? `${element.sourceData.source.name}:${element.sourceData.lineNumber}` : '';
templateData.source.title = element.sourceData ? this.labelService.getUriLabel(element.sourceData.source.uri) : '';
templateData.getReplElementSource = () => element.sourceData;
}
disposeTemplate(templateData: ISimpleReplElementTemplateData): void {
dispose(templateData.toDispose);
}
}
export class ReplVariablesRenderer extends AbstractExpressionsRenderer {
static readonly ID = 'replVariable';
get templateId(): string {
return ReplVariablesRenderer.ID;
}
constructor(
private readonly linkDetector: LinkDetector,
@IDebugService debugService: IDebugService,
@IContextViewService contextViewService: IContextViewService,
@IThemeService themeService: IThemeService,
) {
super(debugService, contextViewService, themeService);
}
protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
renderVariable(expression as Variable, data, true, highlights, this.linkDetector);
}
protected getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {
return undefined;
}
}
export class ReplRawObjectsRenderer implements ITreeRenderer<RawObjectReplElement, FuzzyScore, IRawObjectReplTemplateData> {
static readonly ID = 'rawObject';
constructor(private readonly linkDetector: LinkDetector) { }
get templateId(): string {
return ReplRawObjectsRenderer.ID;
}
renderTemplate(container: HTMLElement): IRawObjectReplTemplateData {
dom.addClass(container, 'output');
const expression = dom.append(container, $('.output.expression'));
const name = dom.append(expression, $('span.name'));
const label = new HighlightedLabel(name, false);
const value = dom.append(expression, $('span.value'));
const annotation = dom.append(expression, $('span'));
return { container, expression, name, label, value, annotation };
}
renderElement(node: ITreeNode<RawObjectReplElement, FuzzyScore>, index: number, templateData: IRawObjectReplTemplateData): void {
// key
const element = node.element;
templateData.label.set(element.name ? `${element.name}:` : '', createMatches(node.filterData));
if (element.name) {
templateData.name.textContent = `${element.name}:`;
} else {
templateData.name.textContent = '';
}
// value
renderExpressionValue(element.value, templateData.value, {
preserveWhitespace: true,
showHover: false,
linkDetector: this.linkDetector
});
// annotation if any
if (element.annotation) {
templateData.annotation.className = 'annotation codicon codicon-info';
templateData.annotation.title = element.annotation;
} else {
templateData.annotation.className = '';
templateData.annotation.title = '';
}
}
disposeTemplate(templateData: IRawObjectReplTemplateData): void {
// noop
}
}
export class ReplDelegate extends CachedListVirtualDelegate<IReplElement> {
constructor(private configurationService: IConfigurationService) {
super();
}
getHeight(element: IReplElement): number {
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
if (!config.console.wordWrap) {
return this.estimateHeight(element, true);
}
return super.getHeight(element);
}
protected estimateHeight(element: IReplElement, ignoreValueLength = false): number {
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
const rowHeight = Math.ceil(1.4 * config.console.fontSize);
const countNumberOfLines = (str: string) => Math.max(1, (str && str.match(/\r\n|\n/g) || []).length);
const hasValue = (e: any): e is { value: string } => typeof e.value === 'string';
// Calculate a rough overestimation for the height
// For every 30 characters increase the number of lines needed
if (hasValue(element)) {
let value = element.value;
let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 30));
return valueRows * rowHeight;
}
return rowHeight;
}
getTemplateId(element: IReplElement): string {
if (element instanceof Variable && element.name) {
return ReplVariablesRenderer.ID;
}
if (element instanceof ReplEvaluationResult) {
return ReplEvaluationResultsRenderer.ID;
}
if (element instanceof ReplEvaluationInput) {
return ReplEvaluationInputsRenderer.ID;
}
if (element instanceof SimpleReplElement || (element instanceof Variable && !element.name)) {
// Variable with no name is a top level variable which should be rendered like a repl element #17404
return ReplSimpleElementsRenderer.ID;
}
return ReplRawObjectsRenderer.ID;
}
hasDynamicHeight(element: IReplElement): boolean {
// Empty elements should not have dynamic height since they will be invisible
return element.toString().length > 0;
}
}
function isDebugSession(obj: any): obj is IDebugSession {
return typeof obj.getReplElements === 'function';
}
export class ReplDataSource implements IAsyncDataSource<IDebugSession, IReplElement> {
hasChildren(element: IReplElement | IDebugSession): boolean {
if (isDebugSession(element)) {
return true;
}
return !!(<IExpressionContainer>element).hasChildren;
}
getChildren(element: IReplElement | IDebugSession): Promise<IReplElement[]> {
if (isDebugSession(element)) {
return Promise.resolve(element.getReplElements());
}
if (element instanceof RawObjectReplElement) {
return element.getChildren();
}
return (<IExpression>element).getChildren();
}
}
export class ReplAccessibilityProvider implements IAccessibilityProvider<IReplElement> {
getAriaLabel(element: IReplElement): string {
if (element instanceof Variable) {
return localize('replVariableAriaLabel', "Variable {0} has value {1}, read eval print loop, debug", element.name, element.value);
}
if (element instanceof SimpleReplElement || element instanceof ReplEvaluationInput || element instanceof ReplEvaluationResult) {
return localize('replValueOutputAriaLabel', "{0}, read eval print loop, debug", element.value);
}
if (element instanceof RawObjectReplElement) {
return localize('replRawObjectAriaLabel', "Repl variable {0} has value {1}, read eval print loop, debug", element.name, element.value);
}
return '';
}
}

View File

@@ -726,13 +726,11 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
groups.push([this.instantiationService.createInstance(UninstallAction)]);
groups.push([this.instantiationService.createInstance(InstallAnotherVersionAction)]);
if (this.extension) {
const extensionActions: ExtensionAction[] = [this.instantiationService.createInstance(ExtensionInfoAction), this.instantiationService.createInstance(CopyExtensionIdAction)];
if (this.extension.local && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.configuration) {
extensionActions.push(this.instantiationService.createInstance(ExtensionSettingsAction));
}
groups.push(extensionActions);
const extensionActions: ExtensionAction[] = [this.instantiationService.createInstance(CopyExtensionInfoAction), this.instantiationService.createInstance(CopyExtensionIdAction)];
if (this.extension && this.extension.local && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.configuration) {
extensionActions.push(this.instantiationService.createInstance(ExtensionSettingsAction));
}
groups.push(extensionActions);
groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = this.extension));
@@ -810,7 +808,7 @@ export class InstallAnotherVersionAction extends ExtensionAction {
}
}
export class ExtensionInfoAction extends ExtensionAction {
export class CopyExtensionInfoAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.copyExtension';
static readonly LABEL = localize('workbench.extensions.action.copyExtension', "Copy");
@@ -818,7 +816,7 @@ export class ExtensionInfoAction extends ExtensionAction {
constructor(
@IClipboardService private readonly clipboardService: IClipboardService
) {
super(ExtensionInfoAction.ID, ExtensionInfoAction.LABEL);
super(CopyExtensionInfoAction.ID, CopyExtensionInfoAction.LABEL);
this.update();
}

View File

@@ -33,7 +33,6 @@ import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/vi
import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { Registry } from 'vs/platform/registry/common/platform';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { withUndefinedAsNull } from 'vs/base/common/types';
import { Viewlet } from 'vs/workbench/browser/viewlet';
export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution {
@@ -202,8 +201,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer {
// without causing the animation in the opened editors view to kick in and change scroll position.
// We try to be smart and only use the delay if we recognize that the user action is likely to cause
// a new entry in the opened editors view.
const delegatingEditorService = this.instantiationService.createInstance(DelegatingEditorService);
delegatingEditorService.setEditorOpenHandler(async (delegate, group, editor, options): Promise<IEditor | null> => {
const delegatingEditorService = this.instantiationService.createInstance(DelegatingEditorService, async (delegate, group, editor, options): Promise<IEditor | null> => {
let openEditorsView = this.getOpenEditorsView();
if (openEditorsView) {
let delay = 0;
@@ -219,19 +217,16 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer {
openEditorsView.setStructuralRefreshDelay(delay);
}
let openedEditor: IEditor | undefined;
try {
openedEditor = await delegate(group, editor, options);
return await delegate(group, editor, options);
} catch (error) {
// ignore
return null; // ignore
} finally {
const openEditorsView = this.getOpenEditorsView();
if (openEditorsView) {
openEditorsView.setStructuralRefreshDelay(0);
}
}
return withUndefinedAsNull(openedEditor);
});
const explorerInstantiator = this.instantiationService.createChild(new ServiceCollection([IEditorService, delegatingEditorService]));

View File

@@ -171,7 +171,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController {
public layoutBody(height: number, width: number): void {
const wasSmallLayout = this.isSmallLayout;
this.isSmallLayout = width < 600;
this.isSmallLayout = width < 600 && height > 100;
if (this.isSmallLayout !== wasSmallLayout) {
this.updateActions();
if (this.filterActionBar) {

View File

@@ -20,13 +20,13 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { ActionBar, ActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { ActionRunner, IAction } from 'vs/base/common/actions';
import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions';
import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IRemoteExplorerService, TunnelModel, MakeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IRemoteExplorerService, TunnelModel, MakeAddress, TunnelType, ITunnelItem } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
@@ -55,6 +55,7 @@ export interface ITunnelViewModel {
readonly forwarded: TunnelItem[];
readonly detected: TunnelItem[];
readonly candidates: Promise<TunnelItem[]>;
readonly input: ITunnelItem | ITunnelGroup | undefined;
groups(): Promise<ITunnelGroup[]>;
}
@@ -62,6 +63,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel {
private _onForwardedPortsChanged: Emitter<void> = new Emitter();
public onForwardedPortsChanged: Event<void> = this._onForwardedPortsChanged.event;
private model: TunnelModel;
private _input: ITunnelItem | ITunnelGroup | undefined;
constructor(
@IRemoteExplorerService remoteExplorerService: IRemoteExplorerService) {
@@ -70,6 +72,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel {
this._register(this.model.onForwardPort(() => this._onForwardedPortsChanged.fire()));
this._register(this.model.onClosePort(() => this._onForwardedPortsChanged.fire()));
this._register(this.model.onPortName(() => this._onForwardedPortsChanged.fire()));
this._register(this.model.onCandidatesChanged(() => this._onForwardedPortsChanged.fire()));
}
async groups(): Promise<ITunnelGroup[]> {
@@ -96,10 +99,13 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel {
items: candidates
});
}
groups.push({
label: nls.localize('remote.tunnelsView.add', "Forward a Port..."),
tunnelType: TunnelType.Add,
});
if (!this._input) {
this._input = {
label: nls.localize('remote.tunnelsView.add', "Forward a Port..."),
tunnelType: TunnelType.Add,
};
}
groups.push(this._input);
return groups;
}
@@ -128,6 +134,10 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel {
});
}
get input(): ITunnelItem | ITunnelGroup | undefined {
return this._input;
}
dispose() {
super.dispose();
}
@@ -197,7 +207,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
templateData.actionBar.clear();
let editableData: IEditableData | undefined;
if (this.isTunnelItem(node)) {
editableData = this.remoteExplorerService.getEditableData(node.remoteHost, node.remotePort);
editableData = this.remoteExplorerService.getEditableData(node);
if (editableData) {
templateData.iconLabel.element.style.display = 'none';
this.renderInputBox(templateData.container, editableData);
@@ -205,7 +215,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
templateData.iconLabel.element.style.display = 'flex';
this.renderTunnel(node, templateData);
}
} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined, undefined))) {
} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined))) {
templateData.iconLabel.element.style.display = 'none';
this.renderInputBox(templateData.container, editableData);
} else {
@@ -217,14 +227,15 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
private renderTunnel(node: ITunnelItem, templateData: ITunnelTemplateData) {
templateData.iconLabel.setLabel(node.label, node.description, { title: node.label + ' - ' + node.description, extraClasses: ['tunnel-view-label'] });
templateData.actionBar.context = node;
const contextKeyService = this.contextKeyService.createScoped();
const contextKeyService = this._register(this.contextKeyService.createScoped());
contextKeyService.createKey('view', this.viewId);
contextKeyService.createKey('tunnelType', node.tunnelType);
contextKeyService.createKey('tunnelCloseable', node.closeable);
const menu = this.menuService.createMenu(MenuId.TunnelInline, contextKeyService);
this._register(menu);
const disposableStore = new DisposableStore();
templateData.elementDisposable = disposableStore;
const menu = disposableStore.add(this.menuService.createMenu(MenuId.TunnelInline, contextKeyService));
const actions: IAction[] = [];
this._register(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
disposableStore.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
if (actions) {
templateData.actionBar.push(actions, { icon: true, label: false });
if (this._actionRunner) {
@@ -324,30 +335,12 @@ class TunnelDataSource implements IAsyncDataSource<ITunnelViewModel, ITunnelItem
}
}
enum TunnelType {
Candidate = 'Candidate',
Detected = 'Detected',
Forwarded = 'Forwarded',
Add = 'Add'
}
interface ITunnelGroup {
tunnelType: TunnelType;
label: string;
items?: ITunnelItem[] | Promise<ITunnelItem[]>;
}
interface ITunnelItem {
tunnelType: TunnelType;
remoteHost: string;
remotePort: number;
localAddress?: string;
name?: string;
closeable?: boolean;
readonly description?: string;
readonly label: string;
}
class TunnelItem implements ITunnelItem {
constructor(
public tunnelType: TunnelType,
@@ -432,12 +425,11 @@ export class TunnelPanel extends ViewPane {
}
protected renderBody(container: HTMLElement): void {
dom.addClass(container, '.tree-explorer-viewlet-tree-view');
const treeContainer = document.createElement('div');
dom.addClass(treeContainer, 'customview-tree');
const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view'));
const treeContainer = dom.append(panelContainer, dom.$('.customview-tree'));
dom.addClass(treeContainer, 'file-icon-themable-tree');
dom.addClass(treeContainer, 'show-file-icons');
container.appendChild(treeContainer);
const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService);
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree,
'RemoteTunnels',
@@ -472,12 +464,12 @@ export class TunnelPanel extends ViewPane {
this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
if (e.element && (e.element.tunnelType === TunnelType.Add)) {
this.commandService.executeCommand(ForwardPortAction.ID, 'inline add');
this.commandService.executeCommand(ForwardPortAction.INLINE_ID);
}
}));
this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
const isEditing = !!this.remoteExplorerService.getEditableData(e.host, e.port);
const isEditing = !!this.remoteExplorerService.getEditableData(e);
if (!isEditing) {
dom.removeClass(treeContainer, 'highlight');
@@ -487,6 +479,7 @@ export class TunnelPanel extends ViewPane {
if (isEditing) {
dom.addClass(treeContainer, 'highlight');
this.tree.reveal(e ? e : this.viewModel.input);
} else {
this.tree.domFocus();
}
@@ -494,8 +487,7 @@ export class TunnelPanel extends ViewPane {
}
private get contributedContextMenu(): IMenu {
const contributedContextMenu = this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService);
this._register(contributedContextMenu);
const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService));
return contributedContextMenu;
}
@@ -578,12 +570,12 @@ namespace LabelTunnelAction {
return async (accessor, arg) => {
if (arg instanceof TunnelItem) {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, {
remoteExplorerService.setEditable(arg, {
onFinish: (value, success) => {
if (success) {
remoteExplorerService.tunnelModel.name(arg.remoteHost, arg.remotePort, value);
}
remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, null);
remoteExplorerService.setEditable(arg, null);
},
validationMessage: () => null,
placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
@@ -596,7 +588,8 @@ namespace LabelTunnelAction {
}
namespace ForwardPortAction {
export const ID = 'remote.tunnel.forward';
export const INLINE_ID = 'remote.tunnel.forwardInline';
export const COMMANDPALETTE_ID = 'remote.tunnel.forwardCommandPalette';
export const LABEL = nls.localize('remote.tunnel.forward', "Forward a Port");
const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000).");
@@ -615,35 +608,40 @@ namespace ForwardPortAction {
return null;
}
export function handler(): ICommandHandler {
export function inlineHandler(): ICommandHandler {
return async (accessor, arg) => {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
if (arg instanceof TunnelItem) {
remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort });
} else if (arg) {
remoteExplorerService.setEditable(undefined, undefined, {
} else {
remoteExplorerService.setEditable(undefined, {
onFinish: (value, success) => {
let parsed: { host: string, port: number } | undefined;
if (success && (parsed = parseInput(value))) {
remoteExplorerService.forward({ host: parsed.host, port: parsed.port });
}
remoteExplorerService.setEditable(undefined, undefined, null);
remoteExplorerService.setEditable(undefined, null);
},
validationMessage: validateInput,
placeholder: forwardPrompt
});
} else {
const viewsService = accessor.get(IViewsService);
const quickInputService = accessor.get(IQuickInputService);
await viewsService.openView(TunnelPanel.ID, true);
const value = await quickInputService.input({
prompt: forwardPrompt,
validateInput: (value) => Promise.resolve(validateInput(value))
});
let parsed: { host: string, port: number } | undefined;
if (value && (parsed = parseInput(value))) {
remoteExplorerService.forward({ host: parsed.host, port: parsed.port });
}
}
};
}
export function commandPaletteHandler(): ICommandHandler {
return async (accessor, arg) => {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
const viewsService = accessor.get(IViewsService);
const quickInputService = accessor.get(IQuickInputService);
await viewsService.openView(TunnelPanel.ID, true);
const value = await quickInputService.input({
prompt: forwardPrompt,
validateInput: (value) => Promise.resolve(validateInput(value))
});
let parsed: { host: string, port: number } | undefined;
if (value && (parsed = parseInput(value))) {
remoteExplorerService.forward({ host: parsed.host, port: parsed.port });
}
};
}
@@ -702,30 +700,51 @@ namespace CopyAddressAction {
}
}
namespace RefreshTunnelViewAction {
export const ID = 'remote.tunnel.refresh';
export const LABEL = nls.localize('remote.tunnel.refreshView', "Refresh");
export function handler(): ICommandHandler {
return (accessor, arg) => {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
return remoteExplorerService.refresh();
};
}
}
CommandsRegistry.registerCommand(LabelTunnelAction.ID, LabelTunnelAction.handler());
CommandsRegistry.registerCommand(ForwardPortAction.ID, ForwardPortAction.handler());
CommandsRegistry.registerCommand(ForwardPortAction.INLINE_ID, ForwardPortAction.inlineHandler());
CommandsRegistry.registerCommand(ForwardPortAction.COMMANDPALETTE_ID, ForwardPortAction.commandPaletteHandler());
CommandsRegistry.registerCommand(ClosePortAction.ID, ClosePortAction.handler());
CommandsRegistry.registerCommand(OpenPortInBrowserAction.ID, OpenPortInBrowserAction.handler());
CommandsRegistry.registerCommand(CopyAddressAction.ID, CopyAddressAction.handler());
CommandsRegistry.registerCommand(RefreshTunnelViewAction.ID, RefreshTunnelViewAction.handler());
MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({
command: {
id: ForwardPortAction.ID,
id: ForwardPortAction.COMMANDPALETTE_ID,
title: ForwardPortAction.LABEL
},
when: forwardedPortsViewEnabled
}));
MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({
group: 'navigation',
order: 0,
command: {
id: ForwardPortAction.ID,
id: ForwardPortAction.INLINE_ID,
title: ForwardPortAction.LABEL,
icon: { id: 'codicon/plus' }
}
}));
MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({
group: 'navigation',
order: 1,
command: {
id: RefreshTunnelViewAction.ID,
title: RefreshTunnelViewAction.LABEL,
icon: { id: 'codicon/refresh' }
}
}));
MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
group: '0_manage',
order: 0,
@@ -757,7 +776,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({
group: '0_manage',
order: 1,
command: {
id: ForwardPortAction.ID,
id: ForwardPortAction.INLINE_ID,
title: ForwardPortAction.LABEL,
},
when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate)
@@ -784,7 +803,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({
order: 0,
command: {
id: ForwardPortAction.ID,
id: ForwardPortAction.INLINE_ID,
title: ForwardPortAction.LABEL,
icon: { id: 'codicon/plus' }
},

View File

@@ -27,6 +27,7 @@ import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/wor
import { ILabelService } from 'vs/platform/label/common/label';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { withNullAsUndefined } from 'vs/base/common/types';
import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver';
type CachedEditorInput = ResourceEditorInput | IFileEditorInput;
type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE;
@@ -51,14 +52,19 @@ export class EditorService extends Disposable implements EditorServiceImpl {
private readonly _onDidOpenEditorFail = this._register(new Emitter<IEditorIdentifier>());
readonly onDidOpenEditorFail = this._onDidOpenEditorFail.event;
private readonly _onDidMostRecentlyActiveEditorsChange = this._register(new Emitter<void>());
readonly onDidMostRecentlyActiveEditorsChange = this._onDidMostRecentlyActiveEditorsChange.event;
//#endregion
private fileInputFactory: IFileInputFactory;
private openEditorHandlers: IOpenEditorOverrideHandler[] = [];
private readonly openEditorHandlers: IOpenEditorOverrideHandler[] = [];
private lastActiveEditor: IEditorInput | undefined = undefined;
private lastActiveGroupId: GroupIdentifier | undefined = undefined;
private readonly editorsObserver = this._register(this.instantiationService.createInstance(EditorsObserver));
constructor(
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
@@ -80,6 +86,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
this.editorGroupService.whenRestored.then(() => this.onEditorsRestored());
this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
this.editorsObserver.onDidChange(() => this._onDidMostRecentlyActiveEditorsChange.fire());
}
private onEditorsRestored(): void {
@@ -188,6 +195,10 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return editors;
}
get mostRecentlyActiveEditors(): IEditorIdentifier[] {
return this.editorsObserver.editors;
}
get activeEditor(): IEditorInput | undefined {
const activeGroup = this.editorGroupService.activeGroup;
@@ -228,6 +239,17 @@ export class EditorService extends Disposable implements EditorServiceImpl {
openEditor(editor: IResourceDiffInput, group?: OpenInEditorGroup): Promise<ITextDiffEditor | undefined>;
openEditor(editor: IResourceSideBySideInput, group?: OpenInEditorGroup): Promise<ITextSideBySideEditor | undefined>;
async openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise<IEditor | undefined> {
const result = this.doResolveEditorOpenRequest(editor, optionsOrGroup, group);
if (result) {
const [resolvedGroup, resolvedEditor, resolvedOptions] = result;
return withNullAsUndefined(await resolvedGroup.openEditor(resolvedEditor, resolvedOptions));
}
return undefined;
}
doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined {
let resolvedGroup: IEditorGroup | undefined;
let candidateGroup: OpenInEditorGroup | undefined;
@@ -275,16 +297,12 @@ export class EditorService extends Disposable implements EditorServiceImpl {
typedOptions.overwrite({ activation: EditorActivation.ACTIVATE });
}
return this.doOpenEditor(resolvedGroup, typedEditor, typedOptions);
return [resolvedGroup, typedEditor, typedOptions];
}
return undefined;
}
protected async doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise<IEditor | undefined> {
return withNullAsUndefined(await group.openEditor(editor, options));
}
private findTargetGroup(input: IEditorInput, options?: IEditorOptions, group?: OpenInEditorGroup): IEditorGroup {
let targetGroup: IEditorGroup | undefined;
@@ -756,7 +774,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
export interface IEditorOpenHandler {
(
delegate: (group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => Promise<IEditor | undefined>,
delegate: (group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => Promise<IEditor | null>,
group: IEditorGroup,
editor: IEditorInput,
options?: IEditorOptions | ITextEditorOptions
@@ -765,51 +783,87 @@ export interface IEditorOpenHandler {
/**
* The delegating workbench editor service can be used to override the behaviour of the openEditor()
* method by providing a IEditorOpenHandler.
* method by providing a IEditorOpenHandler. All calls are being delegated to the existing editor
* service otherwise.
*/
export class DelegatingEditorService extends EditorService {
private editorOpenHandler: IEditorOpenHandler | undefined;
export class DelegatingEditorService implements IEditorService {
_serviceBrand: undefined;
constructor(
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@IInstantiationService instantiationService: IInstantiationService,
@ILabelService labelService: ILabelService,
@IFileService fileService: IFileService,
@IConfigurationService configurationService: IConfigurationService
) {
super(
editorGroupService,
untitledTextEditorService,
instantiationService,
labelService,
fileService,
configurationService
);
}
private editorOpenHandler: IEditorOpenHandler,
@IEditorService private editorService: EditorService
) { }
setEditorOpenHandler(handler: IEditorOpenHandler): void {
this.editorOpenHandler = handler;
}
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise<IEditor | undefined>;
openEditor(editor: IResourceInput | IUntitledTextResourceInput, group?: OpenInEditorGroup): Promise<ITextEditor | undefined>;
openEditor(editor: IResourceDiffInput, group?: OpenInEditorGroup): Promise<ITextDiffEditor | undefined>;
openEditor(editor: IResourceSideBySideInput, group?: OpenInEditorGroup): Promise<ITextSideBySideEditor | undefined>;
async openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise<IEditor | undefined> {
const result = this.editorService.doResolveEditorOpenRequest(editor, optionsOrGroup, group);
if (result) {
const [resolvedGroup, resolvedEditor, resolvedOptions] = result;
protected async doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise<IEditor | undefined> {
if (!this.editorOpenHandler) {
return super.doOpenEditor(group, editor, options);
// Pass on to editor open handler
const control = await this.editorOpenHandler(
(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => group.openEditor(editor, options),
resolvedGroup,
resolvedEditor,
resolvedOptions
);
if (control) {
return control; // the opening was handled, so return early
}
return withNullAsUndefined(await resolvedGroup.openEditor(resolvedEditor, resolvedOptions));
}
const control = await this.editorOpenHandler(
(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => super.doOpenEditor(group, editor, options),
group,
editor,
options
);
if (control) {
return control; // the opening was handled, so return early
}
return super.doOpenEditor(group, editor, options);
return undefined;
}
//#region Delegate to IEditorService
get onDidActiveEditorChange(): Event<void> { return this.editorService.onDidActiveEditorChange; }
get onDidVisibleEditorsChange(): Event<void> { return this.editorService.onDidVisibleEditorsChange; }
get activeEditor(): IEditorInput | undefined { return this.editorService.activeEditor; }
get activeControl(): IVisibleEditor | undefined { return this.editorService.activeControl; }
get activeTextEditorWidget(): ICodeEditor | IDiffEditor | undefined { return this.editorService.activeTextEditorWidget; }
get visibleEditors(): ReadonlyArray<IEditorInput> { return this.editorService.visibleEditors; }
get visibleControls(): ReadonlyArray<IVisibleEditor> { return this.editorService.visibleControls; }
get visibleTextEditorWidgets(): ReadonlyArray<ICodeEditor | IDiffEditor> { return this.editorService.visibleTextEditorWidgets; }
get editors(): ReadonlyArray<IEditorInput> { return this.editorService.editors; }
openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise<IEditor[]>;
openEditors(editors: IResourceEditor[], group?: OpenInEditorGroup): Promise<IEditor[]>;
openEditors(editors: Array<IEditorInputWithOptions | IResourceEditor>, group?: OpenInEditorGroup): Promise<IEditor[]> {
return this.editorService.openEditors(editors, group);
}
replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
replaceEditors(editors: Array<IEditorReplacement | IResourceEditorReplacement>, group: IEditorGroup | GroupIdentifier): Promise<void> {
return this.editorService.replaceEditors(editors as IResourceEditorReplacement[] /* TS fail */, group);
}
isOpen(editor: IEditorInput | IResourceInput | IUntitledTextResourceInput): boolean { return this.editorService.isOpen(editor); }
getOpened(editor: IResourceInput | IUntitledTextResourceInput): IEditorInput | undefined { return this.editorService.getOpened(editor); }
overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { return this.editorService.overrideOpenEditor(handler); }
invokeWithinEditorContext<T>(fn: (accessor: ServicesAccessor) => T): T { return this.editorService.invokeWithinEditorContext(fn); }
createInput(input: IResourceEditor): IEditorInput { return this.editorService.createInput(input); }
save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> { return this.editorService.save(editors, options); }
saveAll(options?: ISaveAllEditorsOptions): Promise<boolean> { return this.editorService.saveAll(options); }
revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean> { return this.editorService.revert(editors, options); }
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean> { return this.editorService.revertAll(options); }
//#endregion
}
registerSingleton(IEditorService, EditorService);

View File

@@ -364,8 +364,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
const ed = instantiationService.createInstance(MyEditor, 'my.editor');
const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate'), undefined);
const delegate = instantiationService.createInstance(DelegatingEditorService);
delegate.setEditorOpenHandler((delegate, group, input) => {
const delegate = instantiationService.createInstance(DelegatingEditorService, (delegate, group, input) => {
assert.strictEqual(input, inp);
done();

View File

@@ -0,0 +1,568 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { EditorOptions, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IEditorInputFactory, IFileEditorInput } from 'vs/workbench/common/editor';
import { URI } from 'vs/base/common/uri';
import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart';
import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { GroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
import { EditorActivation, IEditorModel } from 'vs/platform/editor/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver';
import { timeout } from 'vs/base/common/async';
const TEST_EDITOR_ID = 'MyTestEditorForEditorsObserver';
const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorsObserver';
const TEST_SERIALIZABLE_EDITOR_INPUT_ID = 'testSerializableEditorInputForEditorsObserver';
class TestEditorControl extends BaseEditor {
constructor() { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); }
async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
super.setInput(input, options, token);
await input.resolve();
}
getId(): string { return TEST_EDITOR_ID; }
layout(): void { }
createEditor(): any { }
}
class TestEditorInput extends EditorInput implements IFileEditorInput {
private dirty = false;
constructor(public resource: URI) { super(); }
getTypeId() { return TEST_EDITOR_INPUT_ID; }
resolve(): Promise<IEditorModel | null> { return Promise.resolve(null); }
matches(other: TestEditorInput): boolean { return other && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; }
setEncoding(encoding: string) { }
getEncoding() { return undefined; }
setPreferredEncoding(encoding: string) { }
setMode(mode: string) { }
setPreferredMode(mode: string) { }
getResource(): URI { return this.resource; }
setForceOpenAsBinary(): void { }
isDirty(): boolean { return this.dirty; }
setDirty(): void { this.dirty = true; }
}
class EditorsObserverTestEditorInput extends TestEditorInput {
getTypeId() { return TEST_SERIALIZABLE_EDITOR_INPUT_ID; }
}
interface ISerializedTestInput {
resource: string;
}
class EditorsObserverTestEditorInputFactory implements IEditorInputFactory {
canSerialize(editorInput: EditorInput): boolean {
return true;
}
serialize(editorInput: EditorInput): string {
let testEditorInput = <EditorsObserverTestEditorInput>editorInput;
let testInput: ISerializedTestInput = {
resource: testEditorInput.resource.toString()
};
return JSON.stringify(testInput);
}
deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput {
let testInput: ISerializedTestInput = JSON.parse(serializedEditorInput);
return new EditorsObserverTestEditorInput(URI.parse(testInput.resource));
}
}
suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failing tests due to tabcolormode
let disposables: IDisposable[] = [];
setup(() => {
disposables.push(Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).registerEditorInputFactory(TEST_SERIALIZABLE_EDITOR_INPUT_ID, EditorsObserverTestEditorInputFactory));
disposables.push(Registry.as<IEditorRegistry>(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test Editor For Editors Observer'), [new SyncDescriptor(TestEditorInput), new SyncDescriptor(EditorsObserverTestEditorInput)]));
});
teardown(() => {
dispose(disposables);
disposables = [];
});
test('basics (single group)', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const observer = new EditorsObserver(part, new TestStorageService());
let observerChangeListenerCalled = false;
const listener = observer.onDidChange(() => {
observerChangeListenerCalled = true;
});
let currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 0);
assert.equal(observerChangeListenerCalled, false);
const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1'));
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(observerChangeListenerCalled, true);
const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2'));
const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3'));
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);
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);
observerChangeListenerCalled = 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(observerChangeListenerCalled, true);
await part.activeGroup.closeAllEditors();
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 0);
part.dispose();
listener.dispose();
});
test('basics (multi group)', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const observer = new EditorsObserver(part, new TestStorageService());
let currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 0);
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1'));
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);
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);
// Opening an editor inactive should not change
// the most recent editor, but rather put it behind
const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2'));
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);
await rootGroup.closeAllEditors();
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 1);
assert.equal(currentEditorsMRU[0].groupId, sideGroup.id);
assert.equal(currentEditorsMRU[0].editor, input1);
await sideGroup.closeAllEditors();
currentEditorsMRU = observer.editors;
assert.equal(currentEditorsMRU.length, 0);
part.dispose();
});
test('copy group', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const observer = new EditorsObserver(part, new TestStorageService());
const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1'));
const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2'));
const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3'));
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);
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);
part.dispose();
});
test('initial editors are part of observer and state is persisted & restored (single group)', async () => {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1'));
const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2'));
const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3'));
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);
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);
part.dispose();
});
test('initial editors are part of observer (multi group)', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1'));
const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2'));
const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3'));
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);
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);
part.dispose();
});
test('observer does not restore editors that cannot be serialized', async () => {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
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);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredObserver = new EditorsObserver(part, storage);
await part.whenRestored;
currentEditorsMRU = restoredObserver.editors;
assert.equal(currentEditorsMRU.length, 0);
part.dispose();
});
test('observer closes editors when limit reached (across all groups)', async () => {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
part.enforcePartOptions({ limit: { enabled: true, value: 3 } });
await part.whenRestored;
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
const rootGroup = part.activeGroup;
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
const input3 = new TestEditorInput(URI.parse('foo://bar3'));
const input4 = new TestEditorInput(URI.parse('foo://bar4'));
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);
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);
const input5 = new TestEditorInput(URI.parse('foo://bar5'));
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);
observer.dispose();
part.dispose();
});
test('observer closes editors when limit reached (in group)', async () => {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
part.enforcePartOptions({ limit: { enabled: true, value: 3, perEditorGroup: true } });
await part.whenRestored;
const storage = new TestStorageService();
const observer = new EditorsObserver(part, storage);
const rootGroup = part.activeGroup;
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
const input3 = new TestEditorInput(URI.parse('foo://bar3'));
const input4 = new TestEditorInput(URI.parse('foo://bar4'));
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);
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);
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);
observer.dispose();
part.dispose();
});
});

View File

@@ -7,7 +7,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IEditor } from 'vs/editor/common/editorCommon';
import { ITextEditorOptions, IResourceInput, ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, Extensions } from 'vs/workbench/common/editor';
import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier } from 'vs/workbench/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files';
@@ -16,9 +16,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
import { Event, Emitter } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroupsService, EditorsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { getExcludes, ISearchConfiguration } from 'vs/workbench/services/search/common/search';
import { IExpression } from 'vs/base/common/glob';
@@ -34,9 +34,6 @@ import { withNullAsUndefined } from 'vs/base/common/types';
import { addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { Schemas } from 'vs/base/common/network';
import { LinkedMap, Touch } from 'vs/base/common/map';
//#region Text Editor State helper
/**
* Stores the selection & view state of an editor and allows to compare it to other selection states.
@@ -86,295 +83,6 @@ export class TextEditorState {
}
}
//#endregion
//#region Editors History
interface ISerializedEditorHistory {
history: ISerializedEditorIdentifier[];
}
interface ISerializedEditorIdentifier {
groupId: GroupIdentifier;
index: number;
}
/**
* A history of opened editors across all editor groups by most recently used.
* Rules:
* - the last editor in the history is the one most recently activated
* - the first editor in the history is the one that was activated the longest time ago
* - an editor that opens inactive will be placed behind the currently active editor
*/
export class EditorsHistory extends Disposable {
private static readonly STORAGE_KEY = 'history.editors';
private readonly keyMap = new Map<GroupIdentifier, Map<IEditorInput, IEditorIdentifier>>();
private readonly mostRecentEditorsMap = new LinkedMap<IEditorIdentifier, IEditorIdentifier>();
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
get editors(): IEditorIdentifier[] {
return this.mostRecentEditorsMap.values();
}
constructor(
@IEditorGroupsService private editorGroupsService: IEditorGroupsService,
@IStorageService private readonly storageService: IStorageService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.storageService.onWillSaveState(() => this.saveState()));
this._register(this.editorGroupsService.onDidAddGroup(group => this.onGroupAdded(group)));
this.editorGroupsService.whenRestored.then(() => this.loadState());
}
private onGroupAdded(group: IEditorGroup): void {
// Make sure to add any already existing editor
// of the new group into our history in LRU order
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */);
}
// Make sure that active editor is put as first if group is active
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
// Group Listeners
this.registerGroupListeners(group);
}
private registerGroupListeners(group: IEditorGroup): void {
const groupDisposables = new DisposableStore();
groupDisposables.add(group.onDidGroupChange(e => {
switch (e.kind) {
// Group gets active: put active editor as most recent
case GroupChangeKind.GROUP_ACTIVE: {
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
break;
}
// Editor gets active: put active editor as most recent
// if group is active, otherwise second most recent
case GroupChangeKind.EDITOR_ACTIVE: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group);
}
break;
}
// Editor opens: put it as second most recent
case GroupChangeKind.EDITOR_OPEN: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, false /* is not active */);
}
break;
}
// Editor closes: remove from recently opened
case GroupChangeKind.EDITOR_CLOSE: {
if (e.editor) {
this.removeMostRecentEditor(group, e.editor);
}
break;
}
}
}));
// Make sure to cleanup on dispose
Event.once(group.onWillDispose)(() => dispose(groupDisposables));
}
private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void {
const key = this.ensureKey(group, editor);
const mostRecentEditor = this.mostRecentEditorsMap.first;
// Active or first entry: add to end of map
if (isActive || !mostRecentEditor) {
this.mostRecentEditorsMap.set(key, key, mostRecentEditor ? Touch.AsOld /* make first */ : undefined);
}
// Otherwise: insert before most recent
else {
// we have most recent editors. as such we
// put this newly opened editor right before
// the current most recent one because it cannot
// be the most recently active one unless
// it becomes active. but it is still more
// active then any other editor in the list.
this.mostRecentEditorsMap.set(key, key, Touch.AsOld /* make first */);
this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */);
}
// Event
this._onDidChange.fire();
}
private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void {
const key = this.findKey(group, editor);
if (key) {
// Remove from most recent editors
this.mostRecentEditorsMap.delete(key);
// Remove from key map
const map = this.keyMap.get(group.id);
if (map && map.delete(key.editor) && map.size === 0) {
this.keyMap.delete(group.id);
}
// Event
this._onDidChange.fire();
}
}
private findKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier | undefined {
const groupMap = this.keyMap.get(group.id);
if (!groupMap) {
return undefined;
}
return groupMap.get(editor);
}
private ensureKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier {
let groupMap = this.keyMap.get(group.id);
if (!groupMap) {
groupMap = new Map();
this.keyMap.set(group.id, groupMap);
}
let key = groupMap.get(editor);
if (!key) {
key = { groupId: group.id, editor };
groupMap.set(editor, key);
}
return key;
}
private saveState(): void {
if (this.mostRecentEditorsMap.isEmpty()) {
this.storageService.remove(EditorsHistory.STORAGE_KEY, StorageScope.WORKSPACE);
} else {
this.storageService.store(EditorsHistory.STORAGE_KEY, JSON.stringify(this.serialize()), StorageScope.WORKSPACE);
}
}
private serialize(): ISerializedEditorHistory {
const registry = Registry.as<IEditorInputFactoryRegistry>(Extensions.EditorInputFactories);
const history = this.mostRecentEditorsMap.values();
const mapGroupToSerializableEditorsOfGroup = new Map<IEditorGroup, IEditorInput[]>();
return {
history: coalesce(history.map(({ editor, groupId }) => {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
return undefined;
}
// Find serializable editors of group
let serializableEditorsOfGroup = mapGroupToSerializableEditorsOfGroup.get(group);
if (!serializableEditorsOfGroup) {
serializableEditorsOfGroup = group.getEditors(EditorsOrder.SEQUENTIAL).filter(editor => {
const factory = registry.getEditorInputFactory(editor.getTypeId());
return factory?.canSerialize(editor);
});
mapGroupToSerializableEditorsOfGroup.set(group, serializableEditorsOfGroup);
}
// Only store the index of the editor of that group
// which can be undefined if the editor is not serializable
const index = serializableEditorsOfGroup.indexOf(editor);
if (index === -1) {
return undefined;
}
return { groupId, index };
}))
};
}
private loadState(): void {
const serialized = this.storageService.get(EditorsHistory.STORAGE_KEY, StorageScope.WORKSPACE);
// Previous state:
if (serialized) {
// Load history map from persisted state
this.deserialize(JSON.parse(serialized));
}
// No previous state: best we can do is add each editor
// from oldest to most recently used editor group
else {
const groups = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groups.length - 1; i >= 0; i--) {
const group = groups[i];
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */);
}
}
}
// Ensure we listen on group changes for those that exist on startup
for (const group of this.editorGroupsService.groups) {
this.registerGroupListeners(group);
}
}
deserialize(serialized: ISerializedEditorHistory): void {
const mapValues: [IEditorIdentifier, IEditorIdentifier][] = [];
for (const { groupId, index } of serialized.history) {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
continue;
}
// Find editor for entry
const editor = group.getEditorByIndex(index);
if (!editor) {
continue;
}
// Make sure key is registered as well
const editorIdentifier = this.ensureKey(group, editor);
mapValues.push([editorIdentifier, editorIdentifier]);
}
// Fill map with deserialized values
this.mostRecentEditorsMap.fromJSON(mapValues);
}
}
//#endregion
interface ISerializedEditorHistoryEntry {
resourceJSON?: object;
editorInputJSON?: { typeId: string; deserialized: string; };
@@ -424,7 +132,7 @@ export class HistoryService extends Disposable implements IHistoryService {
this._register(this.storageService.onWillSaveState(() => this.saveState()));
this._register(this.fileService.onFileChanges(event => this.onFileChanges(event)));
this._register(this.resourceFilter.onExpressionChange(() => this.removeExcludedFromHistory()));
this._register(this.mostRecentlyUsedOpenEditors.onDidChange(() => this.handleEditorEventInRecentEditorsStack()));
this._register(this.editorService.onDidMostRecentlyActiveEditorsChange(() => this.handleEditorEventInRecentEditorsStack()));
// if the service is created late enough that an editor is already opened
// make sure to trigger the onActiveEditorChanged() to track the editor
@@ -1102,7 +810,7 @@ export class HistoryService extends Disposable implements IHistoryService {
this.editorHistoryListeners.clear();
}
getHistory(): Array<IEditorInput | IResourceInput> {
getHistory(): ReadonlyArray<IEditorInput | IResourceInput> {
this.ensureHistoryLoaded();
return this.history.slice(0);
@@ -1266,12 +974,10 @@ export class HistoryService extends Disposable implements IHistoryService {
//#region Editor Most Recently Used History
private readonly mostRecentlyUsedOpenEditors = this._register(this.instantiationService.createInstance(EditorsHistory));
private recentlyUsedEditorsStack: IEditorIdentifier[] | undefined = undefined;
private recentlyUsedEditorsStack: ReadonlyArray<IEditorIdentifier> | undefined = undefined;
private recentlyUsedEditorsStackIndex = 0;
private recentlyUsedEditorsInGroupStack: IEditorIdentifier[] | undefined = undefined;
private recentlyUsedEditorsInGroupStack: ReadonlyArray<IEditorIdentifier> | undefined = undefined;
private recentlyUsedEditorsInGroupStackIndex = 0;
private navigatingInRecentlyUsedEditorsStack = false;
@@ -1309,15 +1015,15 @@ export class HistoryService extends Disposable implements IHistoryService {
}
}
private ensureRecentlyUsedStack(indexModifier: (index: number) => number, groupId?: GroupIdentifier): [IEditorIdentifier[], number] {
let editors: IEditorIdentifier[];
private ensureRecentlyUsedStack(indexModifier: (index: number) => number, groupId?: GroupIdentifier): [ReadonlyArray<IEditorIdentifier>, number] {
let editors: ReadonlyArray<IEditorIdentifier>;
let index: number;
const group = typeof groupId === 'number' ? this.editorGroupService.getGroup(groupId) : undefined;
// Across groups
if (!group) {
editors = this.recentlyUsedEditorsStack || this.mostRecentlyUsedOpenEditors.editors;
editors = this.recentlyUsedEditorsStack || this.editorService.mostRecentlyActiveEditors;
index = this.recentlyUsedEditorsStackIndex;
}
@@ -1362,8 +1068,8 @@ export class HistoryService extends Disposable implements IHistoryService {
}
}
getMostRecentlyUsedOpenEditors(): Array<IEditorIdentifier> {
return this.mostRecentlyUsedOpenEditors.editors;
getMostRecentlyUsedOpenEditors(): ReadonlyArray<IEditorIdentifier> {
return this.editorService.mostRecentlyActiveEditors;
}
//#endregion

View File

@@ -57,7 +57,7 @@ export interface IHistoryService {
/**
* Get the entire history of editors that were opened.
*/
getHistory(): Array<IEditorInput | IResourceInput>;
getHistory(): ReadonlyArray<IEditorInput | IResourceInput>;
/**
* Looking at the editor history, returns the workspace root of the last file that was
@@ -91,5 +91,5 @@ export interface IHistoryService {
/**
* Get a list of most recently used editors that are open.
*/
getMostRecentlyUsedOpenEditors(): Array<IEditorIdentifier>;
getMostRecentlyUsedOpenEditors(): ReadonlyArray<IEditorIdentifier>;
}

View File

@@ -11,20 +11,19 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart';
import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { EditorActivation, IEditorModel } from 'vs/platform/editor/common/editor';
import { IEditorGroupsService, GroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { EditorsHistory, HistoryService } from 'vs/workbench/services/history/browser/history';
import { WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { HistoryService } from 'vs/workbench/services/history/browser/history';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { EditorService } from 'vs/workbench/services/editor/browser/editorService';
import { timeout } from 'vs/base/common/async';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { timeout } from 'vs/base/common/async';
const TEST_EDITOR_ID = 'MyTestEditorForEditorHistory';
const TEST_EDITOR_INPUT_ID = 'testEditorInputForHistoyService';
@@ -181,416 +180,81 @@ suite.skip('HistoryService', function () { // {{SQL CARBON EDIT}} TODO @anthonyd
part.dispose();
});
suite('EditorHistory', function () {
test('open next/previous recently used editor (single group)', async () => {
const [part, historyService] = await createServices();
test('basics (single group)', async () => {
const instantiationService = workbenchInstantiationService();
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
assert.equal(part.activeGroup.activeEditor, input1);
await part.whenRestored;
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
assert.equal(part.activeGroup.activeEditor, input2);
const history = new EditorsHistory(part, new TestStorageService());
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input1);
let historyChangeListenerCalled = false;
const listener = history.onDidChange(() => {
historyChangeListenerCalled = true;
});
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
let currentHistory = history.editors;
assert.equal(currentHistory.length, 0);
assert.equal(historyChangeListenerCalled, false);
historyService.openPreviouslyUsedEditor(part.activeGroup.id);
assert.equal(part.activeGroup.activeEditor, input1);
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
historyService.openNextRecentlyUsedEditor(part.activeGroup.id);
assert.equal(part.activeGroup.activeEditor, input2);
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
part.dispose();
});
currentHistory = history.editors;
assert.equal(currentHistory.length, 1);
assert.equal(currentHistory[0].groupId, part.activeGroup.id);
assert.equal(currentHistory[0].editor, input1);
assert.equal(historyChangeListenerCalled, true);
test('open next/previous recently used editor (multi group)', async () => {
const [part, historyService] = await createServices();
const rootGroup = part.activeGroup;
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3'));
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, part.activeGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, part.activeGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, part.activeGroup.id);
assert.equal(currentHistory[2].editor, input1);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup, rootGroup);
assert.equal(rootGroup.activeEditor, input1);
currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, part.activeGroup.id);
assert.equal(currentHistory[0].editor, input2);
assert.equal(currentHistory[1].groupId, part.activeGroup.id);
assert.equal(currentHistory[1].editor, input3);
assert.equal(currentHistory[2].groupId, part.activeGroup.id);
assert.equal(currentHistory[2].editor, input1);
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup, sideGroup);
assert.equal(sideGroup.activeEditor, input2);
historyChangeListenerCalled = false;
await part.activeGroup.closeEditor(input1);
part.dispose();
});
currentHistory = history.editors;
assert.equal(currentHistory.length, 2);
assert.equal(currentHistory[0].groupId, part.activeGroup.id);
assert.equal(currentHistory[0].editor, input2);
assert.equal(currentHistory[1].groupId, part.activeGroup.id);
assert.equal(currentHistory[1].editor, input3);
assert.equal(historyChangeListenerCalled, true);
test('open next/previous recently is reset when other input opens', async () => {
const [part, historyService] = await createServices();
await part.activeGroup.closeAllEditors();
currentHistory = history.editors;
assert.equal(currentHistory.length, 0);
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
const input3 = new TestEditorInput(URI.parse('foo://bar3'));
const input4 = new TestEditorInput(URI.parse('foo://bar4'));
part.dispose();
listener.dispose();
});
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
test('basics (multi group)', async () => {
const instantiationService = workbenchInstantiationService();
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await timeout(0);
await part.activeGroup.openEditor(input4, EditorOptions.create({ pinned: true }));
await part.whenRestored;
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
const rootGroup = part.activeGroup;
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input4);
const history = new EditorsHistory(part, new TestStorageService());
let currentHistory = history.editors;
assert.equal(currentHistory.length, 0);
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 2);
assert.equal(currentHistory[0].groupId, sideGroup.id);
assert.equal(currentHistory[0].editor, input1);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input1);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 2);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input1);
assert.equal(currentHistory[1].groupId, sideGroup.id);
assert.equal(currentHistory[1].editor, input1);
// Opening an editor inactive should not change
// the most recent editor, but rather put it behind
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
await rootGroup.openEditor(input2, EditorOptions.create({ inactive: true }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input1);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, sideGroup.id);
assert.equal(currentHistory[2].editor, input1);
await rootGroup.closeAllEditors();
currentHistory = history.editors;
assert.equal(currentHistory.length, 1);
assert.equal(currentHistory[0].groupId, sideGroup.id);
assert.equal(currentHistory[0].editor, input1);
await sideGroup.closeAllEditors();
currentHistory = history.editors;
assert.equal(currentHistory.length, 0);
part.dispose();
});
test('copy group', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const history = new EditorsHistory(part, new TestStorageService());
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3'));
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 currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
const copiedGroup = part.copyGroup(rootGroup, rootGroup, GroupDirection.RIGHT);
copiedGroup.setActive(true);
currentHistory = history.editors;
assert.equal(currentHistory.length, 6);
assert.equal(currentHistory[0].groupId, copiedGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input3);
assert.equal(currentHistory[2].groupId, copiedGroup.id);
assert.equal(currentHistory[2].editor, input2);
assert.equal(currentHistory[3].groupId, copiedGroup.id);
assert.equal(currentHistory[3].editor, input1);
assert.equal(currentHistory[4].groupId, rootGroup.id);
assert.equal(currentHistory[4].editor, input2);
assert.equal(currentHistory[5].groupId, rootGroup.id);
assert.equal(currentHistory[5].editor, input1);
part.dispose();
});
test('initial editors are part of history and state is persisted & restored (single group)', async () => {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3'));
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 history = new EditorsHistory(part, storage);
await part.whenRestored;
let currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredHistory = new EditorsHistory(part, storage);
await part.whenRestored;
currentHistory = restoredHistory.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
part.dispose();
});
test('initial editors are part of history (multi group)', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3'));
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 history = new EditorsHistory(part, storage);
await part.whenRestored;
let currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, sideGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredHistory = new EditorsHistory(part, storage);
await part.whenRestored;
currentHistory = restoredHistory.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, sideGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
part.dispose();
});
test('history does not restore editors that cannot be serialized', async () => {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
const storage = new TestStorageService();
const history = new EditorsHistory(part, storage);
await part.whenRestored;
let currentHistory = history.editors;
assert.equal(currentHistory.length, 1);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input1);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredHistory = new EditorsHistory(part, storage);
await part.whenRestored;
currentHistory = restoredHistory.editors;
assert.equal(currentHistory.length, 0);
part.dispose();
});
test('open next/previous recently used editor (single group)', async () => {
const [part, historyService] = await createServices();
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
assert.equal(part.activeGroup.activeEditor, input1);
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
assert.equal(part.activeGroup.activeEditor, input2);
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input1);
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
historyService.openPreviouslyUsedEditor(part.activeGroup.id);
assert.equal(part.activeGroup.activeEditor, input1);
historyService.openNextRecentlyUsedEditor(part.activeGroup.id);
assert.equal(part.activeGroup.activeEditor, input2);
part.dispose();
});
test('open next/previous recently used editor (multi group)', async () => {
const [part, historyService] = await createServices();
const rootGroup = part.activeGroup;
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup, rootGroup);
assert.equal(rootGroup.activeEditor, input1);
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup, sideGroup);
assert.equal(sideGroup.activeEditor, input2);
part.dispose();
});
test('open next/previous recently is reset when other input opens', async () => {
const [part, historyService] = await createServices();
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
const input3 = new TestEditorInput(URI.parse('foo://bar3'));
const input4 = new TestEditorInput(URI.parse('foo://bar4'));
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
await timeout(0);
await part.activeGroup.openEditor(input4, EditorOptions.create({ pinned: true }));
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input4);
part.dispose();
});
part.dispose();
});
});

View File

@@ -19,6 +19,24 @@ export const IRemoteExplorerService = createDecorator<IRemoteExplorerService>('r
export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType';
const TUNNELS_TO_RESTORE = 'remote.tunnels.toRestore';
export enum TunnelType {
Candidate = 'Candidate',
Detected = 'Detected',
Forwarded = 'Forwarded',
Add = 'Add'
}
export interface ITunnelItem {
tunnelType: TunnelType;
remoteHost: string;
remotePort: number;
localAddress?: string;
name?: string;
closeable?: boolean;
readonly description?: string;
readonly label: string;
}
export interface Tunnel {
remoteHost: string;
remotePort: number;
@@ -45,7 +63,10 @@ export class TunnelModel extends Disposable {
public onClosePort: Event<{ host: string, port: number }> = this._onClosePort.event;
private _onPortName: Emitter<{ host: string, port: number }> = new Emitter();
public onPortName: Event<{ host: string, port: number }> = this._onPortName.event;
private _candidates: { host: string, port: number, detail: string }[] = [];
private _candidateFinder: (() => Promise<{ host: string, port: number, detail: string }[]>) | undefined;
private _onCandidatesChanged: Emitter<void> = new Emitter();
public onCandidatesChanged: Event<void> = this._onCandidatesChanged.event;
constructor(
@ITunnelService private readonly tunnelService: ITunnelService,
@@ -168,10 +189,18 @@ export class TunnelModel extends Disposable {
}
get candidates(): Promise<{ host: string, port: number, detail: string }[]> {
return this.updateCandidates().then(() => this._candidates);
}
private async updateCandidates(): Promise<void> {
if (this._candidateFinder) {
return this._candidateFinder();
this._candidates = await this._candidateFinder();
}
return Promise.resolve([]);
}
async refresh(): Promise<void> {
await this.updateCandidates();
this._onCandidatesChanged.fire();
}
}
@@ -181,13 +210,14 @@ export interface IRemoteExplorerService {
targetType: string;
readonly helpInformation: HelpInformation[];
readonly tunnelModel: TunnelModel;
onDidChangeEditable: Event<{ host: string, port: number | undefined }>;
setEditable(remoteHost: string | undefined, remotePort: number | undefined, data: IEditableData | null): void;
getEditableData(remoteHost: string | undefined, remotePort: number | undefined): IEditableData | undefined;
onDidChangeEditable: Event<ITunnelItem | undefined>;
setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void;
getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined;
forward(remote: { host: string, port: number }, localPort?: number, name?: string): Promise<RemoteTunnel | void>;
close(remote: { host: string, port: number }): Promise<void>;
addEnvironmentTunnels(tunnels: { remoteAddress: { port: number, host: string }, localAddress: string }[] | undefined): void;
registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void;
refresh(): Promise<void>;
}
export interface HelpInformation {
@@ -232,9 +262,9 @@ class RemoteExplorerService implements IRemoteExplorerService {
public readonly onDidChangeTargetType: Event<string> = this._onDidChangeTargetType.event;
private _helpInformation: HelpInformation[] = [];
private _tunnelModel: TunnelModel;
private _editable: { remoteHost: string, remotePort: number | undefined, data: IEditableData } | undefined;
private readonly _onDidChangeEditable: Emitter<{ host: string, port: number | undefined }> = new Emitter();
public readonly onDidChangeEditable: Event<{ host: string, port: number | undefined }> = this._onDidChangeEditable.event;
private _editable: { tunnelItem: ITunnelItem | undefined, data: IEditableData } | undefined;
private readonly _onDidChangeEditable: Emitter<ITunnelItem | undefined> = new Emitter();
public readonly onDidChangeEditable: Event<ITunnelItem | undefined> = this._onDidChangeEditable.event;
constructor(
@IStorageService private readonly storageService: IStorageService,
@@ -305,17 +335,17 @@ class RemoteExplorerService implements IRemoteExplorerService {
}
}
setEditable(remoteHost: string, remotePort: number | undefined, data: IEditableData | null): void {
setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void {
if (!data) {
this._editable = undefined;
} else {
this._editable = { remoteHost, remotePort, data };
this._editable = { tunnelItem, data };
}
this._onDidChangeEditable.fire({ host: remoteHost, port: remotePort });
this._onDidChangeEditable.fire(tunnelItem);
}
getEditableData(remoteHost: string | undefined, remotePort: number | undefined): IEditableData | undefined {
return (this._editable && (this._editable.remotePort === remotePort) && this._editable.remoteHost === remoteHost) ?
getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined {
return (this._editable && (!tunnelItem || (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost))) ?
this._editable.data : undefined;
}
@@ -323,6 +353,9 @@ class RemoteExplorerService implements IRemoteExplorerService {
this.tunnelModel.registerCandidateFinder(finder);
}
refresh(): Promise<void> {
return this.tunnelModel.refresh();
}
}
registerSingleton(IRemoteExplorerService, RemoteExplorerService, true);

View File

@@ -6,7 +6,7 @@
import * as nls from 'vs/nls';
import * as types from 'vs/base/common/types';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, DETECT_HC_SETTING, HC_THEME_ID, IColorCustomizations, CUSTOM_EDITOR_TOKENSTYLES_SETTING, IExperimentalTokenStyleCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, IColorCustomizations, CUSTOM_EDITOR_TOKENSTYLES_SETTING, IExperimentalTokenStyleCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Registry } from 'vs/platform/registry/common/platform';
@@ -14,7 +14,7 @@ import * as errors from 'vs/base/common/errors';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData';
import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService';
import { ITheme, Extensions as ThemingExtensions, IThemingRegistry, ThemeType, LIGHT, DARK, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService';
import { Event, Emitter } from 'vs/base/common/event';
import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
@@ -35,13 +35,25 @@ import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
// settings
const PREFERRED_DARK_THEME_SETTING = 'workbench.preferredDarkColorTheme';
const PREFERRED_LIGHT_THEME_SETTING = 'workbench.preferredLightColorTheme';
const PREFERRED_HC_THEME_SETTING = 'workbench.preferredHighContrastColorTheme';
const DETECT_COLOR_SCHEME_SETTING = 'workbench.autoDetectColorScheme';
const DETECT_HC_SETTING = 'window.autoDetectHighContrast';
// implementation
// {{SQL CARBON EDIT}}
const DEFAULT_THEME_ID = 'vs sql-theme-carbon-themes-light_carbon-json';
const DEFAULT_THEME_SETTING_VALUE = 'Default Light Azure Data Studio';
const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark Azure Data Studio';
const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light Azure Data Studio';
const DEFAULT_THEME_HC_SETTING_VALUE = 'Default High Contrast Azure Data Studio';
const PERSISTED_THEME_STORAGE_KEY = 'colorThemeData';
const PERSISTED_ICON_THEME_STORAGE_KEY = 'iconThemeData';
const PERSISTED_OS_COLOR_SCHEME = 'osColorScheme';
// {{SQL CARBON EDIT}}
const defaultThemeExtensionId = 'sql-theme-carbon';
@@ -150,6 +162,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
this.initialize().then(undefined, errors.onUnexpectedError).then(_ => {
this.installConfigurationListener();
this.installPreferredSchemeListener();
});
let prevColorId: string | undefined = undefined;
@@ -157,8 +170,8 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
// update settings schema setting for theme specific settings
this.colorThemeStore.onDidChange(async event => {
// updates enum for the 'workbench.colorTheme` setting
colorThemeSettingSchema.enum = event.themes.map(t => t.settingsId);
colorThemeSettingSchema.enumDescriptions = event.themes.map(t => t.description || '');
colorThemeSettingEnum.splice(0, colorThemeSettingEnum.length, ...event.themes.map(t => t.settingsId));
colorThemeSettingEnumDescriptions.splice(0, colorThemeSettingEnumDescriptions.length, ...event.themes.map(t => t.description || ''));
const themeSpecificWorkbenchColors: IJSONSchema = { properties: {} };
const themeSpecificTokenColors: IJSONSchema = { properties: {} };
@@ -250,44 +263,40 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
}
private initialize(): Promise<[IColorTheme | null, IFileIconTheme | null]> {
let detectHCThemeSetting = this.configurationService.getValue<boolean>(DETECT_HC_SETTING);
let colorThemeSetting: string;
if (this.environmentService.configuration.highContrast && detectHCThemeSetting) {
colorThemeSetting = HC_THEME_ID;
} else {
colorThemeSetting = this.configurationService.getValue<string>(COLOR_THEME_SETTING);
}
let iconThemeSetting = this.configurationService.getValue<string | null>(ICON_THEME_SETTING);
const colorThemeSetting = this.configurationService.getValue<string>(COLOR_THEME_SETTING);
const iconThemeSetting = this.configurationService.getValue<string | null>(ICON_THEME_SETTING);
const extDevLocs = this.environmentService.extensionDevelopmentLocationURI;
let uri: URI | undefined;
if (extDevLocs && extDevLocs.length > 0) {
// if there are more than one ext dev paths, use first
uri = extDevLocs[0];
}
return Promise.all([
this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, DEFAULT_THEME_ID).then(theme => {
return this.colorThemeStore.findThemeDataByParentLocation(uri).then(devThemes => {
if (devThemes.length) {
return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
} else {
return this.setColorTheme(theme && theme.id, undefined);
}
});
}),
this.iconThemeStore.findThemeBySettingsId(iconThemeSetting).then(theme => {
return this.iconThemeStore.findThemeDataByParentLocation(uri).then(devThemes => {
if (devThemes.length) {
return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
} else {
return this.setFileIconTheme(theme ? theme.id : DEFAULT_ICON_THEME_ID, undefined);
}
});
}),
]);
const initializeColorTheme = async () => {
if (extDevLocs && extDevLocs.length > 0) { // in dev mode, switch to a theme provided by the extension under dev.
const devThemes = await this.colorThemeStore.findThemeDataByParentLocation(extDevLocs[0]);
if (devThemes.length) {
return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
}
}
let theme = await this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, DEFAULT_THEME_ID);
const persistedColorScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL);
const preferredColorScheme = this.getPreferredColorScheme();
if (persistedColorScheme && preferredColorScheme && persistedColorScheme !== preferredColorScheme) {
return this.applyPreferredColorTheme(preferredColorScheme);
}
return this.setColorTheme(theme && theme.id, undefined);
};
const initializeIconTheme = async () => {
if (extDevLocs && extDevLocs.length > 0) { // in dev mode, switch to a theme provided by the extension under dev.
const devThemes = await this.iconThemeStore.findThemeDataByParentLocation(extDevLocs[0]);
if (devThemes.length) {
return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
}
}
const theme = await this.iconThemeStore.findThemeBySettingsId(iconThemeSetting);
return this.setFileIconTheme(theme ? theme.id : DEFAULT_ICON_THEME_ID, undefined);
};
return Promise.all([initializeColorTheme(), initializeIconTheme()]);
}
private installConfigurationListener() {
@@ -302,6 +311,18 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
});
}
}
if (e.affectsConfiguration(DETECT_COLOR_SCHEME_SETTING)) {
this.handlePreferredSchemeUpdated();
}
if (e.affectsConfiguration(PREFERRED_DARK_THEME_SETTING) && this.getPreferredColorScheme() === DARK) {
this.applyPreferredColorTheme(DARK);
}
if (e.affectsConfiguration(PREFERRED_LIGHT_THEME_SETTING) && this.getPreferredColorScheme() === LIGHT) {
this.applyPreferredColorTheme(LIGHT);
}
if (e.affectsConfiguration(PREFERRED_HC_THEME_SETTING) && this.getPreferredColorScheme() === HIGH_CONTRAST) {
this.applyPreferredColorTheme(HIGH_CONTRAST);
}
if (e.affectsConfiguration(ICON_THEME_SETTING)) {
let iconThemeSetting = this.configurationService.getValue<string | null>(ICON_THEME_SETTING);
if (iconThemeSetting !== this.currentIconTheme.settingsId) {
@@ -332,6 +353,48 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
});
}
// preferred scheme handling
private installPreferredSchemeListener() {
window.matchMedia('(prefers-color-scheme: dark)').addListener(async () => this.handlePreferredSchemeUpdated());
}
private async handlePreferredSchemeUpdated() {
const scheme = this.getPreferredColorScheme();
this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, StorageScope.GLOBAL);
if (scheme) {
return this.applyPreferredColorTheme(scheme);
}
return undefined;
}
private getPreferredColorScheme(): ThemeType | undefined {
let detectHCThemeSetting = this.configurationService.getValue<boolean>(DETECT_HC_SETTING);
if (this.environmentService.configuration.highContrast && detectHCThemeSetting) {
return HIGH_CONTRAST;
}
if (this.configurationService.getValue<boolean>(DETECT_COLOR_SCHEME_SETTING)) {
if (window.matchMedia(`(prefers-color-scheme: light)`).matches) {
return LIGHT;
} else if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) {
return DARK;
}
}
return undefined;
}
private async applyPreferredColorTheme(type: ThemeType): Promise<IColorTheme | null> {
const settingId = type === DARK ? PREFERRED_DARK_THEME_SETTING : type === LIGHT ? PREFERRED_LIGHT_THEME_SETTING : PREFERRED_HC_THEME_SETTING;
const themeSettingId = this.configurationService.getValue<string>(settingId);
if (themeSettingId) {
const theme = await this.colorThemeStore.findThemeDataBySettingsId(themeSettingId, undefined);
if (theme) {
return this.setColorTheme(theme.id, 'auto');
}
}
return null;
}
public getColorTheme(): IColorTheme {
return this.currentColorTheme;
}
@@ -354,11 +417,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
themeId = validateThemeId(themeId); // migrate theme ids
return this.colorThemeStore.findThemeData(themeId, DEFAULT_THEME_ID).then(data => {
if (!data) {
return this.colorThemeStore.findThemeData(themeId, DEFAULT_THEME_ID).then(themeData => {
if (!themeData) {
return null;
}
const themeData = data;
return themeData.ensureLoaded(this.extensionResourceLoaderService).then(_ => {
if (themeId === this.currentColorTheme.id && !this.currentColorTheme.isLoaded && this.currentColorTheme.hasEqualData(themeData)) {
this.currentColorTheme.clearCaches();
@@ -643,14 +705,46 @@ registerFileIconThemeSchemas();
// Configuration: Themes
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
const colorThemeSettingEnum: string[] = [];
const colorThemeSettingEnumDescriptions: string[] = [];
const colorThemeSettingSchema: IConfigurationPropertySchema = {
type: 'string',
description: nls.localize('colorTheme', "Specifies the color theme used in the workbench."),
default: DEFAULT_THEME_SETTING_VALUE,
enum: [],
enumDescriptions: [],
enum: colorThemeSettingEnum,
enumDescriptions: colorThemeSettingEnumDescriptions,
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
};
const preferredDarkThemeSettingSchema: IConfigurationPropertySchema = {
type: 'string',
description: nls.localize('preferredDarkColorTheme', 'Specifies the preferred color theme for dark OS appearance when \'{0}\' is enabled.', DETECT_COLOR_SCHEME_SETTING),
default: DEFAULT_THEME_DARK_SETTING_VALUE,
enum: colorThemeSettingEnum,
enumDescriptions: colorThemeSettingEnumDescriptions,
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
};
const preferredLightThemeSettingSchema: IConfigurationPropertySchema = {
type: 'string',
description: nls.localize('preferredLightColorTheme', 'Specifies the preferred color theme for light OS appearance when \'{0}\' is enabled.', DETECT_COLOR_SCHEME_SETTING),
default: DEFAULT_THEME_LIGHT_SETTING_VALUE,
enum: colorThemeSettingEnum,
enumDescriptions: colorThemeSettingEnumDescriptions,
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
};
const preferredHCThemeSettingSchema: IConfigurationPropertySchema = {
type: 'string',
description: nls.localize('preferredHCColorTheme', 'Specifies the preferred color theme used in high contrast mode when \'{0}\' is enabled.', DETECT_HC_SETTING),
default: DEFAULT_THEME_HC_SETTING_VALUE,
enum: colorThemeSettingEnum,
enumDescriptions: colorThemeSettingEnumDescriptions,
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
};
const detectColorSchemeSettingSchema: IConfigurationPropertySchema = {
type: 'boolean',
description: nls.localize('detectColorScheme', 'If set, automatically switch to the preferred color theme based on the OS appearance.'),
default: true
};
const iconThemeSettingSchema: IConfigurationPropertySchema = {
type: ['string', 'null'],
@@ -677,6 +771,10 @@ const themeSettingsConfiguration: IConfigurationNode = {
type: 'object',
properties: {
[COLOR_THEME_SETTING]: colorThemeSettingSchema,
[PREFERRED_DARK_THEME_SETTING]: preferredDarkThemeSettingSchema,
[PREFERRED_LIGHT_THEME_SETTING]: preferredLightThemeSettingSchema,
[PREFERRED_HC_THEME_SETTING]: preferredHCThemeSettingSchema,
[DETECT_COLOR_SCHEME_SETTING]: detectColorSchemeSettingSchema,
[ICON_THEME_SETTING]: iconThemeSettingSchema,
[CUSTOM_WORKBENCH_COLORS_SETTING]: colorCustomizationsSchema
}

View File

@@ -18,7 +18,6 @@ export const VS_HC_THEME = 'hc-black';
export const HC_THEME_ID = 'Default High Contrast';
export const COLOR_THEME_SETTING = 'workbench.colorTheme';
export const DETECT_HC_SETTING = 'window.autoDetectHighContrast';
export const ICON_THEME_SETTING = 'workbench.iconTheme';
export const CUSTOM_WORKBENCH_COLORS_SETTING = 'workbench.colorCustomizations';
export const CUSTOM_EDITOR_COLORS_SETTING = 'editor.tokenColorCustomizations';

View File

@@ -11,7 +11,7 @@ import * as resources from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier } from 'vs/workbench/common/editor';
import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor';
import { Event, Emitter } from 'vs/base/common/event';
import Severity from 'vs/base/common/severity';
@@ -57,7 +57,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations';
import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor, ISaveEditorsOptions, IRevertAllEditorsOptions } from 'vs/workbench/services/editor/common/editorService';
import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor, ISaveEditorsOptions, IRevertAllEditorsOptions, IResourceEditor } from 'vs/workbench/services/editor/common/editorService';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon';
@@ -316,9 +316,10 @@ export function workbenchInstantiationService(): ITestInstantiationService {
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
instantiationService.stub(IThemeService, new TestThemeService());
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService([new TestEditorGroupView(0)]));
const editorGroupService = new TestEditorGroupsService([new TestEditorGroupView(0)]);
instantiationService.stub(IEditorGroupsService, editorGroupService);
instantiationService.stub(ILabelService, <ILabelService>instantiationService.createInstance(LabelService));
const editorService = new TestEditorService();
const editorService = new TestEditorService(editorGroupService);
instantiationService.stub(IEditorService, editorService);
instantiationService.stub(ICodeEditorService, new TestCodeEditorService());
instantiationService.stub(IViewletService, new TestViewletService());
@@ -373,7 +374,7 @@ export class TestHistoryService implements IHistoryService {
remove(_input: IEditorInput | IResourceInput): void { }
clear(): void { }
clearRecentlyOpened(): void { }
getHistory(): Array<IEditorInput | IResourceInput> { return []; }
getHistory(): ReadonlyArray<IEditorInput | IResourceInput> { return []; }
openNextRecentlyUsedEditor(group?: GroupIdentifier): void { }
openPreviouslyUsedEditor(group?: GroupIdentifier): void { }
getMostRecentlyUsedOpenEditors(): Array<IEditorIdentifier> { return []; }
@@ -901,15 +902,19 @@ export class TestEditorService implements EditorServiceImpl {
onDidVisibleEditorsChange: Event<void> = Event.None;
onDidCloseEditor: Event<IEditorCloseEvent> = Event.None;
onDidOpenEditorFail: Event<IEditorIdentifier> = Event.None;
onDidMostRecentlyActiveEditorsChange: Event<void> = Event.None;
activeControl!: IVisibleEditor;
activeTextEditorWidget: any;
activeEditor!: IEditorInput;
editors: ReadonlyArray<IEditorInput> = [];
mostRecentlyActiveEditors: ReadonlyArray<IEditorIdentifier> = [];
visibleControls: ReadonlyArray<IVisibleEditor> = [];
visibleTextEditorWidgets = [];
visibleEditors: ReadonlyArray<IEditorInput> = [];
constructor(private editorGroupService?: IEditorGroupsService) { }
overrideOpenEditor(_handler: IOpenEditorOverrideHandler): IDisposable {
return toDisposable(() => undefined);
}
@@ -918,6 +923,14 @@ export class TestEditorService implements EditorServiceImpl {
throw new Error('not implemented');
}
doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditor): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined {
if (!this.editorGroupService) {
return undefined;
}
return [this.editorGroupService.activeGroup, editor as EditorInput, undefined];
}
openEditors(_editors: any, _group?: any): Promise<IEditor[]> {
throw new Error('not implemented');
}