提交 a4bc227c 编写于 作者: D Daniel Imms

Merge pull request #5106 from Microsoft/tyriar/open_recently_closed_file

Implement open recently closed file(s) action
......@@ -392,14 +392,19 @@ export class VSCodeMenu {
}
private setOpenRecentMenu(openRecentMenu: Electron.Menu): void {
openRecentMenu.append(this.createMenuItem(nls.localize({ key: 'miReopenClosedFile', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed File"), 'workbench.files.action.reopenClosedFile'));
let recentList = this.getOpenedPathsList();
// Folders
recentList.folders.forEach((folder, index) => {
if (index < VSCodeMenu.MAX_RECENT_ENTRIES) {
openRecentMenu.append(this.createOpenRecentMenuItem(folder));
}
});
if (recentList.folders.length > 0) {
openRecentMenu.append(__separator__());
recentList.folders.forEach((folder, index) => {
if (index < VSCodeMenu.MAX_RECENT_ENTRIES) {
openRecentMenu.append(this.createOpenRecentMenuItem(folder));
}
});
}
// Files
let files = recentList.files;
......@@ -408,9 +413,7 @@ export class VSCodeMenu {
}
if (files.length > 0) {
if (recentList.folders.length > 0) {
openRecentMenu.append(__separator__());
}
openRecentMenu.append(__separator__());
files.forEach((file, index) => {
if (index < VSCodeMenu.MAX_RECENT_ENTRIES) {
......
......@@ -10,7 +10,7 @@ import {Action, IAction} from 'vs/base/common/actions';
import {ActionItem, BaseActionItem, Separator} from 'vs/base/browser/ui/actionbar/actionbar';
import {Scope, IActionBarRegistry, Extensions as ActionBarExtensions, ActionBarContributor} from 'vs/workbench/browser/actionBarRegistry';
import {IEditorInputActionContext, IEditorInputAction, EditorInputActionContributor} from 'vs/workbench/browser/parts/editor/baseEditor';
import {AddToWorkingFiles, FocusWorkingFiles, FocusFilesExplorer, OpenPreviousWorkingFile, OpenNextWorkingFile, CloseAllFilesAction, CloseFileAction, CloseOtherFilesAction, GlobalCompareResourcesAction, GlobalNewFileAction, GlobalNewFolderAction, RevertFileAction, SaveFilesAction, SaveAllAction, SaveFileAction, keybindingForAction, MoveFileToTrashAction, TriggerRenameFileAction, PasteFileAction, CopyFileAction, SelectResourceForCompareAction, CompareResourcesAction, NewFolderAction, NewFileAction, OpenToSideAction, ShowActiveFileInExplorer} from 'vs/workbench/parts/files/browser/fileActions';
import {AddToWorkingFiles, FocusWorkingFiles, FocusFilesExplorer, OpenPreviousWorkingFile, OpenNextWorkingFile, CloseAllFilesAction, CloseFileAction, CloseOtherFilesAction, GlobalCompareResourcesAction, GlobalNewFileAction, GlobalNewFolderAction, RevertFileAction, SaveFilesAction, SaveAllAction, SaveFileAction, keybindingForAction, MoveFileToTrashAction, TriggerRenameFileAction, PasteFileAction, CopyFileAction, SelectResourceForCompareAction, CompareResourcesAction, NewFolderAction, NewFileAction, ReopenClosedFileAction, OpenToSideAction, ShowActiveFileInExplorer} from 'vs/workbench/parts/files/browser/fileActions';
import {RevertLocalChangesAction, AcceptLocalChangesAction, ConflictResolutionDiffEditorInput} from 'vs/workbench/parts/files/browser/saveErrorHandler';
import {SyncActionDescriptor} from 'vs/platform/actions/common/actions';
import {IWorkbenchActionRegistry, Extensions as ActionExtensions} from 'vs/workbench/common/actionRegistry';
......@@ -172,9 +172,10 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalCompareResources
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseFileAction, CloseFileAction.ID, CloseFileAction.LABEL, { primary: KeyMod.chord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_W) }), category, ['close', 'file']);
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseOtherFilesAction, CloseOtherFilesAction.ID, CloseOtherFilesAction.LABEL, { primary: KeyMod.chord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_W) }), category, ['close', 'other', 'files']);
registry.registerWorkbenchAction(new SyncActionDescriptor(CloseAllFilesAction, CloseAllFilesAction.ID, CloseAllFilesAction.LABEL, { primary: KeyMod.chord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_W) }), category, ['close', 'all', 'files']);
registry.registerWorkbenchAction(new SyncActionDescriptor(ReopenClosedFileAction, ReopenClosedFileAction.ID, ReopenClosedFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_T }), category, ['reopen', 'open', 'closed', 'file']);
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenNextWorkingFile, OpenNextWorkingFile.ID, OpenNextWorkingFile.LABEL, { primary: KeyMod.chord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.DownArrow) }), category, ['open', 'next', 'working', 'file']);
registry.registerWorkbenchAction(new SyncActionDescriptor(OpenPreviousWorkingFile, OpenPreviousWorkingFile.ID, OpenPreviousWorkingFile.LABEL, { primary: KeyMod.chord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.UpArrow) }), category, ['open', 'previous', 'working', 'file']);
registry.registerWorkbenchAction(new SyncActionDescriptor(AddToWorkingFiles, AddToWorkingFiles.ID, AddToWorkingFiles.LABEL, { primary: KeyMod.chord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.Enter) }), category, ['add', 'to', 'working', 'files']);
registry.registerWorkbenchAction(new SyncActionDescriptor(FocusWorkingFiles, FocusWorkingFiles.ID, FocusWorkingFiles.LABEL, { primary: KeyMod.chord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_E) }), category, ['focus', 'working', 'files']);
registry.registerWorkbenchAction(new SyncActionDescriptor(FocusFilesExplorer, FocusFilesExplorer.ID, FocusFilesExplorer.LABEL), category, ['focus', 'files', 'explorer']);
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowActiveFileInExplorer, ShowActiveFileInExplorer.ID, ShowActiveFileInExplorer.LABEL), category, ['show', 'active', 'file', 'explorer']);
\ No newline at end of file
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowActiveFileInExplorer, ShowActiveFileInExplorer.ID, ShowActiveFileInExplorer.LABEL), category, ['show', 'active', 'file', 'explorer']);
......@@ -1800,6 +1800,58 @@ export class OpenResourcesAction extends Action {
}
}
export class ReopenClosedFileAction extends Action {
public static ID = 'workbench.files.action.reopenClosedFile';
public static LABEL = nls.localize('reopenClosedFile', "Reopen Closed File");
constructor(
id: string,
label: string,
@IPartService private partService: IPartService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IViewletService private viewletService: IViewletService,
@ITextFileService private textFileService: ITextFileService,
@IFileService private fileService: IFileService
) {
super(id, label);
}
public run(): TPromise<any> {
let viewletPromise = TPromise.as(null);
if (!this.partService.isSideBarHidden()) {
viewletPromise = this.viewletService.openViewlet(Files.VIEWLET_ID, false);
}
return viewletPromise.then(() => {
let workingFilesModel: Files.IWorkingFilesModel = this.textFileService.getWorkingFilesModel();
let entry: Files.IWorkingFileEntry = workingFilesModel.popLastClosedEntry();
if (entry === null) {
return TPromise.as(true);
}
// If the current resource is the recently closed resource, run action again
let activeResource = getUntitledOrFileResource(this.editorService.getActiveEditorInput());
if (activeResource && activeResource.path === entry.resource.path) {
return this.run();
}
return this.fileService.resolveFile(entry.resource).then(() => {
workingFilesModel.addEntry(entry.resource);
return this.editorService.openEditor(entry);
}, (e: any) => {
// If the files no longer exists, run action again
if (e.code === 'ENOENT') {
return this.run();
}
return TPromise.wrapError(e);
});
});
}
}
export abstract class BaseCloseWorkingFileAction extends Action {
protected model: WorkingFilesModel;
private elements: URI[];
......
......@@ -105,6 +105,8 @@ export interface IWorkingFilesModel {
removeEntry(entry: IWorkingFileEntry): IWorkingFileEntry;
removeEntry(arg1: IWorkingFileEntry | URI): IWorkingFileEntry;
popLastClosedEntry(): IWorkingFileEntry;
reorder(source: IWorkingFileEntry, target: IWorkingFileEntry): void;
hasEntry(resource: URI): boolean;
......
......@@ -18,7 +18,7 @@ import {IUntitledEditorService} from 'vs/workbench/services/untitled/common/unti
import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
import {IPartService} from 'vs/workbench/services/part/common/partService';
import {IWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService';
import {asFileEditorInput} from 'vs/workbench/common/editor';
import {asFileEditorInput, getUntitledOrFileResource} from 'vs/workbench/common/editor';
import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage';
import {IEventService} from 'vs/platform/event/common/event';
......@@ -27,6 +27,7 @@ export class WorkingFilesModel implements IWorkingFilesModel {
private static STORAGE_KEY = 'workingFiles.model.entries';
private entries: WorkingFileEntry[];
private recentlyClosedEntries: WorkingFileEntry[];
private pathLabelProvider: labels.PathLabelProvider;
private mapEntryToResource: { [resource: string]: WorkingFileEntry; };
private _onModelChange: Emitter<IWorkingFileModelChangeEvent>;
......@@ -44,6 +45,7 @@ export class WorkingFilesModel implements IWorkingFilesModel {
) {
this.pathLabelProvider = new labels.PathLabelProvider(this.contextService);
this.entries = [];
this.recentlyClosedEntries = [];
this.toDispose = [];
this.mapEntryToResource = Object.create(null);
this._onModelChange = new Emitter<IWorkingFileModelChangeEvent>();
......@@ -276,6 +278,9 @@ export class WorkingFilesModel implements IWorkingFilesModel {
let resource: uri = arg1 instanceof WorkingFileEntry ? (<WorkingFileEntry>arg1).resource : <uri>arg1;
let index = this.indexOf(resource);
if (index >= 0) {
if (resource.scheme === 'file') {
this.recordRecentlyClosedEntries([this.mapEntryToResource[resource.toString()]]);
}
// Remove entry
let removed = this.entries.splice(index, 1)[0];
......@@ -300,6 +305,13 @@ export class WorkingFilesModel implements IWorkingFilesModel {
return null;
}
public popLastClosedEntry(): WorkingFileEntry {
if (this.recentlyClosedEntries.length > 0) {
return this.recentlyClosedEntries.pop();
}
return null;
}
public reorder(source: WorkingFileEntry, target: WorkingFileEntry): void {
let sortedEntries = this.entries.slice(0).sort(WorkingFilesModel.compare);
......@@ -319,6 +331,7 @@ export class WorkingFilesModel implements IWorkingFilesModel {
}
public clear(): void {
this.recordRecentlyClosedEntries(this.entries);
let deleted = this.entries;
this.entries = [];
this.mapEntryToResource = Object.create(null);
......@@ -333,6 +346,28 @@ export class WorkingFilesModel implements IWorkingFilesModel {
return this.mapEntryToResource[resource.toString()];
}
private recordRecentlyClosedEntries(resources: WorkingFileEntry[]): void {
if (resources.length === 0) {
return;
}
// Put the active entry on the top of the stack
let input = this.editorService.getActiveEditorInput();
let resource: uri = getUntitledOrFileResource(input);
let activeEntry: WorkingFileEntry;
if (resource) {
activeEntry = this.findEntry(resource);
}
this.recentlyClosedEntries = this.recentlyClosedEntries.concat(resources.filter(e => {
return !activeEntry || e.resource.path !== activeEntry.resource.path;
}));
if (activeEntry) {
this.recentlyClosedEntries.push(activeEntry);
}
}
private indexOf(resource: uri): number {
let entry = this.findEntry(resource);
if (entry) {
......@@ -375,7 +410,7 @@ export class WorkingFilesModel implements IWorkingFilesModel {
if (options && options.filesToDiff) {
files.push(...options.filesToDiff);
}
arrays
.distinct(files, (r) => r.resource.toString()) // no duplicates
.map((f) => f.resource) // just the resource
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import URI from 'vs/base/common/uri';
import paths = require('vs/base/common/paths');
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {ITelemetryService, NullTelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {IEventService} from 'vs/platform/event/common/event';
import {IMessageService} from 'vs/platform/message/common/message';
import {IModelService} from 'vs/editor/common/services/modelService';
import {IModeService} from 'vs/editor/common/services/modeService';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {IStorageService} from 'vs/platform/storage/common/storage';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {ILifecycleService, NullLifecycleService} from 'vs/platform/lifecycle/common/lifecycle';
import {IFileService} from 'vs/platform/files/common/files';
import {IUntitledEditorService} from 'vs/workbench/services/untitled/common/untitledEditorService';
import {InstantiationService} from 'vs/platform/instantiation/common/instantiationService';
import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
import {IPartService} from 'vs/workbench/services/part/common/partService';
import {ServiceCollection} from 'vs/platform/instantiation/common/serviceCollection';
import {TestFileService, TestPartService, TestEditorService, TestConfigurationService, TestUntitledEditorService, TestStorageService, TestContextService, TestMessageService, TestEventService} from 'vs/workbench/test/browser/servicesTestUtils';
import {WorkingFileEntry, WorkingFilesModel} from 'vs/workbench/parts/files/common/workingFilesModel';
import {TextFileService} from 'vs/workbench/parts/files/browser/textFileServices';
import {createMockModelService, createMockModeService} from 'vs/editor/test/common/servicesTestUtils';
import {EditorInput} from 'vs/workbench/common/editor';
import {FileEditorInput} from 'vs/workbench/parts/files/browser/editors/fileEditorInput';
let baseInstantiationService: IInstantiationService;
let editorService: TestEditorService;
let eventService: TestEventService;
let textFileService: TextFileService;
suite('Files - WorkingFilesModel', () => {
setup(() => {
editorService = new TestEditorService();
eventService = new TestEventService();
let services = new ServiceCollection();
services.set(IEventService, eventService);
services.set(IMessageService, new TestMessageService());
services.set(IFileService, <any> TestFileService);
services.set(IWorkspaceContextService, new TestContextService());
services.set(ITelemetryService, NullTelemetryService);
services.set(IStorageService, new TestStorageService());
services.set(IUntitledEditorService, new TestUntitledEditorService());
services.set(IWorkbenchEditorService, editorService);
services.set(IPartService, new TestPartService());
services.set(IModeService, createMockModeService());
services.set(IModelService, createMockModelService());
services.set(ILifecycleService, NullLifecycleService);
services.set(IConfigurationService, new TestConfigurationService());
baseInstantiationService = new InstantiationService(services);
});
teardown(() => {
eventService.dispose();
});
test("Removed files are added to the closed entries stack", function () {
let model = baseInstantiationService.createInstance(WorkingFilesModel);
let file1: URI = URI.create('file', null, '/file1');
let file2: URI = URI.create('file', null, '/file2');
let file3: URI = URI.create('file', null, '/file3');
model.addEntry(file1);
model.addEntry(file2);
model.addEntry(file3);
model.removeEntry(file2);
model.removeEntry(file3);
model.removeEntry(file1);
let lastClosedEntry1: WorkingFileEntry = model.popLastClosedEntry();
let lastClosedEntry2: WorkingFileEntry = model.popLastClosedEntry();
let lastClosedEntry3: WorkingFileEntry = model.popLastClosedEntry();
assert.equal(model.popLastClosedEntry(), null);
assert.equal(lastClosedEntry1.resource, file1);
assert.equal(lastClosedEntry2.resource, file3);
assert.equal(lastClosedEntry3.resource, file2);
});
test("Untitled entries are not added to the closed entries stack", function () {
let model = baseInstantiationService.createInstance(WorkingFilesModel);
let fileUri: URI = URI.create('file', null, '/test');
let untitledUri: URI = URI.create('untitled', null);
model.addEntry(fileUri);
model.addEntry(untitledUri);
model.removeEntry(fileUri);
let lastClosedEntry: WorkingFileEntry = model.popLastClosedEntry();
assert.equal(lastClosedEntry.resource, fileUri);
model.removeEntry(untitledUri);
assert.equal(model.popLastClosedEntry(), null);
});
test("Clearing the model adds all entries to the closed entries stack", function() {
let model = baseInstantiationService.createInstance(WorkingFilesModel);
model.addEntry(URI.create('file', null, '/foo'));
model.addEntry(URI.create('file', null, '/bar'));
assert.equal(model.popLastClosedEntry(), null);
model.clear();
assert.ok(model.popLastClosedEntry().isFile);
assert.ok(model.popLastClosedEntry().isFile);
assert.equal(model.popLastClosedEntry(), null);
});
test("Reopening multiple files will open the editor in the previously opened file", function() {
let model = baseInstantiationService.createInstance(WorkingFilesModel);
// Open /foo then /bar, set /foo as active input
let fooEntry = model.addEntry(URI.create('file', null, '/foo'));
editorService.getActiveEditorInput = () => {
return baseInstantiationService.createInstance(FileEditorInput, fooEntry.resource, 'text/javascript', void 0);
};
model.addEntry(URI.create('file', null, '/bar'));
model.clear();
assert.equal(model.popLastClosedEntry().resource.path, '/foo');
assert.equal(model.popLastClosedEntry().resource.path, '/bar');
assert.equal(model.popLastClosedEntry(), null);
// Open /bar then /foo, set /foo as active input
model.addEntry(URI.create('file', null, '/bar'));
fooEntry = model.addEntry(URI.create('file', null, '/foo'));
editorService.getActiveEditorInput = () => {
return baseInstantiationService.createInstance(FileEditorInput, fooEntry.resource, 'text/javascript', void 0);
};
model.clear();
assert.equal(model.popLastClosedEntry().resource.path, '/foo');
assert.equal(model.popLastClosedEntry().resource.path, '/bar');
assert.equal(model.popLastClosedEntry(), null);
});
});
\ No newline at end of file
......@@ -890,7 +890,7 @@ let tasksCategory = nls.localize('tasksCategory', "Tasks");
let workbenchActionsRegistry = <IWorkbenchActionRegistry>Registry.as(WorkbenchActionExtensions.WorkbenchActions);
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(ConfigureTaskRunnerAction, ConfigureTaskRunnerAction.ID, ConfigureTaskRunnerAction.TEXT), tasksCategory, ['configure', 'task', 'runner']);
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(BuildAction, BuildAction.ID, BuildAction.TEXT, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_B }), tasksCategory, ['run', 'build', 'task']);
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(TestAction, TestAction.ID, TestAction.TEXT, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_T }), tasksCategory, ['run', 'test', 'talk']);
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(TestAction, TestAction.ID, TestAction.TEXT), tasksCategory, ['run', 'test', 'talk']);
// workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(RebuildAction, RebuildAction.ID, RebuildAction.TEXT), tasksCategory);
// workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(CleanAction, CleanAction.ID, CleanAction.TEXT), tasksCategory);
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(TerminateAction, TerminateAction.ID, TerminateAction.TEXT), tasksCategory, ['terminate', 'running', 'task']);
......
......@@ -2,7 +2,7 @@
## Run
The best way to run the Code tests is from within VS Code. Simply press `CMD+Shift+T` (`Ctrl+Shift+T` on Windows) to launch the tests. To make development changes to unit tests you need to be running `gulp`. See [Development Workflow](https://github.com/Microsoft/vscode/tree/master/wiki/contributing/how-to-contribute.md#incremental-build) for more details.
The best way to run the Code tests is from within VS Code. Simply press<kbd>F1</kbd>, type "run test" and press <kbd>enter</kbd> to launch the tests. To make development changes to unit tests you need to be running `gulp`. See [Development Workflow](https://github.com/Microsoft/vscode/tree/master/wiki/contributing/how-to-contribute.md#incremental-build) for more details.
If you wish to run the tests from a terminal, from the `vscode` folder run:
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册