Vscode merge (#4582)
* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd * fix issues with merges * bump node version in azpipe * replace license headers * remove duplicate launch task * fix build errors * fix build errors * fix tslint issues * working through package and linux build issues * more work * wip * fix packaged builds * working through linux build errors * wip * wip * wip * fix mac and linux file limits * iterate linux pipeline * disable editor typing * revert series to parallel * remove optimize vscode from linux * fix linting issues * revert testing change * add work round for new node * readd packaging for extensions * fix issue with angular not resolving decorator dependencies
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { EditorInput, EditorOptions } from 'vs/workbench/common/editor';
|
||||
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
/**
|
||||
* An implementation of editor for binary files like images.
|
||||
*/
|
||||
export class BinaryFileEditor extends BaseBinaryResourceEditor {
|
||||
|
||||
static readonly ID = BINARY_FILE_EDITOR_ID;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IWindowsService private readonly windowsService: IWindowsService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(
|
||||
BinaryFileEditor.ID,
|
||||
{
|
||||
openInternal: (input, options) => this.openInternal(input, options),
|
||||
openExternal: resource => this.openExternal(resource)
|
||||
},
|
||||
telemetryService,
|
||||
themeService,
|
||||
fileService,
|
||||
storageService
|
||||
);
|
||||
}
|
||||
|
||||
private openInternal(input: EditorInput, options: EditorOptions): Promise<void> {
|
||||
if (input instanceof FileEditorInput) {
|
||||
input.setForceOpenAsText();
|
||||
|
||||
return this.editorService.openEditor(input, options, this.group).then(() => undefined);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private openExternal(resource: URI): void {
|
||||
this.windowsService.openExternal(resource.toString()).then(didOpen => {
|
||||
if (!didOpen) {
|
||||
return this.windowsService.showItemInFolder(resource);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
getTitle(): string | null {
|
||||
return this.input ? this.input.getName() : nls.localize('binaryFileEditor', "Binary File Viewer");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { IEditorViewState } from 'vs/editor/common/editorCommon';
|
||||
import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
|
||||
import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
|
||||
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { distinct, coalesce } from 'vs/base/common/arrays';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { ResourceQueue, timeout } from 'vs/base/common/async';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
|
||||
export class FileEditorTracker extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
protected closeOnFileDelete: boolean;
|
||||
|
||||
private modelLoadQueue: ResourceQueue;
|
||||
private activeOutOfWorkspaceWatchers: ResourceMap<URI>;
|
||||
|
||||
constructor(
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@ILifecycleService private readonly lifecycleService: ILifecycleService,
|
||||
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IWindowService private readonly windowService: IWindowService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.modelLoadQueue = new ResourceQueue();
|
||||
this.activeOutOfWorkspaceWatchers = new ResourceMap<URI>();
|
||||
|
||||
this.onConfigurationUpdated(configurationService.getValue<IWorkbenchEditorConfiguration>());
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Update editors from operation changes
|
||||
this._register(this.fileService.onAfterOperation(e => this.onFileOperation(e)));
|
||||
|
||||
// Update editors from disk changes
|
||||
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
|
||||
|
||||
// Editor changing
|
||||
this._register(this.editorService.onDidVisibleEditorsChange(() => this.handleOutOfWorkspaceWatchers()));
|
||||
|
||||
// Update visible editors when focus is gained
|
||||
this._register(this.windowService.onDidChangeFocus(e => this.onWindowFocusChange(e)));
|
||||
|
||||
// Lifecycle
|
||||
this.lifecycleService.onShutdown(this.dispose, this);
|
||||
|
||||
// Configuration
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue<IWorkbenchEditorConfiguration>())));
|
||||
}
|
||||
|
||||
private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
|
||||
if (configuration.workbench && configuration.workbench.editor && typeof configuration.workbench.editor.closeOnFileDelete === 'boolean') {
|
||||
this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete;
|
||||
} else {
|
||||
this.closeOnFileDelete = false; // default
|
||||
}
|
||||
}
|
||||
|
||||
private onWindowFocusChange(focused: boolean): void {
|
||||
if (focused) {
|
||||
// the window got focus and we use this as a hint that files might have been changed outside
|
||||
// of this window. since file events can be unreliable, we queue a load for models that
|
||||
// are visible in any editor. since this is a fast operation in the case nothing has changed,
|
||||
// we tolerate the additional work.
|
||||
distinct(
|
||||
coalesce(this.editorService.visibleEditors
|
||||
.map(editorInput => {
|
||||
const resource = toResource(editorInput, { supportSideBySide: true });
|
||||
return resource ? this.textFileService.models.get(resource) : undefined;
|
||||
}))
|
||||
.filter(model => !model.isDirty()),
|
||||
m => m.getResource().toString()
|
||||
).forEach(model => this.queueModelLoad(model));
|
||||
}
|
||||
}
|
||||
|
||||
// Note: there is some duplication with the other file event handler below. Since we cannot always rely on the disk events
|
||||
// carrying all necessary data in all environments, we also use the file operation events to make sure operations are handled.
|
||||
// In any case there is no guarantee if the local event is fired first or the disk one. Thus, code must handle the case
|
||||
// that the event ordering is random as well as might not carry all information needed.
|
||||
private onFileOperation(e: FileOperationEvent): void {
|
||||
|
||||
// Handle moves specially when file is opened
|
||||
if (e.operation === FileOperation.MOVE && e.target) {
|
||||
this.handleMovedFileInOpenedEditors(e.resource, e.target.resource);
|
||||
}
|
||||
|
||||
// Handle deletes
|
||||
if (e.operation === FileOperation.DELETE || e.operation === FileOperation.MOVE) {
|
||||
this.handleDeletes(e.resource, false, e.target ? e.target.resource : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private onFileChanges(e: FileChangesEvent): void {
|
||||
|
||||
// Handle updates
|
||||
if (e.gotAdded() || e.gotUpdated()) {
|
||||
this.handleUpdates(e);
|
||||
}
|
||||
|
||||
// Handle deletes
|
||||
if (e.gotDeleted()) {
|
||||
this.handleDeletes(e, true);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDeletes(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void {
|
||||
const nonDirtyFileEditors = this.getOpenedFileEditors(false /* non-dirty only */);
|
||||
nonDirtyFileEditors.forEach(editor => {
|
||||
const resource = editor.getResource();
|
||||
|
||||
// Handle deletes in opened editors depending on:
|
||||
// - the user has not disabled the setting closeOnFileDelete
|
||||
// - the file change is local or external
|
||||
// - the input is not resolved (we need to dispose because we cannot restore otherwise since we do not have the contents)
|
||||
|
||||
// {{SQL CARBON EDIT}} - Support FileEditorInput or QueryInput
|
||||
if (this.closeOnFileDelete || !isExternal || (editor instanceof FileEditorInput && !editor.isResolved())) {
|
||||
|
||||
// Do NOT close any opened editor that matches the resource path (either equal or being parent) of the
|
||||
// resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same
|
||||
// path but different casing.
|
||||
if (movedTo && resources.isEqualOrParent(resource, movedTo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let matches = false;
|
||||
if (arg1 instanceof FileChangesEvent) {
|
||||
matches = arg1.contains(resource, FileChangeType.DELETED);
|
||||
} else {
|
||||
matches = resources.isEqualOrParent(resource, arg1);
|
||||
}
|
||||
|
||||
if (!matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We have received reports of users seeing delete events even though the file still
|
||||
// exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665).
|
||||
// Since we do not want to close an editor without reason, we have to check if the
|
||||
// file is really gone and not just a faulty file event.
|
||||
// This only applies to external file events, so we need to check for the isExternal
|
||||
// flag.
|
||||
let checkExists: Promise<boolean>;
|
||||
if (isExternal) {
|
||||
checkExists = timeout(100).then(() => this.fileService.existsFile(resource));
|
||||
} else {
|
||||
checkExists = Promise.resolve(false);
|
||||
}
|
||||
|
||||
checkExists.then(exists => {
|
||||
if (!exists && !editor.isDisposed()) {
|
||||
editor.dispose();
|
||||
} else if (this.environmentService.verbose) {
|
||||
console.warn(`File exists even though we received a delete event: ${resource.toString()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}} - Support FileEditorInput or QueryInput
|
||||
private getOpenedFileEditors(dirtyState: boolean): (FileEditorInput | QueryInput)[] {
|
||||
const editors: (FileEditorInput | QueryInput)[] = [];
|
||||
|
||||
this.editorService.editors.forEach(editor => {
|
||||
// {{SQL CARBON EDIT}} - Support FileEditorInput or QueryInput
|
||||
if (editor instanceof FileEditorInput || editor instanceof QueryInput) {
|
||||
if (!!editor.isDirty() === dirtyState) {
|
||||
editors.push(editor);
|
||||
}
|
||||
} else if (editor instanceof SideBySideEditorInput) {
|
||||
const master = editor.master;
|
||||
const details = editor.details;
|
||||
|
||||
if (master instanceof FileEditorInput) {
|
||||
if (!!master.isDirty() === dirtyState) {
|
||||
editors.push(master);
|
||||
}
|
||||
}
|
||||
|
||||
if (details instanceof FileEditorInput) {
|
||||
if (!!details.isDirty() === dirtyState) {
|
||||
editors.push(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return editors;
|
||||
}
|
||||
|
||||
private handleMovedFileInOpenedEditors(oldResource: URI, newResource: URI): void {
|
||||
this.editorGroupService.groups.forEach(group => {
|
||||
group.editors.forEach(editor => {
|
||||
// {{SQL CARBON EDIT}} - Support FileEditorInput or QueryInput
|
||||
if (editor instanceof FileEditorInput || editor instanceof QueryInput) {
|
||||
const resource = editor.getResource();
|
||||
|
||||
// Update Editor if file (or any parent of the input) got renamed or moved
|
||||
if (resources.isEqualOrParent(resource, oldResource)) {
|
||||
let reopenFileResource: URI;
|
||||
if (oldResource.toString() === resource.toString()) {
|
||||
reopenFileResource = newResource; // file got moved
|
||||
} else {
|
||||
const index = this.getIndexOfPath(resource.path, oldResource.path);
|
||||
reopenFileResource = resources.joinPath(newResource, resource.path.substr(index + oldResource.path.length + 1)); // parent folder got moved
|
||||
}
|
||||
|
||||
this.editorService.replaceEditors([{
|
||||
editor: { resource },
|
||||
replacement: {
|
||||
resource: reopenFileResource,
|
||||
options: {
|
||||
preserveFocus: true,
|
||||
pinned: group.isPinned(editor),
|
||||
index: group.getIndexOfEditor(editor),
|
||||
inactive: !group.isActive(editor),
|
||||
viewState: this.getViewStateFor(oldResource, group)
|
||||
}
|
||||
},
|
||||
}], group);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getIndexOfPath(path: string, candidate: string): number {
|
||||
if (candidate.length > path.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (path === candidate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!isLinux /* ignore case */) {
|
||||
path = path.toLowerCase();
|
||||
candidate = candidate.toLowerCase();
|
||||
}
|
||||
|
||||
return path.indexOf(candidate);
|
||||
}
|
||||
|
||||
private getViewStateFor(resource: URI, group: IEditorGroup): IEditorViewState | undefined {
|
||||
const editors = this.editorService.visibleControls;
|
||||
|
||||
for (const editor of editors) {
|
||||
if (editor && editor.input && editor.group === group) {
|
||||
const editorResource = editor.input.getResource();
|
||||
if (editorResource && resource.toString() === editorResource.toString()) {
|
||||
const control = editor.getControl();
|
||||
if (isCodeEditor(control)) {
|
||||
return control.saveViewState() || undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private handleUpdates(e: FileChangesEvent): void {
|
||||
|
||||
// Handle updates to text models
|
||||
this.handleUpdatesToTextModels(e);
|
||||
|
||||
// Handle updates to visible binary editors
|
||||
this.handleUpdatesToVisibleBinaryEditors(e);
|
||||
}
|
||||
|
||||
private handleUpdatesToTextModels(e: FileChangesEvent): void {
|
||||
|
||||
// Collect distinct (saved) models to update.
|
||||
//
|
||||
// Note: we also consider the added event because it could be that a file was added
|
||||
// and updated right after.
|
||||
distinct(coalesce([...e.getUpdated(), ...e.getAdded()]
|
||||
.map(u => this.textFileService.models.get(u.resource)))
|
||||
.filter(model => model && !model.isDirty()), m => m.getResource().toString())
|
||||
.forEach(model => this.queueModelLoad(model));
|
||||
}
|
||||
|
||||
private queueModelLoad(model: ITextFileEditorModel): void {
|
||||
|
||||
// Load model to update (use a queue to prevent accumulation of loads
|
||||
// when the load actually takes long. At most we only want the queue
|
||||
// to have a size of 2 (1 running load and 1 queued load).
|
||||
const queue = this.modelLoadQueue.queueFor(model.getResource());
|
||||
if (queue.size <= 1) {
|
||||
queue.queue(() => model.load().then<void>(undefined, onUnexpectedError));
|
||||
}
|
||||
}
|
||||
|
||||
private handleUpdatesToVisibleBinaryEditors(e: FileChangesEvent): void {
|
||||
const editors = this.editorService.visibleControls;
|
||||
editors.forEach(editor => {
|
||||
const resource = editor.input ? toResource(editor.input, { supportSideBySide: true }) : undefined;
|
||||
|
||||
// Support side-by-side binary editors too
|
||||
let isBinaryEditor = false;
|
||||
if (editor instanceof SideBySideEditor) {
|
||||
const masterEditor = editor.getMasterEditor();
|
||||
isBinaryEditor = !!masterEditor && masterEditor.getId() === BINARY_FILE_EDITOR_ID;
|
||||
} else {
|
||||
isBinaryEditor = editor.getId() === BINARY_FILE_EDITOR_ID;
|
||||
}
|
||||
|
||||
// Binary editor that should reload from event
|
||||
if (resource && editor.input && isBinaryEditor && (e.contains(resource, FileChangeType.UPDATED) || e.contains(resource, FileChangeType.ADDED))) {
|
||||
this.editorService.openEditor(editor.input, { forceReload: true, preserveFocus: true }, editor.group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleOutOfWorkspaceWatchers(): void {
|
||||
const visibleOutOfWorkspacePaths = new ResourceMap<URI>();
|
||||
coalesce(this.editorService.visibleEditors.map(editorInput => {
|
||||
return toResource(editorInput, { supportSideBySide: true });
|
||||
})).filter(resource => {
|
||||
return this.fileService.canHandleResource(resource) && !this.contextService.isInsideWorkspace(resource);
|
||||
}).forEach(resource => {
|
||||
visibleOutOfWorkspacePaths.set(resource, resource);
|
||||
});
|
||||
|
||||
// Handle no longer visible out of workspace resources
|
||||
this.activeOutOfWorkspaceWatchers.forEach(resource => {
|
||||
if (!visibleOutOfWorkspacePaths.get(resource)) {
|
||||
this.fileService.unwatchFileChanges(resource);
|
||||
this.activeOutOfWorkspaceWatchers.delete(resource);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle newly visible out of workspace resources
|
||||
visibleOutOfWorkspacePaths.forEach(resource => {
|
||||
if (!this.activeOutOfWorkspaceWatchers.get(resource)) {
|
||||
this.fileService.watchFileChanges(resource);
|
||||
this.activeOutOfWorkspaceWatchers.set(resource, resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
// Dispose watchers if any
|
||||
this.activeOutOfWorkspaceWatchers.forEach(resource => this.fileService.unwatchFileChanges(resource));
|
||||
this.activeOutOfWorkspaceWatchers.clear();
|
||||
}
|
||||
}
|
||||
303
src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { isValidBasename } from 'vs/base/common/extpath';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
|
||||
import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor';
|
||||
import { EditorOptions, TextEditorOptions, IEditorCloseEvent } from 'vs/workbench/common/editor';
|
||||
import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel';
|
||||
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { FileOperationError, FileOperationResult, FileChangesEvent, IFileService, FALLBACK_MAX_MEMORY_SIZE_MB, MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { createErrorWithActions } from 'vs/base/common/errorsWithActions';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
|
||||
/**
|
||||
* An implementation of editor for file system resources.
|
||||
*/
|
||||
export class TextFileEditor extends BaseTextEditor {
|
||||
|
||||
static readonly ID = TEXT_FILE_EDITOR_ID;
|
||||
|
||||
private restoreViewState: boolean;
|
||||
private groupListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IViewletService private readonly viewletService: IViewletService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IEditorGroupsService editorGroupService: IEditorGroupsService,
|
||||
@ITextFileService textFileService: ITextFileService,
|
||||
@IWindowsService private readonly windowsService: IWindowsService,
|
||||
@IPreferencesService private readonly preferencesService: IPreferencesService,
|
||||
@IWindowService windowService: IWindowService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService
|
||||
) {
|
||||
super(TextFileEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService);
|
||||
|
||||
this.updateRestoreViewStateConfiguration();
|
||||
|
||||
// Clear view state for deleted files
|
||||
this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e)));
|
||||
}
|
||||
|
||||
private onFilesChanged(e: FileChangesEvent): void {
|
||||
const deleted = e.getDeleted();
|
||||
if (deleted && deleted.length) {
|
||||
this.clearTextEditorViewState(deleted.map(d => d.resource));
|
||||
}
|
||||
}
|
||||
|
||||
protected handleConfigurationChangeEvent(configuration?: IEditorConfiguration): void {
|
||||
super.handleConfigurationChangeEvent(configuration);
|
||||
|
||||
this.updateRestoreViewStateConfiguration();
|
||||
}
|
||||
|
||||
private updateRestoreViewStateConfiguration(): void {
|
||||
this.restoreViewState = this.configurationService.getValue(undefined, 'workbench.editor.restoreViewState');
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
return this.input ? this.input.getName() : nls.localize('textFileEditor', "Text File Editor");
|
||||
}
|
||||
|
||||
get input(): FileEditorInput {
|
||||
return this._input as FileEditorInput;
|
||||
}
|
||||
|
||||
setEditorVisible(visible: boolean, group: IEditorGroup): void {
|
||||
super.setEditorVisible(visible, group);
|
||||
|
||||
// React to editors closing to preserve or clear view state. This needs to happen
|
||||
// in the onWillCloseEditor because at that time the editor has not yet
|
||||
// been disposed and we can safely persist the view state still as needed.
|
||||
this.groupListener = dispose(this.groupListener);
|
||||
this.groupListener = ((group as IEditorGroupView).onWillCloseEditor(e => this.onWillCloseEditorInGroup(e)));
|
||||
}
|
||||
|
||||
private onWillCloseEditorInGroup(e: IEditorCloseEvent): void {
|
||||
const editor = e.editor;
|
||||
if (!(editor instanceof FileEditorInput)) {
|
||||
return; // only handle files
|
||||
}
|
||||
|
||||
// If the editor is currently active we can always save or clear the view state.
|
||||
// If the editor is not active, we can only clear the view state because it needs
|
||||
// an active editor with the file opened, so we check for the restoreViewState flag
|
||||
// being set.
|
||||
if (editor === this.input || !this.restoreViewState) {
|
||||
this.doSaveOrClearTextEditorViewState(editor);
|
||||
}
|
||||
}
|
||||
|
||||
setOptions(options: EditorOptions): void {
|
||||
const textOptions = <TextEditorOptions>options;
|
||||
if (textOptions && types.isFunction(textOptions.apply)) {
|
||||
textOptions.apply(this.getControl(), ScrollType.Smooth);
|
||||
}
|
||||
}
|
||||
|
||||
setInput(input: FileEditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
|
||||
|
||||
// Update/clear view settings if input changes
|
||||
this.doSaveOrClearTextEditorViewState(this.input);
|
||||
|
||||
// Set input and resolve
|
||||
return super.setInput(input, options, token).then(() => {
|
||||
return input.resolve().then(resolvedModel => {
|
||||
|
||||
// Check for cancellation
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// There is a special case where the text editor has to handle binary file editor input: if a binary file
|
||||
// has been resolved and cached before, it maybe an actual instance of BinaryEditorModel. In this case our text
|
||||
// editor has to open this model using the binary editor. We return early in this case.
|
||||
if (resolvedModel instanceof BinaryEditorModel) {
|
||||
return this.openAsBinary(input, options);
|
||||
}
|
||||
|
||||
const textFileModel = <ITextFileEditorModel>resolvedModel;
|
||||
|
||||
// Editor
|
||||
const textEditor = this.getControl();
|
||||
textEditor.setModel(textFileModel.textEditorModel);
|
||||
|
||||
// Always restore View State if any associated
|
||||
const editorViewState = this.loadTextEditorViewState(this.input.getResource());
|
||||
if (editorViewState) {
|
||||
textEditor.restoreViewState(editorViewState);
|
||||
}
|
||||
|
||||
// TextOptions (avoiding instanceof here for a reason, do not change!)
|
||||
if (options && types.isFunction((<TextEditorOptions>options).apply)) {
|
||||
(<TextEditorOptions>options).apply(textEditor, ScrollType.Immediate);
|
||||
}
|
||||
|
||||
// Readonly flag
|
||||
textEditor.updateOptions({ readOnly: textFileModel.isReadonly() });
|
||||
}, error => {
|
||||
|
||||
// In case we tried to open a file inside the text editor and the response
|
||||
// indicates that this is not a text file, reopen the file through the binary
|
||||
// editor.
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY) {
|
||||
return this.openAsBinary(input, options);
|
||||
}
|
||||
|
||||
// Similar, handle case where we were asked to open a folder in the text editor.
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) {
|
||||
this.openAsFolder(input);
|
||||
|
||||
return Promise.reject<any>(new Error(nls.localize('openFolderError', "File is a directory")));
|
||||
}
|
||||
|
||||
// Offer to create a file from the error if we have a file not found and the name is valid
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.getResource()))) {
|
||||
return Promise.reject(createErrorWithActions(toErrorMessage(error), {
|
||||
actions: [
|
||||
new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, () => {
|
||||
return this.fileService.updateContent(input.getResource(), '').then(() => this.editorService.openEditor({
|
||||
resource: input.getResource(),
|
||||
options: {
|
||||
pinned: true // new file gets pinned by default
|
||||
}
|
||||
}));
|
||||
})
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_EXCEED_MEMORY_LIMIT) {
|
||||
const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.configurationService.getValue<number>(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB);
|
||||
|
||||
return Promise.reject(createErrorWithActions(toErrorMessage(error), {
|
||||
actions: [
|
||||
new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), undefined, true, () => {
|
||||
return this.windowsService.relaunch({
|
||||
addArgs: [
|
||||
`--max-memory=${memoryLimit}`
|
||||
]
|
||||
});
|
||||
}),
|
||||
new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), undefined, true, () => {
|
||||
return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' });
|
||||
})
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
// Otherwise make sure the error bubbles up
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private openAsBinary(input: FileEditorInput, options: EditorOptions): void {
|
||||
input.setForceOpenAsBinary();
|
||||
this.editorService.openEditor(input, options, this.group);
|
||||
}
|
||||
|
||||
private openAsFolder(input: FileEditorInput): void {
|
||||
if (!this.group) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we cannot open a folder, we have to restore the previous input if any and close the editor
|
||||
this.group.closeEditor(this.input).then(() => {
|
||||
|
||||
// Best we can do is to reveal the folder in the explorer
|
||||
if (this.contextService.isInsideWorkspace(input.getResource())) {
|
||||
this.viewletService.openViewlet(VIEWLET_ID).then(() => {
|
||||
this.explorerService.select(input.getResource(), true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getAriaLabel(): string {
|
||||
const input = this.input;
|
||||
const inputName = input && input.getName();
|
||||
|
||||
let ariaLabel: string;
|
||||
if (inputName) {
|
||||
ariaLabel = nls.localize('fileEditorWithInputAriaLabel', "{0}. Text file editor.", inputName);
|
||||
} else {
|
||||
ariaLabel = nls.localize('fileEditorAriaLabel', "Text file editor.");
|
||||
}
|
||||
|
||||
return ariaLabel;
|
||||
}
|
||||
|
||||
clearInput(): void {
|
||||
|
||||
// Update/clear editor view state in settings
|
||||
this.doSaveOrClearTextEditorViewState(this.input);
|
||||
|
||||
// Clear Model
|
||||
this.getControl().setModel(null);
|
||||
|
||||
// Pass to super
|
||||
super.clearInput();
|
||||
}
|
||||
|
||||
protected saveState(): void {
|
||||
|
||||
// Update/clear editor view State
|
||||
this.doSaveOrClearTextEditorViewState(this.input);
|
||||
|
||||
super.saveState();
|
||||
}
|
||||
|
||||
private doSaveOrClearTextEditorViewState(input: FileEditorInput): void {
|
||||
if (!input) {
|
||||
return; // ensure we have an input to handle view state for
|
||||
}
|
||||
|
||||
// If the user configured to not restore view state, we clear the view
|
||||
// state unless the editor is still opened in the group.
|
||||
if (!this.restoreViewState && (!this.group || !this.group.isOpened(input))) {
|
||||
this.clearTextEditorViewState([input.getResource()], this.group);
|
||||
}
|
||||
|
||||
// Otherwise we save the view state to restore it later
|
||||
else if (!input.isDisposed()) {
|
||||
this.saveTextEditorViewState(input.getResource());
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.groupListener = dispose(this.groupListener);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
245
src/vs/workbench/contrib/files/browser/explorerViewlet.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/explorerviewlet';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, OpenEditorsVisibleCondition, VIEW_CONTAINER } from 'vs/workbench/contrib/files/common/files';
|
||||
import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
|
||||
import { ExplorerView } from 'vs/workbench/contrib/files/browser/views/explorerView';
|
||||
import { EmptyView } from 'vs/workbench/contrib/files/browser/views/emptyView';
|
||||
import { OpenEditorsView } from 'vs/workbench/contrib/files/browser/views/openEditorsView';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService';
|
||||
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorOptions } from 'vs/platform/editor/common/editor';
|
||||
import { IEditorInput, IEditor } from 'vs/workbench/common/editor';
|
||||
import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
private openEditorsVisibleContextKey: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerViews();
|
||||
|
||||
this.openEditorsVisibleContextKey = OpenEditorsVisibleContext.bindTo(contextKeyService);
|
||||
this.updateOpenEditorsVisibility();
|
||||
|
||||
this._register(workspaceContextService.onDidChangeWorkbenchState(() => this.registerViews()));
|
||||
this._register(workspaceContextService.onDidChangeWorkspaceFolders(() => this.registerViews()));
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
|
||||
}
|
||||
|
||||
private registerViews(): void {
|
||||
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
|
||||
const viewDescriptors = viewsRegistry.getViews(VIEW_CONTAINER);
|
||||
|
||||
let viewDescriptorsToRegister: IViewDescriptor[] = [];
|
||||
let viewDescriptorsToDeregister: IViewDescriptor[] = [];
|
||||
|
||||
const openEditorsViewDescriptor = this.createOpenEditorsViewDescriptor();
|
||||
if (!viewDescriptors.some(v => v.id === openEditorsViewDescriptor.id)) {
|
||||
viewDescriptorsToRegister.push(openEditorsViewDescriptor);
|
||||
}
|
||||
|
||||
const explorerViewDescriptor = this.createExplorerViewDescriptor();
|
||||
const registeredExplorerViewDescriptor = viewDescriptors.filter(v => v.id === explorerViewDescriptor.id)[0];
|
||||
const emptyViewDescriptor = this.createEmptyViewDescriptor();
|
||||
const registeredEmptyViewDescriptor = viewDescriptors.filter(v => v.id === emptyViewDescriptor.id)[0];
|
||||
|
||||
if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || this.workspaceContextService.getWorkspace().folders.length === 0) {
|
||||
if (registeredExplorerViewDescriptor) {
|
||||
viewDescriptorsToDeregister.push(registeredExplorerViewDescriptor);
|
||||
}
|
||||
if (!registeredEmptyViewDescriptor) {
|
||||
viewDescriptorsToRegister.push(emptyViewDescriptor);
|
||||
}
|
||||
} else {
|
||||
if (registeredEmptyViewDescriptor) {
|
||||
viewDescriptorsToDeregister.push(registeredEmptyViewDescriptor);
|
||||
}
|
||||
if (!registeredExplorerViewDescriptor) {
|
||||
viewDescriptorsToRegister.push(explorerViewDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
if (viewDescriptorsToRegister.length) {
|
||||
viewsRegistry.registerViews(viewDescriptorsToRegister, VIEW_CONTAINER);
|
||||
}
|
||||
if (viewDescriptorsToDeregister.length) {
|
||||
viewsRegistry.deregisterViews(viewDescriptorsToDeregister, VIEW_CONTAINER);
|
||||
}
|
||||
}
|
||||
|
||||
private createOpenEditorsViewDescriptor(): IViewDescriptor {
|
||||
return {
|
||||
id: OpenEditorsView.ID,
|
||||
name: OpenEditorsView.NAME,
|
||||
ctorDescriptor: { ctor: OpenEditorsView },
|
||||
order: 0,
|
||||
when: OpenEditorsVisibleCondition,
|
||||
canToggleVisibility: true,
|
||||
focusCommand: {
|
||||
id: 'workbench.files.action.focusOpenEditorsView',
|
||||
keybindings: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_E) }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private createEmptyViewDescriptor(): IViewDescriptor {
|
||||
return {
|
||||
id: EmptyView.ID,
|
||||
name: EmptyView.NAME,
|
||||
ctorDescriptor: { ctor: EmptyView },
|
||||
order: 1,
|
||||
canToggleVisibility: false
|
||||
};
|
||||
}
|
||||
|
||||
private createExplorerViewDescriptor(): IViewDescriptor {
|
||||
return {
|
||||
id: ExplorerView.ID,
|
||||
name: localize('folders', "Folders"),
|
||||
ctorDescriptor: { ctor: ExplorerView },
|
||||
order: 1,
|
||||
canToggleVisibility: false
|
||||
};
|
||||
}
|
||||
|
||||
private onConfigurationUpdated(e: IConfigurationChangeEvent): void {
|
||||
if (e.affectsConfiguration('explorer.openEditors.visible')) {
|
||||
this.updateOpenEditorsVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private updateOpenEditorsVisibility(): void {
|
||||
this.openEditorsVisibleContextKey.set(this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || this.configurationService.getValue('explorer.openEditors.visible') !== 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExplorerViewlet extends ViewContainerViewlet {
|
||||
|
||||
private static readonly EXPLORER_VIEWS_STATE = 'workbench.explorer.views.state';
|
||||
|
||||
private viewletVisibleContextKey: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
|
||||
@IStorageService protected storageService: IStorageService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IInstantiationService protected instantiationService: IInstantiationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IExtensionService extensionService: IExtensionService
|
||||
) {
|
||||
super(VIEWLET_ID, ExplorerViewlet.EXPLORER_VIEWS_STATE, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService);
|
||||
|
||||
this.viewletVisibleContextKey = ExplorerViewletVisibleContext.bindTo(contextKeyService);
|
||||
|
||||
this._register(this.contextService.onDidChangeWorkspaceName(e => this.updateTitleArea()));
|
||||
}
|
||||
|
||||
create(parent: HTMLElement): void {
|
||||
super.create(parent);
|
||||
DOM.addClass(parent, 'explorer-viewlet');
|
||||
}
|
||||
|
||||
protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewletPanel {
|
||||
if (viewDescriptor.id === ExplorerView.ID) {
|
||||
// Create a delegating editor service for the explorer to be able to delay the refresh in the opened
|
||||
// editors view above. This is a workaround for being able to double click on a file to make it pinned
|
||||
// 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((group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise<IEditor | null> => {
|
||||
let openEditorsView = this.getOpenEditorsView();
|
||||
if (openEditorsView) {
|
||||
let delay = 0;
|
||||
|
||||
const config = this.configurationService.getValue<IFilesConfiguration>();
|
||||
const delayEditorOpeningInOpenedEditors = !!config.workbench.editor.enablePreview; // No need to delay if preview is disabled
|
||||
|
||||
const activeGroup = this.editorGroupService.activeGroup;
|
||||
if (delayEditorOpeningInOpenedEditors && group === activeGroup && !activeGroup.previewEditor) {
|
||||
delay = 250; // a new editor entry is likely because there is either no group or no preview in group
|
||||
}
|
||||
|
||||
openEditorsView.setStructuralRefreshDelay(delay);
|
||||
}
|
||||
|
||||
const onSuccessOrError = (editor: BaseEditor | null) => {
|
||||
const openEditorsView = this.getOpenEditorsView();
|
||||
if (openEditorsView) {
|
||||
openEditorsView.setStructuralRefreshDelay(0);
|
||||
}
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
return this.editorService.openEditor(editor, options, group).then(onSuccessOrError, onSuccessOrError);
|
||||
});
|
||||
|
||||
const explorerInstantiator = this.instantiationService.createChild(new ServiceCollection([IEditorService, delegatingEditorService]));
|
||||
return explorerInstantiator.createInstance(ExplorerView, options);
|
||||
}
|
||||
return super.createView(viewDescriptor, options);
|
||||
}
|
||||
|
||||
public getExplorerView(): ExplorerView {
|
||||
return <ExplorerView>this.getView(ExplorerView.ID);
|
||||
}
|
||||
|
||||
public getOpenEditorsView(): OpenEditorsView {
|
||||
return <OpenEditorsView>this.getView(OpenEditorsView.ID);
|
||||
}
|
||||
|
||||
public getEmptyView(): EmptyView {
|
||||
return <EmptyView>this.getView(EmptyView.ID);
|
||||
}
|
||||
|
||||
public setVisible(visible: boolean): void {
|
||||
this.viewletVisibleContextKey.set(visible);
|
||||
super.setVisible(visible);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
const explorerView = this.getView(ExplorerView.ID);
|
||||
if (explorerView && explorerView.isExpanded()) {
|
||||
explorerView.focus();
|
||||
} else {
|
||||
super.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, ShowOpenedFileInNewWindow, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler } from 'vs/workbench/contrib/files/browser/fileActions';
|
||||
import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/saveErrorHandler';
|
||||
import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions';
|
||||
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
|
||||
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { openWindowCommand, REVEAL_IN_OS_COMMAND_ID, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, REVEAL_IN_OS_LABEL, DirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands';
|
||||
import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService } from 'vs/workbench/contrib/files/common/files';
|
||||
import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from 'vs/workbench/browser/actions/workspaceCommands';
|
||||
import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
|
||||
import { AutoSaveContext } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ResourceContextKey } from 'vs/workbench/common/resources';
|
||||
import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { SupportsWorkspacesContext } from 'vs/workbench/common/contextkeys';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
// Contribute Global Actions
|
||||
const category = nls.localize('filesCategory', "File");
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(SaveAllAction, SaveAllAction.ID, SaveAllAction.LABEL, { primary: undefined, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_S }, win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_S) } }), 'File: Save All', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalCompareResourcesAction, GlobalCompareResourcesAction.ID, GlobalCompareResourcesAction.LABEL), 'File: Compare Active File With...', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(FocusFilesExplorer, FocusFilesExplorer.ID, FocusFilesExplorer.LABEL), 'File: Focus on Files Explorer', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowActiveFileInExplorer, ShowActiveFileInExplorer.ID, ShowActiveFileInExplorer.LABEL), 'File: Reveal Active File in Side Bar', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(CollapseExplorerView, CollapseExplorerView.ID, CollapseExplorerView.LABEL), 'File: Collapse Folders in Explorer', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL), 'File: Refresh Explorer', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalNewUntitledFileAction, GlobalNewUntitledFileAction.ID, GlobalNewUntitledFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_N }), 'File: New Untitled File', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowOpenedFileInNewWindow, ShowOpenedFileInNewWindow.ID, ShowOpenedFileInNewWindow.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(CompareWithClipboardAction, CompareWithClipboardAction.ID, CompareWithClipboardAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleAutoSaveAction, ToggleAutoSaveAction.ID, ToggleAutoSaveAction.LABEL), 'File: Toggle Auto Save', category);
|
||||
|
||||
// Commands
|
||||
CommandsRegistry.registerCommand('_files.windowOpen', openWindowCommand);
|
||||
|
||||
const explorerCommandsWeightBonus = 10; // give our commands a little bit more weight over other default list/tree commands
|
||||
|
||||
const RENAME_ID = 'renameFile';
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: RENAME_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext),
|
||||
primary: KeyCode.F2,
|
||||
mac: {
|
||||
primary: KeyCode.Enter
|
||||
},
|
||||
handler: renameHandler
|
||||
});
|
||||
|
||||
const MOVE_FILE_TO_TRASH_ID = 'moveFileToTrash';
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: MOVE_FILE_TO_TRASH_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.has('config.files.enableTrash')),
|
||||
primary: KeyCode.Delete,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Backspace
|
||||
},
|
||||
handler: moveFileToTrashHandler
|
||||
});
|
||||
|
||||
const DELETE_FILE_ID = 'deleteFile';
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: DELETE_FILE_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.has('config.files.enableTrash')),
|
||||
primary: KeyMod.Shift | KeyCode.Delete,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Backspace
|
||||
},
|
||||
handler: deleteFileHandler
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: DELETE_FILE_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated(), ExplorerResourceNotReadonlyContext, ContextKeyExpr.not('config.files.enableTrash')),
|
||||
primary: KeyCode.Delete,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Backspace
|
||||
},
|
||||
handler: deleteFileHandler
|
||||
});
|
||||
|
||||
const CUT_FILE_ID = 'filesExplorer.cut';
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: CUT_FILE_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated()),
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_X,
|
||||
handler: cutFileHandler,
|
||||
});
|
||||
|
||||
const COPY_FILE_ID = 'filesExplorer.copy';
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: COPY_FILE_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerRootContext.toNegated()),
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
|
||||
handler: copyFileHandler,
|
||||
});
|
||||
|
||||
const PASTE_FILE_ID = 'filesExplorer.paste';
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: PASTE_FILE_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceNotReadonlyContext),
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
|
||||
handler: pasteFileHandler
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'filesExplorer.cancelCut',
|
||||
weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus,
|
||||
when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceCut),
|
||||
primary: KeyCode.Escape,
|
||||
handler: (accessor: ServicesAccessor) => {
|
||||
const explorerService = accessor.get(IExplorerService);
|
||||
explorerService.setToCopy([], true);
|
||||
}
|
||||
});
|
||||
|
||||
const copyPathCommand = {
|
||||
id: COPY_PATH_COMMAND_ID,
|
||||
title: nls.localize('copyPath', "Copy Path")
|
||||
};
|
||||
|
||||
const copyRelativePathCommand = {
|
||||
id: COPY_RELATIVE_PATH_COMMAND_ID,
|
||||
title: nls.localize('copyRelativePath', "Copy Relative Path")
|
||||
};
|
||||
|
||||
// Editor Title Context Menu
|
||||
appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste');
|
||||
appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste');
|
||||
appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, ResourceContextKey.Scheme.isEqualTo(Schemas.file));
|
||||
appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Side Bar"), ResourceContextKey.IsFileSystemResource);
|
||||
|
||||
function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpr, group?: string): void {
|
||||
|
||||
// Menu
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, {
|
||||
command: { id, title },
|
||||
when,
|
||||
group: group || '2_files'
|
||||
});
|
||||
}
|
||||
|
||||
// Editor Title Menu for Conflict Resolution
|
||||
appendSaveConflictEditorTitleAction('workbench.files.action.acceptLocalChanges', nls.localize('acceptLocalChanges', "Use your changes and overwrite disk contents"), {
|
||||
light: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/check.svg`)),
|
||||
dark: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/check-inverse.svg`))
|
||||
}, -10, acceptLocalChangesCommand);
|
||||
appendSaveConflictEditorTitleAction('workbench.files.action.revertLocalChanges', nls.localize('revertLocalChanges', "Discard your changes and revert to content on disk"), {
|
||||
light: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/undo.svg`)),
|
||||
dark: URI.parse(require.toUrl(`vs/workbench/contrib/files/browser/media/undo-inverse.svg`))
|
||||
}, -9, revertLocalChangesCommand);
|
||||
|
||||
function appendSaveConflictEditorTitleAction(id: string, title: string, iconLocation: { dark: URI; light?: URI; }, order: number, command: ICommandHandler): void {
|
||||
|
||||
// Command
|
||||
CommandsRegistry.registerCommand(id, command);
|
||||
|
||||
// Action
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
|
||||
command: { id, title, iconLocation },
|
||||
when: ContextKeyExpr.equals(CONFLICT_RESOLUTION_CONTEXT, true),
|
||||
group: 'navigation',
|
||||
order
|
||||
});
|
||||
}
|
||||
|
||||
// Menu registration - command palette
|
||||
|
||||
function appendToCommandPalette(id: string, title: ILocalizedString, category: string, when?: ContextKeyExpr): void {
|
||||
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
|
||||
command: {
|
||||
id,
|
||||
title,
|
||||
category
|
||||
},
|
||||
when
|
||||
});
|
||||
}
|
||||
appendToCommandPalette(COPY_PATH_COMMAND_ID, { value: nls.localize('copyPathOfActive', "Copy Path of Active File"), original: 'File: Copy Path of Active File' }, category);
|
||||
appendToCommandPalette(COPY_RELATIVE_PATH_COMMAND_ID, { value: nls.localize('copyRelativePathOfActive', "Copy Relative Path of Active File"), original: 'File: Copy Relative Path of Active File' }, category);
|
||||
appendToCommandPalette(SAVE_FILE_COMMAND_ID, { value: SAVE_FILE_LABEL, original: 'File: Save' }, category);
|
||||
appendToCommandPalette(SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, { value: SAVE_FILE_WITHOUT_FORMATTING_LABEL, original: 'File: Save without Formatting' }, category);
|
||||
appendToCommandPalette(SAVE_ALL_IN_GROUP_COMMAND_ID, { value: nls.localize('saveAllInGroup', "Save All in Group"), original: 'File: Save All in Group' }, category);
|
||||
appendToCommandPalette(SAVE_FILES_COMMAND_ID, { value: nls.localize('saveFiles', "Save All Files"), original: 'File: Save All Files' }, category);
|
||||
appendToCommandPalette(REVERT_FILE_COMMAND_ID, { value: nls.localize('revert', "Revert File"), original: 'File: Revert File' }, category);
|
||||
appendToCommandPalette(COMPARE_WITH_SAVED_COMMAND_ID, { value: nls.localize('compareActiveWithSaved', "Compare Active File with Saved"), original: 'File: Compare Active File with Saved' }, category);
|
||||
appendToCommandPalette(REVEAL_IN_OS_COMMAND_ID, { value: REVEAL_IN_OS_LABEL, original: isWindows ? 'File: Reveal in Explorer' : isMacintosh ? 'File: Reveal in Finder' : 'File: Open Containing Folder' }, category);
|
||||
appendToCommandPalette(SAVE_FILE_AS_COMMAND_ID, { value: SAVE_FILE_AS_LABEL, original: 'File: Save As...' }, category);
|
||||
appendToCommandPalette(CLOSE_EDITOR_COMMAND_ID, { value: nls.localize('closeEditor', "Close Editor"), original: 'View: Close Editor' }, nls.localize('view', "View"));
|
||||
appendToCommandPalette(NEW_FILE_COMMAND_ID, { value: NEW_FILE_LABEL, original: 'File: New File' }, category);
|
||||
appendToCommandPalette(NEW_FOLDER_COMMAND_ID, { value: NEW_FOLDER_LABEL, original: 'File: New Folder' }, category);
|
||||
|
||||
// Menu registration - open editors
|
||||
|
||||
const openToSideCommand = {
|
||||
id: OPEN_TO_SIDE_COMMAND_ID,
|
||||
title: nls.localize('openToSide', "Open to the Side")
|
||||
};
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: 'navigation',
|
||||
order: 10,
|
||||
command: openToSideCommand,
|
||||
when: ResourceContextKey.IsFileSystemResource
|
||||
});
|
||||
|
||||
const revealInOsCommand = {
|
||||
id: REVEAL_IN_OS_COMMAND_ID,
|
||||
title: isWindows ? nls.localize('revealInWindows', "Reveal in Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder")
|
||||
};
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: 'navigation',
|
||||
order: 20,
|
||||
command: revealInOsCommand,
|
||||
when: ResourceContextKey.IsFileSystemResource
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '1_cutcopypaste',
|
||||
order: 10,
|
||||
command: copyPathCommand,
|
||||
when: ResourceContextKey.IsFileSystemResource
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '1_cutcopypaste',
|
||||
order: 20,
|
||||
command: copyRelativePathCommand,
|
||||
when: ResourceContextKey.IsFileSystemResource
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '2_save',
|
||||
order: 10,
|
||||
command: {
|
||||
id: SAVE_FILE_COMMAND_ID,
|
||||
title: SAVE_FILE_LABEL,
|
||||
precondition: DirtyEditorContext
|
||||
},
|
||||
when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo(''))
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '2_save',
|
||||
order: 20,
|
||||
command: {
|
||||
id: REVERT_FILE_COMMAND_ID,
|
||||
title: nls.localize('revert', "Revert File"),
|
||||
precondition: DirtyEditorContext
|
||||
},
|
||||
when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo(''))
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '2_save',
|
||||
command: {
|
||||
id: SAVE_FILE_AS_COMMAND_ID,
|
||||
title: SAVE_FILE_AS_LABEL
|
||||
},
|
||||
when: ResourceContextKey.Scheme.isEqualTo(Schemas.untitled)
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '2_save',
|
||||
command: {
|
||||
id: SAVE_ALL_IN_GROUP_COMMAND_ID,
|
||||
title: nls.localize('saveAll', "Save All")
|
||||
},
|
||||
when: ContextKeyExpr.and(OpenEditorsGroupContext, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo(''))
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '3_compare',
|
||||
order: 10,
|
||||
command: {
|
||||
id: COMPARE_WITH_SAVED_COMMAND_ID,
|
||||
title: nls.localize('compareWithSaved', "Compare with Saved"),
|
||||
precondition: DirtyEditorContext
|
||||
},
|
||||
when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo(''), WorkbenchListDoubleSelection.toNegated())
|
||||
});
|
||||
|
||||
const compareResourceCommand = {
|
||||
id: COMPARE_RESOURCE_COMMAND_ID,
|
||||
title: nls.localize('compareWithSelected', "Compare with Selected")
|
||||
};
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '3_compare',
|
||||
order: 20,
|
||||
command: compareResourceCommand,
|
||||
when: ContextKeyExpr.and(ResourceContextKey.HasResource, ResourceSelectedForCompareContext, WorkbenchListDoubleSelection.toNegated())
|
||||
});
|
||||
|
||||
const selectForCompareCommand = {
|
||||
id: SELECT_FOR_COMPARE_COMMAND_ID,
|
||||
title: nls.localize('compareSource', "Select for Compare")
|
||||
};
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '3_compare',
|
||||
order: 30,
|
||||
command: selectForCompareCommand,
|
||||
when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection.toNegated())
|
||||
});
|
||||
|
||||
const compareSelectedCommand = {
|
||||
id: COMPARE_SELECTED_COMMAND_ID,
|
||||
title: nls.localize('compareSelected', "Compare Selected")
|
||||
};
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '3_compare',
|
||||
order: 30,
|
||||
command: compareSelectedCommand,
|
||||
when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection)
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '4_close',
|
||||
order: 10,
|
||||
command: {
|
||||
id: CLOSE_EDITOR_COMMAND_ID,
|
||||
title: nls.localize('close', "Close")
|
||||
},
|
||||
when: OpenEditorsGroupContext.toNegated()
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '4_close',
|
||||
order: 20,
|
||||
command: {
|
||||
id: CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID,
|
||||
title: nls.localize('closeOthers', "Close Others")
|
||||
},
|
||||
when: OpenEditorsGroupContext.toNegated()
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '4_close',
|
||||
order: 30,
|
||||
command: {
|
||||
id: CLOSE_SAVED_EDITORS_COMMAND_ID,
|
||||
title: nls.localize('closeSaved', "Close Saved")
|
||||
}
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
|
||||
group: '4_close',
|
||||
order: 40,
|
||||
command: {
|
||||
id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID,
|
||||
title: nls.localize('closeAll', "Close All")
|
||||
}
|
||||
});
|
||||
|
||||
// Menu registration - explorer
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: 'navigation',
|
||||
order: 4,
|
||||
command: {
|
||||
id: NEW_FILE_COMMAND_ID,
|
||||
title: NEW_FILE_LABEL,
|
||||
precondition: ExplorerResourceNotReadonlyContext
|
||||
},
|
||||
when: ExplorerFolderContext
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: 'navigation',
|
||||
order: 6,
|
||||
command: {
|
||||
id: NEW_FOLDER_COMMAND_ID,
|
||||
title: NEW_FOLDER_LABEL,
|
||||
precondition: ExplorerResourceNotReadonlyContext
|
||||
},
|
||||
when: ExplorerFolderContext
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: 'navigation',
|
||||
order: 10,
|
||||
command: openToSideCommand,
|
||||
when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource)
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: 'navigation',
|
||||
order: 20,
|
||||
command: revealInOsCommand,
|
||||
when: ResourceContextKey.Scheme.isEqualTo(Schemas.file)
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '3_compare',
|
||||
order: 20,
|
||||
command: compareResourceCommand,
|
||||
when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource, ResourceSelectedForCompareContext, WorkbenchListDoubleSelection.toNegated())
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '3_compare',
|
||||
order: 30,
|
||||
command: selectForCompareCommand,
|
||||
when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource, WorkbenchListDoubleSelection.toNegated())
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '3_compare',
|
||||
order: 30,
|
||||
command: compareSelectedCommand,
|
||||
when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource, WorkbenchListDoubleSelection)
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '5_cutcopypaste',
|
||||
order: 8,
|
||||
command: {
|
||||
id: CUT_FILE_ID,
|
||||
title: nls.localize('cut', "Cut")
|
||||
},
|
||||
when: ExplorerRootContext.toNegated()
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '5_cutcopypaste',
|
||||
order: 10,
|
||||
command: {
|
||||
id: COPY_FILE_ID,
|
||||
title: COPY_FILE_LABEL
|
||||
},
|
||||
when: ExplorerRootContext.toNegated()
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '5_cutcopypaste',
|
||||
order: 20,
|
||||
command: {
|
||||
id: PASTE_FILE_ID,
|
||||
title: PASTE_FILE_LABEL,
|
||||
precondition: ContextKeyExpr.and(ExplorerResourceNotReadonlyContext, FileCopiedContext)
|
||||
},
|
||||
when: ExplorerFolderContext
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '6_copypath',
|
||||
order: 30,
|
||||
command: copyPathCommand,
|
||||
when: ResourceContextKey.IsFileSystemResource
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '6_copypath',
|
||||
order: 30,
|
||||
command: copyRelativePathCommand,
|
||||
when: ResourceContextKey.IsFileSystemResource
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '2_workspace',
|
||||
order: 10,
|
||||
command: {
|
||||
id: ADD_ROOT_FOLDER_COMMAND_ID,
|
||||
title: ADD_ROOT_FOLDER_LABEL
|
||||
},
|
||||
when: ContextKeyExpr.and(ExplorerRootContext, SupportsWorkspacesContext)
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '2_workspace',
|
||||
order: 30,
|
||||
command: {
|
||||
id: REMOVE_ROOT_FOLDER_COMMAND_ID,
|
||||
title: REMOVE_ROOT_FOLDER_LABEL
|
||||
},
|
||||
when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext)
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '7_modification',
|
||||
order: 10,
|
||||
command: {
|
||||
id: RENAME_ID,
|
||||
title: TRIGGER_RENAME_LABEL,
|
||||
precondition: ExplorerResourceNotReadonlyContext
|
||||
},
|
||||
when: ExplorerRootContext.toNegated()
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '7_modification',
|
||||
order: 20,
|
||||
command: {
|
||||
id: MOVE_FILE_TO_TRASH_ID,
|
||||
title: MOVE_FILE_TO_TRASH_LABEL,
|
||||
precondition: ExplorerResourceNotReadonlyContext
|
||||
},
|
||||
alt: {
|
||||
id: DELETE_FILE_ID,
|
||||
title: nls.localize('deleteFile', "Delete Permanently"),
|
||||
precondition: ExplorerResourceNotReadonlyContext
|
||||
},
|
||||
when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ContextKeyExpr.has('config.files.enableTrash'))
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerContext, {
|
||||
group: '7_modification',
|
||||
order: 20,
|
||||
command: {
|
||||
id: DELETE_FILE_ID,
|
||||
title: nls.localize('deleteFile', "Delete Permanently"),
|
||||
precondition: ExplorerResourceNotReadonlyContext
|
||||
},
|
||||
when: ContextKeyExpr.and(ExplorerRootContext.toNegated(), ContextKeyExpr.not('config.files.enableTrash'))
|
||||
});
|
||||
|
||||
// Empty Editor Group Context Menu
|
||||
// {{SQL CARBON EDIT}} - Use "New Query" instead of "New File"
|
||||
MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: GlobalNewUntitledFileAction.ID, title: nls.localize('newFile', "New Query") }, group: '1_file', order: 10 });
|
||||
MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.quickOpen', title: nls.localize('openFile', "Open File...") }, group: '1_file', order: 20 });
|
||||
|
||||
// File menu
|
||||
|
||||
// {{SQL CARBON EDIT}} - Use "New Query" instead of "New File"
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
|
||||
group: '1_new',
|
||||
command: {
|
||||
id: GlobalNewUntitledFileAction.ID,
|
||||
title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New Query")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
||||
// {{SQL CARBON EDIT}} - Add "New Notebook" item
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
|
||||
group: '1_new',
|
||||
command: {
|
||||
id: 'notebook.command.new',
|
||||
title: nls.localize({key: 'miNewNotebook', comment: ['&& denotes a mnemonic'] }, "&&New Notebook")
|
||||
},
|
||||
order: 1.1
|
||||
});
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
|
||||
group: '4_save',
|
||||
command: {
|
||||
id: SAVE_FILE_COMMAND_ID,
|
||||
title: nls.localize({ key: 'miSave', comment: ['&& denotes a mnemonic'] }, "&&Save")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
|
||||
group: '4_save',
|
||||
command: {
|
||||
id: SAVE_FILE_AS_COMMAND_ID,
|
||||
title: nls.localize({ key: 'miSaveAs', comment: ['&& denotes a mnemonic'] }, "Save &&As...")
|
||||
},
|
||||
order: 2
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
|
||||
group: '4_save',
|
||||
command: {
|
||||
id: SaveAllAction.ID,
|
||||
title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll")
|
||||
},
|
||||
order: 3
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
|
||||
group: '5_autosave',
|
||||
command: {
|
||||
id: ToggleAutoSaveAction.ID,
|
||||
title: nls.localize({ key: 'miAutoSave', comment: ['&& denotes a mnemonic'] }, "A&&uto Save"),
|
||||
toggled: ContextKeyExpr.notEquals('config.files.autoSave', 'off')
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
|
||||
group: '6_close',
|
||||
command: {
|
||||
id: REVERT_FILE_COMMAND_ID,
|
||||
title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
|
||||
group: '6_close',
|
||||
command: {
|
||||
id: CLOSE_EDITOR_COMMAND_ID,
|
||||
title: nls.localize({ key: 'miCloseEditor', comment: ['&& denotes a mnemonic'] }, "&&Close Editor")
|
||||
},
|
||||
order: 2
|
||||
});
|
||||
|
||||
// Go to menu
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
|
||||
group: '3_global_nav',
|
||||
command: {
|
||||
id: 'workbench.action.quickOpen',
|
||||
title: nls.localize({ key: 'miGotoFile', comment: ['&& denotes a mnemonic'] }, "Go to &&File...")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
1210
src/vs/workbench/contrib/files/browser/fileActions.ts
Normal file
617
src/vs/workbench/contrib/files/browser/fileCommands.ts
Normal file
@@ -0,0 +1,617 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
// {{SQL CARBON EDIT}} - Import EditorInput
|
||||
import { toResource, IEditorCommandsContext, EditorInput } from 'vs/workbench/common/editor';
|
||||
import { IWindowsService, IWindowService, IURIToOpen, IOpenSettings } from 'vs/platform/windows/common/windows';
|
||||
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { ExplorerFocusCondition, FileOnDiskContentProvider, VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
|
||||
import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { IListService } from 'vs/platform/list/browser/listService';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IResourceInput } from 'vs/platform/editor/common/editor';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IEditorViewState } from 'vs/editor/common/editorCommon';
|
||||
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import { isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { sequence } from 'vs/base/common/async';
|
||||
import { getResourceForCommand, getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files';
|
||||
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
|
||||
import { getMultiSelectedEditorContexts } from 'vs/workbench/browser/parts/editor/editorCommands';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
// {{SQL CARBON EDIT}} - Import EditorInput
|
||||
import { IEditorService, SIDE_GROUP, IResourceEditorReplacement } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
|
||||
// Commands
|
||||
|
||||
export const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS';
|
||||
export const REVEAL_IN_OS_LABEL = isWindows ? nls.localize('revealInWindows', "Reveal in Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder");
|
||||
export const REVEAL_IN_EXPLORER_COMMAND_ID = 'revealInExplorer';
|
||||
export const REVERT_FILE_COMMAND_ID = 'workbench.action.files.revert';
|
||||
export const OPEN_TO_SIDE_COMMAND_ID = 'explorer.openToSide';
|
||||
export const SELECT_FOR_COMPARE_COMMAND_ID = 'selectForCompare';
|
||||
|
||||
export const COMPARE_SELECTED_COMMAND_ID = 'compareSelected';
|
||||
export const COMPARE_RESOURCE_COMMAND_ID = 'compareFiles';
|
||||
export const COMPARE_WITH_SAVED_COMMAND_ID = 'workbench.files.action.compareWithSaved';
|
||||
export const COPY_PATH_COMMAND_ID = 'copyFilePath';
|
||||
export const COPY_RELATIVE_PATH_COMMAND_ID = 'copyRelativeFilePath';
|
||||
|
||||
export const SAVE_FILE_AS_COMMAND_ID = 'workbench.action.files.saveAs';
|
||||
export const SAVE_FILE_AS_LABEL = nls.localize('saveAs', "Save As...");
|
||||
export const SAVE_FILE_COMMAND_ID = 'workbench.action.files.save';
|
||||
export const SAVE_FILE_LABEL = nls.localize('save', "Save");
|
||||
export const SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID = 'workbench.action.files.saveWithoutFormatting';
|
||||
export const SAVE_FILE_WITHOUT_FORMATTING_LABEL = nls.localize('saveWithoutFormatting', "Save without Formatting");
|
||||
|
||||
export const SAVE_ALL_COMMAND_ID = 'saveAll';
|
||||
export const SAVE_ALL_LABEL = nls.localize('saveAll', "Save All");
|
||||
|
||||
export const SAVE_ALL_IN_GROUP_COMMAND_ID = 'workbench.files.action.saveAllInGroup';
|
||||
|
||||
export const SAVE_FILES_COMMAND_ID = 'workbench.action.files.saveFiles';
|
||||
|
||||
export const OpenEditorsGroupContext = new RawContextKey<boolean>('groupFocusedInOpenEditors', false);
|
||||
export const DirtyEditorContext = new RawContextKey<boolean>('dirtyEditor', false);
|
||||
export const ResourceSelectedForCompareContext = new RawContextKey<boolean>('resourceSelectedForCompare', false);
|
||||
|
||||
export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder';
|
||||
export const REMOVE_ROOT_FOLDER_LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace");
|
||||
|
||||
export const openWindowCommand = (accessor: ServicesAccessor, urisToOpen: IURIToOpen[], options?: IOpenSettings) => {
|
||||
if (Array.isArray(urisToOpen)) {
|
||||
const windowService = accessor.get(IWindowService);
|
||||
windowService.openWindow(urisToOpen, options);
|
||||
}
|
||||
};
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
function save(
|
||||
resource: URI | null,
|
||||
isSaveAs: boolean,
|
||||
options: ISaveOptions | undefined,
|
||||
editorService: IEditorService,
|
||||
fileService: IFileService,
|
||||
untitledEditorService: IUntitledEditorService,
|
||||
textFileService: ITextFileService,
|
||||
editorGroupService: IEditorGroupsService,
|
||||
queryEditorService: IQueryEditorService
|
||||
): Promise<any> {
|
||||
|
||||
function ensureForcedSave(options?: ISaveOptions): ISaveOptions {
|
||||
if (!options) {
|
||||
options = { force: true };
|
||||
} else {
|
||||
options.force = true;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
if (resource && (fileService.canHandleResource(resource) || resource.scheme === Schemas.untitled)) {
|
||||
// {{SQL CARBON EDIT}}
|
||||
let editorInput = editorService.activeEditor;
|
||||
if (editorInput instanceof EditorInput && !(<EditorInput>editorInput).savingSupported) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// Save As (or Save untitled with associated path)
|
||||
if (isSaveAs || resource.scheme === Schemas.untitled) {
|
||||
let encodingOfSource: string | undefined;
|
||||
if (resource.scheme === Schemas.untitled) {
|
||||
encodingOfSource = untitledEditorService.getEncoding(resource);
|
||||
} else if (fileService.canHandleResource(resource)) {
|
||||
const textModel = textFileService.models.get(resource);
|
||||
encodingOfSource = textModel && textModel.getEncoding(); // text model can be null e.g. if this is a binary file!
|
||||
}
|
||||
|
||||
let viewStateOfSource: IEditorViewState | null;
|
||||
const activeTextEditorWidget = getCodeEditor(editorService.activeTextEditorWidget);
|
||||
if (activeTextEditorWidget) {
|
||||
const activeResource = toResource(editorService.activeEditor || null, { supportSideBySide: true });
|
||||
if (activeResource && (fileService.canHandleResource(activeResource) || resource.scheme === Schemas.untitled) && activeResource.toString() === resource.toString()) {
|
||||
viewStateOfSource = activeTextEditorWidget.saveViewState();
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: an untitled file with associated path gets saved directly unless "saveAs" is true
|
||||
let savePromise: Promise<URI | undefined>;
|
||||
if (!isSaveAs && resource.scheme === Schemas.untitled && untitledEditorService.hasAssociatedFilePath(resource)) {
|
||||
savePromise = textFileService.save(resource, options).then((result) => {
|
||||
if (result) {
|
||||
return resource.with({ scheme: Schemas.file });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, really "Save As..."
|
||||
else {
|
||||
|
||||
// Force a change to the file to trigger external watchers if any
|
||||
// fixes https://github.com/Microsoft/vscode/issues/59655
|
||||
options = ensureForcedSave(options);
|
||||
|
||||
savePromise = textFileService.saveAs(resource, undefined, options);
|
||||
}
|
||||
|
||||
return savePromise.then((target) => {
|
||||
if (!target || target.toString() === resource.toString()) {
|
||||
return false; // save canceled or same resource used
|
||||
}
|
||||
|
||||
const replacement: IResourceInput = {
|
||||
resource: target,
|
||||
encoding: encodingOfSource,
|
||||
options: {
|
||||
pinned: true,
|
||||
viewState: viewStateOfSource || undefined
|
||||
}
|
||||
};
|
||||
|
||||
return Promise.all(editorGroupService.groups.map(g =>
|
||||
editorService.replaceEditors([{
|
||||
editor: { resource },
|
||||
replacement
|
||||
}], g))).then(() => {
|
||||
// {{SQL CARBON EDIT}}
|
||||
queryEditorService.onSaveAsCompleted(resource, target);
|
||||
return true;
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// Pin the active editor if we are saving it
|
||||
const activeControl = editorService.activeControl;
|
||||
const activeEditorResource = activeControl && activeControl.input && activeControl.input.getResource();
|
||||
if (activeControl && activeEditorResource && activeEditorResource.toString() === resource.toString()) {
|
||||
activeControl.group.pinEditor(activeControl.input);
|
||||
}
|
||||
|
||||
// Just save (force a change to the file to trigger external watchers if any)
|
||||
options = ensureForcedSave(options);
|
||||
|
||||
return textFileService.save(resource, options);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
function saveAll(saveAllArguments: any, editorService: IEditorService, untitledEditorService: IUntitledEditorService,
|
||||
textFileService: ITextFileService, editorGroupService: IEditorGroupsService): Promise<any> {
|
||||
|
||||
// Store some properties per untitled file to restore later after save is completed
|
||||
const groupIdToUntitledResourceInput = new Map<number, IResourceInput[]>();
|
||||
|
||||
editorGroupService.groups.forEach(g => {
|
||||
const activeEditorResource = g.activeEditor && g.activeEditor.getResource();
|
||||
g.editors.forEach(e => {
|
||||
const resource = e.getResource();
|
||||
if (resource && untitledEditorService.isDirty(resource)) {
|
||||
if (!groupIdToUntitledResourceInput.has(g.id)) {
|
||||
groupIdToUntitledResourceInput.set(g.id, []);
|
||||
}
|
||||
|
||||
groupIdToUntitledResourceInput.get(g.id)!.push({
|
||||
encoding: untitledEditorService.getEncoding(resource),
|
||||
resource,
|
||||
options: {
|
||||
inactive: activeEditorResource ? activeEditorResource.toString() !== resource.toString() : true,
|
||||
pinned: true,
|
||||
preserveFocus: true,
|
||||
index: g.getIndexOfEditor(e)
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Save all
|
||||
return textFileService.saveAll(saveAllArguments).then((result) => {
|
||||
groupIdToUntitledResourceInput.forEach((inputs, groupId) => {
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Update untitled resources to the saved ones, so we open the proper files
|
||||
|
||||
let replacementPairs: IResourceEditorReplacement[] = [];
|
||||
inputs.forEach(i => {
|
||||
const targetResult = result.results.filter(r => r.success && r.source.toString() === i.resource.toString()).pop();
|
||||
if (targetResult && targetResult.target) {
|
||||
//i.resource = targetResult.target;
|
||||
let editor = i;
|
||||
const replacement: IResourceInput = {
|
||||
resource: targetResult.target,
|
||||
encoding: i.encoding,
|
||||
options: {
|
||||
pinned: true,
|
||||
viewState: undefined
|
||||
}
|
||||
};
|
||||
replacementPairs.push({ editor: editor, replacement: replacement });
|
||||
}
|
||||
});
|
||||
editorService.replaceEditors(replacementPairs, groupId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Command registration
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: REVERT_FILE_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const textFileService = accessor.get(ITextFileService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService)
|
||||
.filter(resource => resource.scheme !== Schemas.untitled);
|
||||
|
||||
if (resources.length) {
|
||||
return textFileService.revertAll(resources, { force: true }).then(undefined, error => {
|
||||
notificationService.error(nls.localize('genericRevertError', "Failed to revert '{0}': {1}", resources.map(r => basename(r)).join(', '), toErrorMessage(error, false)));
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: ExplorerFocusCondition,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Enter,
|
||||
mac: {
|
||||
primary: KeyMod.WinCtrl | KeyCode.Enter
|
||||
},
|
||||
id: OPEN_TO_SIDE_COMMAND_ID, handler: (accessor, resource: URI | object) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const listService = accessor.get(IListService);
|
||||
const fileService = accessor.get(IFileService);
|
||||
const resources = getMultiSelectedResources(resource, listService, editorService);
|
||||
|
||||
// Set side input
|
||||
if (resources.length) {
|
||||
return fileService.resolveFiles(resources.map(resource => ({ resource }))).then(resolved => {
|
||||
const editors = resolved.filter(r => r.stat && r.success && !r.stat.isDirectory).map(r => ({
|
||||
resource: r.stat!.resource
|
||||
}));
|
||||
|
||||
return editorService.openEditors(editors, SIDE_GROUP);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
const COMPARE_WITH_SAVED_SCHEMA = 'showModifications';
|
||||
let provider: FileOnDiskContentProvider;
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: COMPARE_WITH_SAVED_COMMAND_ID,
|
||||
when: undefined,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_D),
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
if (!provider) {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
const textModelService = accessor.get(ITextModelService);
|
||||
provider = instantiationService.createInstance(FileOnDiskContentProvider);
|
||||
textModelService.registerTextModelContentProvider(COMPARE_WITH_SAVED_SCHEMA, provider);
|
||||
}
|
||||
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const uri = getResourceForCommand(resource, accessor.get(IListService), editorService);
|
||||
|
||||
if (uri && uri.scheme === Schemas.file /* only files on disk supported for now */) {
|
||||
const name = basename(uri);
|
||||
const editorLabel = nls.localize('modifiedLabel', "{0} (on disk) ↔ {1}", name, name);
|
||||
|
||||
return editorService.openEditor({ leftResource: uri.with({ scheme: COMPARE_WITH_SAVED_SCHEMA }), rightResource: uri, label: editorLabel }).then(() => undefined);
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
let globalResourceToCompare: URI | null;
|
||||
let resourceSelectedForCompareContext: IContextKey<boolean>;
|
||||
CommandsRegistry.registerCommand({
|
||||
id: SELECT_FOR_COMPARE_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const listService = accessor.get(IListService);
|
||||
|
||||
globalResourceToCompare = getResourceForCommand(resource, listService, accessor.get(IEditorService));
|
||||
if (!resourceSelectedForCompareContext) {
|
||||
resourceSelectedForCompareContext = ResourceSelectedForCompareContext.bindTo(accessor.get(IContextKeyService));
|
||||
}
|
||||
resourceSelectedForCompareContext.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: COMPARE_SELECTED_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService);
|
||||
|
||||
if (resources.length === 2) {
|
||||
return editorService.openEditor({
|
||||
leftResource: resources[0],
|
||||
rightResource: resources[1]
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: COMPARE_RESOURCE_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const listService = accessor.get(IListService);
|
||||
|
||||
const rightResource = getResourceForCommand(resource, listService, editorService);
|
||||
if (globalResourceToCompare && rightResource) {
|
||||
editorService.openEditor({
|
||||
leftResource: globalResourceToCompare,
|
||||
rightResource
|
||||
}).then(undefined, onUnexpectedError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function revealResourcesInOS(resources: URI[], windowsService: IWindowsService, notificationService: INotificationService, workspaceContextService: IWorkspaceContextService): void {
|
||||
if (resources.length) {
|
||||
sequence(resources.map(r => () => windowsService.showItemInFolder(r)));
|
||||
} else if (workspaceContextService.getWorkspace().folders.length) {
|
||||
windowsService.showItemInFolder(workspaceContextService.getWorkspace().folders[0].uri);
|
||||
} else {
|
||||
notificationService.info(nls.localize('openFileToReveal', "Open a file first to reveal"));
|
||||
}
|
||||
}
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: REVEAL_IN_OS_COMMAND_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: EditorContextKeys.focus.toNegated(),
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R,
|
||||
win: {
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_R
|
||||
},
|
||||
handler: (accessor: ServicesAccessor, resource: URI | object) => {
|
||||
const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService));
|
||||
revealResourcesInOS(resources, accessor.get(IWindowsService), accessor.get(INotificationService), accessor.get(IWorkspaceContextService));
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: undefined,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_R),
|
||||
id: 'workbench.action.files.revealActiveFileInWindows',
|
||||
handler: (accessor: ServicesAccessor) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const activeInput = editorService.activeEditor;
|
||||
const resource = activeInput ? activeInput.getResource() : null;
|
||||
const resources = resource ? [resource] : [];
|
||||
revealResourcesInOS(resources, accessor.get(IWindowsService), accessor.get(INotificationService), accessor.get(IWorkspaceContextService));
|
||||
}
|
||||
});
|
||||
|
||||
function resourcesToClipboard(resources: URI[], relative: boolean, clipboardService: IClipboardService, notificationService: INotificationService, labelService: ILabelService): void {
|
||||
if (resources.length) {
|
||||
const lineDelimiter = isWindows ? '\r\n' : '\n';
|
||||
|
||||
const text = resources.map(resource => labelService.getUriLabel(resource, { relative, noPrefix: true }))
|
||||
.join(lineDelimiter);
|
||||
clipboardService.writeText(text);
|
||||
} else {
|
||||
notificationService.info(nls.localize('openFileToCopy', "Open a file first to copy its path"));
|
||||
}
|
||||
}
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: EditorContextKeys.focus.toNegated(),
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C,
|
||||
win: {
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C
|
||||
},
|
||||
id: COPY_PATH_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService));
|
||||
resourcesToClipboard(resources, false, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService));
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: EditorContextKeys.focus.toNegated(),
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_C,
|
||||
win: {
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C)
|
||||
},
|
||||
id: COPY_RELATIVE_PATH_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService));
|
||||
resourcesToClipboard(resources, true, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService));
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: undefined,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_P),
|
||||
id: 'workbench.action.files.copyPathOfActiveFile',
|
||||
handler: (accessor) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const activeInput = editorService.activeEditor;
|
||||
const resource = activeInput ? activeInput.getResource() : null;
|
||||
const resources = resource ? [resource] : [];
|
||||
resourcesToClipboard(resources, false, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService));
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: REVEAL_IN_EXPLORER_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const viewletService = accessor.get(IViewletService);
|
||||
const contextService = accessor.get(IWorkspaceContextService);
|
||||
const explorerService = accessor.get(IExplorerService);
|
||||
const uri = getResourceForCommand(resource, accessor.get(IListService), accessor.get(IEditorService));
|
||||
|
||||
viewletService.openViewlet(VIEWLET_ID, false).then((viewlet: ExplorerViewlet) => {
|
||||
if (uri && contextService.isInsideWorkspace(uri)) {
|
||||
const explorerView = viewlet.getExplorerView();
|
||||
if (explorerView) {
|
||||
explorerView.setExpanded(true);
|
||||
explorerService.select(uri, true).then(() => explorerView.focus(), onUnexpectedError);
|
||||
}
|
||||
} else {
|
||||
const openEditorsView = viewlet.getOpenEditorsView();
|
||||
if (openEditorsView) {
|
||||
openEditorsView.setExpanded(true);
|
||||
openEditorsView.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: SAVE_FILE_AS_COMMAND_ID,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: undefined,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S,
|
||||
handler: (accessor, resourceOrObject: URI | object | { from: string }) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
let resource: URI | null = null;
|
||||
if (resourceOrObject && 'from' in resourceOrObject && resourceOrObject.from === 'menu') {
|
||||
resource = toResource(editorService.activeEditor);
|
||||
} else {
|
||||
resource = getResourceForCommand(resourceOrObject, accessor.get(IListService), editorService);
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
return save(resource, true, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService));
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
when: undefined,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_S,
|
||||
id: SAVE_FILE_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService);
|
||||
|
||||
if (resources.length === 1) {
|
||||
// If only one resource is selected explictly call save since the behavior is a bit different than save all #41841
|
||||
// {{SQL CARBON EDIT}}
|
||||
return save(resources[0], false, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService));
|
||||
}
|
||||
return saveAll(resources, editorService, accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService));
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
when: undefined,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_S),
|
||||
win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S) },
|
||||
id: SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID,
|
||||
handler: accessor => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
|
||||
const resource = toResource(editorService.activeEditor, { supportSideBySide: true });
|
||||
if (resource) {
|
||||
// {{SQL CARBON EDIT}}
|
||||
return save(resource, false, { skipSaveParticipants: true }, editorService, accessor.get(IFileService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IQueryEditorService));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: SAVE_ALL_COMMAND_ID,
|
||||
handler: (accessor) => {
|
||||
return saveAll(true, accessor.get(IEditorService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService));
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: SAVE_ALL_IN_GROUP_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object, editorContext: IEditorCommandsContext) => {
|
||||
const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService), accessor.get(IEditorGroupsService));
|
||||
const editorGroupService = accessor.get(IEditorGroupsService);
|
||||
let saveAllArg: any;
|
||||
if (!contexts.length) {
|
||||
saveAllArg = true;
|
||||
} else {
|
||||
const fileService = accessor.get(IFileService);
|
||||
saveAllArg = [];
|
||||
contexts.forEach(context => {
|
||||
const editorGroup = editorGroupService.getGroup(context.groupId);
|
||||
if (editorGroup) {
|
||||
editorGroup.editors.forEach(editor => {
|
||||
const resource = toResource(editor, { supportSideBySide: true });
|
||||
if (resource && (resource.scheme === Schemas.untitled || fileService.canHandleResource(resource))) {
|
||||
saveAllArg.push(resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return saveAll(saveAllArg, accessor.get(IEditorService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService));
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: SAVE_FILES_COMMAND_ID,
|
||||
handler: (accessor) => {
|
||||
return saveAll(false, accessor.get(IEditorService), accessor.get(IUntitledEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService));
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: REMOVE_ROOT_FOLDER_COMMAND_ID,
|
||||
handler: (accessor, resource: URI | object) => {
|
||||
const workspaceEditingService = accessor.get(IWorkspaceEditingService);
|
||||
const contextService = accessor.get(IWorkspaceContextService);
|
||||
const workspace = contextService.getWorkspace();
|
||||
const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)).filter(r =>
|
||||
// Need to verify resources are workspaces since multi selection can trigger this command on some non workspace resources
|
||||
workspace.folders.some(f => f.uri.toString() === r.toString())
|
||||
);
|
||||
|
||||
return workspaceEditingService.removeFolders(resources);
|
||||
}
|
||||
});
|
||||
424
src/vs/workbench/contrib/files/browser/files.contribution.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction } from 'vs/workbench/browser/viewlet';
|
||||
import * as nls from 'vs/nls';
|
||||
import { sep } from 'vs/base/common/path';
|
||||
import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor';
|
||||
import { AutoSaveConfiguration, HotExitConfiguration, SUPPORTED_ENCODINGS } from 'vs/platform/files/common/files';
|
||||
import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
|
||||
import { FileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/fileEditorTracker';
|
||||
import { SaveErrorHandler } from 'vs/workbench/contrib/files/browser/saveErrorHandler';
|
||||
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
|
||||
import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor';
|
||||
import { BinaryFileEditor } from 'vs/workbench/contrib/files/browser/editors/binaryFileEditor';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { DirtyFilesTracker } from 'vs/workbench/contrib/files/common/dirtyFilesTracker';
|
||||
import { ExplorerViewlet, ExplorerViewletViewsContribution } from 'vs/workbench/contrib/files/browser/explorerViewlet';
|
||||
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
|
||||
import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ExplorerService } from 'vs/workbench/contrib/files/common/explorerService';
|
||||
|
||||
// Viewlet Action
|
||||
export class OpenExplorerViewletAction extends ShowViewletAction {
|
||||
public static readonly ID = VIEWLET_ID;
|
||||
public static readonly LABEL = nls.localize('showExplorerViewlet', "Show Explorer");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IViewletService viewletService: IViewletService,
|
||||
@IEditorGroupsService editorGroupService: IEditorGroupsService,
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService
|
||||
) {
|
||||
super(id, label, VIEWLET_ID, viewletService, editorGroupService, layoutService);
|
||||
}
|
||||
}
|
||||
|
||||
class FileUriLabelContribution implements IWorkbenchContribution {
|
||||
|
||||
constructor(@ILabelService labelService: ILabelService) {
|
||||
labelService.registerFormatter({
|
||||
scheme: 'file',
|
||||
formatting: {
|
||||
label: '${authority}${path}',
|
||||
separator: sep,
|
||||
tildify: !platform.isWindows,
|
||||
normalizeDriveLetter: platform.isWindows,
|
||||
authorityPrefix: sep + sep,
|
||||
workspaceSuffix: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register Viewlet
|
||||
Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).registerViewlet(new ViewletDescriptor(
|
||||
ExplorerViewlet,
|
||||
VIEWLET_ID,
|
||||
nls.localize('explore', "Explorer"),
|
||||
'explore',
|
||||
// {{SQL CARBON EDIT}}
|
||||
10
|
||||
));
|
||||
|
||||
registerSingleton(IExplorerService, ExplorerService, true);
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).setDefaultViewletId(VIEWLET_ID);
|
||||
|
||||
const openViewletKb: IKeybindings = {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_E
|
||||
};
|
||||
|
||||
// Register Action to Open Viewlet
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(OpenExplorerViewletAction, OpenExplorerViewletAction.ID, OpenExplorerViewletAction.LABEL, openViewletKb),
|
||||
'View: Show Explorer',
|
||||
nls.localize('view', "View")
|
||||
);
|
||||
|
||||
// Register file editors
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
|
||||
new EditorDescriptor(
|
||||
TextFileEditor,
|
||||
TextFileEditor.ID,
|
||||
nls.localize('textFileEditor', "Text File Editor")
|
||||
),
|
||||
[
|
||||
new SyncDescriptor<EditorInput>(FileEditorInput)
|
||||
]
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
|
||||
new EditorDescriptor(
|
||||
BinaryFileEditor,
|
||||
BinaryFileEditor.ID,
|
||||
nls.localize('binaryFileEditor', "Binary File Editor")
|
||||
),
|
||||
[
|
||||
new SyncDescriptor<EditorInput>(FileEditorInput),
|
||||
new SyncDescriptor<EditorInput>(DataUriEditorInput)
|
||||
]
|
||||
);
|
||||
|
||||
// Register default file input factory
|
||||
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerFileInputFactory({
|
||||
createFileInput: (resource, encoding, instantiationService): IFileEditorInput => {
|
||||
return instantiationService.createInstance(FileEditorInput, resource, encoding);
|
||||
},
|
||||
|
||||
isFileInput: (obj): obj is IFileEditorInput => {
|
||||
return obj instanceof FileEditorInput;
|
||||
}
|
||||
});
|
||||
|
||||
interface ISerializedFileInput {
|
||||
resource: string;
|
||||
resourceJSON: object;
|
||||
encoding?: string;
|
||||
}
|
||||
|
||||
// Register Editor Input Factory
|
||||
class FileEditorInputFactory implements IEditorInputFactory {
|
||||
|
||||
constructor() { }
|
||||
|
||||
public serialize(editorInput: EditorInput): string {
|
||||
const fileEditorInput = <FileEditorInput>editorInput;
|
||||
const resource = fileEditorInput.getResource();
|
||||
const fileInput: ISerializedFileInput = {
|
||||
resource: resource.toString(), // Keep for backwards compatibility
|
||||
resourceJSON: resource.toJSON(),
|
||||
encoding: fileEditorInput.getEncoding()
|
||||
};
|
||||
|
||||
return JSON.stringify(fileInput);
|
||||
}
|
||||
|
||||
public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): FileEditorInput {
|
||||
return instantiationService.invokeFunction<FileEditorInput>(accessor => {
|
||||
const fileInput: ISerializedFileInput = JSON.parse(serializedEditorInput);
|
||||
const resource = !!fileInput.resourceJSON ? URI.revive(fileInput.resourceJSON) : URI.parse(fileInput.resource);
|
||||
const encoding = fileInput.encoding;
|
||||
|
||||
return accessor.get(IEditorService).createInput({ resource, encoding, forceFile: true }) as FileEditorInput;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(FILE_EDITOR_INPUT_ID, FileEditorInputFactory);
|
||||
|
||||
// Register Explorer views
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExplorerViewletViewsContribution, LifecyclePhase.Starting);
|
||||
|
||||
// Register File Editor Tracker
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileEditorTracker, LifecyclePhase.Starting);
|
||||
|
||||
// Register Save Error Handler
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SaveErrorHandler, LifecyclePhase.Starting);
|
||||
|
||||
// Register Dirty Files Tracker
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DirtyFilesTracker, LifecyclePhase.Starting);
|
||||
|
||||
// Register uri display for file uris
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileUriLabelContribution, LifecyclePhase.Starting);
|
||||
|
||||
|
||||
// Configuration
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'files',
|
||||
'order': 9,
|
||||
'title': nls.localize('filesConfigurationTitle', "Files"),
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'files.exclude': {
|
||||
'type': 'object',
|
||||
'markdownDescription': nls.localize('exclude', "Configure glob patterns for excluding files and folders. For example, the files explorer decides which files and folders to show or hide based on this setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."),
|
||||
'default': { '**/.git': true, '**/.svn': true, '**/.hg': true, '**/CVS': true, '**/.DS_Store': true },
|
||||
'scope': ConfigurationScope.RESOURCE,
|
||||
'additionalProperties': {
|
||||
'anyOf': [
|
||||
{
|
||||
'type': 'boolean',
|
||||
'description': nls.localize('files.exclude.boolean', "The glob pattern to match file paths against. Set to true or false to enable or disable the pattern."),
|
||||
},
|
||||
{
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'when': {
|
||||
'type': 'string', // expression ({ "**/*.js": { "when": "$(basename).js" } })
|
||||
'pattern': '\\w*\\$\\(basename\\)\\w*',
|
||||
'default': '$(basename).ext',
|
||||
'description': nls.localize('files.exclude.when', "Additional check on the siblings of a matching file. Use $(basename) as variable for the matching file name.")
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
'files.associations': {
|
||||
'type': 'object',
|
||||
'markdownDescription': nls.localize('associations', "Configure file associations to languages (e.g. `\"*.extension\": \"html\"`). These have precedence over the default associations of the languages installed."),
|
||||
},
|
||||
'files.encoding': {
|
||||
'type': 'string',
|
||||
'overridable': true,
|
||||
'enum': Object.keys(SUPPORTED_ENCODINGS),
|
||||
'default': 'utf8',
|
||||
'description': nls.localize('encoding', "The default character set encoding to use when reading and writing files. This setting can also be configured per language."),
|
||||
'scope': ConfigurationScope.RESOURCE,
|
||||
'enumDescriptions': Object.keys(SUPPORTED_ENCODINGS).map(key => SUPPORTED_ENCODINGS[key].labelLong)
|
||||
},
|
||||
'files.autoGuessEncoding': {
|
||||
'type': 'boolean',
|
||||
'overridable': true,
|
||||
'default': false,
|
||||
'description': nls.localize('autoGuessEncoding', "When enabled, the editor will attempt to guess the character set encoding when opening files. This setting can also be configured per language."),
|
||||
'scope': ConfigurationScope.RESOURCE
|
||||
},
|
||||
'files.eol': {
|
||||
'type': 'string',
|
||||
'enum': [
|
||||
'\n',
|
||||
'\r\n',
|
||||
'auto'
|
||||
],
|
||||
'enumDescriptions': [
|
||||
nls.localize('eol.LF', "LF"),
|
||||
nls.localize('eol.CRLF', "CRLF"),
|
||||
nls.localize('eol.auto', "Uses operating system specific end of line character.")
|
||||
],
|
||||
'default': 'auto',
|
||||
'description': nls.localize('eol', "The default end of line character."),
|
||||
'scope': ConfigurationScope.RESOURCE
|
||||
},
|
||||
'files.enableTrash': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': nls.localize('useTrash', "Moves files/folders to the OS trash (recycle bin on Windows) when deleting. Disabling this will delete files/folders permanently.")
|
||||
},
|
||||
'files.trimTrailingWhitespace': {
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
'description': nls.localize('trimTrailingWhitespace', "When enabled, will trim trailing whitespace when saving a file."),
|
||||
'overridable': true,
|
||||
'scope': ConfigurationScope.RESOURCE
|
||||
},
|
||||
'files.insertFinalNewline': {
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
'description': nls.localize('insertFinalNewline', "When enabled, insert a final new line at the end of the file when saving it."),
|
||||
'overridable': true,
|
||||
'scope': ConfigurationScope.RESOURCE
|
||||
},
|
||||
'files.trimFinalNewlines': {
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
'description': nls.localize('trimFinalNewlines', "When enabled, will trim all new lines after the final new line at the end of the file when saving it."),
|
||||
'overridable': true,
|
||||
'scope': ConfigurationScope.RESOURCE
|
||||
},
|
||||
'files.autoSave': {
|
||||
'type': 'string',
|
||||
'enum': [AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE],
|
||||
'markdownEnumDescriptions': [
|
||||
nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.off' }, "A dirty file is never automatically saved."),
|
||||
nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.afterDelay' }, "A dirty file is automatically saved after the configured `#files.autoSaveDelay#`."),
|
||||
nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "A dirty file is automatically saved when the editor loses focus."),
|
||||
nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "A dirty file is automatically saved when the window loses focus.")
|
||||
],
|
||||
'default': AutoSaveConfiguration.OFF,
|
||||
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty files. Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save).", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY)
|
||||
},
|
||||
'files.autoSaveDelay': {
|
||||
'type': 'number',
|
||||
'default': 1000,
|
||||
'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty file is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY)
|
||||
},
|
||||
'files.watcherExclude': {
|
||||
'type': 'object',
|
||||
'default': platform.isWindows /* https://github.com/Microsoft/vscode/issues/23954 */ ? { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true } : { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/**': true },
|
||||
'description': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths (i.e. prefix with ** or the full path to match properly). Changing this setting requires a restart. When you experience Code consuming lots of cpu time on startup, you can exclude large folders to reduce the initial load."),
|
||||
'scope': ConfigurationScope.RESOURCE
|
||||
},
|
||||
'files.hotExit': {
|
||||
'type': 'string',
|
||||
'enum': [HotExitConfiguration.OFF, HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE],
|
||||
'default': HotExitConfiguration.ON_EXIT,
|
||||
'markdownEnumDescriptions': [
|
||||
nls.localize('hotExit.off', 'Disable hot exit.'),
|
||||
nls.localize('hotExit.onExit', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu). All windows with backups will be restored upon next launch.'),
|
||||
nls.localize('hotExit.onExitAndWindowClose', 'Hot exit will be triggered when the last window is closed on Windows/Linux or when the `workbench.action.quit` command is triggered (command palette, keybinding, menu), and also for any window with a folder opened regardless of whether it\'s the last window. All windows without folders opened will be restored upon next launch. To restore folder windows as they were before shutdown set `#window.restoreWindows#` to `all`.')
|
||||
],
|
||||
'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE)
|
||||
},
|
||||
'files.useExperimentalFileWatcher': {
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
'description': nls.localize('useExperimentalFileWatcher', "Use the new experimental file watcher.")
|
||||
},
|
||||
'files.defaultLanguage': {
|
||||
'type': 'string',
|
||||
'description': nls.localize('defaultLanguage', "The default language mode that is assigned to new files.")
|
||||
},
|
||||
'files.maxMemoryForLargeFilesMB': {
|
||||
'type': 'number',
|
||||
'default': 4096,
|
||||
'markdownDescription': nls.localize('maxMemoryForLargeFilesMB', "Controls the memory available to VS Code after restart when trying to open large files. Same effect as specifying `--max-memory=NEWSIZE` on the command line.")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
id: 'editor',
|
||||
order: 5,
|
||||
title: nls.localize('editorConfigurationTitle', "Editor"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'editor.formatOnSave': {
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
'description': nls.localize('formatOnSave', "Format a file on save. A formatter must be available, the file must not be saved after delay, and the editor must not be shutting down."),
|
||||
'overridable': true,
|
||||
'scope': ConfigurationScope.RESOURCE
|
||||
},
|
||||
'editor.formatOnSaveTimeout': {
|
||||
'type': 'number',
|
||||
'default': 750,
|
||||
'description': nls.localize('formatOnSaveTimeout', "Timeout in milliseconds after which the formatting that is run on file save is cancelled."),
|
||||
'overridable': true,
|
||||
'scope': ConfigurationScope.RESOURCE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'explorer',
|
||||
'order': 10,
|
||||
'title': nls.localize('explorerConfigurationTitle', "File Explorer"),
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'explorer.openEditors.visible': {
|
||||
'type': 'number',
|
||||
'description': nls.localize({ key: 'openEditorsVisible', comment: ['Open is an adjective'] }, "Number of editors shown in the Open Editors pane."),
|
||||
'default': 9
|
||||
},
|
||||
'explorer.autoReveal': {
|
||||
'type': 'boolean',
|
||||
'description': nls.localize('autoReveal', "Controls whether the explorer should automatically reveal and select files when opening them."),
|
||||
'default': true
|
||||
},
|
||||
'explorer.enableDragAndDrop': {
|
||||
'type': 'boolean',
|
||||
'description': nls.localize('enableDragAndDrop', "Controls whether the explorer should allow to move files and folders via drag and drop."),
|
||||
'default': true
|
||||
},
|
||||
'explorer.confirmDragAndDrop': {
|
||||
'type': 'boolean',
|
||||
'description': nls.localize('confirmDragAndDrop', "Controls whether the explorer should ask for confirmation to move files and folders via drag and drop."),
|
||||
'default': true
|
||||
},
|
||||
'explorer.confirmDelete': {
|
||||
'type': 'boolean',
|
||||
'description': nls.localize('confirmDelete', "Controls whether the explorer should ask for confirmation when deleting a file via the trash."),
|
||||
'default': true
|
||||
},
|
||||
'explorer.sortOrder': {
|
||||
'type': 'string',
|
||||
'enum': [SortOrderConfiguration.DEFAULT, SortOrderConfiguration.MIXED, SortOrderConfiguration.FILES_FIRST, SortOrderConfiguration.TYPE, SortOrderConfiguration.MODIFIED],
|
||||
'default': SortOrderConfiguration.DEFAULT,
|
||||
'enumDescriptions': [
|
||||
nls.localize('sortOrder.default', 'Files and folders are sorted by their names, in alphabetical order. Folders are displayed before files.'),
|
||||
nls.localize('sortOrder.mixed', 'Files and folders are sorted by their names, in alphabetical order. Files are interwoven with folders.'),
|
||||
nls.localize('sortOrder.filesFirst', 'Files and folders are sorted by their names, in alphabetical order. Files are displayed before folders.'),
|
||||
nls.localize('sortOrder.type', 'Files and folders are sorted by their extensions, in alphabetical order. Folders are displayed before files.'),
|
||||
nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date, in descending order. Folders are displayed before files.')
|
||||
],
|
||||
'description': nls.localize('sortOrder', "Controls sorting order of files and folders in the explorer.")
|
||||
},
|
||||
'explorer.decorations.colors': {
|
||||
type: 'boolean',
|
||||
description: nls.localize('explorer.decorations.colors', "Controls whether file decorations should use colors."),
|
||||
default: true
|
||||
},
|
||||
'explorer.decorations.badges': {
|
||||
type: 'boolean',
|
||||
description: nls.localize('explorer.decorations.badges', "Controls whether file decorations should use badges."),
|
||||
default: true
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// View menu
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
|
||||
group: '3_views',
|
||||
command: {
|
||||
id: VIEWLET_ID,
|
||||
title: nls.localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer")
|
||||
},
|
||||
// {{SQL CARBON EDIT}} - Change the order
|
||||
order: 3
|
||||
});
|
||||
84
src/vs/workbench/contrib/files/browser/files.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IListService, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
|
||||
import { OpenEditor } from 'vs/workbench/contrib/files/common/files';
|
||||
import { toResource } from 'vs/workbench/common/editor';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
|
||||
// Commands can get exeucted from a command pallete, from a context menu or from some list using a keybinding
|
||||
// To cover all these cases we need to properly compute the resource on which the command is being executed
|
||||
export function getResourceForCommand(resource: URI | object, listService: IListService, editorService: IEditorService): URI | null {
|
||||
if (URI.isUri(resource)) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
let list = listService.lastFocusedList;
|
||||
if (list && list.getHTMLElement() === document.activeElement) {
|
||||
let focus: any;
|
||||
if (list instanceof List) {
|
||||
const focused = list.getFocusedElements();
|
||||
if (focused.length) {
|
||||
focus = focused[0];
|
||||
}
|
||||
} else if (list instanceof WorkbenchAsyncDataTree) {
|
||||
const focused = list.getFocus();
|
||||
if (focused.length) {
|
||||
focus = focused[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (focus instanceof ExplorerItem) {
|
||||
return focus.resource;
|
||||
} else if (focus instanceof OpenEditor) {
|
||||
return focus.getResource();
|
||||
}
|
||||
}
|
||||
|
||||
return editorService.activeEditor ? toResource(editorService.activeEditor, { supportSideBySide: true }) : null;
|
||||
}
|
||||
|
||||
export function getMultiSelectedResources(resource: URI | object, listService: IListService, editorService: IEditorService): Array<URI> {
|
||||
const list = listService.lastFocusedList;
|
||||
if (list && list.getHTMLElement() === document.activeElement) {
|
||||
// Explorer
|
||||
if (list instanceof WorkbenchAsyncDataTree) {
|
||||
const selection = list.getSelection().map((fs: ExplorerItem) => fs.resource);
|
||||
const focusedElements = list.getFocus();
|
||||
const focus = focusedElements.length ? focusedElements[0] : undefined;
|
||||
const mainUriStr = URI.isUri(resource) ? resource.toString() : focus instanceof ExplorerItem ? focus.resource.toString() : undefined;
|
||||
// If the resource is passed it has to be a part of the returned context.
|
||||
// We only respect the selection if it contains the focused element.
|
||||
if (selection.some(s => URI.isUri(s) && s.toString() === mainUriStr)) {
|
||||
return selection;
|
||||
}
|
||||
}
|
||||
|
||||
// Open editors view
|
||||
if (list instanceof List) {
|
||||
const selection = coalesce(list.getSelectedElements().filter(s => s instanceof OpenEditor).map((oe: OpenEditor) => oe.getResource()));
|
||||
const focusedElements = list.getFocusedElements();
|
||||
const focus = focusedElements.length ? focusedElements[0] : undefined;
|
||||
let mainUriStr: string | undefined = undefined;
|
||||
if (URI.isUri(resource)) {
|
||||
mainUriStr = resource.toString();
|
||||
} else if (focus instanceof OpenEditor) {
|
||||
const focusedResource = focus.getResource();
|
||||
mainUriStr = focusedResource ? focusedResource.toString() : undefined;
|
||||
}
|
||||
// We only respect the selection if it contains the main element.
|
||||
if (selection.some(s => s.toString() === mainUriStr)) {
|
||||
return selection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = getResourceForCommand(resource, listService, editorService);
|
||||
return !!result ? [result] : [];
|
||||
}
|
||||
1
src/vs/workbench/contrib/files/browser/media/AddFile.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><polygon points="13,2 8,2 6,2 6,0 2,0 2,2 0,2 0,6 2,6 2,8 4,8 4,16 16,16 16,5" fill="#F6F6F6"/><polygon points="12,3 8,3 8,4 11,4 11,7 14,7 14,14 6,14 6,8 5,8 5,15 15,15 15,6" fill="#424242"/><path d="M7 3.018h-2v-2.018h-1.981v2.018h-2.019v1.982h2.019v2h1.981v-2h2v-1.982z" fill="#388A34"/><polygon points="11,7 11,4 8,4 8,6 6,6 6,8 6,14 14,14 14,7" fill="#F0EFF1"/></svg>
|
||||
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><polygon fill="#C5C5C5" points="12,3 8,3 8,4 11,4 11,7 14,7 14,14 6,14 6,8 5,8 5,15 15,15 15,6"/><path fill="#89D185" d="M7 3.018h-2v-2.018h-1.981v2.018h-2.019v1.982h2.019v2h1.981v-2h2v-1.982z"/></svg>
|
||||
|
After Width: | Height: | Size: 419 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><polygon points="9,3 8,5 8,2 6,2 6,0 2,0 2,2 0,2 0,6 2,6 2,8 2,15 16,15 16,3" fill="#F6F6F6"/><path d="M14 4h-4.382l-1 2h-2.618v2h-3v6h12v-10h-1zm0 2h-3.882l.5-1h3.382v1z" fill="#656565"/><polygon points="7,3.018 5,3.018 5,1 3.019,1 3.019,3.018 1,3.018 1,5 3.019,5 3.019,7 5,7 5,5 7,5" fill="#388A34"/><polygon points="14,5 14,6 10.118,6 10.618,5" fill="#F0EFF1"/></svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#C5C5C5" d="M14 4h-4.382l-1 2h-2.618v2h-3v6h12v-10h-1zm0 2h-3.882l.5-1h3.382v1z"/><polygon fill="#89D185" points="7,3.018 5,3.018 5,1 3.019,1 3.019,3.018 1,3.018 1,5 3.019,5 3.019,7 5,7 5,5 7,5"/></svg>
|
||||
|
After Width: | Height: | Size: 432 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 0 16 16" enable-background="new -1 0 16 16"><path fill="#424242" d="M14 1v9h-1v-8h-8v-1h9zm-11 2v1h8v8h1v-9h-9zm7 2v9h-9v-9h9zm-2 2h-5v5h5v-5z"/><rect x="4" y="9" fill="#00539C" width="3" height="1"/></svg>
|
||||
|
After Width: | Height: | Size: 281 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 0 16 16" enable-background="new -1 0 16 16"><path fill="#C5C5C5" d="M14 1v9h-1v-8h-8v-1h9zm-11 2v1h8v8h1v-9h-9zm7 2v9h-9v-9h9zm-2 2h-5v5h5v-5z"/><rect x="4" y="9" fill="#75BEFF" width="3" height="1"/></svg>
|
||||
|
After Width: | Height: | Size: 281 B |
1
src/vs/workbench/contrib/files/browser/media/Preview.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0.414 0 16 16" enable-background="new 0.414 0 16 16"><path fill="#F6F6F6" d="M12.363 4c-.252-2.244-2.139-4-4.449-4-2.481 0-4.5 2.019-4.5 4.5 0 .6.12 1.188.35 1.735l-3.764 3.765 2.414 2.414 3-2.999v6.585h11v-12h-4.051z"/><rect x="12.414" y="10" fill="#424242" width="1" height="3"/><rect x="10.414" y="11" fill="#424242" width="1" height="2"/><rect x="8.414" y="12" fill="#424242" width="1" height="1"/><rect x="8.414" y="9" fill="#424242" width="2" height="1"/><path fill="#424242" d="M12.363 5c-.039.347-.112.681-.226 1h2.276v8h-7v-5.05c-.342-.039-.676-.112-1-.228v6.278h9v-10h-3.05z"/><path fill="#424242" d="M10.708 8h1.706v-1h-.762c-.257.384-.585.712-.944 1z"/><path fill="#424242" d="M11.414 4.5c0-1.933-1.567-3.5-3.5-3.5s-3.5 1.567-3.5 3.5c0 .711.215 1.369.579 1.922l-3.579 3.578 1 1 3.579-3.578c.552.363 1.211.578 1.921.578 1.933 0 3.5-1.567 3.5-3.5zm-5.999 0c0-1.381 1.119-2.5 2.5-2.5s2.5 1.119 2.5 2.5-1.12 2.5-2.5 2.5-2.5-1.119-2.5-2.5z"/><path fill="#F0EFF1" d="M12.138 6c-.126.354-.279.693-.485 1h.762v1h-1.706c-.771.616-1.734 1-2.795 1-.169 0-.333-.031-.5-.05v5.05h7v-8h-2.276zm-3.724 3h2v1h-2v-1zm1 4h-1v-1h1v1zm2 0h-1v-2h1v2zm2 0h-1v-3h1v3z"/><circle fill="#F0EFF1" cx="7.914" cy="4.5" r="2.5"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0.414 0 16 16" enable-background="new 0.414 0 16 16"><path fill="#252626" d="M12.363 4c-.252-2.244-2.139-4-4.449-4-2.481 0-4.5 2.019-4.5 4.5 0 .6.12 1.188.35 1.735l-3.764 3.765 2.414 2.414 3-2.999v6.585h11v-12h-4.051z"/><rect x="12.414" y="10" fill="#C5C5C5" width="1" height="3"/><rect x="10.414" y="11" fill="#C5C5C5" width="1" height="2"/><rect x="8.414" y="12" fill="#C5C5C5" width="1" height="1"/><rect x="8.414" y="9" fill="#C5C5C5" width="2" height="1"/><path fill="#C5C5C5" d="M12.363 5c-.039.347-.112.681-.226 1h2.276v8h-7v-5.05c-.342-.039-.676-.112-1-.228v6.278h9v-10h-3.05z"/><path fill="#C5C5C5" d="M10.708 8h1.706v-1h-.762c-.257.384-.585.712-.944 1z"/><path fill="#C5C5C5" d="M11.414 4.5c0-1.933-1.567-3.5-3.5-3.5s-3.5 1.567-3.5 3.5c0 .711.215 1.369.579 1.922l-3.579 3.578 1 1 3.579-3.578c.552.363 1.211.578 1.921.578 1.933 0 3.5-1.567 3.5-3.5zm-5.999 0c0-1.381 1.119-2.5 2.5-2.5s2.5 1.119 2.5 2.5-1.12 2.5-2.5 2.5-2.5-1.119-2.5-2.5z"/><path fill="#2B282E" d="M12.138 6c-.126.354-.279.693-.485 1h.762v1h-1.706c-.771.616-1.734 1-2.795 1-.169 0-.333-.031-.5-.05v5.05h7v-8h-2.276zm-3.724 3h2v1h-2v-1zm1 4h-1v-1h1v1zm2 0h-1v-2h1v2zm2 0h-1v-3h1v3z"/><circle fill="#2B282E" cx="7.914" cy="4.5" r="2.5"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/vs/workbench/contrib/files/browser/media/Refresh.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#F6F6F6"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#424242"/></svg>
|
||||
|
After Width: | Height: | Size: 986 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#2D2D30"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#C5C5C5"/></svg>
|
||||
|
After Width: | Height: | Size: 986 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#e8e8e8" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><circle fill="#C5C5C5" cx="8" cy="8" r="4"/></svg>
|
||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><circle fill="#FFF" cx="8" cy="8" r="4"/></svg>
|
||||
|
After Width: | Height: | Size: 164 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><circle fill="#424242" cx="8" cy="8" r="4"/></svg>
|
||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#FFF" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#424242" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-2 -2 16 16" enable-background="new -2 -2 16 16"><polygon fill="#C5C5C5" points="9,0 4.5,9 3,6 0,6 3,12 6,12 12,0"/></svg>
|
||||
|
After Width: | Height: | Size: 194 B |
1
src/vs/workbench/contrib/files/browser/media/check.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-2 -2 16 16" enable-background="new -2 -2 16 16"><polygon fill="#424242" points="9,0 4.5,9 3,6 0,6 3,12 6,12 12,0"/></svg>
|
||||
|
After Width: | Height: | Size: 194 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><path fill="#F6F6F6" d="M16 12h-2v2h-2v2H0V5h2V3h2V1h12v11z"/><g fill="#424242"><path d="M3 5h9v8h1V4H3zM5 2v1h9v8h1V2zM1 6v9h10V6H1zm8 7H7.5L6 11.5 4.5 13H3l2.3-2.3L3 8.5h1.5L6 10l1.5-1.5H9l-2.3 2.3L9 13z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 335 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><path fill="#1E1E1E" d="M16 12h-2v2h-2v2H0V5h2V3h2V1h12v11z"/><g fill="#C5C5C5"><path d="M3 5h9v8h1V4H3zM5 2v1h9v8h1V2zM1 6v9h10V6H1zm8 7H7.5L6 11.5 4.5 13H3l2.3-2.3L3 8.5h1.5L6 10l1.5-1.5H9l-2.3 2.3L9 13z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 335 B |
153
src/vs/workbench/contrib/files/browser/media/explorerviewlet.css
Normal file
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Activity Bar */
|
||||
.monaco-workbench .activitybar .monaco-action-bar .action-label.explore {
|
||||
-webkit-mask: url('files-dark.svg') no-repeat 50% 50%;
|
||||
}
|
||||
|
||||
/* --- Explorer viewlet --- */
|
||||
.explorer-viewlet,
|
||||
.explorer-folders-view {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.explorer-folders-view .monaco-list-row {
|
||||
padding-left: 4px; /* align top level twistie with `Explorer` title label */
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-folders-view.highlight .monaco-list .explorer-item:not(.explorer-item-edited),
|
||||
.explorer-viewlet .explorer-folders-view.highlight .monaco-list .monaco-tl-twistie {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-item,
|
||||
.explorer-viewlet .open-editor,
|
||||
.explorer-viewlet .editor-group {
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-item {
|
||||
display: flex; /* this helps showing the overflow ellipsis (...) even though we use display:inline-block for the labels */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-item > a,
|
||||
.explorer-viewlet .open-editor > a,
|
||||
.explorer-viewlet .editor-group {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-item,
|
||||
.explorer-viewlet .explorer-item .monaco-inputbox {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-item.cut {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-item.explorer-item-edited .label-name {
|
||||
flex: 0; /* do not steal space when label is hidden because we are in edit mode */
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row {
|
||||
padding-left: 22px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.explorer-viewlet .panel-header .count {
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.explorer-viewlet .panel-header .monaco-count-badge.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row:hover > .monaco-action-bar,
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.focused > .monaco-action-bar,
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty > .monaco-action-bar {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .close-editor-action {
|
||||
width: 8px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-close-all-files,
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .save-all {
|
||||
width: 23px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .open-editor {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .editor-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.explorer-viewlet .monaco-count-badge {
|
||||
padding: 1px 6px;
|
||||
margin-left: 6px;
|
||||
border-radius: 0; /* goes better when ellipsis shows up on narrow sidebar */
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-empty-view {
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-item.nonexistent-root {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-item .monaco-inputbox {
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.monaco-workbench.linux .explorer-viewlet .explorer-item .monaco-inputbox,
|
||||
.monaco-workbench.mac .explorer-viewlet .explorer-item .monaco-inputbox {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.monaco-workbench .explorer-viewlet .explorer-item .monaco-inputbox > .wrapper > .input {
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row .editor-group {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Bold font style does not go well with CJK fonts */
|
||||
.explorer-viewlet:lang(zh-Hans) .explorer-open-editors .monaco-list .monaco-list-row .editor-group,
|
||||
.explorer-viewlet:lang(zh-Hant) .explorer-open-editors .monaco-list .monaco-list-row .editor-group,
|
||||
.explorer-viewlet:lang(ja) .explorer-open-editors .monaco-list .monaco-list-row .editor-group,
|
||||
.explorer-viewlet:lang(ko) .explorer-open-editors .monaco-list .monaco-list-row .editor-group {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* High Contrast Theming */
|
||||
.hc-black .monaco-workbench .explorer-viewlet .explorer-item,
|
||||
.hc-black .monaco-workbench .explorer-viewlet .open-editor,
|
||||
.hc-black .monaco-workbench .explorer-viewlet .editor-group {
|
||||
line-height: 20px;
|
||||
}
|
||||
111
src/vs/workbench/contrib/files/browser/media/fileactions.css
Normal file
@@ -0,0 +1,111 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .explorer-viewlet .action-close-all-files {
|
||||
background: url("closeall.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .explorer-viewlet .action-close-all-files,
|
||||
.hc-black .monaco-workbench .explorer-viewlet .action-close-all-files {
|
||||
background: url("closeall_inverse.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench .explorer-action.new-file {
|
||||
background: url('AddFile.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench .explorer-action.save-all {
|
||||
background: url('saveall.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .explorer-action.save-all,
|
||||
.hc-black .monaco-workbench .explorer-action.save-all {
|
||||
background: url('saveall_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .explorer-action.new-file,
|
||||
.hc-black .monaco-workbench .explorer-action.new-file {
|
||||
background: url('AddFile_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench .explorer-action.new-folder {
|
||||
background: url('AddFolder.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .explorer-action.new-folder,
|
||||
.hc-black .monaco-workbench .explorer-action.new-folder {
|
||||
background: url('AddFolder_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench .explorer-action.refresh-explorer {
|
||||
background: url('Refresh.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .explorer-action.refresh-explorer,
|
||||
.hc-black .monaco-workbench .explorer-action.refresh-explorer {
|
||||
background: url('Refresh_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench .explorer-action.collapse-explorer {
|
||||
background: url('CollapseAll.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .explorer-action.collapse-explorer,
|
||||
.hc-black .monaco-workbench .explorer-action.collapse-explorer {
|
||||
background: url('CollapseAll_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench .quick-open-sidebyside-vertical {
|
||||
background-image: url('split-editor-vertical.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .quick-open-sidebyside-vertical,
|
||||
.hc-black .monaco-workbench .quick-open-sidebyside-vertical {
|
||||
background-image: url('split-editor-vertical-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .quick-open-sidebyside-horizontal {
|
||||
background-image: url('split-editor-horizontal.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .quick-open-sidebyside-horizontal,
|
||||
.hc-black .monaco-workbench .quick-open-sidebyside-horizontal {
|
||||
background-image: url('split-editor-horizontal-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .file-editor-action.action-open-preview {
|
||||
background: url('Preview.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .file-editor-action.action-open-preview ,
|
||||
.hc-black .monaco-workbench .file-editor-action.action-open-preview {
|
||||
background: url('Preview_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .close-editor-action {
|
||||
background: url("action-close.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .focused .monaco-list-row.selected:not(.highlighted) .close-editor-action {
|
||||
background: url("action-close-focus.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .close-editor-action {
|
||||
background: url("action-close-dirty.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .close-editor-action,
|
||||
.hc-black .monaco-workbench .explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .close-editor-action {
|
||||
background: url("action-close-dirty-dark.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.explorer-viewlet .explorer-open-editors .monaco-list.focused .monaco-list-row.selected.dirty:not(:hover) > .monaco-action-bar .close-editor-action {
|
||||
background: url("action-close-dirty-focus.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .explorer-viewlet .explorer-open-editors .close-editor-action,
|
||||
.hc-black .monaco-workbench .explorer-viewlet .explorer-open-editors .close-editor-action {
|
||||
background: url("action-close-dark.svg") center center no-repeat;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" height="28" viewBox="0 0 28 28" width="28" xmlns="http://www.w3.org/2000/svg"><path d="m14.965 7h-8.91642s-2.04858.078-2.04858 2v15s0 2 2.04858 2l11.26722-.004c2.0486.004 2.0486-1.996 2.0486-1.996v-11.491zm-1.7464 2v5h4.0972v10h-11.26722v-15zm5.6428-6h-8.6993s-2.06493.016-2.0803 2h8.2097v.454l4.0265 4.546h1.095v12c2.0485 0 2.0485-1.995 2.0485-1.995v-11.357z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 397 B |
1
src/vs/workbench/contrib/files/browser/media/saveall.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><path fill="#F6F6F6" d="M16 2.6L13.4 0H6v6H0v10h10v-6h6z"/><g fill="#00539C"><path d="M5 7h1v2H5zM11 1h1v2h-1zM13 1v3H9V1H7v5h.4L10 8.6V9h5V3zM7 10H3V7H1v8h8V9L7 7z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><path fill="#1E1E1E" d="M16 2.6L13.4 0H6v6H0v10h10v-6h6z"/><g fill="#75BEFF"><path d="M5 7h1v2H5zM11 1h1v2h-1zM13 1v3H9V1H7v5h.4L10 8.6V9h5V3zM7 10H3V7H1v8h8V9L7 7z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 15H0V1h16v14z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M1 2v12h14V2H1zm13 11H2v-3h12v3zm0-5H2V5h12v3z" id="iconBg"/><g id="iconFg" style="display: none;"><path class="icon-vs-fg" d="M14 8H2V5h12v3zm0 2H2v3h12v-3z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 578 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 15H0V1h16v14z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M1 2v12h14V2H1zm13 11H2v-3h12v3zm0-5H2V5h12v3z" id="iconBg"/><g id="iconFg" style="display: none;"><path class="icon-vs-fg" d="M14 8H2V5h12v3zm0 2H2v3h12v-3z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 578 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -1 16 16" enable-background="new 0 -1 16 16"><path fill="#C5C5C5" d="M1 1v12h14v-12h-14zm1 3h4.999v8h-4.999v-8zm12 8h-5.001v-8h5.001v8z"/></svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -1 16 16" enable-background="new 0 -1 16 16"><path fill="#656565" d="M1 1v12h14v-12h-14zm1 3h4.999v8h-4.999v-8zm12 8h-5.001v-8h5.001v8z"/></svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-0.994 0 16 16" enable-background="new -0.994 0 16 16"><path fill="#75BEFF" d="M13 6c0 1.461-.636 2.846-1.746 3.797l-5.584 4.951-1.324-1.496 5.595-4.962c.678-.582 1.061-1.413 1.061-2.29 0-1.654-1.345-3-2.997-3-.71 0-1.399.253-1.938.713l-1.521 1.287h2.448l-1.998 2h-3.996v-4l1.998-2v2.692l1.775-1.504c.899-.766 2.047-1.188 3.232-1.188 2.754 0 4.995 2.243 4.995 5z"/></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
1
src/vs/workbench/contrib/files/browser/media/undo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-0.994 0 16 16" enable-background="new -0.994 0 16 16"><path fill="#00539C" d="M13 6c0 1.461-.636 2.846-1.746 3.797l-5.584 4.951-1.324-1.496 5.595-4.962c.678-.582 1.061-1.413 1.061-2.29 0-1.654-1.345-3-2.997-3-.71 0-1.399.253-1.938.713l-1.521 1.287h2.448l-1.998 2h-3.996v-4l1.998-2v2.692l1.775-1.504c.899-.766 2.047-1.188 3.232-1.188 2.754 0 4.995 2.243 4.995 5z"/></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
380
src/vs/workbench/contrib/files/browser/saveErrorHandler.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
|
||||
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||
import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { FileOnDiskContentProvider } from 'vs/workbench/contrib/files/common/files';
|
||||
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { SAVE_FILE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands';
|
||||
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
||||
import { INotificationService, INotificationHandle, INotificationActions, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { ExecuteCommandAction } from 'vs/platform/actions/common/actions';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
|
||||
export const CONFLICT_RESOLUTION_CONTEXT = 'saveConflictResolutionContext';
|
||||
export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution';
|
||||
|
||||
const LEARN_MORE_DIRTY_WRITE_IGNORE_KEY = 'learnMoreDirtyWriteError';
|
||||
|
||||
const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content on disk with your changes.");
|
||||
|
||||
// A handler for save error happening with conflict resolution actions
|
||||
export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution {
|
||||
private messages: ResourceMap<INotificationHandle>;
|
||||
private conflictResolutionContext: IContextKey<boolean>;
|
||||
private activeConflictResolutionResource?: URI;
|
||||
|
||||
constructor(
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@ITextModelService textModelService: ITextModelService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IStorageService private readonly storageService: IStorageService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.messages = new ResourceMap<INotificationHandle>();
|
||||
this.conflictResolutionContext = new RawContextKey<boolean>(CONFLICT_RESOLUTION_CONTEXT, false).bindTo(contextKeyService);
|
||||
|
||||
const provider = this._register(instantiationService.createInstance(FileOnDiskContentProvider));
|
||||
this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider));
|
||||
|
||||
// Hook into model
|
||||
TextFileEditorModel.setSaveErrorHandler(this);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.textFileService.models.onModelSaved(e => this.onFileSavedOrReverted(e.resource)));
|
||||
this._register(this.textFileService.models.onModelReverted(e => this.onFileSavedOrReverted(e.resource)));
|
||||
this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged()));
|
||||
}
|
||||
|
||||
private onActiveEditorChanged(): void {
|
||||
let isActiveEditorSaveConflictResolution = false;
|
||||
let activeConflictResolutionResource: URI | undefined;
|
||||
|
||||
const activeInput = this.editorService.activeEditor;
|
||||
if (activeInput instanceof DiffEditorInput && activeInput.originalInput instanceof ResourceEditorInput && activeInput.modifiedInput instanceof FileEditorInput) {
|
||||
const resource = activeInput.originalInput.getResource();
|
||||
if (resource && resource.scheme === CONFLICT_RESOLUTION_SCHEME) {
|
||||
isActiveEditorSaveConflictResolution = true;
|
||||
activeConflictResolutionResource = activeInput.modifiedInput.getResource();
|
||||
}
|
||||
}
|
||||
|
||||
this.conflictResolutionContext.set(isActiveEditorSaveConflictResolution);
|
||||
this.activeConflictResolutionResource = activeConflictResolutionResource;
|
||||
}
|
||||
|
||||
private onFileSavedOrReverted(resource: URI): void {
|
||||
const messageHandle = this.messages.get(resource);
|
||||
if (messageHandle) {
|
||||
messageHandle.close();
|
||||
this.messages.delete(resource);
|
||||
}
|
||||
}
|
||||
|
||||
onSaveError(error: any, model: ITextFileEditorModel): void {
|
||||
const fileOperationError = error as FileOperationError;
|
||||
const resource = model.getResource();
|
||||
|
||||
let message: string;
|
||||
const actions: INotificationActions = { primary: [], secondary: [] };
|
||||
|
||||
// Dirty write prevention
|
||||
if (fileOperationError.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
|
||||
|
||||
// If the user tried to save from the opened conflict editor, show its message again
|
||||
if (this.activeConflictResolutionResource && this.activeConflictResolutionResource.toString() === model.getResource().toString()) {
|
||||
if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
|
||||
return; // return if this message is ignored
|
||||
}
|
||||
|
||||
message = conflictEditorHelp;
|
||||
|
||||
actions.primary!.push(this.instantiationService.createInstance(ResolveConflictLearnMoreAction));
|
||||
actions.secondary!.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction));
|
||||
}
|
||||
|
||||
// Otherwise show the message that will lead the user into the save conflict editor.
|
||||
else {
|
||||
message = nls.localize('staleSaveError', "Failed to save '{0}': The content on disk is newer. Please compare your version with the one on disk.", basename(resource));
|
||||
|
||||
actions.primary!.push(this.instantiationService.createInstance(ResolveSaveConflictAction, model));
|
||||
}
|
||||
}
|
||||
|
||||
// Any other save error
|
||||
else {
|
||||
const isReadonly = fileOperationError.fileOperationResult === FileOperationResult.FILE_READ_ONLY;
|
||||
const triedToMakeWriteable = isReadonly && fileOperationError.options && fileOperationError.options.overwriteReadonly;
|
||||
const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED;
|
||||
|
||||
// Save Elevated
|
||||
if (isPermissionDenied || triedToMakeWriteable) {
|
||||
actions.primary!.push(this.instantiationService.createInstance(SaveElevatedAction, model, triedToMakeWriteable));
|
||||
}
|
||||
|
||||
// Overwrite
|
||||
else if (isReadonly) {
|
||||
actions.primary!.push(this.instantiationService.createInstance(OverwriteReadonlyAction, model));
|
||||
}
|
||||
|
||||
// Retry
|
||||
else {
|
||||
actions.primary!.push(this.instantiationService.createInstance(ExecuteCommandAction, SAVE_FILE_COMMAND_ID, nls.localize('retry', "Retry")));
|
||||
}
|
||||
|
||||
// Save As
|
||||
actions.primary!.push(this.instantiationService.createInstance(ExecuteCommandAction, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL));
|
||||
|
||||
// Discard
|
||||
actions.primary!.push(this.instantiationService.createInstance(ExecuteCommandAction, REVERT_FILE_COMMAND_ID, nls.localize('discard', "Discard")));
|
||||
|
||||
if (isReadonly) {
|
||||
if (triedToMakeWriteable) {
|
||||
message = isWindows ? nls.localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is write protected. Select 'Overwrite as Admin' to retry as administrator.", basename(resource)) : nls.localize('readonlySaveErrorSudo', "Failed to save '{0}': File is write protected. Select 'Overwrite as Sudo' to retry as superuser.", basename(resource));
|
||||
} else {
|
||||
message = nls.localize('readonlySaveError', "Failed to save '{0}': File is write protected. Select 'Overwrite' to attempt to remove protection.", basename(resource));
|
||||
}
|
||||
} else if (isPermissionDenied) {
|
||||
message = isWindows ? nls.localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", basename(resource)) : nls.localize('permissionDeniedSaveErrorSudo', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", basename(resource));
|
||||
} else {
|
||||
message = nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(resource), toErrorMessage(error, false));
|
||||
}
|
||||
}
|
||||
|
||||
// Show message and keep function to hide in case the file gets saved/reverted
|
||||
const handle = this.notificationService.notify({ severity: Severity.Error, message, actions });
|
||||
Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!));
|
||||
this.messages.set(model.getResource(), handle);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.messages.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const pendingResolveSaveConflictMessages: INotificationHandle[] = [];
|
||||
function clearPendingResolveSaveConflictMessages(): void {
|
||||
while (pendingResolveSaveConflictMessages.length > 0) {
|
||||
const item = pendingResolveSaveConflictMessages.pop();
|
||||
if (item) {
|
||||
item.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResolveConflictLearnMoreAction extends Action {
|
||||
|
||||
constructor(
|
||||
@IOpenerService private readonly openerService: IOpenerService
|
||||
) {
|
||||
super('workbench.files.action.resolveConflictLearnMore', nls.localize('learnMore', "Learn More"));
|
||||
}
|
||||
|
||||
run(): Promise<any> {
|
||||
return this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=868264'));
|
||||
}
|
||||
}
|
||||
|
||||
class DoNotShowResolveConflictLearnMoreAction extends Action {
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService
|
||||
) {
|
||||
super('workbench.files.action.resolveConflictLearnMoreDoNotShowAgain', nls.localize('dontShowAgain', "Don't Show Again"));
|
||||
}
|
||||
|
||||
run(notification: IDisposable): Promise<any> {
|
||||
this.storageService.store(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, true, StorageScope.GLOBAL);
|
||||
|
||||
// Hide notification
|
||||
notification.dispose();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
class ResolveSaveConflictAction extends Action {
|
||||
|
||||
constructor(
|
||||
private model: ITextFileEditorModel,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService
|
||||
) {
|
||||
super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare"));
|
||||
}
|
||||
|
||||
run(): Promise<any> {
|
||||
if (!this.model.isDisposed()) {
|
||||
const resource = this.model.getResource();
|
||||
const name = basename(resource);
|
||||
const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (on disk) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong);
|
||||
|
||||
return this.editorService.openEditor(
|
||||
{
|
||||
leftResource: URI.from({ scheme: CONFLICT_RESOLUTION_SCHEME, path: resource.fsPath }),
|
||||
rightResource: resource,
|
||||
label: editorLabel,
|
||||
options: { pinned: true }
|
||||
}
|
||||
).then(() => {
|
||||
if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
|
||||
return; // return if this message is ignored
|
||||
}
|
||||
|
||||
// Show additional help how to resolve the save conflict
|
||||
const actions: INotificationActions = { primary: [], secondary: [] };
|
||||
actions.primary!.push(this.instantiationService.createInstance(ResolveConflictLearnMoreAction));
|
||||
actions.secondary!.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction));
|
||||
|
||||
const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions });
|
||||
Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!));
|
||||
pendingResolveSaveConflictMessages.push(handle);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
class SaveElevatedAction extends Action {
|
||||
|
||||
constructor(
|
||||
private model: ITextFileEditorModel,
|
||||
private triedToMakeWriteable: boolean
|
||||
) {
|
||||
super('workbench.files.action.saveElevated', triedToMakeWriteable ? isWindows ? nls.localize('overwriteElevated', "Overwrite as Admin...") : nls.localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? nls.localize('saveElevated', "Retry as Admin...") : nls.localize('saveElevatedSudo', "Retry as Sudo..."));
|
||||
}
|
||||
|
||||
run(): Promise<any> {
|
||||
if (!this.model.isDisposed()) {
|
||||
this.model.save({
|
||||
writeElevated: true,
|
||||
overwriteReadonly: this.triedToMakeWriteable
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
class OverwriteReadonlyAction extends Action {
|
||||
|
||||
constructor(
|
||||
private model: ITextFileEditorModel
|
||||
) {
|
||||
super('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"));
|
||||
}
|
||||
|
||||
run(): Promise<any> {
|
||||
if (!this.model.isDisposed()) {
|
||||
this.model.save({ overwriteReadonly: true });
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource: URI) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const resolverService = accessor.get(ITextModelService);
|
||||
const modelService = accessor.get(IModelService);
|
||||
|
||||
const control = editorService.activeControl;
|
||||
if (!control) {
|
||||
return;
|
||||
}
|
||||
const editor = control.input;
|
||||
const group = control.group;
|
||||
|
||||
resolverService.createModelReference(resource).then(reference => {
|
||||
const model = reference.object as IResolvedTextFileEditorModel;
|
||||
const localModelSnapshot = model.createSnapshot();
|
||||
|
||||
clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions
|
||||
|
||||
// Revert to be able to save
|
||||
return model.revert().then(() => {
|
||||
|
||||
// Restore user value (without loosing undo stack)
|
||||
modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot));
|
||||
|
||||
// Trigger save
|
||||
return model.save().then(() => {
|
||||
|
||||
// Reopen file input
|
||||
return editorService.openEditor({ resource: model.getResource() }, group).then(() => {
|
||||
|
||||
// Clean up
|
||||
group.closeEditor(editor);
|
||||
editor.dispose();
|
||||
reference.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const revertLocalChangesCommand = (accessor: ServicesAccessor, resource: URI) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const resolverService = accessor.get(ITextModelService);
|
||||
|
||||
const control = editorService.activeControl;
|
||||
if (!control) {
|
||||
return;
|
||||
}
|
||||
const editor = control.input;
|
||||
const group = control.group;
|
||||
|
||||
resolverService.createModelReference(resource).then(reference => {
|
||||
const model = reference.object as ITextFileEditorModel;
|
||||
|
||||
clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions
|
||||
|
||||
// Revert on model
|
||||
return model.revert().then(() => {
|
||||
|
||||
// Reopen file input
|
||||
return editorService.openEditor({ resource: model.getResource() }, group).then(() => {
|
||||
|
||||
// Clean up
|
||||
group.closeEditor(editor);
|
||||
editor.dispose();
|
||||
reference.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
138
src/vs/workbench/contrib/files/browser/views/emptyView.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import * as env from 'vs/base/common/platform';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { Button } from 'vs/base/browser/ui/button/button';
|
||||
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { OpenFolderAction, OpenFileFolderAction, AddRootFolderAction } from 'vs/workbench/browser/actions/workspaceActions';
|
||||
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd';
|
||||
import { listDropBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
|
||||
export class EmptyView extends ViewletPanel {
|
||||
|
||||
static readonly ID: string = 'workbench.explorer.emptyView';
|
||||
static readonly NAME = nls.localize('noWorkspace', "No Folder Opened");
|
||||
|
||||
private button: Button;
|
||||
private messageElement: HTMLElement;
|
||||
private titleElement: HTMLElement;
|
||||
|
||||
constructor(
|
||||
options: IViewletViewOptions,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService);
|
||||
this.contextService.onDidChangeWorkbenchState(() => this.setLabels());
|
||||
}
|
||||
|
||||
renderHeader(container: HTMLElement): void {
|
||||
const titleContainer = document.createElement('div');
|
||||
DOM.addClass(titleContainer, 'title');
|
||||
container.appendChild(titleContainer);
|
||||
|
||||
this.titleElement = document.createElement('span');
|
||||
this.titleElement.textContent = name;
|
||||
titleContainer.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
DOM.addClass(container, 'explorer-empty-view');
|
||||
container.tabIndex = 0;
|
||||
|
||||
const messageContainer = document.createElement('div');
|
||||
DOM.addClass(messageContainer, 'section');
|
||||
container.appendChild(messageContainer);
|
||||
|
||||
this.messageElement = document.createElement('p');
|
||||
messageContainer.appendChild(this.messageElement);
|
||||
|
||||
this.button = new Button(messageContainer);
|
||||
attachButtonStyler(this.button, this.themeService);
|
||||
|
||||
this.disposables.push(this.button.onDidClick(() => {
|
||||
if (!this.actionRunner) {
|
||||
return;
|
||||
}
|
||||
const actionClass = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? AddRootFolderAction : env.isMacintosh ? OpenFileFolderAction : OpenFolderAction;
|
||||
const action = this.instantiationService.createInstance<string, string, IAction>(actionClass, actionClass.ID, actionClass.LABEL);
|
||||
this.actionRunner.run(action).then(() => {
|
||||
action.dispose();
|
||||
}, err => {
|
||||
action.dispose();
|
||||
errors.onUnexpectedError(err);
|
||||
});
|
||||
}));
|
||||
|
||||
this.disposables.push(new DragAndDropObserver(container, {
|
||||
onDrop: e => {
|
||||
const color = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND);
|
||||
container.style.backgroundColor = color ? color.toString() : '';
|
||||
const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true });
|
||||
dropHandler.handleDrop(e, () => undefined, targetGroup => undefined);
|
||||
},
|
||||
onDragEnter: (e) => {
|
||||
const color = this.themeService.getTheme().getColor(listDropBackground);
|
||||
container.style.backgroundColor = color ? color.toString() : '';
|
||||
},
|
||||
onDragEnd: () => {
|
||||
const color = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND);
|
||||
container.style.backgroundColor = color ? color.toString() : '';
|
||||
},
|
||||
onDragLeave: () => {
|
||||
const color = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND);
|
||||
container.style.backgroundColor = color ? color.toString() : '';
|
||||
},
|
||||
onDragOver: e => {
|
||||
e.dataTransfer!.dropEffect = 'copy';
|
||||
}
|
||||
}));
|
||||
|
||||
this.setLabels();
|
||||
}
|
||||
|
||||
private setLabels(): void {
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
|
||||
this.messageElement.textContent = nls.localize('noWorkspaceHelp', "You have not yet added a folder to the workspace.");
|
||||
if (this.button) {
|
||||
this.button.label = nls.localize('addFolder', "Add Folder");
|
||||
}
|
||||
this.titleElement.textContent = EmptyView.NAME;
|
||||
} else {
|
||||
this.messageElement.textContent = nls.localize('noFolderHelp', "You have not yet opened a folder.");
|
||||
if (this.button) {
|
||||
this.button.label = nls.localize('openFolder', "Open Folder");
|
||||
}
|
||||
this.titleElement.textContent = this.title;
|
||||
}
|
||||
}
|
||||
|
||||
layoutBody(size: number): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
focusBody(): void {
|
||||
if (this.button) {
|
||||
this.button.element.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations';
|
||||
import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExplorerService } from 'vs/workbench/contrib/files/common/files';
|
||||
|
||||
export class ExplorerDecorationsProvider implements IDecorationsProvider {
|
||||
readonly label: string = localize('label', "Explorer");
|
||||
private _onDidChange = new Emitter<URI[]>();
|
||||
private toDispose: IDisposable[];
|
||||
|
||||
constructor(
|
||||
@IExplorerService private explorerService: IExplorerService,
|
||||
@IWorkspaceContextService contextService: IWorkspaceContextService
|
||||
) {
|
||||
this.toDispose = [];
|
||||
this.toDispose.push(contextService.onDidChangeWorkspaceFolders(e => {
|
||||
this._onDidChange.fire(e.changed.concat(e.added).map(wf => wf.uri));
|
||||
}));
|
||||
this.toDispose.push(explorerService.onDidChangeItem(item => {
|
||||
if (item) {
|
||||
this._onDidChange.fire([item.resource]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
get onDidChange(): Event<URI[]> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
changed(uris: URI[]): void {
|
||||
this._onDidChange.fire(uris);
|
||||
}
|
||||
|
||||
provideDecorations(resource: URI): IDecorationData | undefined {
|
||||
const fileStat = this.explorerService.findClosest(resource);
|
||||
if (fileStat && fileStat.isRoot && fileStat.isError) {
|
||||
return {
|
||||
tooltip: localize('canNotResolve', "Can not resolve workspace folder"),
|
||||
letter: '!',
|
||||
color: listInvalidItemForeground,
|
||||
};
|
||||
}
|
||||
if (fileStat && fileStat.isSymbolicLink) {
|
||||
return {
|
||||
tooltip: localize('symbolicLlink', "Symbolic Link"),
|
||||
letter: '\u2937'
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dispose(): IDisposable[] {
|
||||
return dispose(this.toDispose);
|
||||
}
|
||||
}
|
||||
554
src/vs/workbench/contrib/files/browser/views/explorerView.ts
Normal file
@@ -0,0 +1,554 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as perf from 'vs/base/common/performance';
|
||||
import { sequence } from 'vs/base/common/async';
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut } from 'vs/workbench/contrib/files/common/files';
|
||||
import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView } from 'vs/workbench/contrib/files/browser/fileActions';
|
||||
import { toResource } from 'vs/workbench/common/editor';
|
||||
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { CollapseAction } from 'vs/workbench/browser/viewlet';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { ExplorerDecorationsProvider } from 'vs/workbench/contrib/files/browser/views/explorerDecorationsProvider';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ResourceContextKey } from 'vs/workbench/common/resources';
|
||||
import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations';
|
||||
import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService';
|
||||
import { DelayedDragHandler } from 'vs/base/browser/dnd';
|
||||
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop } from 'vs/workbench/contrib/files/browser/views/explorerViewer';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
|
||||
import { createFileIconThemableTreeContainerScope } from 'vs/workbench/browser/parts/views/views';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree';
|
||||
import { FuzzyScore } from 'vs/base/common/filters';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
export class ExplorerView extends ViewletPanel {
|
||||
static readonly ID: string = 'workbench.explorer.fileView';
|
||||
static readonly TREE_VIEW_STATE_STORAGE_KEY: string = 'workbench.explorer.treeViewState';
|
||||
|
||||
private tree: WorkbenchAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>;
|
||||
private filter: FilesFilter;
|
||||
|
||||
private resourceContext: ResourceContextKey;
|
||||
private folderContext: IContextKey<boolean>;
|
||||
private readonlyContext: IContextKey<boolean>;
|
||||
private rootContext: IContextKey<boolean>;
|
||||
|
||||
// Refresh is needed on the initial explorer open
|
||||
private shouldRefresh = true;
|
||||
private dragHandler: DelayedDragHandler;
|
||||
private decorationProvider: ExplorerDecorationsProvider;
|
||||
private autoReveal = false;
|
||||
|
||||
constructor(
|
||||
options: IViewletPanelOptions,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IProgressService private readonly progressService: IProgressService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IDecorationsService decorationService: IDecorationsService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IThemeService private readonly themeService: IWorkbenchThemeService,
|
||||
@IMenuService private readonly menuService: IMenuService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IClipboardService private clipboardService: IClipboardService
|
||||
) {
|
||||
super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService);
|
||||
|
||||
this.resourceContext = instantiationService.createInstance(ResourceContextKey);
|
||||
this.disposables.push(this.resourceContext);
|
||||
this.folderContext = ExplorerFolderContext.bindTo(contextKeyService);
|
||||
this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService);
|
||||
this.rootContext = ExplorerRootContext.bindTo(contextKeyService);
|
||||
|
||||
this.decorationProvider = new ExplorerDecorationsProvider(this.explorerService, contextService);
|
||||
decorationService.registerDecorationsProvider(this.decorationProvider);
|
||||
this.disposables.push(this.decorationProvider);
|
||||
this.disposables.push(this.resourceContext);
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.labelService.getWorkspaceLabel(this.contextService.getWorkspace());
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
set title(value: string) {
|
||||
// noop
|
||||
}
|
||||
|
||||
// Memoized locals
|
||||
@memoize private get contributedContextMenu(): IMenu {
|
||||
const contributedContextMenu = this.menuService.createMenu(MenuId.ExplorerContext, this.tree.contextKeyService);
|
||||
this.disposables.push(contributedContextMenu);
|
||||
return contributedContextMenu;
|
||||
}
|
||||
|
||||
@memoize private get fileCopiedContextKey(): IContextKey<boolean> {
|
||||
return FileCopiedContext.bindTo(this.contextKeyService);
|
||||
}
|
||||
|
||||
@memoize private get resourceCutContextKey(): IContextKey<boolean> {
|
||||
return ExplorerResourceCut.bindTo(this.contextKeyService);
|
||||
}
|
||||
|
||||
// Split view methods
|
||||
|
||||
protected renderHeader(container: HTMLElement): void {
|
||||
super.renderHeader(container);
|
||||
|
||||
// Expand on drag over
|
||||
this.dragHandler = new DelayedDragHandler(container, () => this.setExpanded(true));
|
||||
|
||||
const titleElement = container.querySelector('.title') as HTMLElement;
|
||||
const setHeader = () => {
|
||||
const workspace = this.contextService.getWorkspace();
|
||||
const title = workspace.folders.map(folder => folder.name).join();
|
||||
titleElement.textContent = this.name;
|
||||
titleElement.title = title;
|
||||
};
|
||||
|
||||
this.disposables.push(this.contextService.onDidChangeWorkspaceName(setHeader));
|
||||
this.disposables.push(this.labelService.onDidChangeFormatters(setHeader));
|
||||
setHeader();
|
||||
}
|
||||
|
||||
protected layoutBody(height: number, width: number): void {
|
||||
this.tree.layout(height, width);
|
||||
}
|
||||
|
||||
renderBody(container: HTMLElement): void {
|
||||
const treeContainer = DOM.append(container, DOM.$('.explorer-folders-view'));
|
||||
this.createTree(treeContainer);
|
||||
|
||||
if (this.toolbar) {
|
||||
this.toolbar.setActions(this.getActions(), this.getSecondaryActions())();
|
||||
}
|
||||
|
||||
this.disposables.push(this.labelService.onDidChangeFormatters(() => {
|
||||
this._onDidChangeTitleArea.fire();
|
||||
this.refresh();
|
||||
}));
|
||||
|
||||
this.disposables.push(this.explorerService.onDidChangeRoots(() => this.setTreeInput()));
|
||||
this.disposables.push(this.explorerService.onDidChangeItem(e => this.refresh(e)));
|
||||
this.disposables.push(this.explorerService.onDidChangeEditable(async e => {
|
||||
const isEditing = !!this.explorerService.getEditableData(e);
|
||||
|
||||
if (isEditing) {
|
||||
await this.tree.expand(e.parent!);
|
||||
} else {
|
||||
DOM.removeClass(treeContainer, 'highlight');
|
||||
}
|
||||
|
||||
await this.refresh(e.parent);
|
||||
|
||||
if (isEditing) {
|
||||
DOM.addClass(treeContainer, 'highlight');
|
||||
this.tree.reveal(e);
|
||||
} else {
|
||||
this.tree.domFocus();
|
||||
}
|
||||
}));
|
||||
this.disposables.push(this.explorerService.onDidSelectItem(e => this.onSelectItem(e.item, e.reveal)));
|
||||
this.disposables.push(this.explorerService.onDidCopyItems(e => this.onCopyItems(e.items, e.cut, e.previouslyCutItems)));
|
||||
|
||||
// Update configuration
|
||||
const configuration = this.configurationService.getValue<IFilesConfiguration>();
|
||||
this.onConfigurationUpdated(configuration);
|
||||
|
||||
// When the explorer viewer is loaded, listen to changes to the editor input
|
||||
this.disposables.push(this.editorService.onDidActiveEditorChange(() => {
|
||||
this.selectActiveFile(true);
|
||||
}));
|
||||
|
||||
// Also handle configuration updates
|
||||
this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue<IFilesConfiguration>(), e)));
|
||||
|
||||
this.disposables.push(this.onDidChangeBodyVisibility(async visible => {
|
||||
if (visible) {
|
||||
// If a refresh was requested and we are now visible, run it
|
||||
if (this.shouldRefresh) {
|
||||
this.shouldRefresh = false;
|
||||
await this.setTreeInput();
|
||||
}
|
||||
// Find resource to focus from active editor input if set
|
||||
this.selectActiveFile(false, true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
getActions(): IAction[] {
|
||||
const actions: Action[] = [];
|
||||
|
||||
const getFocus = () => {
|
||||
const focus = this.tree.getFocus();
|
||||
return focus.length > 0 ? focus[0] : undefined;
|
||||
};
|
||||
actions.push(this.instantiationService.createInstance(NewFileAction, getFocus));
|
||||
actions.push(this.instantiationService.createInstance(NewFolderAction, getFocus));
|
||||
actions.push(this.instantiationService.createInstance(RefreshExplorerView, RefreshExplorerView.ID, RefreshExplorerView.LABEL));
|
||||
actions.push(this.instantiationService.createInstance(CollapseAction, this.tree, true, 'explorer-action collapse-explorer'));
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.tree.domFocus();
|
||||
|
||||
const focused = this.tree.getFocus();
|
||||
if (focused.length === 1) {
|
||||
if (this.autoReveal) {
|
||||
this.tree.reveal(focused[0], 0.5);
|
||||
}
|
||||
|
||||
const activeFile = this.getActiveFile();
|
||||
if (!activeFile && !focused[0].isDirectory) {
|
||||
// Open the focused element in the editor if there is currently no file opened #67708
|
||||
this.editorService.openEditor({ resource: focused[0].resource, options: { preserveFocus: true, revealIfVisible: true } })
|
||||
.then(undefined, onUnexpectedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private selectActiveFile(deselect?: boolean, reveal = this.autoReveal): void {
|
||||
if (this.autoReveal) {
|
||||
const activeFile = this.getActiveFile();
|
||||
if (activeFile) {
|
||||
const focus = this.tree.getFocus();
|
||||
if (focus.length === 1 && focus[0].resource.toString() === activeFile.toString()) {
|
||||
// No action needed, active file is already focused
|
||||
return;
|
||||
}
|
||||
this.explorerService.select(activeFile, reveal);
|
||||
} else if (deselect) {
|
||||
this.tree.setSelection([]);
|
||||
this.tree.setFocus([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createTree(container: HTMLElement): void {
|
||||
this.filter = this.instantiationService.createInstance(FilesFilter);
|
||||
this.disposables.push(this.filter);
|
||||
const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
|
||||
this.disposables.push(explorerLabels);
|
||||
|
||||
const updateWidth = (stat: ExplorerItem) => this.tree.updateWidth(stat);
|
||||
const filesRenderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels, updateWidth);
|
||||
this.disposables.push(filesRenderer);
|
||||
|
||||
this.disposables.push(createFileIconThemableTreeContainerScope(container, this.themeService));
|
||||
|
||||
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, container, new ExplorerDelegate(), [filesRenderer],
|
||||
this.instantiationService.createInstance(ExplorerDataSource), {
|
||||
accessibilityProvider: new ExplorerAccessibilityProvider(),
|
||||
ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"),
|
||||
identityProvider: {
|
||||
getId: stat => (<ExplorerItem>stat).resource
|
||||
},
|
||||
keyboardNavigationLabelProvider: {
|
||||
getKeyboardNavigationLabel: stat => {
|
||||
const item = <ExplorerItem>stat;
|
||||
if (this.explorerService.isEditable(item)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return item.name;
|
||||
}
|
||||
},
|
||||
multipleSelectionSupport: true,
|
||||
filter: this.filter,
|
||||
sorter: this.instantiationService.createInstance(FileSorter),
|
||||
dnd: this.instantiationService.createInstance(FileDragAndDrop),
|
||||
autoExpandSingleChildren: true
|
||||
}) as WorkbenchAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>;
|
||||
this.disposables.push(this.tree);
|
||||
|
||||
// Bind context keys
|
||||
FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService);
|
||||
ExplorerFocusedContext.bindTo(this.tree.contextKeyService);
|
||||
|
||||
// Update resource context based on focused element
|
||||
this.disposables.push(this.tree.onDidChangeFocus(e => this.onFocusChanged(e.elements)));
|
||||
this.onFocusChanged([]);
|
||||
const explorerNavigator = new TreeResourceNavigator2(this.tree);
|
||||
this.disposables.push(explorerNavigator);
|
||||
// Open when selecting via keyboard
|
||||
this.disposables.push(explorerNavigator.onDidOpenResource(e => {
|
||||
const selection = this.tree.getSelection();
|
||||
// Do not react if the user is expanding selection via keyboard.
|
||||
// Check if the item was previously also selected, if yes the user is simply expanding / collapsing current selection #66589.
|
||||
const shiftDown = e.browserEvent instanceof KeyboardEvent && e.browserEvent.shiftKey;
|
||||
if (selection.length === 1 && !shiftDown) {
|
||||
if (selection[0].isDirectory || this.explorerService.isEditable(undefined)) {
|
||||
// Do not react if user is clicking on explorer items while some are being edited #70276
|
||||
// Do not react if clicking on directories
|
||||
return;
|
||||
}
|
||||
|
||||
/* __GDPR__
|
||||
"workbenchActionExecuted" : {
|
||||
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}*/
|
||||
this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' });
|
||||
this.editorService.openEditor({ resource: selection[0].resource, options: { preserveFocus: e.editorOptions.preserveFocus, pinned: e.editorOptions.pinned } }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP)
|
||||
.then(undefined, onUnexpectedError);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(this.tree.onContextMenu(e => this.onContextMenu(e)));
|
||||
this.disposables.push(this.tree.onKeyDown(e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
const toggleCollapsed = isMacintosh ? (event.keyCode === KeyCode.DownArrow && event.metaKey) : event.keyCode === KeyCode.Enter;
|
||||
if (toggleCollapsed && !this.explorerService.isEditable(undefined)) {
|
||||
const focus = this.tree.getFocus();
|
||||
if (focus.length === 1 && focus[0].isDirectory) {
|
||||
this.tree.toggleCollapsed(focus[0]);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// save view state on shutdown
|
||||
this.storageService.onWillSaveState(() => {
|
||||
this.storageService.store(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE);
|
||||
}, null, this.disposables);
|
||||
}
|
||||
|
||||
// React on events
|
||||
|
||||
private onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): void {
|
||||
this.autoReveal = configuration && configuration.explorer && configuration.explorer.autoReveal;
|
||||
|
||||
// Push down config updates to components of viewer
|
||||
let needsRefresh = false;
|
||||
if (this.filter) {
|
||||
needsRefresh = this.filter.updateConfiguration();
|
||||
}
|
||||
|
||||
if (event && !needsRefresh) {
|
||||
needsRefresh = event.affectsConfiguration('explorer.decorations.colors')
|
||||
|| event.affectsConfiguration('explorer.decorations.badges');
|
||||
}
|
||||
|
||||
// Refresh viewer as needed if this originates from a config event
|
||||
if (event && needsRefresh) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private onContextMenu(e: ITreeContextMenuEvent<ExplorerItem>): void {
|
||||
const stat = e.element;
|
||||
|
||||
// update dynamic contexts
|
||||
this.fileCopiedContextKey.set(this.clipboardService.hasResources());
|
||||
|
||||
const selection = this.tree.getSelection();
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => {
|
||||
const actions: IAction[] = [];
|
||||
// If the click is outside of the elements pass the root resource if there is only one root. If there are multiple roots pass empty object.
|
||||
const roots = this.explorerService.roots;
|
||||
const arg = stat instanceof ExplorerItem ? stat.resource : roots.length === 1 ? roots[0].resource : {};
|
||||
fillInContextMenuActions(this.contributedContextMenu, { arg, shouldForwardArgs: true }, actions, this.contextMenuService);
|
||||
return actions;
|
||||
},
|
||||
onHide: (wasCancelled?: boolean) => {
|
||||
if (wasCancelled) {
|
||||
this.tree.domFocus();
|
||||
}
|
||||
},
|
||||
getActionsContext: () => stat && selection && selection.indexOf(stat) >= 0
|
||||
? selection.map((fs: ExplorerItem) => fs.resource)
|
||||
: stat instanceof ExplorerItem ? [stat.resource] : []
|
||||
});
|
||||
}
|
||||
|
||||
private onFocusChanged(elements: ExplorerItem[]): void {
|
||||
const stat = elements && elements.length ? elements[0] : undefined;
|
||||
const isSingleFolder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER;
|
||||
const resource = stat ? stat.resource : isSingleFolder ? this.contextService.getWorkspace().folders[0].uri : null;
|
||||
this.resourceContext.set(resource);
|
||||
this.folderContext.set((isSingleFolder && !stat) || !!stat && stat.isDirectory);
|
||||
this.readonlyContext.set(!!stat && stat.isReadonly);
|
||||
this.rootContext.set(!stat || (stat && stat.isRoot));
|
||||
}
|
||||
|
||||
// General methods
|
||||
|
||||
/**
|
||||
* Refresh the contents of the explorer to get up to date data from the disk about the file structure.
|
||||
* If the item is passed we refresh only that level of the tree, otherwise we do a full refresh.
|
||||
*/
|
||||
private refresh(item?: ExplorerItem): Promise<void> {
|
||||
if (!this.tree || !this.isBodyVisible()) {
|
||||
this.shouldRefresh = true;
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Tree node doesn't exist yet
|
||||
if (item && !this.tree.hasNode(item)) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const recursive = !item;
|
||||
const toRefresh = item || this.tree.getInput();
|
||||
|
||||
return this.tree.updateChildren(toRefresh, recursive);
|
||||
}
|
||||
|
||||
getOptimalWidth(): number {
|
||||
const parentNode = this.tree.getHTMLElement();
|
||||
const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.explorer-item .label-name')); // select all file labels
|
||||
|
||||
return DOM.getLargestChildWidth(parentNode, childNodes);
|
||||
}
|
||||
|
||||
// private didLoad = false;
|
||||
|
||||
private setTreeInput(): Promise<void> {
|
||||
if (!this.isBodyVisible()) {
|
||||
this.shouldRefresh = true;
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const initialInputSetup = !this.tree.getInput();
|
||||
if (initialInputSetup) {
|
||||
perf.mark('willResolveExplorer');
|
||||
}
|
||||
const roots = this.explorerService.roots;
|
||||
let input: ExplorerItem | ExplorerItem[] = roots[0];
|
||||
if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER || roots[0].isError) {
|
||||
// Display roots only when multi folder workspace
|
||||
input = roots;
|
||||
}
|
||||
|
||||
let viewState: IAsyncDataTreeViewState | undefined;
|
||||
if (this.tree && this.tree.getInput()) {
|
||||
viewState = this.tree.getViewState();
|
||||
} else {
|
||||
const rawViewState = this.storageService.get(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, StorageScope.WORKSPACE);
|
||||
if (rawViewState) {
|
||||
viewState = JSON.parse(rawViewState) as IAsyncDataTreeViewState;
|
||||
}
|
||||
}
|
||||
|
||||
const previousInput = this.tree.getInput();
|
||||
const promise = this.tree.setInput(input, viewState).then(() => {
|
||||
if (Array.isArray(input)) {
|
||||
if (!viewState || previousInput instanceof ExplorerItem) {
|
||||
// There is no view state for this workspace, expand all roots. Or we transitioned from a folder workspace.
|
||||
input.forEach(item => this.tree.expand(item).then(undefined, onUnexpectedError));
|
||||
}
|
||||
if (Array.isArray(previousInput) && previousInput.length < input.length) {
|
||||
// Roots added to the explorer -> expand them.
|
||||
input.slice(previousInput.length).forEach(item => this.tree.expand(item).then(undefined, onUnexpectedError));
|
||||
}
|
||||
}
|
||||
if (initialInputSetup) {
|
||||
perf.mark('didResolveExplorer');
|
||||
}
|
||||
});
|
||||
|
||||
this.progressService.showWhile(promise, this.layoutService.isRestored() ? 800 : 1200 /* less ugly initial startup */);
|
||||
return promise;
|
||||
}
|
||||
|
||||
private getActiveFile(): URI | undefined {
|
||||
const input = this.editorService.activeEditor;
|
||||
|
||||
// ignore diff editor inputs (helps to get out of diffing when returning to explorer)
|
||||
if (input instanceof DiffEditorInput) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// check for files
|
||||
return withNullAsUndefined(toResource(input, { supportSideBySide: true }));
|
||||
}
|
||||
|
||||
private onSelectItem(fileStat: ExplorerItem | undefined, reveal = this.autoReveal): Promise<void> {
|
||||
if (!fileStat || !this.isBodyVisible() || this.tree.getInput() === fileStat) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Expand all stats in the parent chain
|
||||
const toExpand: ExplorerItem[] = [];
|
||||
let parent = fileStat.parent;
|
||||
while (parent) {
|
||||
toExpand.push(parent);
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return sequence(toExpand.reverse().map(s => () => this.tree.expand(s))).then(() => {
|
||||
if (reveal) {
|
||||
this.tree.reveal(fileStat, 0.5);
|
||||
}
|
||||
|
||||
this.tree.setFocus([fileStat]);
|
||||
this.tree.setSelection([fileStat]);
|
||||
});
|
||||
}
|
||||
|
||||
private onCopyItems(stats: ExplorerItem[], cut: boolean, previousCut: ExplorerItem[] | undefined): void {
|
||||
this.fileCopiedContextKey.set(stats.length > 0);
|
||||
this.resourceCutContextKey.set(cut && stats.length > 0);
|
||||
if (previousCut) {
|
||||
previousCut.forEach(item => this.tree.rerender(item));
|
||||
}
|
||||
if (cut) {
|
||||
stats.forEach(s => this.tree.rerender(s));
|
||||
}
|
||||
}
|
||||
|
||||
collapseAll(): void {
|
||||
this.tree.collapseAll();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.dragHandler) {
|
||||
this.dragHandler.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
825
src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
Normal file
@@ -0,0 +1,825 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IFileService, FileKind, IFileStat, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IDisposable, Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import { IFilesConfiguration, IExplorerService, IEditableData } from 'vs/workbench/contrib/files/common/files';
|
||||
import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources';
|
||||
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { localize } from 'vs/nls';
|
||||
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { equals, deepClone } from 'vs/base/common/objects';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
|
||||
import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers';
|
||||
import { fillResourceDataTransfers, CodeDataTransfers, extractResources } from 'vs/workbench/browser/dnd';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
|
||||
import { isMacintosh, isLinux } from 'vs/base/common/platform';
|
||||
import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ITask, sequence } from 'vs/base/common/async';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions';
|
||||
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
|
||||
|
||||
export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {
|
||||
|
||||
private static readonly ITEM_HEIGHT = 22;
|
||||
|
||||
getHeight(element: ExplorerItem): number {
|
||||
return ExplorerDelegate.ITEM_HEIGHT;
|
||||
}
|
||||
|
||||
getTemplateId(element: ExplorerItem): string {
|
||||
return FilesRenderer.ID;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> {
|
||||
|
||||
constructor(
|
||||
@IProgressService private progressService: IProgressService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService,
|
||||
@IFileService private fileService: IFileService
|
||||
) { }
|
||||
|
||||
hasChildren(element: ExplorerItem | ExplorerItem[]): boolean {
|
||||
return Array.isArray(element) || element.isDirectory;
|
||||
}
|
||||
|
||||
getChildren(element: ExplorerItem | ExplorerItem[]): Promise<ExplorerItem[]> {
|
||||
if (Array.isArray(element)) {
|
||||
return Promise.resolve(element);
|
||||
}
|
||||
|
||||
const promise = element.fetchChildren(this.fileService).then(undefined, e => {
|
||||
// Do not show error for roots since we already use an explorer decoration to notify user
|
||||
if (!(element instanceof ExplorerItem && element.isRoot)) {
|
||||
this.notificationService.error(e);
|
||||
}
|
||||
|
||||
return []; // we could not resolve any children because of an error
|
||||
});
|
||||
|
||||
this.progressService.showWhile(promise, this.layoutService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IFileTemplateData {
|
||||
elementDisposable: IDisposable;
|
||||
label: IResourceLabel;
|
||||
container: HTMLElement;
|
||||
}
|
||||
|
||||
export class FilesRenderer implements ITreeRenderer<ExplorerItem, FuzzyScore, IFileTemplateData>, IDisposable {
|
||||
static readonly ID = 'file';
|
||||
|
||||
private config: IFilesConfiguration;
|
||||
private configListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
private labels: ResourceLabels,
|
||||
private updateWidth: (stat: ExplorerItem) => void,
|
||||
@IContextViewService private readonly contextViewService: IContextViewService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService
|
||||
) {
|
||||
this.config = this.configurationService.getValue<IFilesConfiguration>();
|
||||
this.configListener = this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('explorer')) {
|
||||
this.config = this.configurationService.getValue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get templateId(): string {
|
||||
return FilesRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IFileTemplateData {
|
||||
const elementDisposable = Disposable.None;
|
||||
const label = this.labels.create(container, { supportHighlights: true });
|
||||
|
||||
return { elementDisposable, label, container };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
|
||||
templateData.elementDisposable.dispose();
|
||||
const stat = node.element;
|
||||
const editableData = this.explorerService.getEditableData(stat);
|
||||
|
||||
// File Label
|
||||
if (!editableData) {
|
||||
templateData.label.element.style.display = 'flex';
|
||||
const extraClasses = ['explorer-item'];
|
||||
if (this.explorerService.isCut(stat)) {
|
||||
extraClasses.push('cut');
|
||||
}
|
||||
templateData.label.setFile(stat.resource, {
|
||||
hidePath: true,
|
||||
fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
|
||||
extraClasses,
|
||||
fileDecorations: this.config.explorer.decorations,
|
||||
matches: createMatches(node.filterData)
|
||||
});
|
||||
|
||||
templateData.elementDisposable = templateData.label.onDidRender(() => {
|
||||
this.updateWidth(stat);
|
||||
});
|
||||
}
|
||||
|
||||
// Input Box
|
||||
else {
|
||||
templateData.label.element.style.display = 'none';
|
||||
templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
|
||||
}
|
||||
}
|
||||
|
||||
private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
|
||||
|
||||
// Use a file label only for the icon next to the input box
|
||||
const label = this.labels.create(container);
|
||||
const extraClasses = ['explorer-item', 'explorer-item-edited'];
|
||||
const fileKind = stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE;
|
||||
const labelOptions: IFileLabelOptions = { hidePath: true, hideLabel: true, fileKind, extraClasses };
|
||||
|
||||
const parent = stat.name ? dirname(stat.resource) : stat.resource;
|
||||
const value = stat.name || '';
|
||||
|
||||
label.setFile(joinPath(parent, value || ' '), labelOptions); // Use icon for ' ' if name is empty.
|
||||
|
||||
// Input field for name
|
||||
const inputBox = new InputBox(label.element, this.contextViewService, {
|
||||
validationOptions: {
|
||||
validation: (value) => {
|
||||
const content = editableData.validationMessage(value);
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
formatContent: true,
|
||||
type: MessageType.ERROR
|
||||
};
|
||||
}
|
||||
},
|
||||
ariaLabel: localize('fileInputAriaLabel', "Type file name. Press Enter to confirm or Escape to cancel.")
|
||||
});
|
||||
const styler = attachInputBoxStyler(inputBox, this.themeService);
|
||||
|
||||
inputBox.onDidChange(value => {
|
||||
label.setFile(joinPath(parent, value || ' '), labelOptions); // update label icon while typing!
|
||||
});
|
||||
|
||||
const lastDot = value.lastIndexOf('.');
|
||||
|
||||
inputBox.value = value;
|
||||
inputBox.focus();
|
||||
inputBox.select({ start: 0, end: lastDot > 0 && !stat.isDirectory ? lastDot : value.length });
|
||||
|
||||
const done = once(async (success: boolean) => {
|
||||
label.element.style.display = 'none';
|
||||
const value = inputBox.value;
|
||||
dispose(toDispose);
|
||||
container.removeChild(label.element);
|
||||
editableData.onFinish(value, success);
|
||||
});
|
||||
|
||||
let ignoreDisposeAndBlur = true;
|
||||
setTimeout(() => ignoreDisposeAndBlur = false, 100);
|
||||
const blurDisposable = DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
|
||||
if (!ignoreDisposeAndBlur) {
|
||||
done(inputBox.isInputValid());
|
||||
}
|
||||
});
|
||||
|
||||
const toDispose = [
|
||||
inputBox,
|
||||
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
|
||||
if (e.equals(KeyCode.Enter)) {
|
||||
if (inputBox.validate()) {
|
||||
done(true);
|
||||
}
|
||||
} else if (e.equals(KeyCode.Escape)) {
|
||||
done(false);
|
||||
}
|
||||
}),
|
||||
blurDisposable,
|
||||
label,
|
||||
styler
|
||||
];
|
||||
|
||||
return toDisposable(() => {
|
||||
if (!ignoreDisposeAndBlur) {
|
||||
blurDisposable.dispose();
|
||||
done(inputBox.isInputValid());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
|
||||
templateData.elementDisposable.dispose();
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IFileTemplateData): void {
|
||||
templateData.elementDisposable.dispose();
|
||||
templateData.label.dispose();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.configListener.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
|
||||
getAriaLabel(element: ExplorerItem): string {
|
||||
return element.name;
|
||||
}
|
||||
}
|
||||
|
||||
interface CachedParsedExpression {
|
||||
original: glob.IExpression;
|
||||
parsed: glob.ParsedExpression;
|
||||
}
|
||||
|
||||
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
|
||||
private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
|
||||
private workspaceFolderChangeListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService
|
||||
) {
|
||||
this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
|
||||
this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
|
||||
}
|
||||
|
||||
updateConfiguration(): boolean {
|
||||
let needsRefresh = false;
|
||||
this.contextService.getWorkspace().folders.forEach(folder => {
|
||||
const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
|
||||
const excludesConfig: glob.IExpression = (configuration && configuration.files && configuration.files.exclude) || Object.create(null);
|
||||
|
||||
if (!needsRefresh) {
|
||||
const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString());
|
||||
needsRefresh = !cached || !equals(cached.original, excludesConfig);
|
||||
}
|
||||
|
||||
const excludesConfigCopy = deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods
|
||||
|
||||
this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression);
|
||||
});
|
||||
|
||||
return needsRefresh;
|
||||
}
|
||||
|
||||
filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
|
||||
if (parentVisibility === TreeVisibility.Hidden) {
|
||||
return false;
|
||||
}
|
||||
if (this.explorerService.getEditableData(stat) || stat.isRoot) {
|
||||
return true; // always visible
|
||||
}
|
||||
|
||||
// Hide those that match Hidden Patterns
|
||||
const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
|
||||
if (cached && cached.parsed(path.normalize(path.relative(stat.root.resource.path, stat.resource.path)), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) {
|
||||
// review (isidor): is path.normalize necessary? path.relative already returns an os path
|
||||
return false; // hidden through pattern
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.workspaceFolderChangeListener = dispose(this.workspaceFolderChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
// // Explorer Sorter
|
||||
export class FileSorter implements ITreeSorter<ExplorerItem> {
|
||||
|
||||
constructor(
|
||||
@IExplorerService private readonly explorerService: IExplorerService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
|
||||
) { }
|
||||
|
||||
public compare(statA: ExplorerItem, statB: ExplorerItem): number {
|
||||
// Do not sort roots
|
||||
if (statA.isRoot) {
|
||||
if (statB.isRoot) {
|
||||
const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
|
||||
const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
|
||||
return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (statB.isRoot) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const sortOrder = this.explorerService.sortOrder;
|
||||
|
||||
// Sort Directories
|
||||
switch (sortOrder) {
|
||||
case 'type':
|
||||
if (statA.isDirectory && !statB.isDirectory) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (statB.isDirectory && !statA.isDirectory) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (statA.isDirectory && statB.isDirectory) {
|
||||
return compareFileNames(statA.name, statB.name);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'filesFirst':
|
||||
if (statA.isDirectory && !statB.isDirectory) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (statB.isDirectory && !statA.isDirectory) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'mixed':
|
||||
break; // not sorting when "mixed" is on
|
||||
|
||||
default: /* 'default', 'modified' */
|
||||
if (statA.isDirectory && !statB.isDirectory) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (statB.isDirectory && !statA.isDirectory) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Sort Files
|
||||
switch (sortOrder) {
|
||||
case 'type':
|
||||
return compareFileExtensions(statA.name, statB.name);
|
||||
|
||||
case 'modified':
|
||||
if (statA.mtime !== statB.mtime) {
|
||||
return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
|
||||
}
|
||||
|
||||
return compareFileNames(statA.name, statB.name);
|
||||
|
||||
default: /* 'default', 'mixed', 'filesFirst' */
|
||||
return compareFileNames(statA.name, statB.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
|
||||
private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';
|
||||
|
||||
private toDispose: IDisposable[];
|
||||
private dropEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IExplorerService private explorerService: IExplorerService,
|
||||
@IEditorService private editorService: IEditorService,
|
||||
@IDialogService private dialogService: IDialogService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@ITextFileService private textFileService: ITextFileService,
|
||||
@IWindowService private windowService: IWindowService,
|
||||
@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
|
||||
) {
|
||||
this.toDispose = [];
|
||||
|
||||
const updateDropEnablement = () => {
|
||||
this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop');
|
||||
};
|
||||
updateDropEnablement();
|
||||
this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => updateDropEnablement()));
|
||||
}
|
||||
|
||||
onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
|
||||
if (!this.dropEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
|
||||
const fromDesktop = data instanceof DesktopDragAndDropData;
|
||||
const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
|
||||
|
||||
// Desktop DND
|
||||
if (fromDesktop && originalEvent.dataTransfer) {
|
||||
const types = originalEvent.dataTransfer.types;
|
||||
const typesArray: string[] = [];
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase
|
||||
}
|
||||
|
||||
if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Other-Tree DND
|
||||
else if (data instanceof ExternalElementsDragAndDropData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In-Explorer DND
|
||||
else {
|
||||
const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
|
||||
|
||||
if (!target) {
|
||||
// Droping onto the empty area. Do not accept if items dragged are already
|
||||
// children of the root unless we are copying the file
|
||||
if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
|
||||
}
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (items.some((source) => {
|
||||
if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) {
|
||||
return true; // Root folder can not be moved to a non root file stat.
|
||||
}
|
||||
|
||||
if (source.resource.toString() === target.resource.toString()) {
|
||||
return true; // Can not move anything onto itself
|
||||
}
|
||||
|
||||
if (source.isRoot && target instanceof ExplorerItem && target.isRoot) {
|
||||
// Disable moving workspace roots in one another
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isCopy && dirname(source.resource).toString() === target.resource.toString()) {
|
||||
return true; // Can not move a file to the same parent unless we copy
|
||||
}
|
||||
|
||||
if (isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) {
|
||||
return true; // Can not move a parent folder into one of its children
|
||||
}
|
||||
|
||||
return false;
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// All (target = model)
|
||||
if (!target) {
|
||||
return { accept: true, bubble: TreeDragOverBubble.Down, effect };
|
||||
}
|
||||
|
||||
// All (target = file/folder)
|
||||
else {
|
||||
if (target.isDirectory) {
|
||||
if (target.isReadonly) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: true };
|
||||
}
|
||||
|
||||
if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) {
|
||||
return { accept: true, bubble: TreeDragOverBubble.Up, effect };
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getDragURI(element: ExplorerItem): string | null {
|
||||
if (this.explorerService.isEditable(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return element.resource.toString();
|
||||
}
|
||||
|
||||
getDragLabel(elements: ExplorerItem[]): string | undefined {
|
||||
if (elements.length > 1) {
|
||||
return String(elements.length);
|
||||
}
|
||||
|
||||
return elements[0].name;
|
||||
}
|
||||
|
||||
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
|
||||
const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
|
||||
if (items && items.length && originalEvent.dataTransfer) {
|
||||
// Apply some datatransfer types to allow for dragging the element outside of the application
|
||||
this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
|
||||
|
||||
// The only custom data transfer we set from the explorer is a file transfer
|
||||
// to be able to DND between multiple code file explorers across windows
|
||||
const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
|
||||
if (fileResources.length) {
|
||||
originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
|
||||
// Find parent to add to
|
||||
if (!target) {
|
||||
target = this.explorerService.roots[this.explorerService.roots.length - 1];
|
||||
}
|
||||
if (!target.isDirectory && target.parent) {
|
||||
target = target.parent;
|
||||
}
|
||||
if (target.isReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Desktop DND (Import file)
|
||||
if (data instanceof DesktopDragAndDropData) {
|
||||
this.handleExternalDrop(data, target, originalEvent);
|
||||
}
|
||||
// In-Explorer DND (Move/Copy file)
|
||||
else {
|
||||
this.handleExplorerDrop(data, target, originalEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
|
||||
const droppedResources = extractResources(originalEvent, true);
|
||||
|
||||
// Check for dropped external files to be folders
|
||||
return this.fileService.resolveFiles(droppedResources).then(result => {
|
||||
|
||||
// Pass focus to window
|
||||
this.windowService.focusWindow();
|
||||
|
||||
// Handle folders by adding to workspace if we are in workspace context
|
||||
const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource }));
|
||||
if (folders.length > 0) {
|
||||
|
||||
// If we are in no-workspace context, ask for confirmation to create a workspace
|
||||
let confirmedPromise: Promise<IConfirmationResult> = Promise.resolve({ confirmed: true });
|
||||
if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
|
||||
confirmedPromise = this.dialogService.confirm({
|
||||
message: folders.length > 1 ? localize('dropFolders', "Do you want to add the folders to the workspace?") : localize('dropFolder', "Do you want to add the folder to the workspace?"),
|
||||
type: 'question',
|
||||
primaryButton: folders.length > 1 ? localize('addFolders', "&&Add Folders") : localize('addFolder', "&&Add Folder")
|
||||
});
|
||||
}
|
||||
|
||||
return confirmedPromise.then(res => {
|
||||
if (res.confirmed) {
|
||||
return this.workspaceEditingService.addFolders(folders);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle dropped files (only support FileStat as target)
|
||||
else if (target instanceof ExplorerItem) {
|
||||
return this.addResources(target, droppedResources.map(res => res.resource));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private addResources(target: ExplorerItem, resources: URI[]): Promise<any> {
|
||||
if (resources && resources.length > 0) {
|
||||
|
||||
// Resolve target to check for name collisions and ask user
|
||||
return this.fileService.resolveFile(target.resource).then((targetStat: IFileStat) => {
|
||||
|
||||
// Check for name collisions
|
||||
const targetNames = new Set<string>();
|
||||
if (targetStat.children) {
|
||||
targetStat.children.forEach((child) => {
|
||||
targetNames.add(isLinux ? child.name : child.name.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
let overwritePromise: Promise<IConfirmationResult> = Promise.resolve({ confirmed: true });
|
||||
if (resources.some(resource => {
|
||||
return targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase());
|
||||
})) {
|
||||
const confirm: IConfirmation = {
|
||||
message: localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"),
|
||||
detail: localize('irreversible', "This action is irreversible!"),
|
||||
primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
|
||||
type: 'warning'
|
||||
};
|
||||
|
||||
overwritePromise = this.dialogService.confirm(confirm);
|
||||
}
|
||||
|
||||
return overwritePromise.then(res => {
|
||||
if (!res.confirmed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Run add in sequence
|
||||
const addPromisesFactory: ITask<Promise<void>>[] = [];
|
||||
resources.forEach(resource => {
|
||||
addPromisesFactory.push(() => {
|
||||
const sourceFile = resource;
|
||||
const targetFile = joinPath(target.resource, basename(sourceFile));
|
||||
|
||||
// if the target exists and is dirty, make sure to revert it. otherwise the dirty contents
|
||||
// of the target file would replace the contents of the added file. since we already
|
||||
// confirmed the overwrite before, this is OK.
|
||||
let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
|
||||
if (this.textFileService.isDirty(targetFile)) {
|
||||
revertPromise = this.textFileService.revertAll([targetFile], { soft: true });
|
||||
}
|
||||
|
||||
return revertPromise.then(() => {
|
||||
const copyTarget = joinPath(target.resource, basename(sourceFile));
|
||||
return this.fileService.copyFile(sourceFile, copyTarget, true).then(stat => {
|
||||
|
||||
// if we only add one file, just open it directly
|
||||
if (resources.length === 1) {
|
||||
this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return sequence(addPromisesFactory);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
|
||||
const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
|
||||
const items = distinctParents(elementsData, s => s.resource);
|
||||
const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
|
||||
|
||||
let confirmPromise: Promise<IConfirmationResult>;
|
||||
|
||||
// Handle confirm setting
|
||||
const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
|
||||
if (confirmDragAndDrop) {
|
||||
confirmPromise = this.dialogService.confirm({
|
||||
message: items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?")
|
||||
: items.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", items.length), items.map(s => s.resource))
|
||||
: items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name)
|
||||
: localize('confirmMove', "Are you sure you want to move '{0}'?", items[0].name),
|
||||
checkbox: {
|
||||
label: localize('doNotAskAgain', "Do not ask me again")
|
||||
},
|
||||
type: 'question',
|
||||
primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
|
||||
});
|
||||
} else {
|
||||
confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult);
|
||||
}
|
||||
|
||||
return confirmPromise.then(res => {
|
||||
|
||||
// Check for confirmation checkbox
|
||||
let updateConfirmSettingsPromise: Promise<void> = Promise.resolve(undefined);
|
||||
if (res.confirmed && res.checkboxChecked === true) {
|
||||
updateConfirmSettingsPromise = this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
return updateConfirmSettingsPromise.then(() => {
|
||||
if (res.confirmed) {
|
||||
const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
|
||||
return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
|
||||
if (roots.length === 0) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const folders = this.contextService.getWorkspace().folders;
|
||||
let targetIndex: number | undefined;
|
||||
const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
|
||||
const rootsToMove: IWorkspaceFolderCreationData[] = [];
|
||||
|
||||
for (let index = 0; index < folders.length; index++) {
|
||||
const data = {
|
||||
uri: folders[index].uri,
|
||||
name: folders[index].name
|
||||
};
|
||||
if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
|
||||
targetIndex = index;
|
||||
}
|
||||
|
||||
if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
|
||||
workspaceCreationData.push(data);
|
||||
} else {
|
||||
rootsToMove.push(data);
|
||||
}
|
||||
}
|
||||
if (!targetIndex) {
|
||||
targetIndex = workspaceCreationData.length;
|
||||
}
|
||||
|
||||
workspaceCreationData.splice(targetIndex, 0, ...rootsToMove);
|
||||
return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData);
|
||||
}
|
||||
|
||||
private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
|
||||
// Reuse duplicate action if user copies
|
||||
if (isCopy) {
|
||||
|
||||
return this.fileService.copyFile(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwirte: false })).then(stat => {
|
||||
if (!stat.isDirectory) {
|
||||
return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }).then(() => undefined);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise move
|
||||
const targetResource = joinPath(target.resource, source.name);
|
||||
|
||||
return this.textFileService.move(source.resource, targetResource).then(undefined, error => {
|
||||
|
||||
// Conflict
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
|
||||
const confirm: IConfirmation = {
|
||||
message: localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name),
|
||||
detail: localize('irreversible', "This action is irreversible!"),
|
||||
primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
|
||||
type: 'warning'
|
||||
};
|
||||
|
||||
// Move with overwrite if the user confirms
|
||||
return this.dialogService.confirm(confirm).then(res => {
|
||||
if (res.confirmed) {
|
||||
return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(undefined, error => this.notificationService.error(error));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Any other error
|
||||
else {
|
||||
this.notificationService.error(error);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
670
src/vs/workbench/contrib/files/browser/views/openEditorsView.ts
Normal file
@@ -0,0 +1,670 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IAction, ActionRunner } from 'vs/base/common/actions';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IEditorInput } from 'vs/workbench/common/editor';
|
||||
import { SaveAllAction, SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions';
|
||||
import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files';
|
||||
import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { CloseAllEditorsAction, CloseEditorAction } from 'vs/workbench/browser/parts/editor/editorActions';
|
||||
import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/layoutActions';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { WorkbenchList } from 'vs/platform/list/browser/listService';
|
||||
import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list';
|
||||
import { ResourceLabels, IResourceLabel, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
|
||||
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { DirtyEditorContext, OpenEditorsGroupContext } from 'vs/workbench/contrib/files/browser/fileCommands';
|
||||
import { ResourceContextKey } from 'vs/workbench/common/resources';
|
||||
import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers } from 'vs/workbench/browser/dnd';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { ElementsDragAndDropData, DesktopDragAndDropData } from 'vs/base/browser/ui/list/listView';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class OpenEditorsView extends ViewletPanel {
|
||||
|
||||
private static readonly DEFAULT_VISIBLE_OPEN_EDITORS = 9;
|
||||
static readonly ID = 'workbench.explorer.openEditorsView';
|
||||
static NAME = nls.localize({ key: 'openEditors', comment: ['Open is an adjective'] }, "Open Editors");
|
||||
|
||||
private dirtyCountElement: HTMLElement;
|
||||
private listRefreshScheduler: RunOnceScheduler;
|
||||
private structuralRefreshDelay: number;
|
||||
private list: WorkbenchList<OpenEditor | IEditorGroup>;
|
||||
private listLabels: ResourceLabels;
|
||||
private contributedContextMenu: IMenu;
|
||||
private needsRefresh: boolean;
|
||||
private resourceContext: ResourceContextKey;
|
||||
private groupFocusedContext: IContextKey<boolean>;
|
||||
private dirtyEditorFocusedContext: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
options: IViewletViewOptions,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IMenuService private readonly menuService: IMenuService
|
||||
) {
|
||||
super({
|
||||
...(options as IViewletPanelOptions),
|
||||
ariaHeaderLabel: nls.localize({ key: 'openEditosrSection', comment: ['Open is an adjective'] }, "Open Editors Section"),
|
||||
}, keybindingService, contextMenuService, configurationService);
|
||||
|
||||
this.structuralRefreshDelay = 0;
|
||||
this.listRefreshScheduler = new RunOnceScheduler(() => {
|
||||
const previousLength = this.list.length;
|
||||
this.list.splice(0, this.list.length, this.elements);
|
||||
this.focusActiveEditor();
|
||||
if (previousLength !== this.list.length) {
|
||||
this.updateSize();
|
||||
}
|
||||
this.needsRefresh = false;
|
||||
}, this.structuralRefreshDelay);
|
||||
|
||||
this.registerUpdateEvents();
|
||||
|
||||
// Also handle configuration updates
|
||||
this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(e)));
|
||||
|
||||
// Handle dirty counter
|
||||
this.disposables.push(this.untitledEditorService.onDidChangeDirty(() => this.updateDirtyIndicator()));
|
||||
this.disposables.push(this.textFileService.models.onModelsDirty(() => this.updateDirtyIndicator()));
|
||||
this.disposables.push(this.textFileService.models.onModelsSaved(() => this.updateDirtyIndicator()));
|
||||
this.disposables.push(this.textFileService.models.onModelsSaveError(() => this.updateDirtyIndicator()));
|
||||
this.disposables.push(this.textFileService.models.onModelsReverted(() => this.updateDirtyIndicator()));
|
||||
}
|
||||
|
||||
private registerUpdateEvents(): void {
|
||||
const updateWholeList = () => {
|
||||
if (!this.isBodyVisible() || !this.list) {
|
||||
this.needsRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.listRefreshScheduler.schedule(this.structuralRefreshDelay);
|
||||
};
|
||||
|
||||
const groupDisposables = new Map<number, IDisposable>();
|
||||
const addGroupListener = (group: IEditorGroup) => {
|
||||
groupDisposables.set(group.id, group.onDidGroupChange(e => {
|
||||
if (this.listRefreshScheduler.isScheduled()) {
|
||||
return;
|
||||
}
|
||||
if (!this.isBodyVisible() || !this.list) {
|
||||
this.needsRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.getIndex(group, e.editor);
|
||||
switch (e.kind) {
|
||||
case GroupChangeKind.GROUP_LABEL: {
|
||||
if (this.showGroups) {
|
||||
this.list.splice(index, 1, [group]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GroupChangeKind.GROUP_ACTIVE:
|
||||
case GroupChangeKind.EDITOR_ACTIVE: {
|
||||
this.focusActiveEditor();
|
||||
break;
|
||||
}
|
||||
case GroupChangeKind.EDITOR_DIRTY:
|
||||
case GroupChangeKind.EDITOR_LABEL:
|
||||
case GroupChangeKind.EDITOR_PIN: {
|
||||
this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]);
|
||||
break;
|
||||
}
|
||||
case GroupChangeKind.EDITOR_OPEN: {
|
||||
this.list.splice(index, 0, [new OpenEditor(e.editor!, group)]);
|
||||
setTimeout(() => this.updateSize(), this.structuralRefreshDelay);
|
||||
break;
|
||||
}
|
||||
case GroupChangeKind.EDITOR_CLOSE: {
|
||||
const previousIndex = this.getIndex(group, undefined) + (e.editorIndex || 0) + (this.showGroups ? 1 : 0);
|
||||
this.list.splice(previousIndex, 1);
|
||||
this.updateSize();
|
||||
break;
|
||||
}
|
||||
case GroupChangeKind.EDITOR_MOVE: {
|
||||
this.listRefreshScheduler.schedule();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.disposables.push(groupDisposables.get(group.id)!);
|
||||
};
|
||||
|
||||
this.editorGroupService.groups.forEach(g => addGroupListener(g));
|
||||
this.disposables.push(this.editorGroupService.onDidAddGroup(group => {
|
||||
addGroupListener(group);
|
||||
updateWholeList();
|
||||
}));
|
||||
this.disposables.push(this.editorGroupService.onDidMoveGroup(() => updateWholeList()));
|
||||
this.disposables.push(this.editorGroupService.onDidRemoveGroup(group => {
|
||||
dispose(groupDisposables.get(group.id));
|
||||
updateWholeList();
|
||||
}));
|
||||
}
|
||||
|
||||
protected renderHeaderTitle(container: HTMLElement): void {
|
||||
super.renderHeaderTitle(container, this.title);
|
||||
|
||||
const count = dom.append(container, $('.count'));
|
||||
this.dirtyCountElement = dom.append(count, $('.monaco-count-badge'));
|
||||
|
||||
this.disposables.push((attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => {
|
||||
const background = colors.badgeBackground ? colors.badgeBackground.toString() : null;
|
||||
const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : null;
|
||||
const border = colors.contrastBorder ? colors.contrastBorder.toString() : null;
|
||||
|
||||
this.dirtyCountElement.style.backgroundColor = background;
|
||||
this.dirtyCountElement.style.color = foreground;
|
||||
|
||||
this.dirtyCountElement.style.borderWidth = border ? '1px' : null;
|
||||
this.dirtyCountElement.style.borderStyle = border ? 'solid' : null;
|
||||
this.dirtyCountElement.style.borderColor = border;
|
||||
})));
|
||||
|
||||
this.updateDirtyIndicator();
|
||||
}
|
||||
|
||||
public renderBody(container: HTMLElement): void {
|
||||
dom.addClass(container, 'explorer-open-editors');
|
||||
dom.addClass(container, 'show-file-icons');
|
||||
|
||||
const delegate = new OpenEditorsDelegate();
|
||||
|
||||
if (this.list) {
|
||||
this.list.dispose();
|
||||
}
|
||||
if (this.listLabels) {
|
||||
this.listLabels.clear();
|
||||
}
|
||||
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility } as IResourceLabelsContainer);
|
||||
this.list = this.instantiationService.createInstance(WorkbenchList, container, delegate, [
|
||||
new EditorGroupRenderer(this.keybindingService, this.instantiationService),
|
||||
new OpenEditorRenderer(this.listLabels, this.instantiationService, this.keybindingService, this.configurationService)
|
||||
], {
|
||||
identityProvider: { getId: (element: OpenEditor | IEditorGroup) => element instanceof OpenEditor ? element.getId() : element.id.toString() },
|
||||
dnd: new OpenEditorsDragAndDrop(this.instantiationService, this.editorGroupService)
|
||||
}) as WorkbenchList<OpenEditor | IEditorGroup>;
|
||||
this.disposables.push(this.list);
|
||||
this.disposables.push(this.listLabels);
|
||||
|
||||
this.contributedContextMenu = this.menuService.createMenu(MenuId.OpenEditorsContext, this.list.contextKeyService);
|
||||
this.disposables.push(this.contributedContextMenu);
|
||||
|
||||
this.updateSize();
|
||||
|
||||
// Bind context keys
|
||||
OpenEditorsFocusedContext.bindTo(this.list.contextKeyService);
|
||||
ExplorerFocusedContext.bindTo(this.list.contextKeyService);
|
||||
|
||||
this.resourceContext = this.instantiationService.createInstance(ResourceContextKey);
|
||||
this.disposables.push(this.resourceContext);
|
||||
this.groupFocusedContext = OpenEditorsGroupContext.bindTo(this.contextKeyService);
|
||||
this.dirtyEditorFocusedContext = DirtyEditorContext.bindTo(this.contextKeyService);
|
||||
|
||||
this.disposables.push(this.list.onContextMenu(e => this.onListContextMenu(e)));
|
||||
this.list.onFocusChange(e => {
|
||||
this.resourceContext.reset();
|
||||
this.groupFocusedContext.reset();
|
||||
this.dirtyEditorFocusedContext.reset();
|
||||
const element = e.elements.length ? e.elements[0] : undefined;
|
||||
if (element instanceof OpenEditor) {
|
||||
this.dirtyEditorFocusedContext.set(this.textFileService.isDirty(withNullAsUndefined(element.getResource())));
|
||||
this.resourceContext.set(element.getResource());
|
||||
} else if (!!element) {
|
||||
this.groupFocusedContext.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Open when selecting via keyboard
|
||||
this.disposables.push(this.list.onMouseMiddleClick(e => {
|
||||
if (e && e.element instanceof OpenEditor) {
|
||||
e.element.group.closeEditor(e.element.editor, { preserveFocus: true });
|
||||
}
|
||||
}));
|
||||
this.disposables.push(this.list.onDidOpen(e => {
|
||||
const browserEvent = e.browserEvent;
|
||||
|
||||
let openToSide = false;
|
||||
let isSingleClick = false;
|
||||
let isDoubleClick = false;
|
||||
if (browserEvent instanceof MouseEvent) {
|
||||
isSingleClick = browserEvent.detail === 1;
|
||||
isDoubleClick = browserEvent.detail === 2;
|
||||
openToSide = this.list.useAltAsMultipleSelectionModifier ? (browserEvent.ctrlKey || browserEvent.metaKey) : browserEvent.altKey;
|
||||
}
|
||||
|
||||
const focused = this.list.getFocusedElements();
|
||||
const element = focused.length ? focused[0] : undefined;
|
||||
if (element instanceof OpenEditor) {
|
||||
this.openEditor(element, { preserveFocus: isSingleClick, pinned: isDoubleClick, sideBySide: openToSide });
|
||||
} else if (element) {
|
||||
this.editorGroupService.activateGroup(element);
|
||||
}
|
||||
}));
|
||||
|
||||
this.listRefreshScheduler.schedule(0);
|
||||
|
||||
this.disposables.push(this.onDidChangeBodyVisibility(visible => {
|
||||
if (visible && this.needsRefresh) {
|
||||
this.listRefreshScheduler.schedule(0);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public getActions(): IAction[] {
|
||||
return [
|
||||
this.instantiationService.createInstance(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL),
|
||||
this.instantiationService.createInstance(SaveAllAction, SaveAllAction.ID, SaveAllAction.LABEL),
|
||||
this.instantiationService.createInstance(CloseAllEditorsAction, CloseAllEditorsAction.ID, CloseAllEditorsAction.LABEL)
|
||||
];
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
super.focus();
|
||||
this.list.domFocus();
|
||||
}
|
||||
|
||||
public getList(): WorkbenchList<OpenEditor | IEditorGroup> {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
protected layoutBody(height: number, width: number): void {
|
||||
if (this.list) {
|
||||
this.list.layout(height, width);
|
||||
}
|
||||
}
|
||||
|
||||
private get showGroups(): boolean {
|
||||
return this.editorGroupService.groups.length > 1;
|
||||
}
|
||||
|
||||
private get elements(): Array<IEditorGroup | OpenEditor> {
|
||||
const result: Array<IEditorGroup | OpenEditor> = [];
|
||||
this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(g => {
|
||||
if (this.showGroups) {
|
||||
result.push(g);
|
||||
}
|
||||
result.push(...g.editors.map(ei => new OpenEditor(ei, g)));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getIndex(group: IEditorGroup, editor: IEditorInput | undefined | null): number {
|
||||
let index = editor ? group.getIndexOfEditor(editor) : 0;
|
||||
if (!this.showGroups) {
|
||||
return index;
|
||||
}
|
||||
|
||||
for (let g of this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)) {
|
||||
if (g.id === group.id) {
|
||||
return index + (!!editor ? 1 : 0);
|
||||
} else {
|
||||
index += g.count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private openEditor(element: OpenEditor, options: { preserveFocus: boolean; pinned: boolean; sideBySide: boolean; }): void {
|
||||
if (element) {
|
||||
/* __GDPR__
|
||||
"workbenchActionExecuted" : {
|
||||
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'openEditors' });
|
||||
|
||||
const preserveActivateGroup = options.sideBySide && options.preserveFocus; // needed for https://github.com/Microsoft/vscode/issues/42399
|
||||
if (!preserveActivateGroup) {
|
||||
this.editorGroupService.activateGroup(element.groupId); // needed for https://github.com/Microsoft/vscode/issues/6672
|
||||
}
|
||||
this.editorService.openEditor(element.editor, options, options.sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => {
|
||||
if (editor && !preserveActivateGroup && editor.group) {
|
||||
this.editorGroupService.activateGroup(editor.group);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onListContextMenu(e: IListContextMenuEvent<OpenEditor | IEditorGroup>): void {
|
||||
if (!e.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = e.element;
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => {
|
||||
const actions: IAction[] = [];
|
||||
fillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true, arg: element instanceof OpenEditor ? element.editor.getResource() : {} }, actions, this.contextMenuService);
|
||||
return actions;
|
||||
},
|
||||
getActionsContext: () => element instanceof OpenEditor ? { groupId: element.groupId, editorIndex: element.editorIndex } : { groupId: element.id }
|
||||
});
|
||||
}
|
||||
|
||||
private focusActiveEditor(): void {
|
||||
if (this.list.length && this.editorGroupService.activeGroup) {
|
||||
const index = this.getIndex(this.editorGroupService.activeGroup, this.editorGroupService.activeGroup.activeEditor);
|
||||
this.list.setFocus([index]);
|
||||
this.list.setSelection([index]);
|
||||
this.list.reveal(index);
|
||||
} else {
|
||||
this.list.setFocus([]);
|
||||
this.list.setSelection([]);
|
||||
}
|
||||
}
|
||||
|
||||
private onConfigurationChange(event: IConfigurationChangeEvent): void {
|
||||
if (event.affectsConfiguration('explorer.openEditors')) {
|
||||
this.updateSize();
|
||||
}
|
||||
|
||||
// Trigger a 'repaint' when decoration settings change
|
||||
if (event.affectsConfiguration('explorer.decorations')) {
|
||||
this.listRefreshScheduler.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
private updateSize(): void {
|
||||
// Adjust expanded body size
|
||||
this.minimumBodySize = this.getMinExpandedBodySize();
|
||||
this.maximumBodySize = this.getMaxExpandedBodySize();
|
||||
}
|
||||
|
||||
private updateDirtyIndicator(): void {
|
||||
let dirty = this.textFileService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY ? this.textFileService.getDirty().length
|
||||
: this.untitledEditorService.getDirty().length;
|
||||
if (dirty === 0) {
|
||||
dom.addClass(this.dirtyCountElement, 'hidden');
|
||||
} else {
|
||||
this.dirtyCountElement.textContent = nls.localize('dirtyCounter', "{0} unsaved", dirty);
|
||||
dom.removeClass(this.dirtyCountElement, 'hidden');
|
||||
}
|
||||
}
|
||||
|
||||
private get elementCount(): number {
|
||||
return this.editorGroupService.groups.map(g => g.count)
|
||||
.reduce((first, second) => first + second, this.showGroups ? this.editorGroupService.groups.length : 0);
|
||||
}
|
||||
|
||||
private getMaxExpandedBodySize(): number {
|
||||
return this.elementCount * OpenEditorsDelegate.ITEM_HEIGHT;
|
||||
}
|
||||
|
||||
private getMinExpandedBodySize(): number {
|
||||
let visibleOpenEditors = this.configurationService.getValue<number>('explorer.openEditors.visible');
|
||||
if (typeof visibleOpenEditors !== 'number') {
|
||||
visibleOpenEditors = OpenEditorsView.DEFAULT_VISIBLE_OPEN_EDITORS;
|
||||
}
|
||||
|
||||
return this.computeMinExpandedBodySize(visibleOpenEditors);
|
||||
}
|
||||
|
||||
private computeMinExpandedBodySize(visibleOpenEditors = OpenEditorsView.DEFAULT_VISIBLE_OPEN_EDITORS): number {
|
||||
const itemsToShow = Math.min(Math.max(visibleOpenEditors, 1), this.elementCount);
|
||||
return itemsToShow * OpenEditorsDelegate.ITEM_HEIGHT;
|
||||
}
|
||||
|
||||
public setStructuralRefreshDelay(delay: number): void {
|
||||
this.structuralRefreshDelay = delay;
|
||||
}
|
||||
|
||||
public getOptimalWidth(): number {
|
||||
let parentNode = this.list.getHTMLElement();
|
||||
let childNodes: HTMLElement[] = [].slice.call(parentNode.querySelectorAll('.open-editor > a'));
|
||||
|
||||
return dom.getLargestChildWidth(parentNode, childNodes);
|
||||
}
|
||||
}
|
||||
|
||||
interface IOpenEditorTemplateData {
|
||||
container: HTMLElement;
|
||||
root: IResourceLabel;
|
||||
actionBar: ActionBar;
|
||||
actionRunner: OpenEditorActionRunner;
|
||||
}
|
||||
|
||||
interface IEditorGroupTemplateData {
|
||||
root: HTMLElement;
|
||||
name: HTMLSpanElement;
|
||||
actionBar: ActionBar;
|
||||
editorGroup: IEditorGroup;
|
||||
}
|
||||
|
||||
class OpenEditorActionRunner extends ActionRunner {
|
||||
public editor: OpenEditor;
|
||||
|
||||
run(action: IAction, context?: any): Promise<void> {
|
||||
return super.run(action, { groupId: this.editor.groupId, editorIndex: this.editor.editorIndex });
|
||||
}
|
||||
}
|
||||
|
||||
class OpenEditorsDelegate implements IListVirtualDelegate<OpenEditor | IEditorGroup> {
|
||||
|
||||
public static readonly ITEM_HEIGHT = 22;
|
||||
|
||||
getHeight(element: OpenEditor | IEditorGroup): number {
|
||||
return OpenEditorsDelegate.ITEM_HEIGHT;
|
||||
}
|
||||
|
||||
getTemplateId(element: OpenEditor | IEditorGroup): string {
|
||||
if (element instanceof OpenEditor) {
|
||||
return OpenEditorRenderer.ID;
|
||||
}
|
||||
|
||||
return EditorGroupRenderer.ID;
|
||||
}
|
||||
}
|
||||
|
||||
class EditorGroupRenderer implements IListRenderer<IEditorGroup, IEditorGroupTemplateData> {
|
||||
static readonly ID = 'editorgroup';
|
||||
|
||||
constructor(
|
||||
private keybindingService: IKeybindingService,
|
||||
private instantiationService: IInstantiationService,
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
|
||||
get templateId() {
|
||||
return EditorGroupRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IEditorGroupTemplateData {
|
||||
const editorGroupTemplate: IEditorGroupTemplateData = Object.create(null);
|
||||
editorGroupTemplate.root = dom.append(container, $('.editor-group'));
|
||||
editorGroupTemplate.name = dom.append(editorGroupTemplate.root, $('span.name'));
|
||||
editorGroupTemplate.actionBar = new ActionBar(container);
|
||||
|
||||
const saveAllInGroupAction = this.instantiationService.createInstance(SaveAllInGroupAction, SaveAllInGroupAction.ID, SaveAllInGroupAction.LABEL);
|
||||
const saveAllInGroupKey = this.keybindingService.lookupKeybinding(saveAllInGroupAction.id);
|
||||
editorGroupTemplate.actionBar.push(saveAllInGroupAction, { icon: true, label: false, keybinding: saveAllInGroupKey ? saveAllInGroupKey.getLabel() : undefined });
|
||||
|
||||
const closeGroupAction = this.instantiationService.createInstance(CloseGroupAction, CloseGroupAction.ID, CloseGroupAction.LABEL);
|
||||
const closeGroupActionKey = this.keybindingService.lookupKeybinding(closeGroupAction.id);
|
||||
editorGroupTemplate.actionBar.push(closeGroupAction, { icon: true, label: false, keybinding: closeGroupActionKey ? closeGroupActionKey.getLabel() : undefined });
|
||||
|
||||
return editorGroupTemplate;
|
||||
}
|
||||
|
||||
renderElement(editorGroup: IEditorGroup, index: number, templateData: IEditorGroupTemplateData): void {
|
||||
templateData.editorGroup = editorGroup;
|
||||
templateData.name.textContent = editorGroup.label;
|
||||
templateData.actionBar.context = { groupId: editorGroup.id };
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IEditorGroupTemplateData): void {
|
||||
templateData.actionBar.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class OpenEditorRenderer implements IListRenderer<OpenEditor, IOpenEditorTemplateData> {
|
||||
static readonly ID = 'openeditor';
|
||||
|
||||
constructor(
|
||||
private labels: ResourceLabels,
|
||||
private instantiationService: IInstantiationService,
|
||||
private keybindingService: IKeybindingService,
|
||||
private configurationService: IConfigurationService
|
||||
) {
|
||||
// noop
|
||||
}
|
||||
|
||||
get templateId() {
|
||||
return OpenEditorRenderer.ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IOpenEditorTemplateData {
|
||||
const editorTemplate: IOpenEditorTemplateData = Object.create(null);
|
||||
editorTemplate.container = container;
|
||||
editorTemplate.actionRunner = new OpenEditorActionRunner();
|
||||
editorTemplate.actionBar = new ActionBar(container, { actionRunner: editorTemplate.actionRunner });
|
||||
container.draggable = true;
|
||||
|
||||
const closeEditorAction = this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, CloseEditorAction.LABEL);
|
||||
const key = this.keybindingService.lookupKeybinding(closeEditorAction.id);
|
||||
editorTemplate.actionBar.push(closeEditorAction, { icon: true, label: false, keybinding: key ? key.getLabel() : undefined });
|
||||
|
||||
editorTemplate.root = this.labels.create(container);
|
||||
|
||||
return editorTemplate;
|
||||
}
|
||||
|
||||
renderElement(editor: OpenEditor, index: number, templateData: IOpenEditorTemplateData): void {
|
||||
templateData.actionRunner.editor = editor;
|
||||
editor.isDirty() ? dom.addClass(templateData.container, 'dirty') : dom.removeClass(templateData.container, 'dirty');
|
||||
templateData.root.setEditor(editor.editor, {
|
||||
italic: editor.isPreview(),
|
||||
extraClasses: ['open-editor'],
|
||||
fileDecorations: this.configurationService.getValue<IFilesConfiguration>().explorer.decorations
|
||||
});
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IOpenEditorTemplateData): void {
|
||||
templateData.actionBar.dispose();
|
||||
templateData.root.dispose();
|
||||
templateData.actionRunner.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class OpenEditorsDragAndDrop implements IListDragAndDrop<OpenEditor | IEditorGroup> {
|
||||
|
||||
constructor(
|
||||
private instantiationService: IInstantiationService,
|
||||
private editorGroupService: IEditorGroupsService
|
||||
) { }
|
||||
|
||||
@memoize private get dropHandler(): ResourcesDropHandler {
|
||||
return this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false });
|
||||
}
|
||||
|
||||
getDragURI(element: OpenEditor | IEditorGroup): string | null {
|
||||
if (element instanceof OpenEditor) {
|
||||
const resource = element.getResource();
|
||||
if (resource) {
|
||||
return resource.toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getDragLabel?(elements: (OpenEditor | IEditorGroup)[]): string | undefined {
|
||||
if (elements.length > 1) {
|
||||
return String(elements.length);
|
||||
}
|
||||
const element = elements[0];
|
||||
|
||||
return element instanceof OpenEditor ? withNullAsUndefined(element.editor.getName()) : element.label;
|
||||
}
|
||||
|
||||
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
|
||||
const items = (data as ElementsDragAndDropData<OpenEditor | IEditorGroup>).elements;
|
||||
const resources: URI[] = [];
|
||||
if (items) {
|
||||
items.forEach(i => {
|
||||
if (i instanceof OpenEditor) {
|
||||
const resource = i.getResource();
|
||||
if (resource) {
|
||||
resources.push(resource);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (resources.length) {
|
||||
// Apply some datatransfer types to allow for dragging the element outside of the application
|
||||
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
|
||||
}
|
||||
}
|
||||
|
||||
onDragOver(data: IDragAndDropData, targetElement: OpenEditor | IEditorGroup, targetIndex: number, originalEvent: DragEvent): boolean | IListDragOverReaction {
|
||||
if (data instanceof DesktopDragAndDropData && originalEvent.dataTransfer) {
|
||||
const types = originalEvent.dataTransfer.types;
|
||||
const typesArray: string[] = [];
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase
|
||||
}
|
||||
|
||||
if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
drop(data: IDragAndDropData, targetElement: OpenEditor | IEditorGroup, targetIndex: number, originalEvent: DragEvent): void {
|
||||
const group = targetElement instanceof OpenEditor ? targetElement.group : targetElement;
|
||||
const index = targetElement instanceof OpenEditor ? targetElement.group.getIndexOfEditor(targetElement.editor) : 0;
|
||||
|
||||
if (data instanceof ElementsDragAndDropData) {
|
||||
const elementsData = data.elements;
|
||||
elementsData.forEach((oe, offset) => {
|
||||
oe.group.moveEditor(oe.editor, group, { index: index + offset, preserveFocus: true });
|
||||
});
|
||||
this.editorGroupService.activateGroup(group);
|
||||
} else {
|
||||
this.dropHandler.handleDrop(originalEvent, () => group, () => group.focus(), index);
|
||||
}
|
||||
}
|
||||
}
|
||||