Skip to content

Commit 5d64dcc

Browse files
author
Benjamin Pasero
committed
working copy - introduce and adopt a way to participate in file operations
1 parent 44f6625 commit 5d64dcc

14 files changed

Lines changed: 252 additions & 99 deletions

File tree

build/lib/i18n.resources.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@
302302
"name": "vs/workbench/services/textMate",
303303
"project": "vscode-workbench"
304304
},
305+
{
306+
"name": "vs/workbench/services/workingCopy",
307+
"project": "vscode-workbench"
308+
},
305309
{
306310
"name": "vs/workbench/services/workspaces",
307311
"project": "vscode-workbench"

src/vs/base/common/async.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ export function raceCancellation<T>(promise: Promise<T>, token: CancellationToke
5656
return Promise.race([promise, new Promise<T>(resolve => token.onCancellationRequested(() => resolve(defaultValue)))]);
5757
}
5858

59+
export function raceTimeout<T>(promise: Promise<T>, timeout: number, onTimeout?: () => void): Promise<T> {
60+
let promiseResolve: (() => void) | undefined = undefined;
61+
62+
const timer = setTimeout(() => {
63+
promiseResolve?.();
64+
onTimeout?.();
65+
}, timeout);
66+
67+
return Promise.race([
68+
promise.finally(() => clearTimeout(timer)),
69+
new Promise<T>(resolve => promiseResolve = resolve)
70+
]);
71+
}
72+
5973
export function asPromise<T>(callback: () => T | Thenable<T>): Promise<T> {
6074
return new Promise<T>((resolve, reject) => {
6175
const item = callback();

src/vs/base/test/common/async.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as assert from 'assert';
77
import * as async from 'vs/base/common/async';
88
import { isPromiseCanceledError } from 'vs/base/common/errors';
99
import { URI } from 'vs/base/common/uri';
10+
import { CancellationTokenSource } from 'vs/base/common/cancellation';
1011

1112
suite('Async', () => {
1213

@@ -646,4 +647,45 @@ suite('Async', () => {
646647

647648
assert.ok(pendingCancelled);
648649
});
650+
651+
test('raceCancellation', async () => {
652+
const cts = new CancellationTokenSource();
653+
654+
const now = Date.now();
655+
656+
const p = async.raceCancellation(async.timeout(100), cts.token);
657+
cts.cancel();
658+
659+
await p;
660+
661+
assert.ok(Date.now() - now < 100);
662+
});
663+
664+
test('raceTimeout', async () => {
665+
const cts = new CancellationTokenSource();
666+
667+
// timeout wins
668+
let now = Date.now();
669+
let timedout = false;
670+
671+
const p1 = async.raceTimeout(async.timeout(100), 1, () => timedout = true);
672+
cts.cancel();
673+
674+
await p1;
675+
676+
assert.ok(Date.now() - now < 100);
677+
assert.equal(timedout, true);
678+
679+
// promise wins
680+
now = Date.now();
681+
timedout = false;
682+
683+
const p2 = async.raceTimeout(async.timeout(1), 100, () => timedout = true);
684+
cts.cancel();
685+
686+
await p2;
687+
688+
assert.ok(Date.now() - now < 100);
689+
assert.equal(timedout, false);
690+
});
649691
});

src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@ import { FileChangeType, IFileService, FileOperation } from 'vs/platform/files/c
88
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
99
import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol';
1010
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
11-
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
11+
import { IProgressService } from 'vs/platform/progress/common/progress';
1212
import { localize } from 'vs/nls';
1313
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
1414
import { Registry } from 'vs/platform/registry/common/platform';
1515
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1616
import { ILogService } from 'vs/platform/log/common/log';
17-
import { CancellationTokenSource } from 'vs/base/common/cancellation';
1817
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
19-
import { URI } from 'vs/base/common/uri';
20-
import { IWaitUntil } from 'vs/base/common/event';
2118

2219
@extHostCustomer
2320
export class MainThreadFileSystemEventService {
@@ -65,43 +62,11 @@ export class MainThreadFileSystemEventService {
6562

6663

6764
// BEFORE file operation
68-
const messages = new Map<FileOperation, string>();
69-
messages.set(FileOperation.CREATE, localize('msg-create', "Running 'File Create' participants..."));
70-
messages.set(FileOperation.DELETE, localize('msg-delete', "Running 'File Delete' participants..."));
71-
messages.set(FileOperation.MOVE, localize('msg-rename', "Running 'File Rename' participants..."));
72-
73-
function participateInFileOperation(e: IWaitUntil, operation: FileOperation, target: URI, source?: URI): void {
74-
const timeout = configService.getValue<number>('files.participants.timeout');
75-
if (timeout <= 0) {
76-
return; // disabled
65+
workingCopyFileService.addFileOperationParticipant({
66+
participate: (target, source, operation, progress, timeout, token) => {
67+
return proxy.$onWillRunFileOperation(operation, target, source, timeout, token);
7768
}
78-
79-
const p = progressService.withProgress({ location: ProgressLocation.Window }, progress => {
80-
81-
progress.report({ message: messages.get(operation) });
82-
83-
return new Promise((resolve, reject) => {
84-
85-
const cts = new CancellationTokenSource();
86-
87-
const timeoutHandle = setTimeout(() => {
88-
logService.trace('CANCELLED file participants because of timeout', timeout, target, operation);
89-
cts.cancel();
90-
reject(new Error('timeout'));
91-
}, timeout);
92-
93-
proxy.$onWillRunFileOperation(operation, target, source, timeout, cts.token)
94-
.then(resolve, reject)
95-
.finally(() => clearTimeout(timeoutHandle));
96-
});
97-
98-
});
99-
100-
e.waitUntil(p);
101-
}
102-
103-
this._listener.add(textFileService.onWillCreateTextFile(e => participateInFileOperation(e, FileOperation.CREATE, e.resource)));
104-
this._listener.add(workingCopyFileService.onBeforeWorkingCopyFileOperation(e => participateInFileOperation(e, e.operation, e.target, e.source)));
69+
});
10570

10671
// AFTER file operation
10772
this._listener.add(textFileService.onDidCreateTextFile(e => proxy.$onDidRunFileOperation(FileOperation.CREATE, e.resource, undefined)));

src/vs/workbench/services/textfile/browser/textFileService.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { AsyncEmitter } from 'vs/base/common/event';
99
import { ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, TextFileCreateEvent } from 'vs/workbench/services/textfile/common/textfiles';
1010
import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor';
1111
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
12-
import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions } from 'vs/platform/files/common/files';
12+
import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files';
1313
import { Disposable } from 'vs/base/common/lifecycle';
1414
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
1515
import { IUntitledTextEditorService, IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
@@ -33,6 +33,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService
3333
import { suggestFilename } from 'vs/base/common/mime';
3434
import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService';
3535
import { isValidBasename } from 'vs/base/common/extpath';
36+
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
3637

3738
/**
3839
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
@@ -43,9 +44,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
4344

4445
//#region events
4546

46-
private _onWillCreateTextFile = this._register(new AsyncEmitter<TextFileCreateEvent>());
47-
readonly onWillCreateTextFile = this._onWillCreateTextFile.event;
48-
4947
private _onDidCreateTextFile = this._register(new AsyncEmitter<TextFileCreateEvent>());
5048
readonly onDidCreateTextFile = this._onDidCreateTextFile.event;
5149

@@ -70,7 +68,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
7068
@IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService,
7169
@ITextModelService private readonly textModelService: ITextModelService,
7270
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
73-
@IRemotePathService private readonly remotePathService: IRemotePathService
71+
@IRemotePathService private readonly remotePathService: IRemotePathService,
72+
@IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService
7473
) {
7574
super();
7675

@@ -141,8 +140,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
141140

142141
async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
143142

144-
// before event
145-
await this._onWillCreateTextFile.fireAsync({ resource }, CancellationToken.None);
143+
// file operation participation
144+
await this.workingCopyFileService.runFileOperationParticipants(resource, undefined, FileOperation.CREATE);
146145

147146
// create file on disk
148147
const stat = await this.doCreate(resource, value, options);

src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { localize } from 'vs/nls';
67
import { raceCancellation } from 'vs/base/common/async';
78
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
8-
import { localize } from 'vs/nls';
99
import { ILogService } from 'vs/platform/log/common/log';
1010
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
1111
import { ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';

src/vs/workbench/services/textfile/common/textfiles.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,6 @@ export interface ITextFileService extends IDisposable {
9595
*/
9696
write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata>;
9797

98-
/**
99-
* An event that is fired before attempting to create a text file.
100-
*/
101-
readonly onWillCreateTextFile: Event<TextFileCreateEvent>;
102-
10398
/**
10499
* An event that is fired after a text file has been created.
105100
*/

src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura
3838
import { ITextModelService } from 'vs/editor/common/services/resolverService';
3939
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
4040
import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService';
41+
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
4142

4243
export class NativeTextFileService extends AbstractTextFileService {
4344

@@ -55,9 +56,10 @@ export class NativeTextFileService extends AbstractTextFileService {
5556
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,
5657
@ITextModelService textModelService: ITextModelService,
5758
@ICodeEditorService codeEditorService: ICodeEditorService,
58-
@IRemotePathService remotePathService: IRemotePathService
59+
@IRemotePathService remotePathService: IRemotePathService,
60+
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService
5961
) {
60-
super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, remotePathService);
62+
super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, remotePathService, workingCopyFileService);
6163
}
6264

6365
private _encoding: EncodingOracle | undefined;

src/vs/workbench/services/textfile/test/browser/textFileService.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,14 @@ suite('Files - TextFileService', () => {
135135

136136
let eventCounter = 0;
137137

138-
accessor.textFileService.onWillCreateTextFile(e => {
139-
assert.equal(e.resource.toString(), model.resource.toString());
140-
eventCounter++;
138+
const disposable1 = accessor.workingCopyFileService.addFileOperationParticipant({
139+
participate: async target => {
140+
assert.equal(target.toString(), model.resource.toString());
141+
eventCounter++;
142+
}
141143
});
142144

143-
accessor.textFileService.onDidCreateTextFile(e => {
145+
const disposable2 = accessor.textFileService.onDidCreateTextFile(e => {
144146
assert.equal(e.resource.toString(), model.resource.toString());
145147
eventCounter++;
146148
});
@@ -149,5 +151,8 @@ suite('Files - TextFileService', () => {
149151
assert.ok(!accessor.textFileService.isDirty(model.resource));
150152

151153
assert.equal(eventCounter, 2);
154+
155+
disposable1.dispose();
156+
disposable2.dispose();
152157
});
153158
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { localize } from 'vs/nls';
7+
import { raceTimeout } from 'vs/base/common/async';
8+
import { CancellationTokenSource } from 'vs/base/common/cancellation';
9+
import { ILogService } from 'vs/platform/log/common/log';
10+
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
11+
import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
12+
import { IWorkingCopyFileOperationParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
13+
import { URI } from 'vs/base/common/uri';
14+
import { FileOperation } from 'vs/platform/files/common/files';
15+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
16+
17+
export class WorkingCopyFileOperationParticipant extends Disposable {
18+
19+
private readonly participants: IWorkingCopyFileOperationParticipant[] = [];
20+
21+
constructor(
22+
@IProgressService private readonly progressService: IProgressService,
23+
@ILogService private readonly logService: ILogService,
24+
@IConfigurationService private readonly configurationService: IConfigurationService
25+
) {
26+
super();
27+
}
28+
29+
addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable {
30+
this.participants.push(participant);
31+
32+
return toDisposable(() => this.participants.splice(this.participants.indexOf(participant), 1));
33+
}
34+
35+
async participate(target: URI, source: URI | undefined, operation: FileOperation): Promise<void> {
36+
const timeout = this.configurationService.getValue<number>('files.participants.timeout');
37+
if (timeout <= 0) {
38+
return; // disabled
39+
}
40+
41+
const cts = new CancellationTokenSource();
42+
43+
return this.progressService.withProgress({
44+
location: ProgressLocation.Window,
45+
title: this.progressLabel(operation)
46+
}, async progress => {
47+
48+
// For each participant
49+
for (const participant of this.participants) {
50+
if (cts.token.isCancellationRequested) {
51+
break;
52+
}
53+
54+
try {
55+
const promise = participant.participate(target, source, operation, progress, timeout, cts.token);
56+
await raceTimeout(promise, timeout, () => cts.dispose(true /* cancel */));
57+
} catch (err) {
58+
this.logService.warn(err);
59+
}
60+
}
61+
});
62+
}
63+
64+
private progressLabel(operation: FileOperation): string {
65+
switch (operation) {
66+
case FileOperation.CREATE:
67+
return localize('msg-create', "Running 'File Create' participants...");
68+
case FileOperation.MOVE:
69+
return localize('msg-rename', "Running 'File Rename' participants...");
70+
case FileOperation.COPY:
71+
return localize('msg-copy', "Running 'File Copy' participants...");
72+
case FileOperation.DELETE:
73+
return localize('msg-delete', "Running 'File Delete' participants...");
74+
}
75+
}
76+
77+
dispose(): void {
78+
this.participants.splice(0, this.participants.length);
79+
}
80+
}

0 commit comments

Comments
 (0)