@opensumi/ide-workspace-edit 2.21.13 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,354 @@
1
+ import { runInAction } from 'mobx';
2
+
3
+ import { Injectable, Autowired } from '@opensumi/di';
4
+ import { URI, IEventBus, isWindows, isUndefined } from '@opensumi/ide-core-browser';
5
+ import { WorkbenchEditorService } from '@opensumi/ide-editor';
6
+ import { IEditorDocumentModelService, IResource, isDiffResource } from '@opensumi/ide-editor/lib/browser';
7
+ import { EditorGroup } from '@opensumi/ide-editor/lib/browser/workbench-editor.service';
8
+ import { FileSystemError } from '@opensumi/ide-file-service/lib/common';
9
+ import { EndOfLineSequence, EOL } from '@opensumi/ide-monaco/lib/browser/monaco-api/types';
10
+ import { Range } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/range';
11
+ import * as monaco from '@opensumi/monaco-editor-core/esm/vs/editor/editor.api';
12
+
13
+ import {
14
+ IResourceTextEdit,
15
+ IWorkspaceEditService,
16
+ IWorkspaceEdit,
17
+ IResourceFileEdit,
18
+ WorkspaceEditDidRenameFileEvent,
19
+ WorkspaceEditDidDeleteFileEvent,
20
+ IWorkspaceFileService,
21
+ } from '../common';
22
+
23
+ type WorkspaceEdit = ResourceTextEditTask | ResourceFileEdit;
24
+
25
+ @Injectable()
26
+ export class WorkspaceEditServiceImpl implements IWorkspaceEditService {
27
+ private editStack: BulkEdit[] = [];
28
+
29
+ @Autowired(IEditorDocumentModelService)
30
+ documentModelService: IEditorDocumentModelService;
31
+
32
+ @Autowired(IWorkspaceFileService)
33
+ workspaceFileService: IWorkspaceFileService;
34
+
35
+ @Autowired()
36
+ editorService: WorkbenchEditorService;
37
+
38
+ @Autowired(IEventBus)
39
+ eventBus: IEventBus;
40
+
41
+ async apply(edit: IWorkspaceEdit): Promise<void> {
42
+ const bulkEdit = new BulkEdit();
43
+ edit.edits.forEach((edit) => {
44
+ bulkEdit.add(edit);
45
+ });
46
+ await bulkEdit.apply(this.documentModelService, this.editorService, this.workspaceFileService, this.eventBus);
47
+ this.editStack.push(bulkEdit);
48
+ }
49
+
50
+ revertTopFileEdit(): Promise<void> {
51
+ throw new Error('Method not implemented.');
52
+ }
53
+ }
54
+
55
+ export class BulkEdit {
56
+ private edits: WorkspaceEdit[] = [];
57
+
58
+ async apply(
59
+ documentModelService: IEditorDocumentModelService,
60
+ editorService: WorkbenchEditorService,
61
+ workspaceFS: IWorkspaceFileService,
62
+ eventBus: IEventBus,
63
+ ) {
64
+ for (const edit of this.edits) {
65
+ if (edit instanceof ResourceFileEdit) {
66
+ await edit.apply(documentModelService, editorService, workspaceFS, eventBus);
67
+ } else {
68
+ await edit.apply(documentModelService, editorService);
69
+ }
70
+ }
71
+ }
72
+
73
+ add(edit: IResourceTextEdit | IResourceFileEdit) {
74
+ if (isResourceFileEdit(edit)) {
75
+ this.edits.push(new ResourceFileEdit(edit));
76
+ } else {
77
+ const last = this.edits[this.edits.length - 1];
78
+ const textEdit = edit as IResourceTextEdit;
79
+ if (last && !isResourceFileEdit(last)) {
80
+ // 合并连续同目标的edits
81
+ if (last.resource.toString() === textEdit.resource.toString()) {
82
+ let shouldMerge = false;
83
+ if (last.versionId) {
84
+ if (textEdit.versionId) {
85
+ shouldMerge = textEdit.versionId === last.versionId;
86
+ } else {
87
+ shouldMerge = true;
88
+ }
89
+ } else {
90
+ if (!textEdit.versionId) {
91
+ shouldMerge = true;
92
+ }
93
+ }
94
+ if (shouldMerge) {
95
+ last.addEdit(edit as IResourceTextEdit);
96
+ return;
97
+ }
98
+ }
99
+ }
100
+ this.edits.push(new ResourceTextEditTask(edit as IResourceTextEdit));
101
+ }
102
+ }
103
+
104
+ revert(onlyFileEdits: true) {}
105
+ }
106
+
107
+ export class ResourceTextEditTask {
108
+ public edits: IResourceTextEdit[];
109
+ public resource: URI;
110
+ public versionId: number | undefined;
111
+ public options: {
112
+ openDirtyInEditor?: boolean;
113
+ dirtyIfInEditor?: boolean;
114
+ } = {};
115
+
116
+ constructor(edit: IResourceTextEdit) {
117
+ this.resource = edit.resource;
118
+ this.versionId = edit.versionId;
119
+ this.options = edit.options || {};
120
+ this.edits = [edit];
121
+ }
122
+
123
+ addEdit(edit: IResourceTextEdit) {
124
+ this.edits.push(edit);
125
+ }
126
+
127
+ async apply(documentModelService: IEditorDocumentModelService, editorService: WorkbenchEditorService) {
128
+ const docRef = await documentModelService.createModelReference(this.resource, 'bulk-edit');
129
+ const documentModel = docRef.instance;
130
+ const monacoModel = documentModel.getMonacoModel();
131
+ if (this.versionId) {
132
+ if (monacoModel.getVersionId() !== this.versionId) {
133
+ throw new Error('文档版本不一致,无法执行变更');
134
+ }
135
+ }
136
+ const edits: monaco.editor.IIdentifiedSingleEditOperation[] = [];
137
+ let newEOL: EndOfLineSequence | undefined;
138
+ for (const edit of this.edits) {
139
+ if (edit.textEdit.eol && !isUndefined(edit.textEdit.eol)) {
140
+ newEOL = edit.textEdit.eol;
141
+ }
142
+ edits.push({
143
+ forceMoveMarkers: false,
144
+ range: Range.lift(edit.textEdit.range),
145
+ text: edit.textEdit.text,
146
+ });
147
+ }
148
+
149
+ if (edits.length > 0) {
150
+ monacoModel.pushStackElement();
151
+ monacoModel.pushEditOperations(null, edits, () => null);
152
+ monacoModel.pushStackElement();
153
+ }
154
+
155
+ if (newEOL && !isUndefined(newEOL)) {
156
+ monacoModel.pushStackElement();
157
+ documentModel.eol = newEOL === EndOfLineSequence.CRLF ? EOL.CRLF : EOL.LF;
158
+ monacoModel.pushStackElement();
159
+ }
160
+ const shouldSave = await this.editorOperation(editorService);
161
+ if (shouldSave) {
162
+ documentModel.save();
163
+ }
164
+ docRef.dispose();
165
+ }
166
+
167
+ // 返回是否保存
168
+ private async editorOperation(editorService: WorkbenchEditorService): Promise<boolean> {
169
+ if (this.options.openDirtyInEditor) {
170
+ for (const group of editorService.editorGroups) {
171
+ if (group.resources.findIndex((r) => isDocumentUriInResource(r, this.resource)) !== -1) {
172
+ return false;
173
+ }
174
+ }
175
+ editorService.open(this.resource, { backend: true });
176
+ return false;
177
+ } else if (this.options.dirtyIfInEditor) {
178
+ for (const group of editorService.editorGroups) {
179
+ if (group.resources.findIndex((r) => isDocumentUriInResource(r, this.resource)) !== -1) {
180
+ return false;
181
+ }
182
+ }
183
+ }
184
+ return true;
185
+ }
186
+
187
+ async revert(): Promise<void> {}
188
+ }
189
+
190
+ export class ResourceFileEdit implements IResourceFileEdit {
191
+ oldResource?: URI;
192
+ newResource?: URI;
193
+ options: {
194
+ overwrite?: boolean | undefined;
195
+ ignoreIfNotExists?: boolean | undefined;
196
+ recursive?: boolean | undefined;
197
+ showInEditor?: boolean;
198
+ isDirectory?: boolean;
199
+ copy?: boolean;
200
+ ignoreIfExists?: boolean | undefined;
201
+ content?: string;
202
+ } = {};
203
+
204
+ constructor(edit: IResourceFileEdit) {
205
+ this.oldResource = edit.oldResource;
206
+ this.newResource = edit.newResource;
207
+ this.options = edit.options;
208
+ }
209
+
210
+ async notifyEditor(editorService: WorkbenchEditorService, documentModelService: IEditorDocumentModelService) {
211
+ if (this.oldResource && this.newResource) {
212
+ const promises: Promise<any>[] = [];
213
+ const urisToDealWith: Set<string> = new Set();
214
+ editorService.editorGroups.forEach((g) => {
215
+ g.resources.forEach((r) => {
216
+ if (this.oldResource!.isEqualOrParent(r.uri)) {
217
+ urisToDealWith.add(r.uri.toString());
218
+ }
219
+ });
220
+ });
221
+ urisToDealWith.forEach((uriString) => {
222
+ const oldResource = new URI(uriString);
223
+ const subPath = uriString.substr(this.oldResource!.toString().length);
224
+ const newResource = new URI(this.newResource!.toString()! + subPath);
225
+ promises.push(this.notifyOnResource(oldResource, newResource, editorService, documentModelService));
226
+ });
227
+ return Promise.all(promises);
228
+ }
229
+ }
230
+
231
+ async notifyOnResource(
232
+ oldResource: URI,
233
+ newResource: URI,
234
+ editorService: WorkbenchEditorService,
235
+ documentModelService: IEditorDocumentModelService,
236
+ ) {
237
+ const docRef = documentModelService.getModelReference(oldResource, 'bulk-file-move');
238
+ let dirtyContent: string | undefined;
239
+ let dirtyEOL: EOL | undefined;
240
+ if (docRef && docRef.instance.dirty) {
241
+ dirtyContent = docRef.instance.getText();
242
+ dirtyEOL = docRef.instance.eol;
243
+ await docRef.instance.revert(true);
244
+ }
245
+ if (docRef) {
246
+ docRef.dispose();
247
+ }
248
+ // 如果之前的文件在编辑器中被打开,重新打开文件
249
+ await Promise.all([
250
+ editorService.editorGroups.map(async (g) => {
251
+ const index = g.resources.findIndex((r) => r.uri.isEqual(oldResource));
252
+ if (index !== -1) {
253
+ await runInAction(async () => {
254
+ await g.open(newResource, {
255
+ index,
256
+ backend: !(g.currentResource && g.currentResource.uri.isEqual(oldResource)),
257
+ // 如果旧的是preview模式,应该保持,如果不是,应该不要关闭其他处于preview模式的资源tab
258
+ preview: (g as EditorGroup).previewURI ? (g as EditorGroup).previewURI!.isEqual(oldResource) : false,
259
+ });
260
+ await g.close(oldResource);
261
+ });
262
+ }
263
+ }),
264
+ ]);
265
+
266
+ if (dirtyContent) {
267
+ const newDocRef = await documentModelService.createModelReference(newResource, 'bulk-file-move');
268
+ newDocRef.instance.updateContent(dirtyContent, dirtyEOL);
269
+ newDocRef.dispose();
270
+ }
271
+ }
272
+
273
+ async apply(
274
+ documentModelService: IEditorDocumentModelService,
275
+ editorService: WorkbenchEditorService,
276
+ workspaceFS: IWorkspaceFileService,
277
+ eventBus: IEventBus,
278
+ ) {
279
+ const options = this.options || {};
280
+
281
+ if (this.newResource && this.oldResource) {
282
+ if (options.copy) {
283
+ await workspaceFS.copy([{ source: this.oldResource.codeUri, target: this.newResource.codeUri }], options);
284
+ } else {
285
+ // rename
286
+ await workspaceFS.move([{ source: this.oldResource.codeUri, target: this.newResource.codeUri }], options);
287
+
288
+ await this.notifyEditor(editorService, documentModelService);
289
+
290
+ // TODO: 文件夹rename应该带传染性, 但是遍历实现比较坑,先不实现
291
+ eventBus.fire(new WorkspaceEditDidRenameFileEvent({ oldUri: this.oldResource, newUri: this.newResource }));
292
+ }
293
+
294
+ if (options.showInEditor) {
295
+ editorService.open(this.newResource);
296
+ }
297
+ } else if (!this.newResource && this.oldResource) {
298
+ // 删除文件
299
+ try {
300
+ // electron windows下moveToTrash大量文件会导致IDE卡死,如果检测到这个情况就不使用moveToTrash
301
+ await workspaceFS.delete([this.oldResource], {
302
+ useTrash: !(isWindows && this.oldResource.path.name === 'node_modules'),
303
+ });
304
+ // 默认recursive
305
+ await editorService.close(this.oldResource, true);
306
+ eventBus.fire(new WorkspaceEditDidDeleteFileEvent({ oldUri: this.oldResource }));
307
+ } catch (err) {
308
+ if (FileSystemError.FileNotFound.is(err) && options.ignoreIfNotExists) {
309
+ // 不抛出错误
310
+ } else {
311
+ throw err;
312
+ }
313
+ }
314
+ } else if (this.newResource && !this.oldResource) {
315
+ // 创建文件
316
+ try {
317
+ if (options.isDirectory) {
318
+ await workspaceFS.createFolder(this.newResource);
319
+ } else {
320
+ await workspaceFS.create(this.newResource, options.content || '', { overwrite: options.overwrite });
321
+ }
322
+ } catch (err) {
323
+ if (FileSystemError.FileExists.is(err) && options.ignoreIfExists) {
324
+ // 不抛出错误
325
+ } else {
326
+ throw err;
327
+ }
328
+ }
329
+ if (!options.isDirectory && options.showInEditor) {
330
+ editorService.open(this.newResource);
331
+ }
332
+ }
333
+ }
334
+
335
+ async revert(): Promise<void> {}
336
+ }
337
+
338
+ export function isResourceFileEdit(thing: any): thing is ResourceFileEdit {
339
+ return !!(thing as ResourceFileEdit).newResource || !!(thing as ResourceFileEdit).oldResource;
340
+ }
341
+
342
+ /**
343
+ * 当前编辑器的文档是否在指定的编辑器 resource (tab) 中
344
+ * 此处需要额外判断一下 diffEditor 的情况
345
+ * @param resource
346
+ * @param uri
347
+ */
348
+ function isDocumentUriInResource(resource: IResource<any>, uri: URI) {
349
+ if (isDiffResource(resource)) {
350
+ return resource.metadata?.modified.isEqual(uri) || resource.metadata?.original.isEqual(uri);
351
+ } else {
352
+ return resource.uri.isEqual(uri);
353
+ }
354
+ }
@@ -0,0 +1,206 @@
1
+ import { Injectable, Autowired } from '@opensumi/di';
2
+ import { IProgressService } from '@opensumi/ide-core-browser/lib/progress';
3
+ import {
4
+ URI,
5
+ Uri,
6
+ CancellationTokenSource,
7
+ CancellationToken,
8
+ Disposable,
9
+ IDisposable,
10
+ getDebugLogger,
11
+ AsyncEmitter,
12
+ Event,
13
+ FileStat,
14
+ } from '@opensumi/ide-core-common';
15
+ import { IFileServiceClient } from '@opensumi/ide-file-service';
16
+
17
+ import {
18
+ FileOperation,
19
+ FILE_OPERATION_TIMEOUT,
20
+ IWorkspaceFileOperationParticipant,
21
+ IWorkspaceFileService,
22
+ SourceTargetPair,
23
+ WorkspaceFileEvent,
24
+ } from '..';
25
+
26
+ @Injectable()
27
+ export class WorkspaceFileOperationParticipant extends Disposable {
28
+ @Autowired(IProgressService)
29
+ progressService: IProgressService;
30
+
31
+ participants: IWorkspaceFileOperationParticipant[] = [];
32
+
33
+ registerParticipant(participant: IWorkspaceFileOperationParticipant): IDisposable {
34
+ this.participants.push(participant);
35
+ return {
36
+ dispose: () => {
37
+ const index = this.participants.findIndex((item) => item === participant);
38
+ this.participants.splice(index, 1);
39
+ },
40
+ };
41
+ }
42
+
43
+ async participate(files: { source?: Uri; target: Uri }[], operation: FileOperation): Promise<void> {
44
+ const cts = new CancellationTokenSource();
45
+ for (const participant of this.participants) {
46
+ if (cts.token.isCancellationRequested) {
47
+ break;
48
+ }
49
+
50
+ try {
51
+ await participant.participate(files, operation, undefined, FILE_OPERATION_TIMEOUT, cts.token);
52
+ } catch (err) {
53
+ getDebugLogger().error(err);
54
+ }
55
+ }
56
+ }
57
+
58
+ dispose() {
59
+ this.participants.splice(0, this.participants.length);
60
+ }
61
+ }
62
+
63
+ @Injectable()
64
+ export class WorkspaceFileService implements IWorkspaceFileService {
65
+ @Autowired(IFileServiceClient)
66
+ private readonly fileService: IFileServiceClient;
67
+
68
+ @Autowired(WorkspaceFileOperationParticipant)
69
+ private readonly fileOperationParticipants: WorkspaceFileOperationParticipant;
70
+
71
+ private correlationIds = 0;
72
+
73
+ private readonly _onWillRunWorkspaceFileOperation = new AsyncEmitter<WorkspaceFileEvent>();
74
+ public readonly onWillRunWorkspaceFileOperation: Event<WorkspaceFileEvent> =
75
+ this._onWillRunWorkspaceFileOperation.event;
76
+
77
+ private readonly _onDidFailWorkspaceFileOperation = new AsyncEmitter<WorkspaceFileEvent>();
78
+ public readonly onDidFailWorkspaceFileOperation: Event<WorkspaceFileEvent> =
79
+ this._onDidFailWorkspaceFileOperation.event;
80
+
81
+ private readonly _onDidRunWorkspaceFileOperation = new AsyncEmitter<WorkspaceFileEvent>();
82
+ public readonly onDidRunWorkspaceFileOperation: Event<WorkspaceFileEvent> =
83
+ this._onDidRunWorkspaceFileOperation.event;
84
+
85
+ public create(resource: URI, contents?: string, options?: { overwrite?: boolean }) {
86
+ return this.doCreate(resource, true, contents, options);
87
+ }
88
+
89
+ public createFolder(resource: URI) {
90
+ return this.doCreate(resource, false);
91
+ }
92
+
93
+ public move(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<FileStat[]> {
94
+ return this.doMoveOrCopy(files, true, options);
95
+ }
96
+
97
+ public copy(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<FileStat[]> {
98
+ return this.doMoveOrCopy(files, false, options);
99
+ }
100
+
101
+ public async delete(resources: URI[], options?: { useTrash?: boolean; recursive?: boolean }): Promise<void> {
102
+ // file operation participant
103
+ const files = resources.map((target) => ({ target: target.codeUri }));
104
+ await this.runOperationParticipant(files, FileOperation.DELETE);
105
+
106
+ // before events
107
+ const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, files };
108
+ await this._onWillRunWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
109
+ // now actually delete from disk
110
+ try {
111
+ for (const resource of resources) {
112
+ // TODO: support recursive option
113
+ await this.fileService.delete(resource.toString(), { moveToTrash: options?.useTrash });
114
+ }
115
+ } catch (error) {
116
+ // error event
117
+ await this._onDidFailWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
118
+
119
+ throw error;
120
+ }
121
+
122
+ // after event
123
+ await this._onDidRunWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
124
+ }
125
+
126
+ public registerFileOperationParticipant(participant: IWorkspaceFileOperationParticipant): IDisposable {
127
+ return this.fileOperationParticipants.registerParticipant(participant);
128
+ }
129
+
130
+ private runOperationParticipant(files: SourceTargetPair[], operation: FileOperation) {
131
+ return this.fileOperationParticipants.participate(files, operation);
132
+ }
133
+
134
+ private async doMoveOrCopy(
135
+ files: Required<SourceTargetPair>[],
136
+ move: boolean,
137
+ options?: { overwrite?: boolean },
138
+ ): Promise<FileStat[]> {
139
+ const overwrite = options?.overwrite;
140
+ const stats: FileStat[] = [];
141
+
142
+ // file operation participant
143
+ await this.runOperationParticipant(files, move ? FileOperation.MOVE : FileOperation.COPY);
144
+
145
+ // before event
146
+ const event = {
147
+ correlationId: this.correlationIds++,
148
+ operation: move ? FileOperation.MOVE : FileOperation.COPY,
149
+ files,
150
+ };
151
+ await this._onWillRunWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
152
+
153
+ try {
154
+ for (const { source, target } of files) {
155
+ // now we can rename the source to target via file operation
156
+ if (move) {
157
+ stats.push(await this.fileService.move(source.toString(), target.toString(), { overwrite }));
158
+ } else {
159
+ stats.push(await this.fileService.copy(source.toString(), target.toString(), { overwrite }));
160
+ }
161
+ }
162
+ } catch (error) {
163
+ // error event
164
+ await this._onDidFailWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
165
+
166
+ throw error;
167
+ }
168
+
169
+ // after event
170
+ await this._onDidRunWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
171
+
172
+ return stats;
173
+ }
174
+
175
+ private async doCreate(resource: URI, isFile: boolean, content?: string, options?: { overwrite?: boolean }) {
176
+ // file operation participant
177
+ await this.runOperationParticipant([{ target: resource.codeUri }], FileOperation.CREATE);
178
+ // before events
179
+ const event = {
180
+ correlationId: this.correlationIds++,
181
+ operation: FileOperation.CREATE,
182
+ files: [{ target: resource.codeUri }],
183
+ };
184
+ await this._onWillRunWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
185
+
186
+ // now actually create on disk
187
+ let stat: FileStat;
188
+ try {
189
+ if (isFile) {
190
+ stat = await this.fileService.createFile(resource.toString(), { overwrite: options?.overwrite, content });
191
+ } else {
192
+ stat = await this.fileService.createFolder(resource.toString());
193
+ }
194
+ } catch (error) {
195
+ // error event
196
+ await this._onDidFailWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
197
+
198
+ throw error;
199
+ }
200
+
201
+ // after event
202
+ await this._onDidRunWorkspaceFileOperation.fireAsync(event, CancellationToken.None);
203
+
204
+ return stat;
205
+ }
206
+ }
@@ -0,0 +1,151 @@
1
+ import {
2
+ Uri,
3
+ URI,
4
+ IRange,
5
+ BasicEvent,
6
+ FileStat,
7
+ CancellationToken,
8
+ WaitUntilEvent,
9
+ IDisposable,
10
+ Event,
11
+ } from '@opensumi/ide-core-common';
12
+ // eslint-disable-next-line import/no-restricted-paths
13
+ import type { EndOfLineSequence } from '@opensumi/ide-monaco/lib/browser/monaco-api/types';
14
+ import type { IBulkEditService } from '@opensumi/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService';
15
+
16
+ // 对文件位置(添加,删除,移动, 复制)
17
+ export interface IResourceFileEdit {
18
+ oldResource?: URI;
19
+ newResource?: URI;
20
+ options: {
21
+ overwrite?: boolean;
22
+ ignoreIfNotExists?: boolean;
23
+ ignoreIfExists?: boolean;
24
+ recursive?: boolean;
25
+ showInEditor?: boolean;
26
+ isDirectory?: boolean;
27
+ copy?: boolean;
28
+ content?: string;
29
+ };
30
+ }
31
+
32
+ // 对文件内容的编辑
33
+ export interface IResourceTextEdit {
34
+ resource: URI;
35
+ versionId?: number; // monaco's version id
36
+ textEdit: ITextEdit;
37
+ options?: {
38
+ openDirtyInEditor?: boolean;
39
+ dirtyIfInEditor?: boolean;
40
+ };
41
+ }
42
+
43
+ export interface ITextEdit {
44
+ range: IRange;
45
+ text: string;
46
+ eol?: EndOfLineSequence;
47
+ }
48
+
49
+ export interface IWorkspaceEdit {
50
+ edits: Array<IResourceFileEdit | IResourceTextEdit>;
51
+ }
52
+
53
+ export const IWorkspaceEditService = Symbol('IWorkspaceEditService');
54
+
55
+ export interface IWorkspaceEditService {
56
+ apply(edit: IWorkspaceEdit): Promise<void>;
57
+
58
+ // 回复最上层的文件变更
59
+ revertTopFileEdit(): Promise<void>;
60
+ }
61
+
62
+ export const IWorkspaceFileService = Symbol('IWorkspaceFileService');
63
+
64
+ // 区分开 monaco 内部的 IBulkEditServiceShape
65
+ export const IBulkEditServiceShape = Symbol('IBulkEditServiceShape');
66
+ export type IBulkEditServiceShape = IBulkEditService;
67
+
68
+ export const enum FileOperation {
69
+ CREATE,
70
+ DELETE,
71
+ MOVE,
72
+ COPY,
73
+ }
74
+
75
+ export const FILE_OPERATION_TIMEOUT = 5000;
76
+
77
+ export interface SourceTargetPair {
78
+ /**
79
+ * The source resource that is defined for move operations.
80
+ */
81
+ readonly source?: Uri;
82
+
83
+ /**
84
+ * The target resource the event is about.
85
+ */
86
+ readonly target: Uri;
87
+ }
88
+
89
+ /**
90
+ * not supported yet
91
+ */
92
+ export interface IFileOperationUndoRedoInfo {
93
+ /**
94
+ * Id of the undo group that the file operation belongs to.
95
+ */
96
+ undoRedoGroupId?: number;
97
+
98
+ /**
99
+ * Flag indicates if the operation is an undo.
100
+ */
101
+ isUndoing?: boolean;
102
+ }
103
+
104
+ export interface IWorkspaceFileOperationParticipant {
105
+ /**
106
+ * Participate in a file operation of working copies. Allows to
107
+ * change the working copies before they are being saved to disk.
108
+ */
109
+ participate(
110
+ files: SourceTargetPair[],
111
+ operation: FileOperation,
112
+ undoInfo: IFileOperationUndoRedoInfo | undefined,
113
+ timeout: number,
114
+ token: CancellationToken,
115
+ ): Promise<void>;
116
+ }
117
+
118
+ export interface WorkspaceFileEvent extends WaitUntilEvent {
119
+ /**
120
+ * An identifier to correlate the operation through the
121
+ * different event types (before, after, error).
122
+ */
123
+ readonly correlationId: number;
124
+
125
+ /**
126
+ * The file operation that is taking place.
127
+ */
128
+ readonly operation: FileOperation;
129
+
130
+ /**
131
+ * The array of source/target pair of files involved in given operation.
132
+ */
133
+ readonly files: SourceTargetPair[];
134
+ }
135
+
136
+ export interface IWorkspaceFileService {
137
+ readonly onWillRunWorkspaceFileOperation: Event<WorkspaceFileEvent>;
138
+ readonly onDidFailWorkspaceFileOperation: Event<WorkspaceFileEvent>;
139
+ readonly onDidRunWorkspaceFileOperation: Event<WorkspaceFileEvent>;
140
+
141
+ create(resource: URI, contents?: string, options?: { overwrite?: boolean }): Promise<FileStat>;
142
+ createFolder(resource: URI): Promise<FileStat>;
143
+ move(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<FileStat[]>;
144
+ copy(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<FileStat[]>;
145
+ delete(resources: URI[], options?: { useTrash?: boolean; recursive?: boolean }): Promise<void>;
146
+
147
+ registerFileOperationParticipant(participant: IWorkspaceFileOperationParticipant): IDisposable;
148
+ }
149
+
150
+ export class WorkspaceEditDidRenameFileEvent extends BasicEvent<{ oldUri: URI; newUri: URI }> {}
151
+ export class WorkspaceEditDidDeleteFileEvent extends BasicEvent<{ oldUri: URI }> {}