debt - more async/await adoption in workbench

This commit is contained in:
Benjamin Pasero 2019-05-23 12:43:17 +02:00
parent 4929acf926
commit f41d863849
19 changed files with 430 additions and 400 deletions

View file

@ -31,46 +31,47 @@ export class BackupRestorer implements IWorkbenchContribution {
this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.doRestoreBackups());
}
private doRestoreBackups(): Promise<URI[] | undefined> {
private async doRestoreBackups(): Promise<URI[] | undefined> {
// Find all files and untitled with backups
return this.backupFileService.getWorkspaceFileBackups().then(backups => {
const backups = await this.backupFileService.getWorkspaceFileBackups();
const unresolvedBackups = await this.doResolveOpenedBackups(backups);
// Resolve backups that are opened
return this.doResolveOpenedBackups(backups).then((unresolved): Promise<URI[] | undefined> | undefined => {
// Some failed to restore or were not opened at all so we open and resolve them manually
if (unresolvedBackups.length > 0) {
await this.doOpenEditors(unresolvedBackups);
// Some failed to restore or were not opened at all so we open and resolve them manually
if (unresolved.length > 0) {
return this.doOpenEditors(unresolved).then(() => this.doResolveOpenedBackups(unresolved));
}
return this.doResolveOpenedBackups(unresolvedBackups);
}
return undefined;
});
});
return undefined;
}
private doResolveOpenedBackups(backups: URI[]): Promise<URI[]> {
const restorePromises: Promise<unknown>[] = [];
const unresolved: URI[] = [];
private async doResolveOpenedBackups(backups: URI[]): Promise<URI[]> {
const unresolvedBackups: URI[] = [];
backups.forEach(backup => {
await Promise.all(backups.map(async backup => {
const openedEditor = this.editorService.getOpened({ resource: backup });
if (openedEditor) {
restorePromises.push(openedEditor.resolve().then(undefined, () => unresolved.push(backup)));
try {
await openedEditor.resolve(); // trigger load
} catch (error) {
unresolvedBackups.push(backup); // ignore error and remember as unresolved
}
} else {
unresolved.push(backup);
unresolvedBackups.push(backup);
}
});
}));
return Promise.all(restorePromises).then(() => unresolved, () => unresolved);
return unresolvedBackups;
}
private doOpenEditors(resources: URI[]): Promise<void> {
private async doOpenEditors(resources: URI[]): Promise<void> {
const hasOpenedEditors = this.editorService.visibleEditors.length > 0;
const inputs = resources.map((resource, index) => this.resolveInput(resource, index, hasOpenedEditors));
// Open all remaining backups as editors and resolve them to load their backups
return this.editorService.openEditors(inputs).then(() => undefined);
await this.editorService.openEditors(inputs);
}
private resolveInput(resource: URI, index: number, hasOpenedEditors: boolean): IResourceInput | IUntitledResourceInput {

View file

@ -47,24 +47,19 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor {
);
}
private openInternal(input: EditorInput, options: EditorOptions): Promise<void> {
private async openInternal(input: EditorInput, options: EditorOptions): Promise<void> {
if (input instanceof FileEditorInput) {
input.setForceOpenAsText();
return this.editorService.openEditor(input, options, this.group).then(() => undefined);
await this.editorService.openEditor(input, options, this.group);
}
return Promise.resolve();
}
private openExternal(resource: URI): void {
this.windowsService.openExternal(resource.toString()).then(didOpen => {
if (!didOpen) {
return this.windowsService.showItemInFolder(resource);
}
return undefined;
});
private async openExternal(resource: URI): Promise<void> {
const didOpen = await this.windowsService.openExternal(resource.toString());
if (!didOpen) {
return this.windowsService.showItemInFolder(resource);
}
}
getTitle(): string | null {

View file

@ -132,7 +132,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
private handleDeletes(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void {
const nonDirtyFileEditors = this.getOpenedFileEditors(false /* non-dirty only */);
nonDirtyFileEditors.forEach(editor => {
nonDirtyFileEditors.forEach(async editor => {
const resource = editor.getResource();
// Handle deletes in opened editors depending on:
@ -165,20 +165,17 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
// 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>;
let exists = false;
if (isExternal) {
checkExists = timeout(100).then(() => this.fileService.exists(resource));
} else {
checkExists = Promise.resolve(false);
await timeout(100);
exists = await this.fileService.exists(resource);
}
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()}`);
}
});
if (!exists && !editor.isDisposed()) {
editor.dispose();
} else if (this.environmentService.verbose) {
console.warn(`File exists even though we received a delete event: ${resource.toString()}`);
}
}
});
}

View file

@ -125,101 +125,103 @@ export class TextFileEditor extends BaseTextEditor {
}
}
setInput(input: FileEditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
async 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 => {
await super.setInput(input, options, token);
try {
const resolvedModel = await input.resolve();
// Check for cancellation
if (token.isCancellationRequested) {
return undefined;
}
// Check for cancellation
if (token.isCancellationRequested) {
return;
}
// 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);
}
// 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;
const textFileModel = <ITextFileEditorModel>resolvedModel;
// Editor
const textEditor = this.getControl();
textEditor.setModel(textFileModel.textEditorModel);
// 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);
}
// 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);
}
// 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 => {
// Readonly flag
textEditor.updateOptions({ readOnly: textFileModel.isReadonly() });
} catch (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 ((<TextFileOperationError>error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY) {
return this.openAsBinary(input, options);
}
// 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 ((<TextFileOperationError>error).textFileOperationResult === TextFileOperationResult.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);
// 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(new Error(nls.localize('openFolderError', "File is a directory")));
}
throw 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.textFileService.create(input.getResource()).then(() => this.editorService.openEditor({
resource: input.getResource(),
options: {
pinned: true // new file gets pinned by default
}
}));
})
]
}));
}
// 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()))) {
throw createErrorWithActions(toErrorMessage(error), {
actions: [
new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, async () => {
await this.textFileService.create(input.getResource());
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 this.editorService.openEditor({
resource: input.getResource(),
options: {
pinned: true // new file gets pinned by default
}
});
})
]
});
}
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' });
})
]
}));
}
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);
// Otherwise make sure the error bubbles up
return Promise.reject(error);
});
});
throw 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
throw error;
}
}
private openAsBinary(input: FileEditorInput, options: EditorOptions): void {
@ -227,21 +229,20 @@ export class TextFileEditor extends BaseTextEditor {
this.editorService.openEditor(input, options, this.group);
}
private openAsFolder(input: FileEditorInput): void {
private async openAsFolder(input: FileEditorInput): Promise<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(() => {
await this.group.closeEditor(this.input);
// 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);
});
}
});
// Best we can do is to reveal the folder in the explorer
if (this.contextService.isInsideWorkspace(input.getResource())) {
await this.viewletService.openViewlet(VIEWLET_ID);
this.explorerService.select(input.getResource(), true);
}
}
protected getAriaLabel(): string {

View file

@ -240,26 +240,26 @@ class ResolveSaveConflictAction extends Action {
super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare"));
}
run(): Promise<any> {
async run(): Promise<any> {
if (!this.model.isDisposed()) {
const resource = this.model.getResource();
const name = basename(resource);
const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong);
return TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true }).then(() => {
if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
return; // return if this message is ignored
}
await TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true });
// 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));
if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) {
return; // return if this message is ignored
}
const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions });
Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!));
pendingResolveSaveConflictMessages.push(handle);
});
// 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);
@ -316,31 +316,28 @@ export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource:
const editor = control.input;
const group = control.group;
resolverService.createModelReference(resource).then(reference => {
resolverService.createModelReference(resource).then(async 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(() => {
await model.revert();
// Restore user value (without loosing undo stack)
modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot));
// Restore user value (without loosing undo stack)
modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot));
// Trigger save
return model.save().then(() => {
// Trigger save
await model.save();
// Reopen file input
return editorService.openEditor({ resource: model.getResource() }, group).then(() => {
// Reopen file input
await editorService.openEditor({ resource: model.getResource() }, group);
// Clean up
group.closeEditor(editor);
editor.dispose();
reference.dispose();
});
});
});
// Clean up
group.closeEditor(editor);
editor.dispose();
reference.dispose();
});
};
@ -355,22 +352,20 @@ export const revertLocalChangesCommand = (accessor: ServicesAccessor, resource:
const editor = control.input;
const group = control.group;
resolverService.createModelReference(resource).then(reference => {
resolverService.createModelReference(resource).then(async 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(() => {
await model.revert();
// Reopen file input
return editorService.openEditor({ resource: model.getResource() }, group).then(() => {
// Reopen file input
await editorService.openEditor({ resource: model.getResource() }, group);
// Clean up
group.closeEditor(editor);
editor.dispose();
reference.dispose();
});
});
// Clean up
group.closeEditor(editor);
editor.dispose();
reference.dispose();
});
};

View file

@ -273,16 +273,17 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
return this.doResolveAsText();
}
private doResolveAsText(): Promise<TextFileEditorModel | BinaryEditorModel> {
private async doResolveAsText(): Promise<TextFileEditorModel | BinaryEditorModel> {
// Resolve as text
return this.textFileService.models.loadOrCreate(this.resource, {
mode: this.preferredMode,
encoding: this.preferredEncoding,
reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model
allowBinary: this.forceOpenAsText,
reason: LoadReason.EDITOR
}).then(model => {
try {
await this.textFileService.models.loadOrCreate(this.resource, {
mode: this.preferredMode,
encoding: this.preferredEncoding,
reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model
allowBinary: this.forceOpenAsText,
reason: LoadReason.EDITOR
});
// This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary
// or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into
@ -292,8 +293,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
this.textModelReference = this.textModelResolverService.createModelReference(this.resource);
}
return this.textModelReference.then(ref => ref.object as TextFileEditorModel);
}, error => {
const ref = await this.textModelReference;
return ref.object as TextFileEditorModel;
} catch (error) {
// In case of an error that indicates that the file is binary or too large, just return with the binary editor model
if (
@ -304,12 +307,12 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput {
}
// Bubble any other error up
return Promise.reject(error);
});
throw error;
}
}
private doResolveAsBinary(): Promise<BinaryEditorModel> {
return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load().then(m => m as BinaryEditorModel);
private async doResolveAsBinary(): Promise<BinaryEditorModel> {
return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load();
}
isResolved(): boolean {

View file

@ -143,15 +143,13 @@ export class TextFileContentProvider implements ITextModelContentProvider {
@IModelService private readonly modelService: IModelService
) { }
static open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise<void> {
return editorService.openEditor(
{
leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource),
rightResource: resource,
label,
options
}
).then();
static async open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise<void> {
await editorService.openEditor({
leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource),
rightResource: resource,
label,
options
});
}
private static resourceToTextFile(scheme: string, resource: URI): URI {
@ -162,56 +160,55 @@ export class TextFileContentProvider implements ITextModelContentProvider {
return resource.with({ scheme: JSON.parse(resource.query)['scheme'], query: null });
}
provideTextContent(resource: URI): Promise<ITextModel> {
async provideTextContent(resource: URI): Promise<ITextModel> {
const savedFileResource = TextFileContentProvider.textFileToResource(resource);
// Make sure our text file is resolved up to date
return this.resolveEditorModel(resource).then(codeEditorModel => {
const codeEditorModel = await this.resolveEditorModel(resource);
// Make sure to keep contents up to date when it changes
if (!this.fileWatcherDisposable) {
this.fileWatcherDisposable = this.fileService.onFileChanges(changes => {
if (changes.contains(savedFileResource, FileChangeType.UPDATED)) {
this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes
}
});
if (codeEditorModel) {
once(codeEditorModel.onWillDispose)(() => {
dispose(this.fileWatcherDisposable);
this.fileWatcherDisposable = undefined;
});
// Make sure to keep contents up to date when it changes
if (!this.fileWatcherDisposable) {
this.fileWatcherDisposable = this.fileService.onFileChanges(changes => {
if (changes.contains(savedFileResource, FileChangeType.UPDATED)) {
this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes
}
}
});
return codeEditorModel;
});
if (codeEditorModel) {
once(codeEditorModel.onWillDispose)(() => {
dispose(this.fileWatcherDisposable);
this.fileWatcherDisposable = undefined;
});
}
}
return codeEditorModel;
}
private resolveEditorModel(resource: URI, createAsNeeded?: true): Promise<ITextModel>;
private resolveEditorModel(resource: URI, createAsNeeded?: boolean): Promise<ITextModel | null>;
private resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise<ITextModel | null> {
private async resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise<ITextModel | null> {
const savedFileResource = TextFileContentProvider.textFileToResource(resource);
return this.textFileService.readStream(savedFileResource).then(content => {
let codeEditorModel = this.modelService.getModel(resource);
if (codeEditorModel) {
this.modelService.updateModel(codeEditorModel, content.value);
} else if (createAsNeeded) {
const textFileModel = this.modelService.getModel(savedFileResource);
const content = await this.textFileService.readStream(savedFileResource);
let languageSelector: ILanguageSelection;
if (textFileModel) {
languageSelector = this.modeService.create(textFileModel.getModeId());
} else {
languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.path);
}
let codeEditorModel = this.modelService.getModel(resource);
if (codeEditorModel) {
this.modelService.updateModel(codeEditorModel, content.value);
} else if (createAsNeeded) {
const textFileModel = this.modelService.getModel(savedFileResource);
codeEditorModel = this.modelService.createModel(content.value, languageSelector, resource);
let languageSelector: ILanguageSelection;
if (textFileModel) {
languageSelector = this.modeService.create(textFileModel.getModeId());
} else {
languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.path);
}
return codeEditorModel;
});
codeEditorModel = this.modelService.createModel(content.value, languageSelector, resource);
}
return codeEditorModel;
}
dispose(): void {

View file

@ -34,26 +34,25 @@ suite('Files - FileEditorTracker', () => {
accessor = instantiationService.createInstance(ServiceAccessor);
});
test('file change event updates model', function () {
test('file change event updates model', async function () {
const tracker = instantiationService.createInstance(FileEditorTracker);
const resource = toResource.call(this, '/path/index.txt');
return accessor.textFileService.models.loadOrCreate(resource).then((model: IResolvedTextFileEditorModel) => {
model.textEditorModel.setValue('Super Good');
assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good');
const model = await accessor.textFileService.models.loadOrCreate(resource) as IResolvedTextFileEditorModel;
return model.save().then(() => {
model.textEditorModel.setValue('Super Good');
assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good');
// change event (watcher)
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }]));
await model.save();
return timeout(0).then(() => { // due to event updating model async
assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html');
// change event (watcher)
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }]));
tracker.dispose();
});
});
});
await timeout(0); // due to event updating model async
assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html');
tracker.dispose();
});
});

View file

@ -390,7 +390,7 @@ export class GotoSymbolHandler extends QuickOpenHandler {
this.rangeHighlightDecorationId = undefined;
}
getResults(searchValue: string, token: CancellationToken): Promise<QuickOpenModel | null> {
async getResults(searchValue: string, token: CancellationToken): Promise<QuickOpenModel | null> {
searchValue = searchValue.trim();
// Support to cancel pending outline requests
@ -407,20 +407,19 @@ export class GotoSymbolHandler extends QuickOpenHandler {
}
// Resolve Outline Model
return this.getOutline().then(outline => {
if (!outline) {
return outline;
}
if (token.isCancellationRequested) {
return outline;
}
// Filter by search
outline.applyFilter(searchValue);
const outline = await this.getOutline();
if (!outline) {
return outline;
});
}
if (token.isCancellationRequested) {
return outline;
}
// Filter by search
outline.applyFilter(searchValue);
return outline;
}
getEmptyLabel(searchString: string): string {

View file

@ -147,43 +147,45 @@ export class OpenFileHandler extends QuickOpenHandler {
return this.doFindResults(query, token, this.cacheState.cacheKey, maxSortedResults);
}
private doFindResults(query: IPreparedQuery, token: CancellationToken, cacheKey?: string, maxSortedResults?: number): Promise<FileQuickOpenModel> {
private async doFindResults(query: IPreparedQuery, token: CancellationToken, cacheKey?: string, maxSortedResults?: number): Promise<FileQuickOpenModel> {
const queryOptions = this.doResolveQueryOptions(query, cacheKey, maxSortedResults);
let iconClass: string;
let iconClass: string | undefined = undefined;
if (this.options && this.options.forceUseIcons && !this.themeService.getFileIconTheme()) {
iconClass = 'file'; // only use a generic file icon if we are forced to use an icon and have no icon theme set otherwise
}
return this.getAbsolutePathResult(query).then(result => {
if (token.isCancellationRequested) {
return Promise.resolve(<ISearchComplete>{ results: [] });
let complete: ISearchComplete | undefined = undefined;
const result = await this.getAbsolutePathResult(query);
if (token.isCancellationRequested) {
complete = <ISearchComplete>{ results: [] };
}
// If the original search value is an existing file on disk, return it immediately and bypass the search service
else if (result) {
complete = <ISearchComplete>{ results: [{ resource: result }] };
}
else {
complete = await this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token);
}
const results: QuickOpenEntry[] = [];
if (!token.isCancellationRequested) {
for (const fileMatch of complete.results) {
const label = basename(fileMatch.resource);
const description = this.labelService.getUriLabel(dirname(fileMatch.resource), { relative: true });
results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass));
}
}
// If the original search value is an existing file on disk, return it immediately and bypass the search service
if (result) {
return Promise.resolve(<ISearchComplete>{ results: [{ resource: result }] });
}
return this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token);
}).then(complete => {
const results: QuickOpenEntry[] = [];
if (!token.isCancellationRequested) {
for (const fileMatch of complete.results) {
const label = basename(fileMatch.resource);
const description = this.labelService.getUriLabel(dirname(fileMatch.resource), { relative: true });
results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass));
}
}
return new FileQuickOpenModel(results, <IFileSearchStats>complete.stats);
});
return new FileQuickOpenModel(results, <IFileSearchStats>complete.stats);
}
private getAbsolutePathResult(query: IPreparedQuery): Promise<URI | undefined> {
private async getAbsolutePathResult(query: IPreparedQuery): Promise<URI | undefined> {
const detildifiedQuery = untildify(query.original, this.environmentService.userHome);
if (isAbsolute(detildifiedQuery)) {
const workspaceFolders = this.contextService.getWorkspace().folders;
@ -191,12 +193,16 @@ export class OpenFileHandler extends QuickOpenHandler {
workspaceFolders[0].uri.with({ path: detildifiedQuery }) :
URI.file(detildifiedQuery);
return this.fileService.resolve(resource).then(
stat => stat.isDirectory ? undefined : resource,
error => undefined);
try {
const stat = await this.fileService.resolve(resource);
return stat.isDirectory ? undefined : resource;
} catch (error) {
// ignore
}
}
return Promise.resolve(undefined);
return undefined;
}
private doResolveQueryOptions(query: IPreparedQuery, cacheKey?: string, maxSortedResults?: number): IFileQueryBuilderOptions {

View file

@ -172,7 +172,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
}
}
private runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IContextMenuEvent): void {
private async runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IContextMenuEvent): Promise<void> {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
@ -182,9 +182,15 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
this.telemetryService.publicLog('workbenchActionExecuted', { id: actionToRun.id, from: 'contextMenu' });
const context = delegate.getActionsContext ? delegate.getActionsContext(event) : event;
const res = actionRunner.run(actionToRun, context) || Promise.resolve(null);
res.then(undefined, e => this.notificationService.error(e));
const runnable = actionRunner.run(actionToRun, context);
if (runnable) {
try {
await runnable;
} catch (error) {
this.notificationService.error(error);
}
}
}
}

View file

@ -78,17 +78,16 @@ class NativeDialogService implements IDialogService {
sharedProcessService.registerChannel('dialog', new DialogChannel(this));
}
confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
this.logService.trace('DialogService#confirm', confirmation.message);
const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
return this.windowService.showMessageBox(options).then(result => {
return {
confirmed: buttonIndexMap[result.button] === 0 ? true : false,
checkboxChecked: result.checkboxChecked
};
});
const result = await this.windowService.showMessageBox(options);
return {
confirmed: buttonIndexMap[result.button] === 0 ? true : false,
checkboxChecked: result.checkboxChecked
};
}
private getConfirmOptions(confirmation: IConfirmation): Electron.MessageBoxOptions {
@ -128,7 +127,7 @@ class NativeDialogService implements IDialogService {
return opts;
}
show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise<number> {
async show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise<number> {
this.logService.trace('DialogService#show', message);
const { options, buttonIndexMap } = this.massageMessageBoxOptions({
@ -139,7 +138,8 @@ class NativeDialogService implements IDialogService {
detail: dialogOptions ? dialogOptions.detail : undefined
});
return this.windowService.showMessageBox(options).then(result => buttonIndexMap[result.button]);
const result = await this.windowService.showMessageBox(options);
return buttonIndexMap[result.button];
}
private massageMessageBoxOptions(options: Electron.MessageBoxOptions): IMassagedMessageBoxOptions {

View file

@ -62,17 +62,16 @@ export class CodeEditorService extends CodeEditorServiceImpl {
return this.doOpenCodeEditor(input, source, sideBySide);
}
private doOpenCodeEditor(input: IResourceInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
return this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(control => {
if (control) {
const widget = control.getControl();
if (isCodeEditor(widget)) {
return widget;
}
private async doOpenCodeEditor(input: IResourceInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
const control = await this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
if (control) {
const widget = control.getControl();
if (isCodeEditor(widget)) {
return widget;
}
}
return null;
});
return null;
}
}

View file

@ -328,7 +328,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor[]>;
openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor[]>;
openEditors(editors: Array<IEditorInputWithOptions | IResourceEditor>, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor[]> {
async openEditors(editors: Array<IEditorInputWithOptions | IResourceEditor>, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor[]> {
// Convert to typed editors and options
const typedEditors: IEditorInputWithOptions[] = [];
@ -364,7 +364,9 @@ export class EditorService extends Disposable implements EditorServiceImpl {
result.push(group.openEditors(editorsWithOptions));
});
return Promise.all(result).then(editors => coalesce(editors));
const openedEditors = await Promise.all(result);
return coalesce(openedEditors);
}
//#endregion

View file

@ -71,9 +71,9 @@ export class IntegrityServiceImpl implements IIntegrityService {
this.isPure().then(r => {
if (r.isPure) {
// all is good
return;
return; // all is good
}
this._prompt();
});
}
@ -106,29 +106,25 @@ export class IntegrityServiceImpl implements IIntegrityService {
return this._isPurePromise;
}
private _isPure(): Promise<IntegrityTestResult> {
private async _isPure(): Promise<IntegrityTestResult> {
const expectedChecksums = product.checksums || {};
return this.lifecycleService.when(LifecyclePhase.Eventually).then(() => {
let asyncResults: Promise<ChecksumPair>[] = Object.keys(expectedChecksums).map((filename) => {
return this._resolve(filename, expectedChecksums[filename]);
});
await this.lifecycleService.when(LifecyclePhase.Eventually);
return Promise.all(asyncResults).then<IntegrityTestResult>((allResults) => {
let isPure = true;
for (let i = 0, len = allResults.length; i < len; i++) {
if (!allResults[i].isPure) {
isPure = false;
break;
}
}
const allResults = await Promise.all(Object.keys(expectedChecksums).map(filename => this._resolve(filename, expectedChecksums[filename])));
return {
isPure: isPure,
proof: allResults
};
});
});
let isPure = true;
for (let i = 0, len = allResults.length; i < len; i++) {
if (!allResults[i].isPure) {
isPure = false;
break;
}
}
return {
isPure: isPure,
proof: allResults
};
}
private _resolve(filename: string, expected: string): Promise<ChecksumPair> {

View file

@ -208,7 +208,8 @@ export class ScopedProgressService extends ScopedService implements IProgressSer
};
}
showWhile(promise: Promise<any>, delay?: number): Promise<void> {
async showWhile(promise: Promise<any>, delay?: number): Promise<void> {
// Join with existing running promise to ensure progress is accurate
if (this.progressState.type === ProgressState.Type.While) {
promise = Promise.all([promise, this.progressState.whilePromise]);
@ -217,24 +218,25 @@ export class ScopedProgressService extends ScopedService implements IProgressSer
// Keep Promise in State
this.progressState = new ProgressState.While(promise, delay || 0, Date.now());
let stop = () => {
try {
this.doShowWhile(delay);
// If this is not the last promise in the list of joined promises, return early
if (this.progressState.type === ProgressState.Type.While && this.progressState.whilePromise !== promise) {
return;
await promise;
} catch (error) {
// ignore
} finally {
// If this is not the last promise in the list of joined promises, skip this
if (this.progressState.type !== ProgressState.Type.While || this.progressState.whilePromise === promise) {
// The while promise is either null or equal the promise we last hooked on
this.progressState = ProgressState.None;
if (this.isActive) {
this.progressbar.stop().hide();
}
}
// The while promise is either null or equal the promise we last hooked on
this.progressState = ProgressState.None;
if (this.isActive) {
this.progressbar.stop().hide();
}
};
this.doShowWhile(delay);
return promise.then(stop, stop);
}
}
private doShowWhile(delay?: number): void {
@ -280,13 +282,15 @@ export class ProgressService implements IProgressService {
};
}
showWhile(promise: Promise<any>, delay?: number): Promise<void> {
const stop = () => {
async showWhile(promise: Promise<any>, delay?: number): Promise<void> {
try {
this.progressbar.infinite().show(delay);
await promise;
} catch (error) {
// ignore
} finally {
this.progressbar.stop().hide();
};
this.progressbar.infinite().show(delay);
return promise.then(stop, stop);
}
}
}

View file

@ -34,28 +34,28 @@ export class RemoteExtensionEnvironmentChannelClient {
constructor(private channel: IChannel) { }
getEnvironmentData(remoteAuthority: string, extensionDevelopmentPath?: URI[]): Promise<IRemoteAgentEnvironment> {
async getEnvironmentData(remoteAuthority: string, extensionDevelopmentPath?: URI[]): Promise<IRemoteAgentEnvironment> {
const args: IGetEnvironmentDataArguments = {
language: platform.language,
remoteAuthority,
extensionDevelopmentPath
};
return this.channel.call<IRemoteAgentEnvironmentDTO>('getEnvironmentData', args)
.then((data: IRemoteAgentEnvironmentDTO): IRemoteAgentEnvironment => {
return {
pid: data.pid,
appRoot: URI.revive(data.appRoot),
appSettingsHome: URI.revive(data.appSettingsHome),
settingsPath: URI.revive(data.settingsPath),
logsPath: URI.revive(data.logsPath),
extensionsPath: URI.revive(data.extensionsPath),
extensionHostLogsPath: URI.revive(data.extensionHostLogsPath),
globalStorageHome: URI.revive(data.globalStorageHome),
userHome: URI.revive(data.userHome),
extensions: data.extensions.map(ext => { (<any>ext).extensionLocation = URI.revive(ext.extensionLocation); return ext; }),
os: data.os
};
});
const data = await this.channel.call<IRemoteAgentEnvironmentDTO>('getEnvironmentData', args);
return {
pid: data.pid,
appRoot: URI.revive(data.appRoot),
appSettingsHome: URI.revive(data.appSettingsHome),
settingsPath: URI.revive(data.settingsPath),
logsPath: URI.revive(data.logsPath),
extensionsPath: URI.revive(data.extensionsPath),
extensionHostLogsPath: URI.revive(data.extensionHostLogsPath),
globalStorageHome: URI.revive(data.globalStorageHome),
userHome: URI.revive(data.userHome),
extensions: data.extensions.map(ext => { (<any>ext).extensionLocation = URI.revive(ext.extensionLocation); return ext; }),
os: data.os
};
}
getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo> {

View file

@ -31,7 +31,7 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod
super();
}
createReferencedObject(key: string, skipActivateProvider?: boolean): Promise<ITextEditorModel> {
async createReferencedObject(key: string, skipActivateProvider?: boolean): Promise<ITextEditorModel> {
this.modelsToDispose.delete(key);
const resource = URI.parse(key);
@ -43,15 +43,19 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod
// Virtual documents
if (this.providers[resource.scheme]) {
return this.resolveTextModelContent(key).then(() => this.instantiationService.createInstance(ResourceEditorModel, resource));
await this.resolveTextModelContent(key);
return this.instantiationService.createInstance(ResourceEditorModel, resource);
}
// Either unknown schema, or not yet registered, try to activate
if (!skipActivateProvider) {
return this.fileService.activateProvider(resource.scheme).then(() => this.createReferencedObject(key, true));
await this.fileService.activateProvider(resource.scheme);
return this.createReferencedObject(key, true);
}
return Promise.reject(new Error('resource is not available'));
throw new Error('resource is not available');
}
destroyReferencedObject(key: string, modelPromise: Promise<ITextEditorModel>): void {
@ -101,18 +105,17 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod
return this.providers[scheme] !== undefined;
}
private resolveTextModelContent(key: string): Promise<ITextModel> {
private async resolveTextModelContent(key: string): Promise<ITextModel> {
const resource = URI.parse(key);
const providers = this.providers[resource.scheme] || [];
const factories = providers.map(p => () => Promise.resolve(p.provideTextContent(resource)));
return first(factories).then(model => {
if (!model) {
return Promise.reject(new Error('resource is not available'));
}
const model = await first(factories);
if (!model) {
throw new Error('resource is not available');
}
return model;
});
return model;
}
}
@ -131,14 +134,16 @@ export class TextModelResolverService implements ITextModelService {
}
createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
return this._createModelReference(resource);
return this.doCreateModelReference(resource);
}
private _createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
private async doCreateModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
// Untitled Schema: go through cached input
if (resource.scheme === network.Schemas.untitled) {
return this.untitledEditorService.loadOrCreate({ resource }).then(model => new ImmortalReference(model as IResolvedTextEditorModel));
const model = await this.untitledEditorService.loadOrCreate({ resource });
return new ImmortalReference(model as IResolvedTextEditorModel);
}
// InMemory Schema: go through model service cache
@ -154,14 +159,15 @@ export class TextModelResolverService implements ITextModelService {
const ref = this.resourceModelCollection.acquire(resource.toString());
return ref.object.then(
model => ({ object: model, dispose: () => ref.dispose() }),
err => {
ref.dispose();
try {
const model = await ref.object;
return Promise.reject(err);
}
);
return { object: model as IResolvedTextEditorModel, dispose: () => ref.dispose() };
} catch (error) {
ref.dispose();
throw error;
}
}
registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable {

View file

@ -71,6 +71,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
if (reason !== ShutdownReason.LOAD && reason !== ShutdownReason.CLOSE) {
return undefined; // only interested when window is closing or loading
}
const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();
if (!workspaceIdentifier || !isEqualOrParent(workspaceIdentifier.configPath, this.environmentService.untitledWorkspacesHome)) {
return undefined; // only care about untitled workspaces to ask for saving
@ -190,16 +191,23 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
}
}
private doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToDelete: URI[], index?: number, donotNotifyError: boolean = false): Promise<void> {
return this.contextService.updateFolders(foldersToAdd, foldersToDelete, index)
.then(() => null, error => donotNotifyError ? Promise.reject(error) : this.handleWorkspaceConfigurationEditingError(error));
private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToDelete: URI[], index?: number, donotNotifyError: boolean = false): Promise<void> {
try {
await this.contextService.updateFolders(foldersToAdd, foldersToDelete, index);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): Promise<void> {
return this.doAddFolders(foldersToAdd, undefined, donotNotifyError);
}
private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): Promise<void> {
private async doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): Promise<void> {
const state = this.contextService.getWorkbenchState();
// If we are in no-workspace or single-folder workspace, adding folders has to
@ -217,11 +225,18 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
}
// Delegate addition of folders to workspace service otherwise
return this.contextService.addFolders(foldersToAdd, index)
.then(() => null, error => donotNotifyError ? Promise.reject(error) : this.handleWorkspaceConfigurationEditingError(error));
try {
await this.contextService.addFolders(foldersToAdd, index);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): Promise<void> {
async removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): Promise<void> {
// If we are in single-folder state and the opened folder is to be removed,
// we create an empty workspace and enter it.
@ -230,8 +245,15 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
}
// Delegate removal of folders to workspace service otherwise
return this.contextService.removeFolders(foldersToRemove)
.then(() => null, error => donotNotifyError ? Promise.reject(error) : this.handleWorkspaceConfigurationEditingError(error));
try {
await this.contextService.removeFolders(foldersToRemove);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
private includesSingleFolderWorkspace(folders: URI[]): boolean {
@ -283,10 +305,12 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
detail: nls.localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."),
noLink: true
};
return this.windowService.showMessageBox(options).then(() => false);
await this.windowService.showMessageBox(options);
return false;
}
return Promise.resolve(true); // OK
return true; // OK
}
private async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise<any> {