Skip to content

Commit be7b089

Browse files
author
Benjamin Pasero
committed
working copy files - add a create method and adopt
@jrieken fyi
1 parent 4952232 commit be7b089

12 files changed

Lines changed: 204 additions & 64 deletions

File tree

src/vs/platform/files/common/fileService.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,28 @@ export class FileService extends Disposable implements IFileService {
287287

288288
//#region File Reading/Writing
289289

290-
async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
290+
async canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true> {
291+
try {
292+
await this.doValidateCreateFile(resource, options);
293+
} catch (error) {
294+
return error;
295+
}
296+
297+
return true;
298+
}
299+
300+
private async doValidateCreateFile(resource: URI, options?: ICreateFileOptions): Promise<void> {
291301

292302
// validate overwrite
293303
if (!options?.overwrite && await this.exists(resource)) {
294304
throw new FileOperationError(localize('fileExists', "Unable to create file '{0}' that already exists when overwrite flag is not set", this.resourceForError(resource)), FileOperationResult.FILE_MODIFIED_SINCE, options);
295305
}
306+
}
307+
308+
async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
309+
310+
// validate
311+
await this.doValidateCreateFile(resource, options);
296312

297313
// do write into file (this will create it too)
298314
const fileStat = await this.writeFile(resource, bufferOrReadableOrStream);

src/vs/platform/files/common/files.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ export interface IFileService {
140140
*/
141141
canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true>;
142142

143+
/**
144+
* Find out if a file create operation is possible given the arguments. No changes on disk will
145+
* be performed. Returns an Error if the operation cannot be done.
146+
*/
147+
canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true>;
148+
143149
/**
144150
* Creates a new file with the given path and optional contents. The returned promise
145151
* will have the stat model object as a result.

src/vs/platform/files/test/electron-browser/diskFileService.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,6 +1611,8 @@ suite('Disk File Service', function () {
16111611

16121612
const contents = 'Hello World';
16131613
const resource = URI.file(join(testDir, 'test.txt'));
1614+
1615+
assert.equal(await service.canCreateFile(resource), true);
16141616
const fileStat = await service.createFile(resource, converter(contents));
16151617
assert.equal(fileStat.name, 'test.txt');
16161618
assert.equal(existsSync(fileStat.resource.fsPath), true);
@@ -1628,6 +1630,8 @@ suite('Disk File Service', function () {
16281630

16291631
writeFileSync(resource.fsPath, ''); // create file
16301632

1633+
assert.ok((await service.canCreateFile(resource)) instanceof Error);
1634+
16311635
let error;
16321636
try {
16331637
await service.createFile(resource, VSBuffer.fromString(contents));
@@ -1647,6 +1651,7 @@ suite('Disk File Service', function () {
16471651

16481652
writeFileSync(resource.fsPath, ''); // create file
16491653

1654+
assert.equal(await service.canCreateFile(resource, { overwrite: true }), true);
16501655
const fileStat = await service.createFile(resource, VSBuffer.fromString(contents), { overwrite: true });
16511656
assert.equal(fileStat.name, 'test.txt');
16521657
assert.equal(existsSync(fileStat.resource.fsPath), true);

src/vs/workbench/contrib/files/browser/fileActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => {
10331033
defaultUri
10341034
});
10351035
if (destination) {
1036-
await workingCopyFileService.copy([{ source: s.resource, target: destination }], true);
1036+
await workingCopyFileService.copy([{ source: s.resource, target: destination }], { overwrite: true });
10371037
} else {
10381038
// User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100
10391039
canceled = true;

src/vs/workbench/contrib/files/browser/views/explorerViewer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,7 +1276,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
12761276
const sourceFile = resource;
12771277
const targetFile = joinPath(target.resource, basename(sourceFile));
12781278

1279-
const stat = (await this.workingCopyFileService.copy([{ source: sourceFile, target: targetFile }], true))[0];
1279+
const stat = (await this.workingCopyFileService.copy([{ source: sourceFile, target: targetFile }], { overwrite: true }))[0];
12801280
// if we only add one file, just open it directly
12811281
if (resources.length === 1 && !stat.isDirectory) {
12821282
this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
@@ -1398,7 +1398,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
13981398
const { confirmed } = await this.dialogService.confirm(confirm);
13991399
if (confirmed) {
14001400
try {
1401-
await this.workingCopyFileService.move(sourceTargetPairs, true /* overwrite */);
1401+
await this.workingCopyFileService.move(sourceTargetPairs, { overwrite: true });
14021402
} catch (error) {
14031403
this.notificationService.error(error);
14041404
}

