@theia/api-tests 1.34.2 → 1.34.3
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.
- package/LICENSE +641 -641
- package/package.json +3 -3
- package/src/api-tests.d.ts +21 -21
- package/src/browser-utils.spec.js +53 -53
- package/src/contribution-filter.spec.js +36 -36
- package/src/file-search.spec.js +132 -132
- package/src/find-replace.spec.js +129 -129
- package/src/keybindings.spec.js +117 -117
- package/src/launch-preferences.spec.js +697 -697
- package/src/menus.spec.js +176 -176
- package/src/monaco-api.spec.js +188 -188
- package/src/navigator.spec.js +92 -92
- package/src/preferences.spec.js +207 -207
- package/src/saveable.spec.js +503 -503
- package/src/scm.spec.js +173 -173
- package/src/shell.spec.js +41 -41
- package/src/task-configurations.spec.js +112 -112
- package/src/typescript.spec.js +779 -779
- package/src/undo-redo-selectAll.spec.js +193 -193
- package/src/views.spec.js +76 -76
package/src/saveable.spec.js
CHANGED
|
@@ -1,503 +1,503 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2020 TypeFox and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
// @ts-check
|
|
18
|
-
describe('Saveable', function () {
|
|
19
|
-
this.timeout(5000);
|
|
20
|
-
|
|
21
|
-
const { assert } = chai;
|
|
22
|
-
|
|
23
|
-
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
|
24
|
-
const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget');
|
|
25
|
-
const { PreferenceService } = require('@theia/core/lib/browser/preferences/preference-service');
|
|
26
|
-
const { Saveable, SaveableWidget } = require('@theia/core/lib/browser/saveable');
|
|
27
|
-
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
|
28
|
-
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
|
|
29
|
-
const { FileResource } = require('@theia/filesystem/lib/browser/file-resource');
|
|
30
|
-
const { ETAG_DISABLED } = require('@theia/filesystem/lib/common/files');
|
|
31
|
-
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
|
|
32
|
-
const { Deferred } = require('@theia/core/lib/common/promise-util');
|
|
33
|
-
const { Disposable, DisposableCollection } = require('@theia/core/lib/common/disposable');
|
|
34
|
-
const { Range } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/range');
|
|
35
|
-
|
|
36
|
-
const container = window.theia.container;
|
|
37
|
-
/** @type {EditorManager} */
|
|
38
|
-
const editorManager = container.get(EditorManager);
|
|
39
|
-
const workspaceService = container.get(WorkspaceService);
|
|
40
|
-
const fileService = container.get(FileService);
|
|
41
|
-
/** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */
|
|
42
|
-
const preferences = container.get(PreferenceService);
|
|
43
|
-
|
|
44
|
-
/** @type {EditorWidget & SaveableWidget} */
|
|
45
|
-
let widget;
|
|
46
|
-
/** @type {MonacoEditor} */
|
|
47
|
-
let editor;
|
|
48
|
-
|
|
49
|
-
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
|
50
|
-
const fileUri = rootUri.resolve('.test/foo.txt');
|
|
51
|
-
|
|
52
|
-
const closeOnFileDelete = 'workbench.editor.closeOnFileDelete';
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @param {FileResource['shouldOverwrite']} shouldOverwrite
|
|
56
|
-
* @returns {Disposable}
|
|
57
|
-
*/
|
|
58
|
-
function setShouldOverwrite(shouldOverwrite) {
|
|
59
|
-
const resource = editor.document['resource'];
|
|
60
|
-
assert.isTrue(resource instanceof FileResource);
|
|
61
|
-
const fileResource = /** @type {FileResource} */ (resource);
|
|
62
|
-
const originalShouldOverwrite = fileResource['shouldOverwrite'];
|
|
63
|
-
fileResource['shouldOverwrite'] = shouldOverwrite;
|
|
64
|
-
return Disposable.create(() => fileResource['shouldOverwrite'] = originalShouldOverwrite);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const toTearDown = new DisposableCollection();
|
|
68
|
-
|
|
69
|
-
/** @type {string | undefined} */
|
|
70
|
-
const autoSave = preferences.get('files.autoSave', undefined, rootUri.toString());
|
|
71
|
-
|
|
72
|
-
beforeEach(async () => {
|
|
73
|
-
await preferences.set('files.autoSave', 'off', undefined, rootUri.toString());
|
|
74
|
-
await preferences.set(closeOnFileDelete, true);
|
|
75
|
-
await editorManager.closeAll({ save: false });
|
|
76
|
-
await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true });
|
|
77
|
-
widget = /** @type {EditorWidget & SaveableWidget} */
|
|
78
|
-
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
|
79
|
-
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
afterEach(async () => {
|
|
83
|
-
toTearDown.dispose();
|
|
84
|
-
await preferences.set('files.autoSave', autoSave, undefined, rootUri.toString());
|
|
85
|
-
// @ts-ignore
|
|
86
|
-
editor = undefined;
|
|
87
|
-
// @ts-ignore
|
|
88
|
-
widget = undefined;
|
|
89
|
-
await editorManager.closeAll({ save: false });
|
|
90
|
-
await fileService.delete(fileUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('normal save', async function () {
|
|
94
|
-
for (const edit of ['bar', 'baz']) {
|
|
95
|
-
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty before '${edit}' edit`);
|
|
96
|
-
editor.getControl().setValue(edit);
|
|
97
|
-
assert.isTrue(Saveable.isDirty(widget), `should be dirty before '${edit}' save`);
|
|
98
|
-
await Saveable.save(widget);
|
|
99
|
-
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty after '${edit}' save`);
|
|
100
|
-
assert.equal(editor.getControl().getValue().trimRight(), edit, `model should be updated with '${edit}'`);
|
|
101
|
-
const state = await fileService.read(fileUri);
|
|
102
|
-
assert.equal(state.value.trimRight(), edit, `fs should be updated with '${edit}'`);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('reject save with incremental update', async function () {
|
|
107
|
-
let longContent = 'foobarbaz';
|
|
108
|
-
for (let i = 0; i < 5; i++) {
|
|
109
|
-
longContent += longContent + longContent;
|
|
110
|
-
}
|
|
111
|
-
editor.getControl().setValue(longContent);
|
|
112
|
-
await Saveable.save(widget);
|
|
113
|
-
|
|
114
|
-
// @ts-ignore
|
|
115
|
-
editor.getControl().getModel().applyEdits([{
|
|
116
|
-
range: Range.fromPositions({ lineNumber: 1, column: 1 }, { lineNumber: 1, column: 4 }),
|
|
117
|
-
forceMoveMarkers: false,
|
|
118
|
-
text: ''
|
|
119
|
-
}]);
|
|
120
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
121
|
-
|
|
122
|
-
const resource = editor.document['resource'];
|
|
123
|
-
const version = resource.version;
|
|
124
|
-
// @ts-ignore
|
|
125
|
-
await resource.saveContents('baz');
|
|
126
|
-
assert.notEqual(version, resource.version, 'latest version should be different after write');
|
|
127
|
-
|
|
128
|
-
let outOfSync = false;
|
|
129
|
-
let outOfSyncCount = 0;
|
|
130
|
-
toTearDown.push(setShouldOverwrite(async () => {
|
|
131
|
-
outOfSync = true;
|
|
132
|
-
outOfSyncCount++;
|
|
133
|
-
return false;
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
let incrementalUpdate = false;
|
|
137
|
-
const saveContentChanges = resource.saveContentChanges;
|
|
138
|
-
resource.saveContentChanges = async (changes, options) => {
|
|
139
|
-
incrementalUpdate = true;
|
|
140
|
-
// @ts-ignore
|
|
141
|
-
return saveContentChanges.bind(resource)(changes, options);
|
|
142
|
-
};
|
|
143
|
-
try {
|
|
144
|
-
await Saveable.save(widget);
|
|
145
|
-
} finally {
|
|
146
|
-
resource.saveContentChanges = saveContentChanges;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
assert.isTrue(incrementalUpdate, 'should tried to update incrementaly');
|
|
150
|
-
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
151
|
-
assert.equal(outOfSyncCount, 1, 'user should be prompted only once with out of sync dialog');
|
|
152
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
|
|
153
|
-
assert.equal(editor.getControl().getValue().trimRight(), longContent.substring(3), 'model should be updated');
|
|
154
|
-
const state = await fileService.read(fileUri);
|
|
155
|
-
assert.equal(state.value, 'baz', 'fs should NOT be updated');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('accept rejected save', async function () {
|
|
159
|
-
let outOfSync = false;
|
|
160
|
-
toTearDown.push(setShouldOverwrite(async () => {
|
|
161
|
-
outOfSync = true;
|
|
162
|
-
return false;
|
|
163
|
-
}));
|
|
164
|
-
editor.getControl().setValue('bar');
|
|
165
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
166
|
-
|
|
167
|
-
const resource = editor.document['resource'];
|
|
168
|
-
const version = resource.version;
|
|
169
|
-
// @ts-ignore
|
|
170
|
-
await resource.saveContents('bazz');
|
|
171
|
-
assert.notEqual(version, resource.version, 'latest version should be different after write');
|
|
172
|
-
|
|
173
|
-
await Saveable.save(widget);
|
|
174
|
-
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
175
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
|
|
176
|
-
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
|
177
|
-
let state = await fileService.read(fileUri);
|
|
178
|
-
assert.equal(state.value, 'bazz', 'fs should NOT be updated');
|
|
179
|
-
|
|
180
|
-
outOfSync = false;
|
|
181
|
-
toTearDown.push(setShouldOverwrite(async () => {
|
|
182
|
-
outOfSync = true;
|
|
183
|
-
return true;
|
|
184
|
-
}));
|
|
185
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
186
|
-
await Saveable.save(widget);
|
|
187
|
-
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
188
|
-
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
|
189
|
-
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
|
190
|
-
state = await fileService.read(fileUri);
|
|
191
|
-
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('accept new save', async () => {
|
|
195
|
-
let outOfSync = false;
|
|
196
|
-
toTearDown.push(setShouldOverwrite(async () => {
|
|
197
|
-
outOfSync = true;
|
|
198
|
-
return true;
|
|
199
|
-
}));
|
|
200
|
-
editor.getControl().setValue('bar');
|
|
201
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
202
|
-
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
|
203
|
-
await Saveable.save(widget);
|
|
204
|
-
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
205
|
-
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
|
206
|
-
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
|
207
|
-
const state = await fileService.read(fileUri);
|
|
208
|
-
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('cancel save on close', async () => {
|
|
212
|
-
editor.getControl().setValue('bar');
|
|
213
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before close');
|
|
214
|
-
|
|
215
|
-
await widget.closeWithSaving({
|
|
216
|
-
shouldSave: () => undefined
|
|
217
|
-
});
|
|
218
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be still dirty after canceled close');
|
|
219
|
-
assert.isFalse(widget.isDisposed, 'should NOT be disposed after canceled close');
|
|
220
|
-
const state = await fileService.read(fileUri);
|
|
221
|
-
assert.equal(state.value, 'foo', 'fs should NOT be updated after canceled close');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('reject save on close', async () => {
|
|
225
|
-
editor.getControl().setValue('bar');
|
|
226
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejected close');
|
|
227
|
-
await widget.closeWithSaving({
|
|
228
|
-
shouldSave: () => false
|
|
229
|
-
});
|
|
230
|
-
assert.isTrue(widget.isDisposed, 'should be disposed after rejected close');
|
|
231
|
-
const state = await fileService.read(fileUri);
|
|
232
|
-
assert.equal(state.value, 'foo', 'fs should NOT be updated after rejected close');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('accept save on close and reject it', async () => {
|
|
236
|
-
let outOfSync = false;
|
|
237
|
-
toTearDown.push(setShouldOverwrite(async () => {
|
|
238
|
-
outOfSync = true;
|
|
239
|
-
return false;
|
|
240
|
-
}));
|
|
241
|
-
editor.getControl().setValue('bar');
|
|
242
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejecting save on close');
|
|
243
|
-
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
|
244
|
-
await widget.closeWithSaving({
|
|
245
|
-
shouldSave: () => true
|
|
246
|
-
});
|
|
247
|
-
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
248
|
-
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
|
|
249
|
-
const state = await fileService.read(fileUri);
|
|
250
|
-
assert.equal(state.value, 'foo2', 'fs should NOT be updated');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('accept save on close and accept new save', async () => {
|
|
254
|
-
let outOfSync = false;
|
|
255
|
-
toTearDown.push(setShouldOverwrite(async () => {
|
|
256
|
-
outOfSync = true;
|
|
257
|
-
return true;
|
|
258
|
-
}));
|
|
259
|
-
editor.getControl().setValue('bar');
|
|
260
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before accepting save on close');
|
|
261
|
-
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
|
262
|
-
await widget.closeWithSaving({
|
|
263
|
-
shouldSave: () => true
|
|
264
|
-
});
|
|
265
|
-
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
266
|
-
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
|
|
267
|
-
const state = await fileService.read(fileUri);
|
|
268
|
-
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('no save prompt when multiple editors open for same file', async () => {
|
|
272
|
-
const secondWidget = await editorManager.openToSide(fileUri);
|
|
273
|
-
editor.getControl().setValue('two widgets');
|
|
274
|
-
assert.isTrue(Saveable.isDirty(widget), 'the first widget should be dirty');
|
|
275
|
-
assert.isTrue(Saveable.isDirty(secondWidget), 'the second widget should also be dirty');
|
|
276
|
-
await Promise.resolve(secondWidget.close());
|
|
277
|
-
assert.isTrue(secondWidget.isDisposed, 'the widget should have closed without requesting user action');
|
|
278
|
-
assert.isTrue(Saveable.isDirty(widget), 'the original widget should still be dirty.');
|
|
279
|
-
assert.equal(editor.getControl().getValue(), 'two widgets', 'should still have the same value');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('normal close', async () => {
|
|
283
|
-
editor.getControl().setValue('bar');
|
|
284
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before before close');
|
|
285
|
-
await widget.closeWithSaving({
|
|
286
|
-
shouldSave: () => true
|
|
287
|
-
});
|
|
288
|
-
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
|
|
289
|
-
const state = await fileService.read(fileUri);
|
|
290
|
-
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('delete and add again file for dirty', async () => {
|
|
294
|
-
editor.getControl().setValue('bar');
|
|
295
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before delete');
|
|
296
|
-
assert.isTrue(editor.document.valid, 'should be valid before delete');
|
|
297
|
-
let waitForDidChangeTitle = new Deferred();
|
|
298
|
-
const listener = () => waitForDidChangeTitle.resolve();
|
|
299
|
-
widget.title.changed.connect(listener);
|
|
300
|
-
try {
|
|
301
|
-
await fileService.delete(fileUri);
|
|
302
|
-
await waitForDidChangeTitle.promise;
|
|
303
|
-
assert.isTrue(widget.title.label.endsWith('(deleted)'), 'should be marked as deleted');
|
|
304
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after delete');
|
|
305
|
-
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after delete');
|
|
306
|
-
} finally {
|
|
307
|
-
widget.title.changed.disconnect(listener);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
waitForDidChangeTitle = new Deferred();
|
|
311
|
-
widget.title.changed.connect(listener);
|
|
312
|
-
try {
|
|
313
|
-
await fileService.create(fileUri, 'foo');
|
|
314
|
-
await waitForDidChangeTitle.promise;
|
|
315
|
-
assert.isFalse(widget.title.label.endsWith('(deleted)'), 'should NOT be marked as deleted');
|
|
316
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after added again');
|
|
317
|
-
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after added again');
|
|
318
|
-
} finally {
|
|
319
|
-
widget.title.changed.disconnect(listener);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('save deleted file for dirty', async function () {
|
|
324
|
-
editor.getControl().setValue('bar');
|
|
325
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save deleted');
|
|
326
|
-
|
|
327
|
-
assert.isTrue(editor.document.valid, 'should be valid before delete');
|
|
328
|
-
const waitForInvalid = new Deferred();
|
|
329
|
-
const listener = editor.document.onDidChangeValid(() => waitForInvalid.resolve());
|
|
330
|
-
try {
|
|
331
|
-
await fileService.delete(fileUri);
|
|
332
|
-
await waitForInvalid.promise;
|
|
333
|
-
assert.isFalse(editor.document.valid, 'should be invalid after delete');
|
|
334
|
-
} finally {
|
|
335
|
-
listener.dispose();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
339
|
-
await Saveable.save(widget);
|
|
340
|
-
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
|
341
|
-
assert.isTrue(editor.document.valid, 'should be valid after save');
|
|
342
|
-
const state = await fileService.read(fileUri);
|
|
343
|
-
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('move file for saved', async function () {
|
|
347
|
-
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before move');
|
|
348
|
-
|
|
349
|
-
const targetUri = fileUri.parent.resolve('bar.txt');
|
|
350
|
-
await fileService.move(fileUri, targetUri, { overwrite: true });
|
|
351
|
-
assert.isTrue(widget.isDisposed, 'old model should be disposed after move');
|
|
352
|
-
|
|
353
|
-
const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri));
|
|
354
|
-
assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move');
|
|
355
|
-
assert.equal(renamed.editor.document.getText(), 'foo', 'new model should be created after move');
|
|
356
|
-
assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after move');
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('move file for dirty', async function () {
|
|
360
|
-
editor.getControl().setValue('bar');
|
|
361
|
-
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before move');
|
|
362
|
-
|
|
363
|
-
const targetUri = fileUri.parent.resolve('bar.txt');
|
|
364
|
-
|
|
365
|
-
await fileService.move(fileUri, targetUri, { overwrite: true });
|
|
366
|
-
assert.isTrue(widget.isDisposed, 'old model should be disposed after move');
|
|
367
|
-
|
|
368
|
-
const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri));
|
|
369
|
-
assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move');
|
|
370
|
-
assert.equal(renamed.editor.document.getText(), 'bar', 'new model should be created after move');
|
|
371
|
-
assert.isTrue(Saveable.isDirty(renamed), 'new model should be dirty after move');
|
|
372
|
-
|
|
373
|
-
await Saveable.save(renamed);
|
|
374
|
-
assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after save');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('fail to open invalid file', async function () {
|
|
378
|
-
const invalidFile = fileUri.parent.resolve('invalid_file.txt');
|
|
379
|
-
try {
|
|
380
|
-
await editorManager.open(invalidFile, { mode: 'reveal' });
|
|
381
|
-
assert.fail('should not be possible to open an editor for invalid file');
|
|
382
|
-
} catch (e) {
|
|
383
|
-
assert.equal(e.code, 'MODEL_IS_INVALID');
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('decode without save', async function () {
|
|
388
|
-
assert.strictEqual('utf8', editor.document.getEncoding());
|
|
389
|
-
assert.strictEqual('foo', editor.document.getText());
|
|
390
|
-
await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */);
|
|
391
|
-
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
392
|
-
assert.notEqual('foo', editor.document.getText().trimRight());
|
|
393
|
-
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode');
|
|
394
|
-
|
|
395
|
-
await widget.closeWithSaving({
|
|
396
|
-
shouldSave: () => undefined
|
|
397
|
-
});
|
|
398
|
-
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
|
399
|
-
|
|
400
|
-
widget = /** @type {EditorWidget & SaveableWidget} */
|
|
401
|
-
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
|
402
|
-
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
|
403
|
-
|
|
404
|
-
assert.strictEqual('utf8', editor.document.getEncoding());
|
|
405
|
-
assert.strictEqual('foo', editor.document.getText().trimRight());
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it('decode with save', async function () {
|
|
409
|
-
assert.strictEqual('utf8', editor.document.getEncoding());
|
|
410
|
-
assert.strictEqual('foo', editor.document.getText());
|
|
411
|
-
await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */);
|
|
412
|
-
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
413
|
-
assert.notEqual('foo', editor.document.getText().trimRight());
|
|
414
|
-
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode');
|
|
415
|
-
|
|
416
|
-
await Saveable.save(widget);
|
|
417
|
-
|
|
418
|
-
await widget.closeWithSaving({
|
|
419
|
-
shouldSave: () => undefined
|
|
420
|
-
});
|
|
421
|
-
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
|
422
|
-
|
|
423
|
-
widget = /** @type {EditorWidget & SaveableWidget} */
|
|
424
|
-
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
|
425
|
-
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
|
426
|
-
|
|
427
|
-
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
428
|
-
assert.notEqual('foo', editor.document.getText().trimRight());
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('encode', async function () {
|
|
432
|
-
assert.strictEqual('utf8', editor.document.getEncoding());
|
|
433
|
-
assert.strictEqual('foo', editor.document.getText());
|
|
434
|
-
await editor.setEncoding('utf16le', 0 /* EncodingMode.Encode */);
|
|
435
|
-
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
436
|
-
assert.strictEqual('foo', editor.document.getText().trimRight());
|
|
437
|
-
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after encode');
|
|
438
|
-
|
|
439
|
-
await widget.closeWithSaving({
|
|
440
|
-
shouldSave: () => undefined
|
|
441
|
-
});
|
|
442
|
-
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
|
443
|
-
|
|
444
|
-
widget = /** @type {EditorWidget & SaveableWidget} */
|
|
445
|
-
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
|
446
|
-
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
|
447
|
-
|
|
448
|
-
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
449
|
-
assert.strictEqual('foo', editor.document.getText().trimRight());
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it('delete file for saved', async () => {
|
|
453
|
-
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before delete');
|
|
454
|
-
const waitForDisposed = new Deferred();
|
|
455
|
-
const listener = editor.onDispose(() => waitForDisposed.resolve());
|
|
456
|
-
try {
|
|
457
|
-
await fileService.delete(fileUri);
|
|
458
|
-
await waitForDisposed.promise;
|
|
459
|
-
assert.isTrue(widget.isDisposed, 'model should be disposed after delete');
|
|
460
|
-
} finally {
|
|
461
|
-
listener.dispose();
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it(`'${closeOnFileDelete}' should keep the editor opened when set to 'false'`, async () => {
|
|
466
|
-
|
|
467
|
-
await preferences.set(closeOnFileDelete, false);
|
|
468
|
-
assert.isFalse(preferences.get(closeOnFileDelete));
|
|
469
|
-
assert.isFalse(Saveable.isDirty(widget));
|
|
470
|
-
|
|
471
|
-
const waitForDidChangeTitle = new Deferred();
|
|
472
|
-
const listener = () => waitForDidChangeTitle.resolve();
|
|
473
|
-
widget.title.changed.connect(listener);
|
|
474
|
-
try {
|
|
475
|
-
await fileService.delete(fileUri);
|
|
476
|
-
await waitForDidChangeTitle.promise;
|
|
477
|
-
assert.isTrue(widget.title.label.endsWith('(deleted)'));
|
|
478
|
-
assert.isFalse(widget.isDisposed);
|
|
479
|
-
} finally {
|
|
480
|
-
widget.title.changed.disconnect(listener);
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
it(`'${closeOnFileDelete}' should close the editor when set to 'true'`, async () => {
|
|
485
|
-
|
|
486
|
-
await preferences.set(closeOnFileDelete, true);
|
|
487
|
-
assert.isTrue(preferences.get(closeOnFileDelete));
|
|
488
|
-
assert.isFalse(Saveable.isDirty(widget));
|
|
489
|
-
|
|
490
|
-
const waitForDisposed = new Deferred();
|
|
491
|
-
// Must pass in 5 seconds, so check state after 4.5.
|
|
492
|
-
const listener = editor.onDispose(() => waitForDisposed.resolve());
|
|
493
|
-
const fourSeconds = new Promise(resolve => setTimeout(resolve, 4500));
|
|
494
|
-
try {
|
|
495
|
-
const deleteThenDispose = fileService.delete(fileUri).then(() => waitForDisposed.promise);
|
|
496
|
-
await Promise.race([deleteThenDispose, fourSeconds]);
|
|
497
|
-
assert.isTrue(widget.isDisposed);
|
|
498
|
-
} finally {
|
|
499
|
-
listener.dispose();
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
});
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2020 TypeFox and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
// @ts-check
|
|
18
|
+
describe('Saveable', function () {
|
|
19
|
+
this.timeout(5000);
|
|
20
|
+
|
|
21
|
+
const { assert } = chai;
|
|
22
|
+
|
|
23
|
+
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
|
24
|
+
const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget');
|
|
25
|
+
const { PreferenceService } = require('@theia/core/lib/browser/preferences/preference-service');
|
|
26
|
+
const { Saveable, SaveableWidget } = require('@theia/core/lib/browser/saveable');
|
|
27
|
+
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
|
28
|
+
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
|
|
29
|
+
const { FileResource } = require('@theia/filesystem/lib/browser/file-resource');
|
|
30
|
+
const { ETAG_DISABLED } = require('@theia/filesystem/lib/common/files');
|
|
31
|
+
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
|
|
32
|
+
const { Deferred } = require('@theia/core/lib/common/promise-util');
|
|
33
|
+
const { Disposable, DisposableCollection } = require('@theia/core/lib/common/disposable');
|
|
34
|
+
const { Range } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/range');
|
|
35
|
+
|
|
36
|
+
const container = window.theia.container;
|
|
37
|
+
/** @type {EditorManager} */
|
|
38
|
+
const editorManager = container.get(EditorManager);
|
|
39
|
+
const workspaceService = container.get(WorkspaceService);
|
|
40
|
+
const fileService = container.get(FileService);
|
|
41
|
+
/** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */
|
|
42
|
+
const preferences = container.get(PreferenceService);
|
|
43
|
+
|
|
44
|
+
/** @type {EditorWidget & SaveableWidget} */
|
|
45
|
+
let widget;
|
|
46
|
+
/** @type {MonacoEditor} */
|
|
47
|
+
let editor;
|
|
48
|
+
|
|
49
|
+
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
|
50
|
+
const fileUri = rootUri.resolve('.test/foo.txt');
|
|
51
|
+
|
|
52
|
+
const closeOnFileDelete = 'workbench.editor.closeOnFileDelete';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {FileResource['shouldOverwrite']} shouldOverwrite
|
|
56
|
+
* @returns {Disposable}
|
|
57
|
+
*/
|
|
58
|
+
function setShouldOverwrite(shouldOverwrite) {
|
|
59
|
+
const resource = editor.document['resource'];
|
|
60
|
+
assert.isTrue(resource instanceof FileResource);
|
|
61
|
+
const fileResource = /** @type {FileResource} */ (resource);
|
|
62
|
+
const originalShouldOverwrite = fileResource['shouldOverwrite'];
|
|
63
|
+
fileResource['shouldOverwrite'] = shouldOverwrite;
|
|
64
|
+
return Disposable.create(() => fileResource['shouldOverwrite'] = originalShouldOverwrite);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const toTearDown = new DisposableCollection();
|
|
68
|
+
|
|
69
|
+
/** @type {string | undefined} */
|
|
70
|
+
const autoSave = preferences.get('files.autoSave', undefined, rootUri.toString());
|
|
71
|
+
|
|
72
|
+
beforeEach(async () => {
|
|
73
|
+
await preferences.set('files.autoSave', 'off', undefined, rootUri.toString());
|
|
74
|
+
await preferences.set(closeOnFileDelete, true);
|
|
75
|
+
await editorManager.closeAll({ save: false });
|
|
76
|
+
await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true });
|
|
77
|
+
widget = /** @type {EditorWidget & SaveableWidget} */
|
|
78
|
+
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
|
79
|
+
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(async () => {
|
|
83
|
+
toTearDown.dispose();
|
|
84
|
+
await preferences.set('files.autoSave', autoSave, undefined, rootUri.toString());
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
editor = undefined;
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
widget = undefined;
|
|
89
|
+
await editorManager.closeAll({ save: false });
|
|
90
|
+
await fileService.delete(fileUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('normal save', async function () {
|
|
94
|
+
for (const edit of ['bar', 'baz']) {
|
|
95
|
+
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty before '${edit}' edit`);
|
|
96
|
+
editor.getControl().setValue(edit);
|
|
97
|
+
assert.isTrue(Saveable.isDirty(widget), `should be dirty before '${edit}' save`);
|
|
98
|
+
await Saveable.save(widget);
|
|
99
|
+
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty after '${edit}' save`);
|
|
100
|
+
assert.equal(editor.getControl().getValue().trimRight(), edit, `model should be updated with '${edit}'`);
|
|
101
|
+
const state = await fileService.read(fileUri);
|
|
102
|
+
assert.equal(state.value.trimRight(), edit, `fs should be updated with '${edit}'`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('reject save with incremental update', async function () {
|
|
107
|
+
let longContent = 'foobarbaz';
|
|
108
|
+
for (let i = 0; i < 5; i++) {
|
|
109
|
+
longContent += longContent + longContent;
|
|
110
|
+
}
|
|
111
|
+
editor.getControl().setValue(longContent);
|
|
112
|
+
await Saveable.save(widget);
|
|
113
|
+
|
|
114
|
+
// @ts-ignore
|
|
115
|
+
editor.getControl().getModel().applyEdits([{
|
|
116
|
+
range: Range.fromPositions({ lineNumber: 1, column: 1 }, { lineNumber: 1, column: 4 }),
|
|
117
|
+
forceMoveMarkers: false,
|
|
118
|
+
text: ''
|
|
119
|
+
}]);
|
|
120
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
121
|
+
|
|
122
|
+
const resource = editor.document['resource'];
|
|
123
|
+
const version = resource.version;
|
|
124
|
+
// @ts-ignore
|
|
125
|
+
await resource.saveContents('baz');
|
|
126
|
+
assert.notEqual(version, resource.version, 'latest version should be different after write');
|
|
127
|
+
|
|
128
|
+
let outOfSync = false;
|
|
129
|
+
let outOfSyncCount = 0;
|
|
130
|
+
toTearDown.push(setShouldOverwrite(async () => {
|
|
131
|
+
outOfSync = true;
|
|
132
|
+
outOfSyncCount++;
|
|
133
|
+
return false;
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
let incrementalUpdate = false;
|
|
137
|
+
const saveContentChanges = resource.saveContentChanges;
|
|
138
|
+
resource.saveContentChanges = async (changes, options) => {
|
|
139
|
+
incrementalUpdate = true;
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
return saveContentChanges.bind(resource)(changes, options);
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
await Saveable.save(widget);
|
|
145
|
+
} finally {
|
|
146
|
+
resource.saveContentChanges = saveContentChanges;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
assert.isTrue(incrementalUpdate, 'should tried to update incrementaly');
|
|
150
|
+
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
151
|
+
assert.equal(outOfSyncCount, 1, 'user should be prompted only once with out of sync dialog');
|
|
152
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
|
|
153
|
+
assert.equal(editor.getControl().getValue().trimRight(), longContent.substring(3), 'model should be updated');
|
|
154
|
+
const state = await fileService.read(fileUri);
|
|
155
|
+
assert.equal(state.value, 'baz', 'fs should NOT be updated');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('accept rejected save', async function () {
|
|
159
|
+
let outOfSync = false;
|
|
160
|
+
toTearDown.push(setShouldOverwrite(async () => {
|
|
161
|
+
outOfSync = true;
|
|
162
|
+
return false;
|
|
163
|
+
}));
|
|
164
|
+
editor.getControl().setValue('bar');
|
|
165
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
166
|
+
|
|
167
|
+
const resource = editor.document['resource'];
|
|
168
|
+
const version = resource.version;
|
|
169
|
+
// @ts-ignore
|
|
170
|
+
await resource.saveContents('bazz');
|
|
171
|
+
assert.notEqual(version, resource.version, 'latest version should be different after write');
|
|
172
|
+
|
|
173
|
+
await Saveable.save(widget);
|
|
174
|
+
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
175
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
|
|
176
|
+
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
|
177
|
+
let state = await fileService.read(fileUri);
|
|
178
|
+
assert.equal(state.value, 'bazz', 'fs should NOT be updated');
|
|
179
|
+
|
|
180
|
+
outOfSync = false;
|
|
181
|
+
toTearDown.push(setShouldOverwrite(async () => {
|
|
182
|
+
outOfSync = true;
|
|
183
|
+
return true;
|
|
184
|
+
}));
|
|
185
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
186
|
+
await Saveable.save(widget);
|
|
187
|
+
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
188
|
+
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
|
189
|
+
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
|
190
|
+
state = await fileService.read(fileUri);
|
|
191
|
+
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('accept new save', async () => {
|
|
195
|
+
let outOfSync = false;
|
|
196
|
+
toTearDown.push(setShouldOverwrite(async () => {
|
|
197
|
+
outOfSync = true;
|
|
198
|
+
return true;
|
|
199
|
+
}));
|
|
200
|
+
editor.getControl().setValue('bar');
|
|
201
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
202
|
+
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
|
203
|
+
await Saveable.save(widget);
|
|
204
|
+
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
205
|
+
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
|
206
|
+
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
|
207
|
+
const state = await fileService.read(fileUri);
|
|
208
|
+
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('cancel save on close', async () => {
|
|
212
|
+
editor.getControl().setValue('bar');
|
|
213
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before close');
|
|
214
|
+
|
|
215
|
+
await widget.closeWithSaving({
|
|
216
|
+
shouldSave: () => undefined
|
|
217
|
+
});
|
|
218
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be still dirty after canceled close');
|
|
219
|
+
assert.isFalse(widget.isDisposed, 'should NOT be disposed after canceled close');
|
|
220
|
+
const state = await fileService.read(fileUri);
|
|
221
|
+
assert.equal(state.value, 'foo', 'fs should NOT be updated after canceled close');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('reject save on close', async () => {
|
|
225
|
+
editor.getControl().setValue('bar');
|
|
226
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejected close');
|
|
227
|
+
await widget.closeWithSaving({
|
|
228
|
+
shouldSave: () => false
|
|
229
|
+
});
|
|
230
|
+
assert.isTrue(widget.isDisposed, 'should be disposed after rejected close');
|
|
231
|
+
const state = await fileService.read(fileUri);
|
|
232
|
+
assert.equal(state.value, 'foo', 'fs should NOT be updated after rejected close');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('accept save on close and reject it', async () => {
|
|
236
|
+
let outOfSync = false;
|
|
237
|
+
toTearDown.push(setShouldOverwrite(async () => {
|
|
238
|
+
outOfSync = true;
|
|
239
|
+
return false;
|
|
240
|
+
}));
|
|
241
|
+
editor.getControl().setValue('bar');
|
|
242
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejecting save on close');
|
|
243
|
+
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
|
244
|
+
await widget.closeWithSaving({
|
|
245
|
+
shouldSave: () => true
|
|
246
|
+
});
|
|
247
|
+
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
248
|
+
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
|
|
249
|
+
const state = await fileService.read(fileUri);
|
|
250
|
+
assert.equal(state.value, 'foo2', 'fs should NOT be updated');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('accept save on close and accept new save', async () => {
|
|
254
|
+
let outOfSync = false;
|
|
255
|
+
toTearDown.push(setShouldOverwrite(async () => {
|
|
256
|
+
outOfSync = true;
|
|
257
|
+
return true;
|
|
258
|
+
}));
|
|
259
|
+
editor.getControl().setValue('bar');
|
|
260
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before accepting save on close');
|
|
261
|
+
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
|
262
|
+
await widget.closeWithSaving({
|
|
263
|
+
shouldSave: () => true
|
|
264
|
+
});
|
|
265
|
+
assert.isTrue(outOfSync, 'file should be out of sync');
|
|
266
|
+
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
|
|
267
|
+
const state = await fileService.read(fileUri);
|
|
268
|
+
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('no save prompt when multiple editors open for same file', async () => {
|
|
272
|
+
const secondWidget = await editorManager.openToSide(fileUri);
|
|
273
|
+
editor.getControl().setValue('two widgets');
|
|
274
|
+
assert.isTrue(Saveable.isDirty(widget), 'the first widget should be dirty');
|
|
275
|
+
assert.isTrue(Saveable.isDirty(secondWidget), 'the second widget should also be dirty');
|
|
276
|
+
await Promise.resolve(secondWidget.close());
|
|
277
|
+
assert.isTrue(secondWidget.isDisposed, 'the widget should have closed without requesting user action');
|
|
278
|
+
assert.isTrue(Saveable.isDirty(widget), 'the original widget should still be dirty.');
|
|
279
|
+
assert.equal(editor.getControl().getValue(), 'two widgets', 'should still have the same value');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('normal close', async () => {
|
|
283
|
+
editor.getControl().setValue('bar');
|
|
284
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before before close');
|
|
285
|
+
await widget.closeWithSaving({
|
|
286
|
+
shouldSave: () => true
|
|
287
|
+
});
|
|
288
|
+
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
|
|
289
|
+
const state = await fileService.read(fileUri);
|
|
290
|
+
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('delete and add again file for dirty', async () => {
|
|
294
|
+
editor.getControl().setValue('bar');
|
|
295
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before delete');
|
|
296
|
+
assert.isTrue(editor.document.valid, 'should be valid before delete');
|
|
297
|
+
let waitForDidChangeTitle = new Deferred();
|
|
298
|
+
const listener = () => waitForDidChangeTitle.resolve();
|
|
299
|
+
widget.title.changed.connect(listener);
|
|
300
|
+
try {
|
|
301
|
+
await fileService.delete(fileUri);
|
|
302
|
+
await waitForDidChangeTitle.promise;
|
|
303
|
+
assert.isTrue(widget.title.label.endsWith('(deleted)'), 'should be marked as deleted');
|
|
304
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after delete');
|
|
305
|
+
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after delete');
|
|
306
|
+
} finally {
|
|
307
|
+
widget.title.changed.disconnect(listener);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
waitForDidChangeTitle = new Deferred();
|
|
311
|
+
widget.title.changed.connect(listener);
|
|
312
|
+
try {
|
|
313
|
+
await fileService.create(fileUri, 'foo');
|
|
314
|
+
await waitForDidChangeTitle.promise;
|
|
315
|
+
assert.isFalse(widget.title.label.endsWith('(deleted)'), 'should NOT be marked as deleted');
|
|
316
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after added again');
|
|
317
|
+
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after added again');
|
|
318
|
+
} finally {
|
|
319
|
+
widget.title.changed.disconnect(listener);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('save deleted file for dirty', async function () {
|
|
324
|
+
editor.getControl().setValue('bar');
|
|
325
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save deleted');
|
|
326
|
+
|
|
327
|
+
assert.isTrue(editor.document.valid, 'should be valid before delete');
|
|
328
|
+
const waitForInvalid = new Deferred();
|
|
329
|
+
const listener = editor.document.onDidChangeValid(() => waitForInvalid.resolve());
|
|
330
|
+
try {
|
|
331
|
+
await fileService.delete(fileUri);
|
|
332
|
+
await waitForInvalid.promise;
|
|
333
|
+
assert.isFalse(editor.document.valid, 'should be invalid after delete');
|
|
334
|
+
} finally {
|
|
335
|
+
listener.dispose();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
|
339
|
+
await Saveable.save(widget);
|
|
340
|
+
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
|
341
|
+
assert.isTrue(editor.document.valid, 'should be valid after save');
|
|
342
|
+
const state = await fileService.read(fileUri);
|
|
343
|
+
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('move file for saved', async function () {
|
|
347
|
+
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before move');
|
|
348
|
+
|
|
349
|
+
const targetUri = fileUri.parent.resolve('bar.txt');
|
|
350
|
+
await fileService.move(fileUri, targetUri, { overwrite: true });
|
|
351
|
+
assert.isTrue(widget.isDisposed, 'old model should be disposed after move');
|
|
352
|
+
|
|
353
|
+
const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri));
|
|
354
|
+
assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move');
|
|
355
|
+
assert.equal(renamed.editor.document.getText(), 'foo', 'new model should be created after move');
|
|
356
|
+
assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after move');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('move file for dirty', async function () {
|
|
360
|
+
editor.getControl().setValue('bar');
|
|
361
|
+
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before move');
|
|
362
|
+
|
|
363
|
+
const targetUri = fileUri.parent.resolve('bar.txt');
|
|
364
|
+
|
|
365
|
+
await fileService.move(fileUri, targetUri, { overwrite: true });
|
|
366
|
+
assert.isTrue(widget.isDisposed, 'old model should be disposed after move');
|
|
367
|
+
|
|
368
|
+
const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri));
|
|
369
|
+
assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move');
|
|
370
|
+
assert.equal(renamed.editor.document.getText(), 'bar', 'new model should be created after move');
|
|
371
|
+
assert.isTrue(Saveable.isDirty(renamed), 'new model should be dirty after move');
|
|
372
|
+
|
|
373
|
+
await Saveable.save(renamed);
|
|
374
|
+
assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after save');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('fail to open invalid file', async function () {
|
|
378
|
+
const invalidFile = fileUri.parent.resolve('invalid_file.txt');
|
|
379
|
+
try {
|
|
380
|
+
await editorManager.open(invalidFile, { mode: 'reveal' });
|
|
381
|
+
assert.fail('should not be possible to open an editor for invalid file');
|
|
382
|
+
} catch (e) {
|
|
383
|
+
assert.equal(e.code, 'MODEL_IS_INVALID');
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('decode without save', async function () {
|
|
388
|
+
assert.strictEqual('utf8', editor.document.getEncoding());
|
|
389
|
+
assert.strictEqual('foo', editor.document.getText());
|
|
390
|
+
await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */);
|
|
391
|
+
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
392
|
+
assert.notEqual('foo', editor.document.getText().trimRight());
|
|
393
|
+
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode');
|
|
394
|
+
|
|
395
|
+
await widget.closeWithSaving({
|
|
396
|
+
shouldSave: () => undefined
|
|
397
|
+
});
|
|
398
|
+
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
|
399
|
+
|
|
400
|
+
widget = /** @type {EditorWidget & SaveableWidget} */
|
|
401
|
+
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
|
402
|
+
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
|
403
|
+
|
|
404
|
+
assert.strictEqual('utf8', editor.document.getEncoding());
|
|
405
|
+
assert.strictEqual('foo', editor.document.getText().trimRight());
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('decode with save', async function () {
|
|
409
|
+
assert.strictEqual('utf8', editor.document.getEncoding());
|
|
410
|
+
assert.strictEqual('foo', editor.document.getText());
|
|
411
|
+
await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */);
|
|
412
|
+
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
413
|
+
assert.notEqual('foo', editor.document.getText().trimRight());
|
|
414
|
+
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode');
|
|
415
|
+
|
|
416
|
+
await Saveable.save(widget);
|
|
417
|
+
|
|
418
|
+
await widget.closeWithSaving({
|
|
419
|
+
shouldSave: () => undefined
|
|
420
|
+
});
|
|
421
|
+
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
|
422
|
+
|
|
423
|
+
widget = /** @type {EditorWidget & SaveableWidget} */
|
|
424
|
+
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
|
425
|
+
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
|
426
|
+
|
|
427
|
+
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
428
|
+
assert.notEqual('foo', editor.document.getText().trimRight());
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('encode', async function () {
|
|
432
|
+
assert.strictEqual('utf8', editor.document.getEncoding());
|
|
433
|
+
assert.strictEqual('foo', editor.document.getText());
|
|
434
|
+
await editor.setEncoding('utf16le', 0 /* EncodingMode.Encode */);
|
|
435
|
+
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
436
|
+
assert.strictEqual('foo', editor.document.getText().trimRight());
|
|
437
|
+
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after encode');
|
|
438
|
+
|
|
439
|
+
await widget.closeWithSaving({
|
|
440
|
+
shouldSave: () => undefined
|
|
441
|
+
});
|
|
442
|
+
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
|
443
|
+
|
|
444
|
+
widget = /** @type {EditorWidget & SaveableWidget} */
|
|
445
|
+
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
|
446
|
+
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
|
447
|
+
|
|
448
|
+
assert.strictEqual('utf16le', editor.document.getEncoding());
|
|
449
|
+
assert.strictEqual('foo', editor.document.getText().trimRight());
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('delete file for saved', async () => {
|
|
453
|
+
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before delete');
|
|
454
|
+
const waitForDisposed = new Deferred();
|
|
455
|
+
const listener = editor.onDispose(() => waitForDisposed.resolve());
|
|
456
|
+
try {
|
|
457
|
+
await fileService.delete(fileUri);
|
|
458
|
+
await waitForDisposed.promise;
|
|
459
|
+
assert.isTrue(widget.isDisposed, 'model should be disposed after delete');
|
|
460
|
+
} finally {
|
|
461
|
+
listener.dispose();
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it(`'${closeOnFileDelete}' should keep the editor opened when set to 'false'`, async () => {
|
|
466
|
+
|
|
467
|
+
await preferences.set(closeOnFileDelete, false);
|
|
468
|
+
assert.isFalse(preferences.get(closeOnFileDelete));
|
|
469
|
+
assert.isFalse(Saveable.isDirty(widget));
|
|
470
|
+
|
|
471
|
+
const waitForDidChangeTitle = new Deferred();
|
|
472
|
+
const listener = () => waitForDidChangeTitle.resolve();
|
|
473
|
+
widget.title.changed.connect(listener);
|
|
474
|
+
try {
|
|
475
|
+
await fileService.delete(fileUri);
|
|
476
|
+
await waitForDidChangeTitle.promise;
|
|
477
|
+
assert.isTrue(widget.title.label.endsWith('(deleted)'));
|
|
478
|
+
assert.isFalse(widget.isDisposed);
|
|
479
|
+
} finally {
|
|
480
|
+
widget.title.changed.disconnect(listener);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it(`'${closeOnFileDelete}' should close the editor when set to 'true'`, async () => {
|
|
485
|
+
|
|
486
|
+
await preferences.set(closeOnFileDelete, true);
|
|
487
|
+
assert.isTrue(preferences.get(closeOnFileDelete));
|
|
488
|
+
assert.isFalse(Saveable.isDirty(widget));
|
|
489
|
+
|
|
490
|
+
const waitForDisposed = new Deferred();
|
|
491
|
+
// Must pass in 5 seconds, so check state after 4.5.
|
|
492
|
+
const listener = editor.onDispose(() => waitForDisposed.resolve());
|
|
493
|
+
const fourSeconds = new Promise(resolve => setTimeout(resolve, 4500));
|
|
494
|
+
try {
|
|
495
|
+
const deleteThenDispose = fileService.delete(fileUri).then(() => waitForDisposed.promise);
|
|
496
|
+
await Promise.race([deleteThenDispose, fourSeconds]);
|
|
497
|
+
assert.isTrue(widget.isDisposed);
|
|
498
|
+
} finally {
|
|
499
|
+
listener.dispose();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
});
|