mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Merge from vscode 4f85c3c94c15457e1d4c9e67da6800630394ea54 (#8757)
* Merge from vscode 4f85c3c94c15457e1d4c9e67da6800630394ea54 * disable failing tests
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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. -->
|
||||
|
||||
|
||||
@@ -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-'] {
|
||||
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
|
||||
@@ -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}; }`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
src/vs/vscode.proposed.d.ts
vendored
36
src/vs/vscode.proposed.d.ts
vendored
@@ -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>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import './mainThreadStatusBar';
|
||||
import './mainThreadStorage';
|
||||
import './mainThreadTelemetry';
|
||||
import './mainThreadTerminalService';
|
||||
import './mainThreadTheming';
|
||||
import './mainThreadTreeViews';
|
||||
import './mainThreadDownloadService';
|
||||
import './mainThreadUrls';
|
||||
|
||||
33
src/vs/workbench/api/browser/mainThreadTheming.ts
Normal file
33
src/vs/workbench/api/browser/mainThreadTheming.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
|
||||
38
src/vs/workbench/api/common/extHostTheming.ts
Normal file
38
src/vs/workbench/api/common/extHostTheming.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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+/)
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
396
src/vs/workbench/browser/parts/editor/editorsObserver.ts
Normal file
396
src/vs/workbench/browser/parts/editor/editorsObserver.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,7 +34,8 @@ export function getSimpleEditorOptions(): IEditorOptions {
|
||||
acceptSuggestionOnEnter: 'smart',
|
||||
minimap: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
renderIndentGuides: false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
352
src/vs/workbench/contrib/debug/browser/replViewer.ts
Normal file
352
src/vs/workbench/contrib/debug/browser/replViewer.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' }
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user