src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
import { WorkspaceFileEdit, WorkspaceFileEditOptions } from 'vs/editor/common/modes';
88
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
99
import { IProgress } from 'vs/platform/progress/common/progress';
10-
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
1110
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1211
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
1312
import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
1413
import { URI } from 'vs/base/common/uri';
1514
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1615
import { ILogService } from 'vs/platform/log/common/log';
16+
import { VSBuffer } from 'vs/base/common/buffer';
1717

1818
interface IFileOperation {
1919
uris: URI[];
@@ -44,7 +44,7 @@ class RenameOperation implements IFileOperation {
4444
if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) {
4545
return new Noop(); // not overwriting, but ignoring, and the target file exists
4646
}
47-
await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], this.options.overwrite);
47+
await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite });
4848
return new RenameOperation(this.oldUri, this.newUri, this.options, this._workingCopyFileService, this._fileService);
4949
}
5050
}
@@ -54,9 +54,9 @@ class CreateOperation implements IFileOperation {
5454
constructor(
5555
readonly newUri: URI,
5656
readonly options: WorkspaceFileEditOptions,
57-
readonly contents: string | undefined,
57+
readonly contents: VSBuffer | undefined,
5858
@IFileService private readonly _fileService: IFileService,
59-
@ITextFileService private readonly _textFileService: ITextFileService,
59+
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
6060
@IInstantiationService private readonly _instaService: IInstantiationService,
6161
) { }
6262

@@ -69,7 +69,7 @@ class CreateOperation implements IFileOperation {
6969
if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) {
7070
return new Noop(); // not overwriting, but ignoring, and the target file exists
7171
}
72-
await this._textFileService.create(this.newUri, this.contents, { overwrite: this.options.overwrite });
72+
await this._workingCopyFileService.create(this.newUri, this.contents, { overwrite: this.options.overwrite });
7373
return this._instaService.createInstance(DeleteOperation, this.newUri, this.options);
7474
}
7575
}
@@ -81,7 +81,6 @@ class DeleteOperation implements IFileOperation {
8181
readonly options: WorkspaceFileEditOptions,
8282
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
8383
@IFileService private readonly _fileService: IFileService,
84-
@ITextFileService private readonly _textFileService: ITextFileService,
8584
@IConfigurationService private readonly _configurationService: IConfigurationService,
8685
@IInstantiationService private readonly _instaService: IInstantiationService,
8786
@ILogService private readonly _logService: ILogService
@@ -100,9 +99,9 @@ class DeleteOperation implements IFileOperation {
10099
return new Noop();
101100
}
102101

103-
let contents: string | undefined;
102+
let contents: VSBuffer | undefined;
104103
try {
105-
contents = (await this._textFileService.read(this.oldUri)).value;
104+
contents = (await this._fileService.readFile(this.oldUri)).value;
106105
} catch (err) {
107106
this._logService.critical(err);
108107
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
155155
async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
156156

157157
// file operation participation
158-
await this.workingCopyFileService.runFileOperationParticipants([{ target: resource, source: undefined }], FileOperation.CREATE);
158+
await this.workingCopyFileService.runFileOperationParticipants([{ target: resource }], FileOperation.CREATE);
159159

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

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

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
135135
private onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void {
136136

137137
// Move / Copy: remember models to restore after the operation
138-
if (e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE) {
138+
if (e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY) {
139139
const modelsToRestore: { source: URI, target: URI, snapshot?: ITextSnapshot; mode?: string; encoding?: string; }[] = [];
140140

141141
for (const { source, target } of e.files) {
@@ -196,7 +196,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
196196
private onDidFailWorkingCopyFileOperation(e: WorkingCopyFileEvent): void {
197197

198198
// Move / Copy: restore dirty flag on models to restore that were dirty
199-
if ((e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) {
199+
if ((e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY)) {
200200
const modelsToRestore = this.mapCorrelationIdToModelsToRestore.get(e.correlationId);
201201
if (modelsToRestore) {
202202
this.mapCorrelationIdToModelsToRestore.delete(e.correlationId);
@@ -214,40 +214,55 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
214214
}
215215

216216
private onDidRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void {
217-
218-
// Move / Copy: restore models that were loaded before the operation took place
219-
if ((e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) {
220-
e.waitUntil((async () => {
221-
const modelsToRestore = this.mapCorrelationIdToModelsToRestore.get(e.correlationId);
222-
if (modelsToRestore) {
223-
this.mapCorrelationIdToModelsToRestore.delete(e.correlationId);
224-
225-
await Promise.all(modelsToRestore.map(async modelToRestore => {
226-
227-
// restore the model, forcing a reload. this is important because
228-
// we know the file has changed on disk after the move and the
229-
// model might have still existed with the previous state. this
230-
// ensures we are not tracking a stale state.
231-
const restoredModel = await this.resolve(modelToRestore.target, { reload: { async: false }, encoding: modelToRestore.encoding });
232-
233-
// restore previous dirty content if any and ensure to mark the model as dirty
234-
let textBufferFactory: ITextBufferFactory | undefined = undefined;
235-
if (modelToRestore.snapshot) {
236-
textBufferFactory = createTextBufferFactoryFromSnapshot(modelToRestore.snapshot);
237-
}
238-
239-
// restore previous mode only if the mode is now unspecified
240-
let preferredMode: string | undefined = undefined;
241-
if (restoredModel.getMode() === PLAINTEXT_MODE_ID && modelToRestore.mode !== PLAINTEXT_MODE_ID) {
242-
preferredMode = modelToRestore.mode;
243-
}
244-
245-
if (textBufferFactory || preferredMode) {
246-
restoredModel.updateTextEditorModel(textBufferFactory, preferredMode);
217+
switch (e.operation) {
218+
219+
// Create: Revert existing models
220+
case FileOperation.CREATE:
221+
e.waitUntil((async () => {
222+
for (const { target } of e.files) {
223+
const model = this.get(target);
224+
if (model && !model.isDisposed()) {
225+
await model.revert();
247226
}
248-
}));
249-
}
250-
})());
227+
}
228+
})());
229+
break;
230+
231+
// Move/Copy: restore models that were loaded before the operation took place
232+
case FileOperation.MOVE:
233+
case FileOperation.COPY:
234+
e.waitUntil((async () => {
235+
const modelsToRestore = this.mapCorrelationIdToModelsToRestore.get(e.correlationId);
236+
if (modelsToRestore) {
237+
this.mapCorrelationIdToModelsToRestore.delete(e.correlationId);
238+
239+
await Promise.all(modelsToRestore.map(async modelToRestore => {
240+
241+
// restore the model, forcing a reload. this is important because
242+
// we know the file has changed on disk after the move and the
243+
// model might have still existed with the previous state. this
244+
// ensures we are not tracking a stale state.
245+
const restoredModel = await this.resolve(modelToRestore.target, { reload: { async: false }, encoding: modelToRestore.encoding });
246+
247+
// restore previous dirty content if any and ensure to mark the model as dirty
248+
let textBufferFactory: ITextBufferFactory | undefined = undefined;
249+
if (modelToRestore.snapshot) {
250+
textBufferFactory = createTextBufferFactoryFromSnapshot(modelToRestore.snapshot);
251+
}
252+
253+
// restore previous mode only if the mode is now unspecified
254+
let preferredMode: string | undefined = undefined;
255+
if (restoredModel.getMode() === PLAINTEXT_MODE_ID && modelToRestore.mode !== PLAINTEXT_MODE_ID) {
256+
preferredMode = modelToRestore.mode;
257+
}
258+
259+
if (textBufferFactory || preferredMode) {
260+
restoredModel.updateTextEditorModel(textBufferFactory, preferredMode);
261+
}
262+
}));
263+
}
264+
})());
265+
break;
251266
}
252267
}
253268

src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/working
1515
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
1616
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
1717
import { WorkingCopyFileOperationParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant';
18+
import { VSBuffer } from 'vs/base/common/buffer';
1819

1920
export const IWorkingCopyFileService = createDecorator<IWorkingCopyFileService>('workingCopyFileService');
2021

@@ -128,14 +129,22 @@ export interface IWorkingCopyFileService {
128129

129130
//#region File operations
130131

132+
/**
133+
* Will create a resource with the provided optional contents, optionally overwriting any target.
134+
*
135+
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
136+
* `onDidRunWorkingCopyFileOperation` events to participate.
137+
*/
138+
create(resource: URI, contents?: VSBuffer, options?: { overwrite?: boolean }): Promise<void>;
139+
131140
/**
132141
* Will move working copies matching the provided resources and corresponding children
133142
* to the target resources using the associated file service for those resources.
134143
*
135144
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
136145
* `onDidRunWorkingCopyFileOperation` events to participate.
137146
*/
138-
move(files: Required<SourceTargetPair>[], overwrite?: boolean): Promise<IFileStatWithMetadata[]>;
147+
move(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]>;
139148

140149
/**
141150
* Will copy working copies matching the provided resources and corresponding children
@@ -144,7 +153,7 @@ export interface IWorkingCopyFileService {
144153
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
145154
* `onDidRunWorkingCopyFileOperation` events to participate.
146155
*/
147-
copy(files: Required<SourceTargetPair>[], overwrite?: boolean): Promise<IFileStatWithMetadata[]>;
156+
copy(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]>;
148157

149158
/**
150159
* Will delete working copies matching the provided resources and children
@@ -219,17 +228,49 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
219228
});
220229
}
221230

231+
222232
//#region File operations
223233

224-
async move(files: Required<SourceTargetPair>[], overwrite?: boolean): Promise<IFileStatWithMetadata[]> {
225-
return this.doMoveOrCopy(files, true, overwrite);
234+
async create(resource: URI, contents?: VSBuffer, options?: { overwrite?: boolean }): Promise<void> {
235+
236+
// validate create operation before starting
237+
const validateCreate = await this.fileService.canCreateFile(resource, options);
238+
if (validateCreate instanceof Error) {
239+
throw validateCreate;
240+
}
241+
242+
// file operation participant
243+
await this.runFileOperationParticipants([{ target: resource }], FileOperation.CREATE);
244+
245+
// before events
246+
const event = { correlationId: this.correlationIds++, operation: FileOperation.CREATE, files: [{ target: resource }] };
247+
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
248+
249+
// now actually create on disk
250+
try {
251+
await this.fileService.createFile(resource, contents, { overwrite: options?.overwrite });
252+
} catch (error) {
253+
254+
// error event
255+
await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
256+
257+
throw error;
258+
}
259+
260+
// after event
261+
await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
262+
}
263+
264+
async move(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
265+
return this.doMoveOrCopy(files, true, options);
226266
}
227267

228-
async copy(files: Required<SourceTargetPair>[], overwrite?: boolean): Promise<IFileStatWithMetadata[]> {
229-
return this.doMoveOrCopy(files, false, overwrite);
268+
async copy(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
269+
return this.doMoveOrCopy(files, false, options);
230270
}
231271

232-
private async doMoveOrCopy(files: Required<SourceTargetPair>[], move: boolean, overwrite?: boolean): Promise<IFileStatWithMetadata[]> {
272+
private async doMoveOrCopy(files: Required<SourceTargetPair>[], move: boolean, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
273+
const overwrite = options?.overwrite;
233274
const stats: IFileStatWithMetadata[] = [];
234275

235276
// validate move/copy operation before starting
@@ -250,7 +291,7 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
250291
try {
251292
for (const { source, target } of files) {
252293

253-
// If source and target are not equal, handle dirty working copies
294+
// if source and target are not equal, handle dirty working copies
254295
// depending on the operation:
255296
// - move: revert both source and target (if any)
256297
// - copy: revert target (if any)
@@ -298,15 +339,15 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
298339
const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, files };
299340
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
300341

301-
// Check for any existing dirty working copies for the resource
342+
// check for any existing dirty working copies for the resource
302343
// and do a soft revert before deleting to be able to close
303344
// any opened editor with these working copies
304345
for (const resource of resources) {
305346
const dirtyWorkingCopies = this.getDirty(resource);
306347
await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
307348
}
308349

309-
// Now actually delete from disk
350+
// now actually delete from disk
310351
try {
311352
for (const resource of resources) {
312353
await this.fileService.del(resource, options);

0 commit comments

Comments
 (0)