mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-14 12:08:36 -05:00
Merge from vscode 5e80bf449c995aa32a59254c0ff845d37da11b70 (#9317)
This commit is contained in:
@@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { MainContext, MainThreadTimelineShape, IExtHostContext, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { TimelineChangeEvent, TimelineCursor, TimelineProviderDescriptor, ITimelineService } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTimeline)
|
||||
export class MainThreadTimeline implements MainThreadTimelineShape {
|
||||
@@ -39,8 +39,8 @@ export class MainThreadTimeline implements MainThreadTimelineShape {
|
||||
this._timelineService.registerTimelineProvider({
|
||||
...provider,
|
||||
onDidChange: onDidChange.event,
|
||||
provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }) {
|
||||
return proxy.$getTimeline(provider.id, uri, cursor, token, options);
|
||||
provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }) {
|
||||
return proxy.$getTimeline(provider.id, uri, options, token, internalOptions);
|
||||
},
|
||||
dispose() {
|
||||
emitters.delete(provider.id);
|
||||
|
||||
@@ -49,7 +49,7 @@ import { SaveReason } from 'vs/workbench/common/editor';
|
||||
import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator';
|
||||
import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
|
||||
import { TunnelOptions } from 'vs/platform/remote/common/tunnel';
|
||||
import { Timeline, TimelineChangeEvent, TimelineCursor, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
|
||||
import { Dto } from 'vs/base/common/types';
|
||||
@@ -1472,7 +1472,7 @@ export interface ExtHostTunnelServiceShape {
|
||||
}
|
||||
|
||||
export interface ExtHostTimelineShape {
|
||||
$getTimeline(source: string, uri: UriComponents, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
|
||||
$getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
|
||||
}
|
||||
|
||||
// --- proxy identifiers
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
|
||||
import { UriComponents, URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { Timeline, TimelineCursor, TimelineItem, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { Timeline, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
|
||||
@@ -16,7 +16,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export interface IExtHostTimeline extends ExtHostTimelineShape {
|
||||
readonly _serviceBrand: undefined;
|
||||
$getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken, options?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
|
||||
$getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
|
||||
}
|
||||
|
||||
export const IExtHostTimeline = createDecorator<IExtHostTimeline>('IExtHostTimeline');
|
||||
@@ -50,9 +50,9 @@ export class ExtHostTimeline implements IExtHostTimeline {
|
||||
});
|
||||
}
|
||||
|
||||
async $getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken, options?: { cacheResults?: boolean }): Promise<Timeline | undefined> {
|
||||
async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise<Timeline | undefined> {
|
||||
const provider = this._providers.get(id);
|
||||
return provider?.provideTimeline(URI.revive(uri), cursor, token, options);
|
||||
return provider?.provideTimeline(URI.revive(uri), options, token, internalOptions);
|
||||
}
|
||||
|
||||
registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, _extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable {
|
||||
@@ -70,15 +70,15 @@ export class ExtHostTimeline implements IExtHostTimeline {
|
||||
...provider,
|
||||
scheme: scheme,
|
||||
onDidChange: undefined,
|
||||
async provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }) {
|
||||
async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }) {
|
||||
timelineDisposables.clear();
|
||||
|
||||
// For now, only allow the caching of a single Uri
|
||||
if (options?.cacheResults && !itemsBySourceByUriMap.has(getUriKey(uri))) {
|
||||
if (internalOptions?.cacheResults && !itemsBySourceByUriMap.has(getUriKey(uri))) {
|
||||
itemsBySourceByUriMap.clear();
|
||||
}
|
||||
|
||||
const result = await provider.provideTimeline(uri, cursor, token);
|
||||
const result = await provider.provideTimeline(uri, options, token);
|
||||
// Intentional == we don't know how a provider will respond
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (result == null) {
|
||||
@@ -86,7 +86,7 @@ export class ExtHostTimeline implements IExtHostTimeline {
|
||||
}
|
||||
|
||||
// TODO: Determine if we should cache dependent on who calls us (internal vs external)
|
||||
const convertItem = convertTimelineItem(uri, options?.cacheResults ?? false);
|
||||
const convertItem = convertTimelineItem(uri, internalOptions?.cacheResults ?? false);
|
||||
return {
|
||||
...result,
|
||||
source: provider.id,
|
||||
@@ -143,6 +143,7 @@ export class ExtHostTimeline implements IExtHostTimeline {
|
||||
|
||||
return {
|
||||
...props,
|
||||
id: props.id ?? undefined,
|
||||
handle: handle,
|
||||
source: source,
|
||||
command: item.command ? commandConverter.toInternal(item.command, disposables) : undefined,
|
||||
|
||||
@@ -389,7 +389,7 @@ class WebviewDocumentStore {
|
||||
}
|
||||
|
||||
private key(viewType: string, resource: vscode.Uri): string {
|
||||
return `${viewType}@@@${resource.toString}`;
|
||||
return `${viewType}@@@${resource}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ import { isWindows, isLinux, isWeb } from 'vs/base/common/platform';
|
||||
import { IsMacNativeContext } from 'vs/workbench/browser/contextkeys';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext } from 'vs/workbench/common/editor';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { SideBarVisibleContext } from 'vs/workbench/common/viewlet';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views';
|
||||
import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, FocusedViewContext, ViewContainerLocation } from 'vs/workbench/common/views';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(WorkbenchExtensions.WorkbenchActions);
|
||||
const viewCategory = nls.localize('view', "View");
|
||||
@@ -518,6 +521,92 @@ export class ResetViewLocationsAction extends Action {
|
||||
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.create(ResetViewLocationsAction, ResetViewLocationsAction.ID, ResetViewLocationsAction.LABEL), 'View: Reset View Locations', viewCategory);
|
||||
|
||||
// --- Move View with Command
|
||||
export class MoveFocusedViewAction extends Action {
|
||||
static readonly ID = 'workbench.action.moveFocusedView';
|
||||
static readonly LABEL = nls.localize('moveFocusedView', "Move Focused View");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IViewDescriptorService private viewDescriptorService: IViewDescriptorService,
|
||||
@IViewsService private viewsService: IViewsService,
|
||||
@IQuickInputService private quickInputService: IQuickInputService,
|
||||
@IContextKeyService private contextKeyService: IContextKeyService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IViewletService private viewletService: IViewletService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(): Promise<void> {
|
||||
const viewContainerRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
|
||||
const focusedView = FocusedViewContext.getValue(this.contextKeyService);
|
||||
|
||||
if (focusedView === undefined || focusedView.trim() === '') {
|
||||
this.notificationService.error(nls.localize('moveFocusedView.error.noFocusedView', "There is no view currently focused."));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const viewDescriptor = this.viewDescriptorService.getViewDescriptor(focusedView);
|
||||
if (!viewDescriptor || !viewDescriptor.canMoveView) {
|
||||
this.notificationService.error(nls.localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable {0}.", focusedView));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const quickPick = this.quickInputService.createQuickPick();
|
||||
quickPick.placeholder = nls.localize('moveFocusedView.selectDestination', "Select a destination area for the view...");
|
||||
quickPick.autoFocusOnList = true;
|
||||
|
||||
quickPick.items = [
|
||||
{
|
||||
id: 'sidebar',
|
||||
label: nls.localize('sidebar', "Sidebar")
|
||||
},
|
||||
{
|
||||
id: 'panel',
|
||||
label: nls.localize('panel', "Panel")
|
||||
}
|
||||
];
|
||||
|
||||
quickPick.onDidAccept(() => {
|
||||
const destination = quickPick.selectedItems[0];
|
||||
|
||||
if (destination.id === 'panel') {
|
||||
quickPick.hide();
|
||||
this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Panel);
|
||||
this.viewsService.openView(focusedView, true);
|
||||
|
||||
return;
|
||||
} else if (destination.id === 'sidebar') {
|
||||
quickPick.placeholder = nls.localize('moveFocusedView.selectDestinationContainer', "Select a destination view group...");
|
||||
quickPick.items = this.viewletService.getViewlets().map(viewlet => {
|
||||
return {
|
||||
id: viewlet.id,
|
||||
label: viewlet.name
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (destination.id) {
|
||||
quickPick.hide();
|
||||
this.viewDescriptorService.moveViewsToContainer([viewDescriptor], viewContainerRegistry.get(destination.id)!);
|
||||
this.viewsService.openView(focusedView, true);
|
||||
return;
|
||||
}
|
||||
|
||||
quickPick.hide();
|
||||
});
|
||||
|
||||
quickPick.show();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.create(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL), 'View: Move Focused View', viewCategory);
|
||||
|
||||
|
||||
// --- Resize View
|
||||
|
||||
|
||||
@@ -4,19 +4,17 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { dirname, isEqual, basenameOrAuthority } from 'vs/base/common/resources';
|
||||
import { IconLabel, IIconLabelValueOptions, IIconLabelCreationOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IDecorationsService, IResourceDecorationChangeEvent } from 'vs/workbench/services/decorations/browser/decorations';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { FileKind, FILES_ASSOCIATIONS_CONFIG, IFileService } from 'vs/platform/files/common/files';
|
||||
import { FileKind, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
@@ -89,7 +87,6 @@ export class ResourceLabels extends Disposable {
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IDecorationsService private readonly decorationsService: IDecorationsService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService
|
||||
) {
|
||||
@@ -114,10 +111,6 @@ export class ResourceLabels extends Disposable {
|
||||
return; // we need the resource to compare
|
||||
}
|
||||
|
||||
if (this.fileService.canHandleResource(e.model.uri) && e.oldModeId === PLAINTEXT_MODE_ID) {
|
||||
return; // ignore transitions in files from no mode to specific mode because this happens each time a model is created
|
||||
}
|
||||
|
||||
this._widgets.forEach(widget => widget.notifyModelModeChanged(e.model));
|
||||
}));
|
||||
|
||||
@@ -218,11 +211,10 @@ export class ResourceLabel extends ResourceLabels {
|
||||
@IModelService modelService: IModelService,
|
||||
@IDecorationsService decorationsService: IDecorationsService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IFileService fileService: IFileService,
|
||||
@ILabelService labelService: ILabelService,
|
||||
@ITextFileService textFileService: ITextFileService
|
||||
) {
|
||||
super(DEFAULT_LABELS_CONTAINER, instantiationService, extensionService, configurationService, modelService, decorationsService, themeService, fileService, labelService, textFileService);
|
||||
super(DEFAULT_LABELS_CONTAINER, instantiationService, extensionService, configurationService, modelService, decorationsService, themeService, labelService, textFileService);
|
||||
|
||||
this._label = this._register(this.create(container, options));
|
||||
}
|
||||
@@ -372,8 +364,8 @@ class ResourceLabelWidget extends IconLabel {
|
||||
let untitledDescription = untitledModel.resource.path;
|
||||
if (label.name !== untitledDescription) {
|
||||
label.description = untitledDescription;
|
||||
} else if (label.description === posix.sep) {
|
||||
label.description = undefined; // unset showing just "/" for untitled without associated resource
|
||||
} else {
|
||||
label.description = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -547,6 +547,13 @@ export class CompositeActionViewItem extends ActivityActionViewItem {
|
||||
this.updateFromDragging(container, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) {
|
||||
const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype);
|
||||
if (Array.isArray(data) && data[0].id !== this.activity.id) {
|
||||
this.updateFromDragging(container, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onDragOver: e => {
|
||||
@@ -575,7 +582,8 @@ export class CompositeActionViewItem extends ActivityActionViewItem {
|
||||
},
|
||||
|
||||
onDragLeave: e => {
|
||||
if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) {
|
||||
if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) ||
|
||||
this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) {
|
||||
this.updateFromDragging(container, false);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -44,7 +44,10 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
id: descriptor.id,
|
||||
weight: weight,
|
||||
when: (descriptor.keybindingContext || when ? ContextKeyExpr.and(descriptor.keybindingContext, when) : null),
|
||||
when:
|
||||
descriptor.keybindingContext && when
|
||||
? ContextKeyExpr.and(descriptor.keybindingContext, when)
|
||||
: descriptor.keybindingContext || when || null,
|
||||
primary: keybindings ? keybindings.primary : 0,
|
||||
secondary: keybindings?.secondary,
|
||||
win: keybindings?.win,
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
|
||||
.extensions-viewlet > .extensions .monaco-list-row > .extension > .details > .header-container > .header .extension-remote-badge > .codicon {
|
||||
font-size: 12px;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.extensions-viewlet.narrow > .extensions .extension > .icon-container,
|
||||
|
||||
@@ -80,7 +80,7 @@ export class SettingsEditor2 extends BaseEditor {
|
||||
private static CONFIG_SCHEMA_UPDATE_DELAYER = 500;
|
||||
|
||||
private static readonly SUGGESTIONS: string[] = [
|
||||
`@${MODIFIED_SETTING_TAG}`, '@tag:usesOnlineServices', `@${EXTENSION_SETTING_TAG}`
|
||||
`@${MODIFIED_SETTING_TAG}`, '@tag:usesOnlineServices', '@tag:sync', `@${EXTENSION_SETTING_TAG}`
|
||||
];
|
||||
|
||||
private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean {
|
||||
|
||||
@@ -556,8 +556,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, {
|
||||
order: 1
|
||||
});
|
||||
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusNextSearchResultAction, FocusNextSearchResultAction.ID, FocusNextSearchResultAction.LABEL, { primary: KeyCode.F4 }, ContextKeyExpr.and(Constants.HasSearchResults)), 'Focus Next Search Result', category);
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusPreviousSearchResultAction, FocusPreviousSearchResultAction.ID, FocusPreviousSearchResultAction.LABEL, { primary: KeyMod.Shift | KeyCode.F4 }, ContextKeyExpr.and(Constants.HasSearchResults)), 'Focus Previous Search Result', category);
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusNextSearchResultAction, FocusNextSearchResultAction.ID, FocusNextSearchResultAction.LABEL, { primary: KeyCode.F4 }, ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor)), 'Focus Next Search Result', category);
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusPreviousSearchResultAction, FocusPreviousSearchResultAction.ID, FocusPreviousSearchResultAction.LABEL, { primary: KeyMod.Shift | KeyCode.F4 }, ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor)), 'Focus Previous Search Result', category);
|
||||
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.create(ReplaceInFilesAction, ReplaceInFilesAction.ID, ReplaceInFilesAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_H }), 'Replace in Files', category);
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, {
|
||||
|
||||
@@ -488,12 +488,19 @@ export class FocusNextSearchResultAction extends Action {
|
||||
static readonly LABEL = nls.localize('FocusNextSearchResult.label', "Focus Next Search Result");
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IViewsService private readonly viewsService: IViewsService
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(): Promise<any> {
|
||||
async run(): Promise<any> {
|
||||
const input = this.editorService.activeEditor;
|
||||
if (input instanceof SearchEditorInput) {
|
||||
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
|
||||
return (this.editorService.activeControl as SearchEditor).focusNextResult();
|
||||
}
|
||||
|
||||
return openSearchView(this.viewsService).then(searchView => {
|
||||
if (searchView) {
|
||||
searchView.selectNextMatch();
|
||||
@@ -507,12 +514,19 @@ export class FocusPreviousSearchResultAction extends Action {
|
||||
static readonly LABEL = nls.localize('FocusPreviousSearchResult.label', "Focus Previous Search Result");
|
||||
|
||||
constructor(id: string, label: string,
|
||||
@IViewsService private readonly viewsService: IViewsService
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
run(): Promise<any> {
|
||||
async run(): Promise<any> {
|
||||
const input = this.editorService.activeEditor;
|
||||
if (input instanceof SearchEditorInput) {
|
||||
// cast as we cannot import SearchEditor as a value b/c cyclic dependency.
|
||||
return (this.editorService.activeControl as SearchEditor).focusPreviousResult();
|
||||
}
|
||||
|
||||
return openSearchView(this.viewsService).then(searchView => {
|
||||
if (searchView) {
|
||||
searchView.selectPreviousMatch();
|
||||
|
||||
@@ -181,25 +181,26 @@ export class SearchView extends ViewPane {
|
||||
|
||||
this.container = dom.$('.search-view');
|
||||
|
||||
// globals
|
||||
this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(this.contextKeyService);
|
||||
this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(this.contextKeyService);
|
||||
this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(this.contextKeyService);
|
||||
this.fileMatchOrFolderMatchFocus = Constants.FileMatchOrFolderMatchFocusKey.bindTo(this.contextKeyService);
|
||||
this.fileMatchOrFolderMatchWithResourceFocus = Constants.FileMatchOrFolderMatchWithResourceFocusKey.bindTo(this.contextKeyService);
|
||||
this.fileMatchFocused = Constants.FileFocusKey.bindTo(this.contextKeyService);
|
||||
this.folderMatchFocused = Constants.FolderFocusKey.bindTo(this.contextKeyService);
|
||||
this.hasSearchResultsKey = Constants.HasSearchResults.bindTo(this.contextKeyService);
|
||||
this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService);
|
||||
|
||||
// scoped
|
||||
this.contextKeyService = this._register(this.contextKeyService.createScoped(this.container));
|
||||
const viewletFocused = Constants.SearchViewFocusedKey.bindTo(this.contextKeyService);
|
||||
viewletFocused.set(true);
|
||||
|
||||
this.instantiationService = this.instantiationService.createChild(
|
||||
new ServiceCollection([IContextKeyService, this.contextKeyService]));
|
||||
|
||||
this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService);
|
||||
Constants.SearchViewFocusedKey.bindTo(this.contextKeyService).set(true);
|
||||
this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService);
|
||||
this.inputPatternIncludesFocused = Constants.PatternIncludesFocusedKey.bindTo(this.contextKeyService);
|
||||
this.inputPatternExclusionsFocused = Constants.PatternExcludesFocusedKey.bindTo(this.contextKeyService);
|
||||
this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(contextKeyService);
|
||||
this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(contextKeyService);
|
||||
this.fileMatchOrFolderMatchFocus = Constants.FileMatchOrFolderMatchFocusKey.bindTo(contextKeyService);
|
||||
this.fileMatchOrFolderMatchWithResourceFocus = Constants.FileMatchOrFolderMatchWithResourceFocusKey.bindTo(contextKeyService);
|
||||
this.fileMatchFocused = Constants.FileFocusKey.bindTo(contextKeyService);
|
||||
this.folderMatchFocused = Constants.FolderFocusKey.bindTo(contextKeyService);
|
||||
this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService);
|
||||
this.hasSearchResultsKey = Constants.HasSearchResults.bindTo(this.contextKeyService);
|
||||
|
||||
this.instantiationService = this.instantiationService.createChild(
|
||||
new ServiceCollection([IContextKeyService, this.contextKeyService]));
|
||||
|
||||
this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('search.sortOrder')) {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord
|
||||
export const ToggleSearchEditorRegexCommandId = 'toggleSearchEditorRegex';
|
||||
export const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines';
|
||||
export const RerunSearchEditorSearchCommandId = 'rerunSearchEditorSearch';
|
||||
export const SelectAllSearchEditorMatchesCommandId = 'selectAllSearchEditorMatches';
|
||||
|
||||
export const InSearchEditor = new RawContextKey<boolean>('inSearchEditor', false);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE
|
||||
import * as SearchConstants from 'vs/workbench/contrib/search/common/constants';
|
||||
import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants';
|
||||
import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor';
|
||||
import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions';
|
||||
import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions';
|
||||
import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
@@ -146,6 +146,14 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L }
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: SearchEditorConstants.SelectAllSearchEditorMatchesCommandId,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor),
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L,
|
||||
handler: selectAllSearchEditorMatchesCommand
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand(
|
||||
SearchEditorConstants.RerunSearchEditorSearchCommandId,
|
||||
(accessor: ServicesAccessor) => {
|
||||
|
||||
@@ -47,6 +47,8 @@ import { assertIsDefined } from 'vs/base/common/types';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
|
||||
const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/;
|
||||
const FILE_LINE_REGEX = /^(\S.*):$/;
|
||||
@@ -299,6 +301,42 @@ export class SearchEditor extends BaseTextEditor {
|
||||
return this.configurationService.getValue<ISearchConfigurationProperties>('search');
|
||||
}
|
||||
|
||||
private iterateThroughMatches(reverse: boolean) {
|
||||
const model = this.searchResultEditor.getModel();
|
||||
if (!model) { return; }
|
||||
|
||||
const lastLine = model.getLineCount() ?? 1;
|
||||
const lastColumn = model.getLineLength(lastLine);
|
||||
|
||||
const fallbackStart = reverse ? new Position(lastLine, lastColumn) : new Position(1, 1);
|
||||
|
||||
const currentPosition = this.searchResultEditor.getSelection()?.getStartPosition() ?? fallbackStart;
|
||||
|
||||
const matchRanges = this.getInput()?.getMatchRanges();
|
||||
if (!matchRanges) { return; }
|
||||
|
||||
const matchRange = (reverse ? findPrevRange : findNextRange)(matchRanges, currentPosition);
|
||||
|
||||
this.searchResultEditor.setSelection(matchRange);
|
||||
this.searchResultEditor.revealLineInCenterIfOutsideViewport(matchRange.startLineNumber);
|
||||
this.searchResultEditor.focus();
|
||||
}
|
||||
|
||||
focusNextResult() {
|
||||
this.iterateThroughMatches(false);
|
||||
}
|
||||
|
||||
focusPreviousResult() {
|
||||
this.iterateThroughMatches(true);
|
||||
}
|
||||
|
||||
focusAllResults() {
|
||||
this.searchResultEditor
|
||||
.setSelections((this.getInput()?.getMatchRanges() ?? []).map(
|
||||
range => new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)));
|
||||
this.searchResultEditor.focus();
|
||||
}
|
||||
|
||||
async triggerSearch(_options?: { resetCursor?: boolean; delay?: number; }) {
|
||||
const options = { resetCursor: true, delay: 0, ..._options };
|
||||
|
||||
@@ -307,7 +345,7 @@ export class SearchEditor extends BaseTextEditor {
|
||||
await this.doRunSearch();
|
||||
this.toggleRunAgainMessage(false);
|
||||
if (options.resetCursor) {
|
||||
this.searchResultEditor.setSelection(new Range(1, 1, 1, 1));
|
||||
this.searchResultEditor.setPosition(new Position(1, 1));
|
||||
this.searchResultEditor.setScrollPosition({ scrollTop: 0, scrollLeft: 0 });
|
||||
}
|
||||
}, options.delay);
|
||||
@@ -523,3 +561,24 @@ registerThemingParticipant((theme, collector) => {
|
||||
});
|
||||
|
||||
export const searchEditorTextInputBorder = registerColor('searchEditor.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Search editor text input box border."));
|
||||
|
||||
function findNextRange(matchRanges: Range[], currentPosition: Position) {
|
||||
for (const matchRange of matchRanges) {
|
||||
if (Position.isBefore(currentPosition, matchRange.getStartPosition())) {
|
||||
return matchRange;
|
||||
}
|
||||
}
|
||||
return matchRanges[0];
|
||||
}
|
||||
|
||||
function findPrevRange(matchRanges: Range[], currentPosition: Position) {
|
||||
for (let i = matchRanges.length - 1; i >= 0; i--) {
|
||||
const matchRange = matchRanges[i];
|
||||
if (Position.isBefore(matchRange.getStartPosition(), currentPosition)) {
|
||||
{
|
||||
return matchRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
return matchRanges[matchRanges.length - 1];
|
||||
}
|
||||
|
||||
@@ -54,6 +54,14 @@ export const toggleSearchEditorContextLinesCommand = (accessor: ServicesAccessor
|
||||
}
|
||||
};
|
||||
|
||||
export const selectAllSearchEditorMatchesCommand = (accessor: ServicesAccessor) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const input = editorService.activeEditor;
|
||||
if (input instanceof SearchEditorInput) {
|
||||
(editorService.activeControl as SearchEditor).focusAllResults();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export class OpenSearchEditorAction extends Action {
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-icon-label {
|
||||
|
||||
@@ -14,6 +14,8 @@ import { TimelineService } from 'vs/workbench/contrib/timeline/common/timelineSe
|
||||
import { TimelinePane } from './timelinePane';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
|
||||
import { ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
|
||||
export class TimelinePaneDescriptor implements IViewDescriptor {
|
||||
@@ -42,13 +44,102 @@ configurationRegistry.registerConfiguration({
|
||||
'timeline.showView': {
|
||||
type: 'boolean',
|
||||
description: localize('timeline.showView', "Experimental: When enabled, shows a Timeline view in the Explorer sidebar."),
|
||||
default: false //product.quality !== 'stable'
|
||||
default: product.quality !== 'stable'
|
||||
},
|
||||
'timeline.excludeSources': {
|
||||
type: 'array',
|
||||
description: localize('timeline.excludeSources', "Experimental: An array of Timeline sources that should be excluded from the Timeline view"),
|
||||
default: null
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
if (product.quality !== 'stable') {
|
||||
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([new TimelinePaneDescriptor()], VIEW_CONTAINER);
|
||||
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([new TimelinePaneDescriptor()], VIEW_CONTAINER);
|
||||
|
||||
namespace TimelineViewRefreshAction {
|
||||
|
||||
export const ID = 'timeline.refresh';
|
||||
export const LABEL = localize('timeline.refreshView', "Refresh");
|
||||
|
||||
export function handler(): ICommandHandler {
|
||||
return (accessor, arg) => {
|
||||
const service = accessor.get(ITimelineService);
|
||||
return service.reset();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
CommandsRegistry.registerCommand(TimelineViewRefreshAction.ID, TimelineViewRefreshAction.handler());
|
||||
|
||||
// namespace TimelineViewRefreshHardAction {
|
||||
|
||||
// export const ID = 'timeline.refreshHard';
|
||||
// export const LABEL = localize('timeline.refreshHard', "Refresh (Hard)");
|
||||
|
||||
// export function handler(fetch?: 'all' | 'more'): ICommandHandler {
|
||||
// return (accessor, arg) => {
|
||||
// const service = accessor.get(ITimelineService);
|
||||
// return service.refresh(fetch);
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
// CommandsRegistry.registerCommand(TimelineViewRefreshAction.ID, TimelineViewRefreshAction.handler());
|
||||
|
||||
// namespace TimelineViewLoadMoreAction {
|
||||
|
||||
// export const ID = 'timeline.loadMore';
|
||||
// export const LABEL = localize('timeline.loadMoreInView', "Load More");
|
||||
|
||||
// export function handler(): ICommandHandler {
|
||||
// return (accessor, arg) => {
|
||||
// const service = accessor.get(ITimelineService);
|
||||
// return service.refresh('more');
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
// CommandsRegistry.registerCommand(TimelineViewLoadMoreAction.ID, TimelineViewLoadMoreAction.handler());
|
||||
|
||||
// namespace TimelineViewLoadAllAction {
|
||||
|
||||
// export const ID = 'timeline.loadAll';
|
||||
// export const LABEL = localize('timeline.loadAllInView', "Load All");
|
||||
|
||||
// export function handler(): ICommandHandler {
|
||||
// return (accessor, arg) => {
|
||||
// const service = accessor.get(ITimelineService);
|
||||
// return service.refresh('all');
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
// CommandsRegistry.registerCommand(TimelineViewLoadAllAction.ID, TimelineViewLoadAllAction.handler());
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.TimelineTitle, ({
|
||||
group: 'navigation',
|
||||
order: 1,
|
||||
command: {
|
||||
id: TimelineViewRefreshAction.ID,
|
||||
title: TimelineViewRefreshAction.LABEL,
|
||||
icon: { id: 'codicon/refresh' }
|
||||
}
|
||||
}));
|
||||
|
||||
// MenuRegistry.appendMenuItem(MenuId.TimelineTitle, ({
|
||||
// group: 'navigation',
|
||||
// order: 2,
|
||||
// command: {
|
||||
// id: TimelineViewLoadMoreAction.ID,
|
||||
// title: TimelineViewLoadMoreAction.LABEL,
|
||||
// icon: { id: 'codicon/unfold' }
|
||||
// },
|
||||
// alt: {
|
||||
// id: TimelineViewLoadAllAction.ID,
|
||||
// title: TimelineViewLoadAllAction.LABEL,
|
||||
// icon: { id: 'codicon/unfold' }
|
||||
|
||||
// }
|
||||
// }));
|
||||
|
||||
registerSingleton(ITimelineService, TimelineService, true);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { localize } from 'vs/nls';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
|
||||
import { Iterator } from 'vs/base/common/iterator';
|
||||
import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
@@ -18,9 +19,9 @@ import { TreeResourceNavigator, WorkbenchObjectTree } from 'vs/platform/list/bro
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITimelineService, TimelineChangeEvent, TimelineProvidersChangeEvent, TimelineRequest, TimelineItem } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { ITimelineService, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvidersChangeEvent, TimelineRequest, Timeline } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { SideBySideEditor, toResource } from 'vs/workbench/common/editor';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
@@ -28,7 +29,6 @@ import { IThemeService, LIGHT, ThemeIcon } from 'vs/platform/theme/common/themeS
|
||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files';
|
||||
import { debounce } from 'vs/base/common/decorators';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IActionViewItemProvider, ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
@@ -40,13 +40,53 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
// TODO[ECA]: Localize all the strings
|
||||
|
||||
type TreeElement = TimelineItem;
|
||||
const InitialPageSize = 20;
|
||||
const SubsequentPageSize = 40;
|
||||
|
||||
interface CommandItem {
|
||||
handle: 'vscode-command:loadMore';
|
||||
timestamp: number;
|
||||
label: string;
|
||||
themeIcon?: { id: string };
|
||||
description?: string;
|
||||
detail?: string;
|
||||
contextValue?: string;
|
||||
|
||||
// Make things easier for duck typing
|
||||
id: undefined;
|
||||
icon: undefined;
|
||||
iconDark: undefined;
|
||||
source: undefined;
|
||||
}
|
||||
|
||||
type TreeElement = TimelineItem | CommandItem;
|
||||
|
||||
// function isCommandItem(item: TreeElement | undefined): item is CommandItem {
|
||||
// return item?.handle.startsWith('vscode-command:') ?? false;
|
||||
// }
|
||||
|
||||
function isLoadMoreCommandItem(item: TreeElement | undefined): item is CommandItem & {
|
||||
handle: 'vscode-command:loadMore';
|
||||
} {
|
||||
return item?.handle === 'vscode-command:loadMore';
|
||||
}
|
||||
|
||||
function isTimelineItem(item: TreeElement | undefined): item is TimelineItem {
|
||||
return !item?.handle.startsWith('vscode-command:') ?? false;
|
||||
}
|
||||
|
||||
|
||||
interface TimelineActionContext {
|
||||
uri: URI | undefined;
|
||||
item: TreeElement;
|
||||
}
|
||||
|
||||
interface TimelineCursors {
|
||||
startCursors?: { before: any; after?: any };
|
||||
endCursors?: { before: any; after?: any };
|
||||
more: boolean;
|
||||
}
|
||||
|
||||
export class TimelinePane extends ViewPane {
|
||||
static readonly ID = 'timeline';
|
||||
static readonly TITLE = localize('timeline', 'Timeline');
|
||||
@@ -59,8 +99,9 @@ export class TimelinePane extends ViewPane {
|
||||
private _menus: TimelineMenus;
|
||||
private _visibilityDisposables: DisposableStore | undefined;
|
||||
|
||||
// private _excludedSources: Set<string> | undefined;
|
||||
private _items: TimelineItem[] = [];
|
||||
private _excludedSources: Set<string>;
|
||||
private _cursorsByProvider: Map<string, TimelineCursors> = new Map();
|
||||
private _items: { element: TreeElement }[] = [];
|
||||
private _loadingMessageTimer: any | undefined;
|
||||
private _pendingRequests = new Map<string, TimelineRequest>();
|
||||
private _uri: URI | undefined;
|
||||
@@ -87,6 +128,18 @@ export class TimelinePane extends ViewPane {
|
||||
|
||||
const scopedContextKeyService = this._register(this.contextKeyService.createScoped());
|
||||
scopedContextKeyService.createKey('view', TimelinePane.ID);
|
||||
|
||||
this._excludedSources = new Set(configurationService.getValue('timeline.excludeSources'));
|
||||
configurationService.onDidChangeConfiguration(this.onConfigurationChanged, this);
|
||||
}
|
||||
|
||||
private onConfigurationChanged(e: IConfigurationChangeEvent) {
|
||||
if (!e.affectsConfiguration('timeline.excludeSources')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._excludedSources = new Set(this.configurationService.getValue('timeline.excludeSources'));
|
||||
this.loadTimeline(true);
|
||||
}
|
||||
|
||||
private onActiveEditorChanged() {
|
||||
@@ -105,7 +158,7 @@ export class TimelinePane extends ViewPane {
|
||||
|
||||
this._uri = uri;
|
||||
this._treeRenderer?.setUri(uri);
|
||||
this.loadTimeline();
|
||||
this.loadTimeline(true);
|
||||
}
|
||||
|
||||
private onProvidersChanged(e: TimelineProvidersChangeEvent) {
|
||||
@@ -116,16 +169,20 @@ export class TimelinePane extends ViewPane {
|
||||
}
|
||||
|
||||
if (e.added) {
|
||||
this.loadTimeline(e.added);
|
||||
this.loadTimeline(true, e.added);
|
||||
}
|
||||
}
|
||||
|
||||
private onTimelineChanged(e: TimelineChangeEvent) {
|
||||
if (e.uri === undefined || e.uri.toString(true) !== this._uri?.toString(true)) {
|
||||
this.loadTimeline([e.id]);
|
||||
if (e?.uri === undefined || e.uri.toString(true) !== this._uri?.toString(true)) {
|
||||
this.loadTimeline(e.reset ?? false, e?.id === undefined ? undefined : [e.id], { before: !e.reset });
|
||||
}
|
||||
}
|
||||
|
||||
private onReset() {
|
||||
this.loadTimeline(true);
|
||||
}
|
||||
|
||||
private _message: string | undefined;
|
||||
get message(): string | undefined {
|
||||
return this._message;
|
||||
@@ -160,22 +217,27 @@ export class TimelinePane extends ViewPane {
|
||||
DOM.clearNode(this._messageElement);
|
||||
}
|
||||
|
||||
private async loadTimeline(sources?: string[]) {
|
||||
private async loadTimeline(reset: boolean, sources?: string[], options: TimelineOptions = {}) {
|
||||
const defaultPageSize = reset ? InitialPageSize : SubsequentPageSize;
|
||||
|
||||
// If we have no source, we are reseting all sources, so cancel everything in flight and reset caches
|
||||
if (sources === undefined) {
|
||||
this._items.length = 0;
|
||||
if (reset) {
|
||||
this._items.length = 0;
|
||||
this._cursorsByProvider.clear();
|
||||
|
||||
if (this._loadingMessageTimer) {
|
||||
clearTimeout(this._loadingMessageTimer);
|
||||
this._loadingMessageTimer = undefined;
|
||||
if (this._loadingMessageTimer) {
|
||||
clearTimeout(this._loadingMessageTimer);
|
||||
this._loadingMessageTimer = undefined;
|
||||
}
|
||||
|
||||
for (const { tokenSource } of this._pendingRequests.values()) {
|
||||
tokenSource.dispose(true);
|
||||
}
|
||||
|
||||
this._pendingRequests.clear();
|
||||
}
|
||||
|
||||
for (const { tokenSource } of this._pendingRequests.values()) {
|
||||
tokenSource.dispose(true);
|
||||
}
|
||||
|
||||
this._pendingRequests.clear();
|
||||
|
||||
// TODO[ECA]: Are these the right the list of schemes to exclude? Is there a better way?
|
||||
if (this._uri && (this._uri.scheme === 'vscode-settings' || this._uri.scheme === 'webview-panel' || this._uri.scheme === 'walkThrough')) {
|
||||
this.message = 'The active editor cannot provide timeline information.';
|
||||
@@ -184,7 +246,7 @@ export class TimelinePane extends ViewPane {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._uri !== undefined) {
|
||||
if (reset && this._uri !== undefined) {
|
||||
this._loadingMessageTimer = setTimeout((uri: URI) => {
|
||||
if (uri !== this._uri) {
|
||||
return;
|
||||
@@ -200,50 +262,169 @@ export class TimelinePane extends ViewPane {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const source of sources ?? this.timelineService.getSources()) {
|
||||
const filteredSources = (sources ?? this.timelineService.getSources()).filter(s => !this._excludedSources.has(s));
|
||||
if (filteredSources.length === 0) {
|
||||
if (reset) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let lastIndex = this._items.length - 1;
|
||||
let lastItem = this._items[lastIndex]?.element;
|
||||
if (isLoadMoreCommandItem(lastItem)) {
|
||||
lastItem.themeIcon = { id: 'sync~spin' };
|
||||
// this._items.splice(lastIndex, 1);
|
||||
lastIndex--;
|
||||
|
||||
if (!reset && !options.before) {
|
||||
lastItem = this._items[lastIndex]?.element;
|
||||
const selection = [lastItem];
|
||||
this._tree.setSelection(selection);
|
||||
this._tree.setFocus(selection);
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of filteredSources) {
|
||||
let request = this._pendingRequests.get(source);
|
||||
request?.tokenSource.dispose(true);
|
||||
|
||||
request = this.timelineService.getTimeline(source, this._uri, {}, new CancellationTokenSource(), { cacheResults: true })!;
|
||||
const cursors = this._cursorsByProvider.get(source);
|
||||
if (!reset) {
|
||||
// TODO: Handle pending request
|
||||
|
||||
this._pendingRequests.set(source, request);
|
||||
request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source));
|
||||
if (cursors?.more === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reusingToken = request?.tokenSource !== undefined;
|
||||
request = this.timelineService.getTimeline(
|
||||
source, this._uri,
|
||||
{
|
||||
cursor: options.before ? cursors?.startCursors?.before : (cursors?.endCursors ?? cursors?.startCursors)?.after,
|
||||
...options,
|
||||
limit: options.limit === 0 ? undefined : options.limit ?? defaultPageSize
|
||||
},
|
||||
request?.tokenSource ?? new CancellationTokenSource(), { cacheResults: true }
|
||||
)!;
|
||||
|
||||
this._pendingRequests.set(source, request);
|
||||
if (!reusingToken) {
|
||||
request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source));
|
||||
}
|
||||
} else {
|
||||
request?.tokenSource.dispose(true);
|
||||
|
||||
request = this.timelineService.getTimeline(
|
||||
source, this._uri,
|
||||
{
|
||||
...options,
|
||||
limit: options.limit === 0 ? undefined : (reset ? cursors?.endCursors?.after : undefined) ?? options.limit ?? defaultPageSize
|
||||
},
|
||||
new CancellationTokenSource(), { cacheResults: true }
|
||||
)!;
|
||||
|
||||
this._pendingRequests.set(source, request);
|
||||
request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source));
|
||||
}
|
||||
|
||||
this.handleRequest(request);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRequest(request: TimelineRequest) {
|
||||
let items;
|
||||
let timeline: Timeline | undefined;
|
||||
try {
|
||||
items = await this.progressService.withProgress({ location: VIEWLET_ID }, () => request.result.then(r => r?.items ?? []));
|
||||
timeline = await this.progressService.withProgress({ location: this.getProgressLocation() }, () => request.result);
|
||||
}
|
||||
finally {
|
||||
this._pendingRequests.delete(request.source);
|
||||
}
|
||||
catch { }
|
||||
|
||||
this._pendingRequests.delete(request.source);
|
||||
if (request.tokenSource.token.isCancellationRequested || request.uri !== this._uri) {
|
||||
if (
|
||||
timeline === undefined ||
|
||||
request.tokenSource.token.isCancellationRequested ||
|
||||
request.uri !== this._uri
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.replaceItems(request.source, items);
|
||||
}
|
||||
let items: TreeElement[];
|
||||
|
||||
private replaceItems(source: string, items?: TimelineItem[]) {
|
||||
const hasItems = this._items.length !== 0;
|
||||
const source = request.source;
|
||||
|
||||
if (items?.length) {
|
||||
this._items.splice(0, this._items.length, ...this._items.filter(i => i.source !== source), ...items);
|
||||
this._items.sort((a, b) => (b.timestamp - a.timestamp) || b.source.localeCompare(a.source, undefined, { numeric: true, sensitivity: 'base' }));
|
||||
if (timeline !== undefined) {
|
||||
if (timeline.paging !== undefined) {
|
||||
let cursors = this._cursorsByProvider.get(timeline.source ?? source);
|
||||
if (cursors === undefined) {
|
||||
cursors = { startCursors: timeline.paging.cursors, more: timeline.paging.more ?? false };
|
||||
this._cursorsByProvider.set(timeline.source, cursors);
|
||||
} else {
|
||||
if (request.options.before) {
|
||||
if (cursors.endCursors === undefined) {
|
||||
cursors.endCursors = cursors.startCursors;
|
||||
}
|
||||
cursors.startCursors = timeline.paging.cursors;
|
||||
}
|
||||
else {
|
||||
if (cursors.startCursors === undefined) {
|
||||
cursors.startCursors = timeline.paging.cursors;
|
||||
}
|
||||
cursors.endCursors = timeline.paging.cursors;
|
||||
}
|
||||
cursors.more = timeline.paging.more ?? true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._cursorsByProvider.delete(source);
|
||||
}
|
||||
else if (this._items.length && this._items.some(i => i.source === source)) {
|
||||
this._items = this._items.filter(i => i.source !== source);
|
||||
items = (timeline.items as TreeElement[]) ?? [];
|
||||
|
||||
const alreadyHadItems = this._items.length !== 0;
|
||||
|
||||
let changed;
|
||||
if (request.options.cursor) {
|
||||
changed = this.mergeItems(request.source, items, request.options);
|
||||
} else {
|
||||
changed = this.replaceItems(request.source, items);
|
||||
}
|
||||
else {
|
||||
|
||||
if (!changed) {
|
||||
// If there are no items at all and no pending requests, make sure to refresh (to show the no timeline info message)
|
||||
if (this._items.length === 0 && this._pendingRequests.size === 0) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._pendingRequests.size === 0 && this._items.length !== 0) {
|
||||
const lastIndex = this._items.length - 1;
|
||||
const lastItem = this._items[lastIndex]?.element;
|
||||
|
||||
if (timeline.paging?.more || Iterator.some(this._cursorsByProvider.values(), cursors => cursors.more)) {
|
||||
if (isLoadMoreCommandItem(lastItem)) {
|
||||
lastItem.themeIcon = undefined;
|
||||
}
|
||||
else {
|
||||
this._items.push({
|
||||
element: {
|
||||
handle: 'vscode-command:loadMore',
|
||||
label: 'Load more',
|
||||
timestamp: 0
|
||||
} as CommandItem
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isLoadMoreCommandItem(lastItem)) {
|
||||
this._items.splice(lastIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have items already and there are other pending requests, debounce for a bit to wait for other requests
|
||||
if (hasItems && this._pendingRequests.size !== 0) {
|
||||
if (alreadyHadItems && this._pendingRequests.size !== 0) {
|
||||
this.refreshDebounced();
|
||||
}
|
||||
else {
|
||||
@@ -251,6 +432,79 @@ export class TimelinePane extends ViewPane {
|
||||
}
|
||||
}
|
||||
|
||||
private mergeItems(source: string, items: TreeElement[] | undefined, options: TimelineOptions): boolean {
|
||||
if (items?.length === undefined || items.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.before) {
|
||||
const ids = new Set();
|
||||
const timestamps = new Set();
|
||||
|
||||
for (const item of items) {
|
||||
if (item.id === undefined) {
|
||||
timestamps.add(item.timestamp);
|
||||
}
|
||||
else {
|
||||
ids.add(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any duplicate items
|
||||
// I don't think we need to check all the items, just the most recent page
|
||||
let i = Math.min(SubsequentPageSize, this._items.length);
|
||||
let item;
|
||||
while (i--) {
|
||||
item = this._items[i].element;
|
||||
if (
|
||||
(item.id === undefined && ids.has(item.id)) ||
|
||||
(item.timestamp === undefined && timestamps.has(item.timestamp))
|
||||
) {
|
||||
this._items.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this._items.splice(0, 0, ...items.map(item => ({ element: item })));
|
||||
} else {
|
||||
this._items.push(...items.map(item => ({ element: item })));
|
||||
}
|
||||
|
||||
this.sortItems();
|
||||
return true;
|
||||
}
|
||||
|
||||
private replaceItems(source: string, items?: TreeElement[]): boolean {
|
||||
if (items?.length) {
|
||||
this._items.splice(
|
||||
0, this._items.length,
|
||||
...this._items.filter(item => item.element.source !== source),
|
||||
...items.map(item => ({ element: item }))
|
||||
);
|
||||
this.sortItems();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._items.length && this._items.some(item => item.element.source === source)) {
|
||||
this._items = this._items.filter(item => item.element.source !== source);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private sortItems() {
|
||||
this._items.sort(
|
||||
(a, b) =>
|
||||
(b.element.timestamp - a.element.timestamp) ||
|
||||
(a.element.source === undefined
|
||||
? b.element.source === undefined ? 0 : 1
|
||||
: b.element.source === undefined ? -1 : b.element.source.localeCompare(a.element.source, undefined, { numeric: true, sensitivity: 'base' }))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
if (this._loadingMessageTimer) {
|
||||
clearTimeout(this._loadingMessageTimer);
|
||||
@@ -263,7 +517,7 @@ export class TimelinePane extends ViewPane {
|
||||
this.message = undefined;
|
||||
}
|
||||
|
||||
this._tree.setChildren(null, this._items.map(item => ({ element: item })));
|
||||
this._tree.setChildren(null, this._items);
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
@@ -282,6 +536,7 @@ export class TimelinePane extends ViewPane {
|
||||
|
||||
this.timelineService.onDidChangeProviders(this.onProvidersChanged, this, this._visibilityDisposables);
|
||||
this.timelineService.onDidChangeTimeline(this.onTimelineChanged, this, this._visibilityDisposables);
|
||||
this.timelineService.onDidReset(this.onReset, this, this._visibilityDisposables);
|
||||
this.editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._visibilityDisposables);
|
||||
|
||||
this.onActiveEditorChanged();
|
||||
@@ -329,9 +584,24 @@ export class TimelinePane extends ViewPane {
|
||||
}
|
||||
|
||||
const selection = this._tree.getSelection();
|
||||
const command = selection.length === 1 ? selection[0]?.command : undefined;
|
||||
if (command) {
|
||||
this.commandService.executeCommand(command.id, ...(command.arguments || []));
|
||||
const item = selection.length === 1 ? selection[0] : undefined;
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTimelineItem(item)) {
|
||||
if (item.command) {
|
||||
this.commandService.executeCommand(item.command.id, ...(item.command.arguments || []));
|
||||
}
|
||||
}
|
||||
else if (isLoadMoreCommandItem(item)) {
|
||||
// TODO: Change this, but right now this is the pending signal
|
||||
if (item.themeIcon !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadTimeline(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -417,6 +687,11 @@ export class TimelineIdentityProvider implements IIdentityProvider<TreeElement>
|
||||
class TimelineActionRunner extends ActionRunner {
|
||||
|
||||
runAction(action: IAction, { uri, item }: TimelineActionContext): Promise<any> {
|
||||
if (!isTimelineItem(item)) {
|
||||
// TODO
|
||||
return action.run();
|
||||
}
|
||||
|
||||
return action.run(...[
|
||||
{
|
||||
$mid: 11,
|
||||
@@ -499,7 +774,7 @@ class TimelineTreeRenderer implements ITreeRenderer<TreeElement, FuzzyScore, Tim
|
||||
matches: createMatches(node.filterData)
|
||||
});
|
||||
|
||||
template.timestamp.textContent = fromNow(item.timestamp);
|
||||
template.timestamp.textContent = isTimelineItem(item) ? fromNow(item.timestamp) : '';
|
||||
|
||||
template.actionBar.context = { uri: this._uri, item: item } as TimelineActionContext;
|
||||
template.actionBar.actionRunner = new TimelineActionRunner();
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface TimelineItem {
|
||||
handle: string;
|
||||
source: string;
|
||||
|
||||
id?: string;
|
||||
timestamp: number;
|
||||
label: string;
|
||||
icon?: URI,
|
||||
@@ -31,28 +32,34 @@ export interface TimelineItem {
|
||||
}
|
||||
|
||||
export interface TimelineChangeEvent {
|
||||
id: string;
|
||||
id?: string;
|
||||
uri?: URI;
|
||||
reset?: boolean
|
||||
}
|
||||
|
||||
export interface TimelineCursor {
|
||||
cursor?: any;
|
||||
export interface TimelineOptions {
|
||||
cursor?: string;
|
||||
before?: boolean;
|
||||
limit?: number;
|
||||
limit?: number | string;
|
||||
}
|
||||
|
||||
export interface Timeline {
|
||||
source: string;
|
||||
items: TimelineItem[];
|
||||
|
||||
cursor?: any;
|
||||
more?: boolean;
|
||||
paging?: {
|
||||
cursors: {
|
||||
before: string;
|
||||
after?: string
|
||||
};
|
||||
more?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable {
|
||||
onDidChange?: Event<TimelineChangeEvent>;
|
||||
|
||||
provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
|
||||
provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise<Timeline | undefined>;
|
||||
}
|
||||
|
||||
export interface TimelineProviderDescriptor {
|
||||
@@ -68,6 +75,7 @@ export interface TimelineProvidersChangeEvent {
|
||||
|
||||
export interface TimelineRequest {
|
||||
readonly result: Promise<Timeline | undefined>;
|
||||
readonly options: TimelineOptions;
|
||||
readonly source: string;
|
||||
readonly tokenSource: CancellationTokenSource;
|
||||
readonly uri: URI;
|
||||
@@ -78,13 +86,17 @@ export interface ITimelineService {
|
||||
|
||||
onDidChangeProviders: Event<TimelineProvidersChangeEvent>;
|
||||
onDidChangeTimeline: Event<TimelineChangeEvent>;
|
||||
onDidReset: Event<void>;
|
||||
|
||||
registerTimelineProvider(provider: TimelineProvider): IDisposable;
|
||||
unregisterTimelineProvider(id: string): void;
|
||||
|
||||
getSources(): string[];
|
||||
|
||||
getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource, options?: { cacheResults?: boolean }): TimelineRequest | undefined;
|
||||
getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: { cacheResults?: boolean }): TimelineRequest | undefined;
|
||||
|
||||
// refresh(fetch?: 'all' | 'more'): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
const TIMELINE_SERVICE_ID = 'timeline';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
// import { basename } from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ITimelineService, TimelineChangeEvent, TimelineCursor, TimelineProvidersChangeEvent, TimelineProvider } from './timeline';
|
||||
import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider } from './timeline';
|
||||
|
||||
export class TimelineService implements ITimelineService {
|
||||
_serviceBrand: undefined;
|
||||
@@ -20,6 +20,9 @@ export class TimelineService implements ITimelineService {
|
||||
private readonly _onDidChangeTimeline = new Emitter<TimelineChangeEvent>();
|
||||
readonly onDidChangeTimeline: Event<TimelineChangeEvent> = this._onDidChangeTimeline.event;
|
||||
|
||||
private readonly _onDidReset = new Emitter<void>();
|
||||
readonly onDidReset: Event<void> = this._onDidReset.event;
|
||||
|
||||
private readonly _providers = new Map<string, TimelineProvider>();
|
||||
private readonly _providerSubscriptions = new Map<string, IDisposable>();
|
||||
|
||||
@@ -81,7 +84,7 @@ export class TimelineService implements ITimelineService {
|
||||
return [...this._providers.keys()];
|
||||
}
|
||||
|
||||
getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource, options?: { cacheResults?: boolean }) {
|
||||
getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: { cacheResults?: boolean }) {
|
||||
this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`);
|
||||
|
||||
const provider = this._providers.get(id);
|
||||
@@ -98,7 +101,7 @@ export class TimelineService implements ITimelineService {
|
||||
}
|
||||
|
||||
return {
|
||||
result: provider.provideTimeline(uri, cursor, tokenSource.token, options)
|
||||
result: provider.provideTimeline(uri, options, tokenSource.token, internalOptions)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
return undefined;
|
||||
@@ -109,6 +112,7 @@ export class TimelineService implements ITimelineService {
|
||||
|
||||
return result;
|
||||
}),
|
||||
options: options,
|
||||
source: provider.id,
|
||||
tokenSource: tokenSource,
|
||||
uri: uri
|
||||
@@ -156,4 +160,12 @@ export class TimelineService implements ITimelineService {
|
||||
this._providerSubscriptions.delete(id);
|
||||
this._onDidChangeProviders.fire({ removed: [id] });
|
||||
}
|
||||
|
||||
// refresh(fetch?: 'all' | 'more') {
|
||||
// this._onDidChangeTimeline.fire({ fetch: fetch });
|
||||
// }
|
||||
|
||||
reset() {
|
||||
this._onDidReset.fire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { UserDataSyncWorkbenchContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSync';
|
||||
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import { IUserDataSyncEnablementService, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
||||
class UserDataSyncSettingsMigrationContribution implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@IProductService productService: IProductService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
|
||||
) {
|
||||
if (getUserDataSyncStore(configurationService)) {
|
||||
if (getUserDataSyncStore(productService, configurationService)) {
|
||||
if (!configurationService.getValue('sync.enableSettings')) {
|
||||
userDataSyncEnablementService.setResourceEnablement('settings', false);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr
|
||||
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
|
||||
import { fromNow } from 'vs/base/common/date';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
|
||||
const enum AuthStatus {
|
||||
Initializing = 'Initializing',
|
||||
@@ -135,9 +137,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IOpenerService private readonly openerService: IOpenerService,
|
||||
) {
|
||||
super();
|
||||
this.userDataSyncStore = getUserDataSyncStore(configurationService);
|
||||
this.userDataSyncStore = getUserDataSyncStore(productService, configurationService);
|
||||
this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService);
|
||||
this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
|
||||
this.authenticationState = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService);
|
||||
@@ -235,7 +239,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
}
|
||||
|
||||
this.updateBadge();
|
||||
this.registerSyncStatusAction();
|
||||
}
|
||||
|
||||
private async onDidChangeSessions(providerId: string): Promise<void> {
|
||||
@@ -396,7 +399,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
severity: Severity.Info,
|
||||
message: localize('turned off', "Sync was turned off from another device."),
|
||||
actions: {
|
||||
primary: [new Action('turn on sync', localize('Turn sync back on', "Turn Sync Back On"), undefined, true, () => this.turnOn())]
|
||||
primary: [new Action('turn on sync', localize('turn on sync', "Turn on Sync"), undefined, true, () => this.turnOn())]
|
||||
}
|
||||
});
|
||||
return;
|
||||
@@ -484,6 +487,24 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
}
|
||||
|
||||
private async turnOn(): Promise<void> {
|
||||
if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) {
|
||||
const result = await this.dialogService.show(
|
||||
Severity.Info,
|
||||
localize('sync preview message', "Synchronizing your preferences is a preview feature, please read the documentation before turning it on."),
|
||||
[
|
||||
localize('open doc', "Open Documentation"),
|
||||
localize('confirm', "Continue"),
|
||||
localize('cancel', "Cancel"),
|
||||
],
|
||||
{
|
||||
cancelId: 2
|
||||
}
|
||||
);
|
||||
switch (result.choice) {
|
||||
case 0: this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?LinkId=827846')); return;
|
||||
case 2: return;
|
||||
}
|
||||
}
|
||||
return new Promise((c, e) => {
|
||||
const disposables: DisposableStore = new DisposableStore();
|
||||
const quickPick = this.quickInputService.createQuickPick<ConfigureSyncQuickPickItem>();
|
||||
@@ -495,7 +516,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
quickPick.customLabel = localize('turn on', "Turn on");
|
||||
} else {
|
||||
const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId);
|
||||
quickPick.description = localize('sign in and turn on sync detail', "Please sign in with your {0} account to synchronize your following data across all your devices.", displayName);
|
||||
quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName);
|
||||
quickPick.customLabel = localize('sign in and turn on sync', "Sign in & Turn on");
|
||||
}
|
||||
quickPick.placeholder = localize('configure sync placeholder', "Choose what to sync");
|
||||
@@ -523,6 +544,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
await this.handleFirstTimeSync();
|
||||
this.userDataSyncEnablementService.setEnablement(true);
|
||||
this.notificationService.info(localize('sync turned on', "Sync will happen automatically from now on."));
|
||||
this.storageService.store('sync.donotAskPreviewConfirmation', true, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] {
|
||||
@@ -746,7 +768,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
|
||||
private registerSignInAction(): void {
|
||||
const that = this;
|
||||
registerAction2(class StopSyncAction extends Action2 {
|
||||
this._register(registerAction2(class StopSyncAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: signInCommand.id,
|
||||
@@ -766,7 +788,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
that.notificationService.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private registerShowSettingsConflictsAction(): void {
|
||||
@@ -824,17 +846,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
|
||||
}
|
||||
|
||||
private readonly _syncStatusActionDisposable = this._register(new MutableDisposable());
|
||||
private registerSyncStatusAction(): void {
|
||||
const that = this;
|
||||
const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedIn), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized));
|
||||
this._syncStatusActionDisposable.value = registerAction2(class SyncStatusAction extends Action2 {
|
||||
this._register(registerAction2(class SyncStatusAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.userData.actions.syncStatus',
|
||||
get title() {
|
||||
return getIdentityTitle(localize('sync is on', "Sync is on"), that.activeAccount);
|
||||
},
|
||||
title: localize('sync is on', "Sync is on"),
|
||||
menu: [
|
||||
{
|
||||
id: MenuId.GlobalActivity,
|
||||
@@ -890,12 +909,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
quickPick.show();
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private registerTurnOffSyncAction(): void {
|
||||
const that = this;
|
||||
registerAction2(class StopSyncAction extends Action2 {
|
||||
this._register(registerAction2(class StopSyncAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: stopSyncCommand.id,
|
||||
@@ -915,12 +934,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private registerConfigureSyncAction(): void {
|
||||
const that = this;
|
||||
registerAction2(class ShowSyncActivityAction extends Action2 {
|
||||
this._register(registerAction2(class ShowSyncActivityAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: configureSyncCommand.id,
|
||||
@@ -932,12 +951,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
});
|
||||
}
|
||||
run(): any { return that.configureSyncOptions(); }
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private registerShowActivityAction(): void {
|
||||
const that = this;
|
||||
registerAction2(class ShowSyncActivityAction extends Action2 {
|
||||
this._register(registerAction2(class ShowSyncActivityAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: showSyncActivityCommand.id,
|
||||
@@ -949,11 +968,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
});
|
||||
}
|
||||
run(): any { return that.showSyncActivity(); }
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private registerShowSettingsAction(): void {
|
||||
registerAction2(class ShowSyncSettingsAction extends Action2 {
|
||||
this._register(registerAction2(class ShowSyncSettingsAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: showSyncSettingsCommand.id,
|
||||
@@ -965,9 +984,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
|
||||
});
|
||||
}
|
||||
run(accessor: ServicesAccessor): any {
|
||||
accessor.get(IPreferencesService).openGlobalSettings(false, { query: 'sync:' });
|
||||
accessor.get(IPreferencesService).openGlobalSettings(false, { query: '@tag:sync' });
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import { addDisposableListener, EventType, EventHelper } from 'vs/base/browser/d
|
||||
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
/**
|
||||
* Stores the selection & view state of an editor and allows to compare it to other selection states.
|
||||
@@ -112,8 +111,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -277,7 +275,9 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
// History
|
||||
this.clearRecentlyOpened();
|
||||
|
||||
// Navigation (next, previous)
|
||||
this.navigationStackIndex = -1;
|
||||
@@ -289,9 +289,6 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
// Closed files
|
||||
this.recentlyClosedFiles = [];
|
||||
|
||||
// History
|
||||
this.clearRecentlyOpened();
|
||||
|
||||
// Context Keys
|
||||
this.updateContextKeys();
|
||||
}
|
||||
@@ -719,8 +716,8 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
private static readonly MAX_HISTORY_ITEMS = 200;
|
||||
private static readonly HISTORY_STORAGE_KEY = 'history.entries';
|
||||
|
||||
private history: Array<IEditorInput | IResourceInput> = [];
|
||||
private loaded = false;
|
||||
private history: Array<IEditorInput | IResourceInput> | undefined = undefined;
|
||||
|
||||
private readonly resourceFilter = this._register(this.instantiationService.createInstance(
|
||||
ResourceGlobMatcher,
|
||||
(root?: URI) => this.getExcludes(root),
|
||||
@@ -741,11 +738,10 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
const historyInput = this.preferResourceInput(input);
|
||||
|
||||
// Remove any existing entry and add to the beginning
|
||||
this.ensureHistoryLoaded(this.history);
|
||||
this.removeFromHistory(input);
|
||||
this.history.unshift(historyInput);
|
||||
|
||||
@@ -772,7 +768,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
private removeExcludedFromHistory(): void {
|
||||
this.ensureHistoryLoaded();
|
||||
this.ensureHistoryLoaded(this.history);
|
||||
|
||||
this.history = this.history.filter(e => {
|
||||
const include = this.include(e);
|
||||
@@ -787,7 +783,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
private removeFromHistory(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
|
||||
this.ensureHistoryLoaded();
|
||||
this.ensureHistoryLoaded(this.history);
|
||||
|
||||
this.history = this.history.filter(e => {
|
||||
const matches = this.matches(arg1, e);
|
||||
@@ -809,17 +805,59 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
getHistory(): ReadonlyArray<IEditorInput | IResourceInput> {
|
||||
this.ensureHistoryLoaded();
|
||||
this.ensureHistoryLoaded(this.history);
|
||||
|
||||
return this.history.slice(0);
|
||||
}
|
||||
|
||||
private ensureHistoryLoaded(): void {
|
||||
if (!this.loaded) {
|
||||
this.loadHistory();
|
||||
private ensureHistoryLoaded(history: Array<IEditorInput | IResourceInput> | undefined): asserts history {
|
||||
if (!this.history) {
|
||||
this.history = this.loadHistory();
|
||||
}
|
||||
}
|
||||
|
||||
private loadHistory(): Array<IEditorInput | IResourceInput> {
|
||||
let entries: ISerializedEditorHistoryEntry[] = [];
|
||||
|
||||
const entriesRaw = this.storageService.get(HistoryService.HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);
|
||||
if (entriesRaw) {
|
||||
entries = coalesce(JSON.parse(entriesRaw));
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);
|
||||
|
||||
return coalesce(entries.map(entry => {
|
||||
try {
|
||||
return this.safeLoadHistoryEntry(registry, entry);
|
||||
} catch (error) {
|
||||
return undefined; // https://github.com/Microsoft/vscode/issues/60960
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceInput | undefined {
|
||||
const serializedEditorHistoryEntry = entry;
|
||||
|
||||
// File resource: via URI.revive()
|
||||
if (serializedEditorHistoryEntry.resourceJSON) {
|
||||
return { resource: URI.revive(<UriComponents>serializedEditorHistoryEntry.resourceJSON) };
|
||||
}
|
||||
|
||||
// Editor input: via factory
|
||||
const { editorInputJSON } = serializedEditorHistoryEntry;
|
||||
if (editorInputJSON?.deserialized) {
|
||||
const factory = registry.getEditorInputFactory(editorInputJSON.typeId);
|
||||
if (factory) {
|
||||
const input = factory.deserialize(this.instantiationService, editorInputJSON.deserialized);
|
||||
if (input) {
|
||||
this.onEditorDispose(input, () => this.removeFromHistory(input), this.editorHistoryListeners);
|
||||
}
|
||||
|
||||
return withNullAsUndefined(input);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private saveState(): void {
|
||||
@@ -850,58 +888,9 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
return undefined;
|
||||
}));
|
||||
|
||||
this.logService.trace(`[editor history] saving ${entries.length} entries`);
|
||||
this.storageService.store(HistoryService.HISTORY_STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE);
|
||||
}
|
||||
|
||||
private loadHistory(): void {
|
||||
let entries: ISerializedEditorHistoryEntry[] = [];
|
||||
|
||||
const entriesRaw = this.storageService.get(HistoryService.HISTORY_STORAGE_KEY, StorageScope.WORKSPACE);
|
||||
if (entriesRaw) {
|
||||
entries = coalesce(JSON.parse(entriesRaw));
|
||||
}
|
||||
|
||||
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);
|
||||
|
||||
this.history = coalesce(entries.map(entry => {
|
||||
try {
|
||||
return this.safeLoadHistoryEntry(registry, entry);
|
||||
} catch (error) {
|
||||
this.logService.error(`[editor history] error loading one editor history entry: ${error.toString()}`);
|
||||
|
||||
return undefined; // https://github.com/Microsoft/vscode/issues/60960
|
||||
}
|
||||
}));
|
||||
|
||||
this.logService.trace(`[editor history] loading ${this.history.length} entries`);
|
||||
}
|
||||
|
||||
private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceInput | undefined {
|
||||
const serializedEditorHistoryEntry = entry;
|
||||
|
||||
// File resource: via URI.revive()
|
||||
if (serializedEditorHistoryEntry.resourceJSON) {
|
||||
return { resource: URI.revive(<UriComponents>serializedEditorHistoryEntry.resourceJSON) };
|
||||
}
|
||||
|
||||
// Editor input: via factory
|
||||
const { editorInputJSON } = serializedEditorHistoryEntry;
|
||||
if (editorInputJSON?.deserialized) {
|
||||
const factory = registry.getEditorInputFactory(editorInputJSON.typeId);
|
||||
if (factory) {
|
||||
const input = factory.deserialize(this.instantiationService, editorInputJSON.deserialized);
|
||||
if (input) {
|
||||
this.onEditorDispose(input, () => this.removeFromHistory(input), this.editorHistoryListeners);
|
||||
}
|
||||
|
||||
return withNullAsUndefined(input);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Last Active Workspace/File
|
||||
@@ -925,8 +914,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
// Multiple folders: find the last active one
|
||||
const history = this.getHistory();
|
||||
for (const input of history) {
|
||||
for (const input of this.getHistory()) {
|
||||
if (input instanceof EditorInput) {
|
||||
continue;
|
||||
}
|
||||
@@ -954,8 +942,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
getLastActiveFile(filterByScheme: string): URI | undefined {
|
||||
const history = this.getHistory();
|
||||
for (const input of history) {
|
||||
for (const input of this.getHistory()) {
|
||||
let resource: URI | undefined;
|
||||
if (input instanceof EditorInput) {
|
||||
resource = toResource(input, { filterByScheme });
|
||||
|
||||
@@ -100,7 +100,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
|
||||
return this.labelService.getUriBasenameLabel(this.resource);
|
||||
}
|
||||
|
||||
private dirty = false;
|
||||
private dirty = this.hasAssociatedFilePath || !!this.initialValue;
|
||||
private ignoreDirtyOnModelContentChange = false;
|
||||
|
||||
private versionId = 0;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbe
|
||||
import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput';
|
||||
@@ -120,9 +119,13 @@ suite('Untitled text editors', () => {
|
||||
test('associated resource is dirty', async () => {
|
||||
const service = accessor.untitledTextEditorService;
|
||||
const file = URI.file(join('C:\\', '/foo/file.txt'));
|
||||
const untitled = await service.resolve({ associatedResource: file });
|
||||
|
||||
assert.ok(untitled.hasAssociatedFilePath);
|
||||
const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file }));
|
||||
assert.ok(untitled.isDirty());
|
||||
|
||||
const model = await untitled.resolve();
|
||||
|
||||
assert.ok(model.hasAssociatedFilePath);
|
||||
assert.equal(untitled.isDirty(), true);
|
||||
|
||||
untitled.dispose();
|
||||
@@ -197,20 +200,14 @@ suite('Untitled text editors', () => {
|
||||
const workingCopyService = accessor.workingCopyService;
|
||||
|
||||
const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' }));
|
||||
|
||||
let onDidChangeDirty: IWorkingCopy | undefined = undefined;
|
||||
const listener = workingCopyService.onDidChangeDirty(copy => {
|
||||
onDidChangeDirty = copy;
|
||||
});
|
||||
assert.ok(untitled.isDirty());
|
||||
|
||||
// dirty
|
||||
const model = await untitled.resolve();
|
||||
assert.ok(model.isDirty());
|
||||
assert.equal(workingCopyService.dirtyCount, 1);
|
||||
assert.equal(onDidChangeDirty, model);
|
||||
|
||||
untitled.dispose();
|
||||
listener.dispose();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user