@theia/scm 1.77.0-next.47 → 1.77.0-next.56
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/lib/browser/decorations/scm-navigator-decorator.js +1 -1
- package/lib/browser/decorations/scm-navigator-decorator.js.map +1 -1
- package/lib/browser/dirty-diff/content-lines.spec.js +2 -1
- package/lib/browser/dirty-diff/content-lines.spec.js.map +1 -1
- package/lib/browser/dirty-diff/diff-computer.js +2 -1
- package/lib/browser/dirty-diff/diff-computer.js.map +1 -1
- package/lib/browser/dirty-diff/diff-computer.spec.js +2 -1
- package/lib/browser/dirty-diff/diff-computer.spec.js.map +1 -1
- package/lib/browser/dirty-diff/dirty-diff-navigator.js.map +1 -1
- package/lib/browser/dirty-diff/dirty-diff-widget.js +1 -1
- package/lib/browser/dirty-diff/dirty-diff-widget.js.map +1 -1
- package/lib/browser/merge-editor/merge-editor-contribution.d.ts +1 -1
- package/lib/browser/merge-editor/merge-editor-contribution.d.ts.map +1 -1
- package/lib/browser/merge-editor/merge-editor-dev-contribution.d.ts +1 -1
- package/lib/browser/merge-editor/merge-editor-dev-contribution.d.ts.map +1 -1
- package/lib/browser/merge-editor/view/merge-editor-panes/merge-editor-pane-header.js +1 -1
- package/lib/browser/merge-editor/view/merge-editor-panes/merge-editor-pane-header.js.map +1 -1
- package/lib/browser/merge-editor/view/merge-editor-view-zones.js +1 -1
- package/lib/browser/merge-editor/view/merge-editor-view-zones.js.map +1 -1
- package/lib/browser/multi-diff-editor/multi-diff-editor-module.d.ts +4 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-module.d.ts.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-module.js +34 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-module.js.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-uri.d.ts +15 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-uri.d.ts.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-uri.js +68 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-uri.js.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-widget-factory.d.ts +18 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-widget-factory.d.ts.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-widget-factory.js +76 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-widget-factory.js.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-widget.spec.d.ts +2 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-widget.spec.d.ts.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-widget.spec.js +835 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor-widget.spec.js.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor.d.ts +163 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor.d.ts.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor.js +563 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor.js.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor.spec.d.ts +2 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor.spec.d.ts.map +1 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor.spec.js +167 -0
- package/lib/browser/multi-diff-editor/multi-diff-editor.spec.js.map +1 -0
- package/lib/browser/scm-action-button-widget.js +1 -1
- package/lib/browser/scm-action-button-widget.js.map +1 -1
- package/lib/browser/scm-amend-component.js +2 -1
- package/lib/browser/scm-amend-component.js.map +1 -1
- package/lib/browser/scm-amend-widget.js +1 -1
- package/lib/browser/scm-amend-widget.js.map +1 -1
- package/lib/browser/scm-commit-widget.js +2 -2
- package/lib/browser/scm-commit-widget.js.map +1 -1
- package/lib/browser/scm-contribution.js +1 -1
- package/lib/browser/scm-contribution.js.map +1 -1
- package/lib/browser/scm-frontend-module.d.ts.map +1 -1
- package/lib/browser/scm-frontend-module.js +2 -0
- package/lib/browser/scm-frontend-module.js.map +1 -1
- package/lib/browser/scm-history-graph-widget.js +2 -2
- package/lib/browser/scm-history-graph-widget.js.map +1 -1
- package/lib/browser/scm-history-graph-widget.spec.js +2 -1
- package/lib/browser/scm-history-graph-widget.spec.js.map +1 -1
- package/lib/browser/scm-input.js +3 -2
- package/lib/browser/scm-input.js.map +1 -1
- package/lib/browser/scm-provider.d.ts +2 -0
- package/lib/browser/scm-provider.d.ts.map +1 -1
- package/lib/browser/scm-quick-open-service.js +1 -1
- package/lib/browser/scm-quick-open-service.js.map +1 -1
- package/lib/browser/scm-repositories-widget.js +2 -2
- package/lib/browser/scm-repositories-widget.js.map +1 -1
- package/lib/browser/scm-repositories-widget.spec.js +2 -1
- package/lib/browser/scm-repositories-widget.spec.js.map +1 -1
- package/lib/browser/scm-service.js +1 -1
- package/lib/browser/scm-service.js.map +1 -1
- package/lib/browser/scm-tree-label-provider.js +1 -1
- package/lib/browser/scm-tree-label-provider.js.map +1 -1
- package/lib/browser/scm-tree-model.js +1 -1
- package/lib/browser/scm-tree-model.js.map +1 -1
- package/lib/browser/scm-tree-widget.js +2 -2
- package/lib/browser/scm-tree-widget.js.map +1 -1
- package/package.json +6 -6
- package/src/browser/dirty-diff/dirty-diff-navigator.ts +1 -1
- package/src/browser/multi-diff-editor/multi-diff-editor-module.ts +35 -0
- package/src/browser/multi-diff-editor/multi-diff-editor-uri.ts +83 -0
- package/src/browser/multi-diff-editor/multi-diff-editor-widget-factory.ts +81 -0
- package/src/browser/multi-diff-editor/multi-diff-editor-widget.spec.ts +878 -0
- package/src/browser/multi-diff-editor/multi-diff-editor.spec.ts +189 -0
- package/src/browser/multi-diff-editor/multi-diff-editor.ts +628 -0
- package/src/browser/scm-frontend-module.ts +3 -0
- package/src/browser/scm-input.ts +1 -1
- package/src/browser/scm-provider.ts +2 -0
- package/src/browser/style/multi-diff-editor.css +113 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource 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-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
20
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
|
|
22
|
+
import { expect } from 'chai';
|
|
23
|
+
import * as sinon from 'sinon';
|
|
24
|
+
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
|
25
|
+
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
|
26
|
+
import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor';
|
|
27
|
+
import { MultiDiffEditorWidgetFactory } from './multi-diff-editor-widget-factory';
|
|
28
|
+
import { Emitter, URI } from '@theia/core';
|
|
29
|
+
import { LabelProvider, MessageLoop, Widget } from '@theia/core/lib/browser';
|
|
30
|
+
import { Container } from '@theia/core/shared/inversify';
|
|
31
|
+
import {
|
|
32
|
+
DiffEntryErrorWidget,
|
|
33
|
+
DiffEntryHeaderWidget,
|
|
34
|
+
DiffEntryLoadingWidget,
|
|
35
|
+
DiffEntryWidget,
|
|
36
|
+
HEADER_HEIGHT,
|
|
37
|
+
MIN_ENTRY_HEIGHT,
|
|
38
|
+
MultiDiffEditor,
|
|
39
|
+
MultiDiffEditorData,
|
|
40
|
+
MultiDiffEditorLabelProvider,
|
|
41
|
+
MultiDiffEditorOpenHandler
|
|
42
|
+
} from './multi-diff-editor';
|
|
43
|
+
import { MultiDiffEditorResourcePair, MultiDiffEditorUri } from './multi-diff-editor-uri';
|
|
44
|
+
|
|
45
|
+
disableJSDOM();
|
|
46
|
+
|
|
47
|
+
describe('Multi-Diff Editor — widgets', () => {
|
|
48
|
+
|
|
49
|
+
before(() => {
|
|
50
|
+
disableJSDOM = enableJSDOM();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
after(() => {
|
|
54
|
+
disableJSDOM();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Minimal LabelProvider mock — only the methods DiffEntryHeaderWidget uses.
|
|
58
|
+
const mockLabelProvider = {
|
|
59
|
+
getIcon: (_: URI) => 'codicon codicon-file',
|
|
60
|
+
getName: (uri: URI) => uri.path.base,
|
|
61
|
+
getLongName: (uri: URI) => uri.path.toString()
|
|
62
|
+
} as unknown as LabelProvider;
|
|
63
|
+
|
|
64
|
+
const sampleResource: MultiDiffEditorResourcePair = {
|
|
65
|
+
originalUri: new URI('file:///workspace/a.ts'),
|
|
66
|
+
modifiedUri: new URI('file:///workspace/b.ts')
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
describe('DiffEntryHeaderWidget', () => {
|
|
70
|
+
|
|
71
|
+
it('should render icon, name and description', () => {
|
|
72
|
+
const widget = new DiffEntryHeaderWidget(sampleResource, mockLabelProvider);
|
|
73
|
+
try {
|
|
74
|
+
expect(widget.node.querySelector('.multi-diff-entry-icon')).to.exist;
|
|
75
|
+
expect(widget.node.querySelector('.multi-diff-entry-label')?.textContent).to.equal('b.ts');
|
|
76
|
+
expect(widget.node.querySelector('.multi-diff-entry-description')?.textContent).to.equal('/workspace/b.ts');
|
|
77
|
+
} finally {
|
|
78
|
+
widget.dispose();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should expose aria-expanded=true by default', () => {
|
|
83
|
+
const widget = new DiffEntryHeaderWidget(sampleResource, mockLabelProvider);
|
|
84
|
+
try {
|
|
85
|
+
expect(widget.node.getAttribute('aria-expanded')).to.equal('true');
|
|
86
|
+
expect(widget.node.querySelector('.codicon-chevron-down')).to.exist;
|
|
87
|
+
} finally {
|
|
88
|
+
widget.dispose();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should toggle chevron and aria-expanded on setCollapsed', () => {
|
|
93
|
+
const widget = new DiffEntryHeaderWidget(sampleResource, mockLabelProvider);
|
|
94
|
+
try {
|
|
95
|
+
widget.setCollapsed(true);
|
|
96
|
+
expect(widget.node.getAttribute('aria-expanded')).to.equal('false');
|
|
97
|
+
expect(widget.node.querySelector('.codicon-chevron-right')).to.exist;
|
|
98
|
+
expect(widget.node.querySelector('.codicon-chevron-down')).to.be.null;
|
|
99
|
+
|
|
100
|
+
widget.setCollapsed(false);
|
|
101
|
+
expect(widget.node.getAttribute('aria-expanded')).to.equal('true');
|
|
102
|
+
expect(widget.node.querySelector('.codicon-chevron-down')).to.exist;
|
|
103
|
+
} finally {
|
|
104
|
+
widget.dispose();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should fire onDidToggleCollapse when clicked', () => {
|
|
109
|
+
const widget = new DiffEntryHeaderWidget(sampleResource, mockLabelProvider);
|
|
110
|
+
try {
|
|
111
|
+
let fired = 0;
|
|
112
|
+
widget.onDidToggleCollapse(() => fired++);
|
|
113
|
+
widget.node.click();
|
|
114
|
+
expect(fired).to.equal(1);
|
|
115
|
+
} finally {
|
|
116
|
+
widget.dispose();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('DiffEntryWidget', () => {
|
|
122
|
+
|
|
123
|
+
function createEntry(): { entry: DiffEntryWidget; header: DiffEntryHeaderWidget } {
|
|
124
|
+
const header = new DiffEntryHeaderWidget(sampleResource, mockLabelProvider);
|
|
125
|
+
const entry = new DiffEntryWidget(sampleResource, header);
|
|
126
|
+
return { entry, header };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it('should start in loading state with MIN_ENTRY_HEIGHT', () => {
|
|
130
|
+
const { entry } = createEntry();
|
|
131
|
+
try {
|
|
132
|
+
expect(entry.editorWidget).to.be.undefined;
|
|
133
|
+
expect(entry.isCollapsed).to.be.false;
|
|
134
|
+
expect(entry.node.style.height).to.equal(`${MIN_ENTRY_HEIGHT}px`);
|
|
135
|
+
expect(entry.node.querySelector('.multi-diff-entry-loading')).to.exist;
|
|
136
|
+
} finally {
|
|
137
|
+
entry.dispose();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should transition to error state on setError', () => {
|
|
142
|
+
const { entry } = createEntry();
|
|
143
|
+
try {
|
|
144
|
+
entry.setError('boom');
|
|
145
|
+
expect(entry.node.querySelector('.multi-diff-entry-loading')).to.be.null;
|
|
146
|
+
expect(entry.node.querySelector('.multi-diff-entry-error')?.textContent).to.contain('boom');
|
|
147
|
+
expect(entry.editorWidget).to.be.undefined;
|
|
148
|
+
} finally {
|
|
149
|
+
entry.dispose();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should fit short content and cap large entries before the viewport size is known', () => {
|
|
154
|
+
const { entry } = createEntry();
|
|
155
|
+
try {
|
|
156
|
+
entry.setHeight(50);
|
|
157
|
+
expect(entry.node.style.height).to.equal('50px');
|
|
158
|
+
|
|
159
|
+
entry.setHeight(10_000);
|
|
160
|
+
expect(entry.node.style.height).to.equal('5000px');
|
|
161
|
+
|
|
162
|
+
entry.setHeight(MIN_ENTRY_HEIGHT + 42);
|
|
163
|
+
expect(entry.node.style.height).to.equal(`${MIN_ENTRY_HEIGHT + 42}px`);
|
|
164
|
+
} finally {
|
|
165
|
+
entry.dispose();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should shrink to HEADER_HEIGHT when collapsed and restore height when expanded', () => {
|
|
170
|
+
const { entry } = createEntry();
|
|
171
|
+
try {
|
|
172
|
+
entry.setHeight(MIN_ENTRY_HEIGHT + 100);
|
|
173
|
+
expect(entry.node.style.height).to.equal(`${MIN_ENTRY_HEIGHT + 100}px`);
|
|
174
|
+
|
|
175
|
+
entry.setCollapsed(true);
|
|
176
|
+
expect(entry.isCollapsed).to.be.true;
|
|
177
|
+
expect(entry.node.style.height).to.equal(`${HEADER_HEIGHT}px`);
|
|
178
|
+
|
|
179
|
+
entry.setCollapsed(false);
|
|
180
|
+
expect(entry.isCollapsed).to.be.false;
|
|
181
|
+
expect(entry.node.style.height).to.equal(`${MIN_ENTRY_HEIGHT + 100}px`);
|
|
182
|
+
} finally {
|
|
183
|
+
entry.dispose();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should remember requested height while collapsed and apply it on expand', () => {
|
|
188
|
+
const { entry } = createEntry();
|
|
189
|
+
try {
|
|
190
|
+
entry.setCollapsed(true);
|
|
191
|
+
// setHeight while collapsed updates the remembered expanded height only.
|
|
192
|
+
entry.setHeight(MIN_ENTRY_HEIGHT + 50);
|
|
193
|
+
expect(entry.node.style.height).to.equal(`${HEADER_HEIGHT}px`);
|
|
194
|
+
|
|
195
|
+
entry.setCollapsed(false);
|
|
196
|
+
expect(entry.node.style.height).to.equal(`${MIN_ENTRY_HEIGHT + 50}px`);
|
|
197
|
+
} finally {
|
|
198
|
+
entry.dispose();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should hide content when collapsed, including content loaded afterwards', () => {
|
|
203
|
+
const { entry } = createEntry();
|
|
204
|
+
try {
|
|
205
|
+
entry.setCollapsed(true);
|
|
206
|
+
expect(entry['currentContent'].isHidden).to.be.true;
|
|
207
|
+
entry.setError('boom');
|
|
208
|
+
expect(entry['currentContent'].isHidden).to.be.true;
|
|
209
|
+
entry.setCollapsed(false);
|
|
210
|
+
expect(entry['currentContent'].isHidden).to.be.false;
|
|
211
|
+
} finally {
|
|
212
|
+
entry.dispose();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should retain the available height across content updates and collapse', () => {
|
|
217
|
+
const { entry } = createEntry();
|
|
218
|
+
try {
|
|
219
|
+
entry.setAvailableHeight(800);
|
|
220
|
+
entry.setHeight(200);
|
|
221
|
+
expect(entry.node.style.height).to.equal('800px');
|
|
222
|
+
entry.setCollapsed(true);
|
|
223
|
+
entry.setAvailableHeight(600);
|
|
224
|
+
expect(entry.node.style.height).to.equal(`${HEADER_HEIGHT}px`);
|
|
225
|
+
entry.setCollapsed(false);
|
|
226
|
+
expect(entry.node.style.height).to.equal('600px');
|
|
227
|
+
} finally {
|
|
228
|
+
entry.dispose();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should be a no-op when setCollapsed is called with the current value', () => {
|
|
233
|
+
const { entry } = createEntry();
|
|
234
|
+
try {
|
|
235
|
+
let fired = 0;
|
|
236
|
+
entry.onDidChangeCollapsed(() => fired++);
|
|
237
|
+
entry.setCollapsed(false);
|
|
238
|
+
expect(fired).to.equal(0);
|
|
239
|
+
entry.setCollapsed(true);
|
|
240
|
+
expect(fired).to.equal(1);
|
|
241
|
+
entry.setCollapsed(true);
|
|
242
|
+
expect(fired).to.equal(1);
|
|
243
|
+
} finally {
|
|
244
|
+
entry.dispose();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should toggle collapsed when the header fires onDidToggleCollapse', () => {
|
|
249
|
+
const { entry, header } = createEntry();
|
|
250
|
+
try {
|
|
251
|
+
expect(entry.isCollapsed).to.be.false;
|
|
252
|
+
header.node.click();
|
|
253
|
+
expect(entry.isCollapsed).to.be.true;
|
|
254
|
+
header.node.click();
|
|
255
|
+
expect(entry.isCollapsed).to.be.false;
|
|
256
|
+
} finally {
|
|
257
|
+
entry.dispose();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('entry loading', () => {
|
|
263
|
+
|
|
264
|
+
function createEntry(load: () => Promise<EditorWidget>): DiffEntryWidget {
|
|
265
|
+
return new DiffEntryWidget(sampleResource, new DiffEntryHeaderWidget(sampleResource, mockLabelProvider), load);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
it('should skip collapsed entries, load on expansion, and suppress concurrent or repeated loads', async () => {
|
|
269
|
+
let resolve!: (widget: EditorWidget) => void;
|
|
270
|
+
const load = sinon.stub().returns(new Promise<EditorWidget>(r => resolve = r));
|
|
271
|
+
const entry = createEntry(load);
|
|
272
|
+
const widget = new Widget() as EditorWidget;
|
|
273
|
+
try {
|
|
274
|
+
entry.setCollapsed(true);
|
|
275
|
+
await entry.loadEditor();
|
|
276
|
+
expect(load.callCount).to.equal(0);
|
|
277
|
+
entry.setCollapsed(false);
|
|
278
|
+
await entry.loadEditor();
|
|
279
|
+
expect(load.callCount).to.equal(1);
|
|
280
|
+
entry.setCollapsed(true);
|
|
281
|
+
resolve(widget);
|
|
282
|
+
await Promise.resolve();
|
|
283
|
+
expect(entry.editorWidget).to.equal(widget);
|
|
284
|
+
expect(widget.isHidden).to.be.true;
|
|
285
|
+
entry.setCollapsed(false);
|
|
286
|
+
await entry.loadEditor();
|
|
287
|
+
expect(load.callCount).to.equal(1);
|
|
288
|
+
} finally {
|
|
289
|
+
entry.dispose();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should offer a native Retry button, restore loading state, and serialize retries', async () => {
|
|
294
|
+
let resolve!: (widget: EditorWidget) => void;
|
|
295
|
+
const load = sinon.stub();
|
|
296
|
+
load.onFirstCall().rejects(new Error('provider unavailable'));
|
|
297
|
+
load.onSecondCall().returns(new Promise<EditorWidget>(r => resolve = r));
|
|
298
|
+
const entry = createEntry(load);
|
|
299
|
+
const widget = new Widget() as EditorWidget;
|
|
300
|
+
try {
|
|
301
|
+
await entry.loadEditor();
|
|
302
|
+
const button = entry.node.querySelector('button')!;
|
|
303
|
+
expect(button.textContent).to.equal('Retry');
|
|
304
|
+
expect(button.type).to.equal('button');
|
|
305
|
+
await entry.loadEditor();
|
|
306
|
+
expect(load.callCount).to.equal(1);
|
|
307
|
+
button.click();
|
|
308
|
+
expect(entry.node.querySelector('.multi-diff-entry-loading')).to.exist;
|
|
309
|
+
button.click();
|
|
310
|
+
await entry.loadEditor(true);
|
|
311
|
+
expect(load.callCount).to.equal(2);
|
|
312
|
+
resolve(widget);
|
|
313
|
+
await Promise.resolve();
|
|
314
|
+
expect(entry.editorWidget).to.equal(widget);
|
|
315
|
+
} finally {
|
|
316
|
+
entry.dispose();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should dispose editors completing after disposal without emitting a load event', async () => {
|
|
321
|
+
let resolve!: (widget: EditorWidget) => void;
|
|
322
|
+
const entry = createEntry(() => new Promise(r => resolve = r));
|
|
323
|
+
const loaded = sinon.spy();
|
|
324
|
+
entry.onDidLoadEditor(loaded);
|
|
325
|
+
const pending = entry.loadEditor();
|
|
326
|
+
entry.dispose();
|
|
327
|
+
const widget = new Widget() as EditorWidget;
|
|
328
|
+
resolve(widget);
|
|
329
|
+
await pending;
|
|
330
|
+
expect(widget.isDisposed).to.be.true;
|
|
331
|
+
expect(loaded.called).to.be.false;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should ignore rejected loads after disposal', async () => {
|
|
335
|
+
let reject!: (error: Error) => void;
|
|
336
|
+
const entry = createEntry(() => new Promise((_resolve, r) => reject = r));
|
|
337
|
+
const setError = sinon.spy(entry, 'setError');
|
|
338
|
+
const pending = entry.loadEditor();
|
|
339
|
+
entry.dispose();
|
|
340
|
+
reject(new Error('late failure'));
|
|
341
|
+
await pending;
|
|
342
|
+
expect(setError.called).to.be.false;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should track both diff content heights as unchanged regions are hidden or expanded and dispose listeners', () => {
|
|
346
|
+
const originalChanged = new Emitter<void>();
|
|
347
|
+
const modifiedChanged = new Emitter<void>();
|
|
348
|
+
let originalHeight = 300;
|
|
349
|
+
let modifiedHeight = 400;
|
|
350
|
+
const monacoEditor = Object.create(MonacoDiffEditor.prototype);
|
|
351
|
+
Object.defineProperty(monacoEditor, 'diffEditor', {
|
|
352
|
+
value: {
|
|
353
|
+
updateOptions: sinon.spy(),
|
|
354
|
+
getOriginalEditor: () => ({ getContentHeight: () => originalHeight, onDidContentSizeChange: originalChanged.event }),
|
|
355
|
+
getModifiedEditor: () => ({ getContentHeight: () => modifiedHeight, onDidContentSizeChange: modifiedChanged.event })
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
const widget = new Widget() as EditorWidget;
|
|
359
|
+
Object.defineProperty(widget, 'editor', { value: monacoEditor });
|
|
360
|
+
const entry = createEntry(async () => widget);
|
|
361
|
+
try {
|
|
362
|
+
entry.setEditor(widget);
|
|
363
|
+
expect(entry.node.style.height).to.equal(`${400 + HEADER_HEIGHT}px`);
|
|
364
|
+
originalHeight = 140;
|
|
365
|
+
modifiedHeight = 150;
|
|
366
|
+
modifiedChanged.fire();
|
|
367
|
+
expect(entry.node.style.height).to.equal(`${150 + HEADER_HEIGHT}px`);
|
|
368
|
+
entry.setCollapsed(true);
|
|
369
|
+
originalHeight = 350;
|
|
370
|
+
originalChanged.fire();
|
|
371
|
+
expect(entry.node.style.height).to.equal(`${HEADER_HEIGHT}px`);
|
|
372
|
+
entry.setCollapsed(false);
|
|
373
|
+
expect(entry.node.style.height).to.equal(`${350 + HEADER_HEIGHT}px`);
|
|
374
|
+
entry.dispose();
|
|
375
|
+
const setHeight = sinon.spy(entry, 'setHeight');
|
|
376
|
+
originalChanged.fire();
|
|
377
|
+
modifiedChanged.fire();
|
|
378
|
+
expect(setHeight.called).to.be.false;
|
|
379
|
+
} finally {
|
|
380
|
+
entry.dispose();
|
|
381
|
+
originalChanged.dispose();
|
|
382
|
+
modifiedChanged.dispose();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('content-fit sizing and inner scrolling', () => {
|
|
388
|
+
for (const isDiff of [false, true]) {
|
|
389
|
+
it(`should release vertical wheel events at ${isDiff ? 'diff' : 'code'} boundaries without preventing outer scrolling`, () => {
|
|
390
|
+
const changed = new Emitter<void>();
|
|
391
|
+
let scrollTop = 0;
|
|
392
|
+
let contentHeight = 6000;
|
|
393
|
+
const control = {
|
|
394
|
+
getContentHeight: () => contentHeight,
|
|
395
|
+
onDidContentSizeChange: changed.event,
|
|
396
|
+
updateOptions: sinon.spy(),
|
|
397
|
+
getScrollTop: () => scrollTop,
|
|
398
|
+
getScrollHeight: () => 6000,
|
|
399
|
+
getLayoutInfo: () => ({ height: 1000 })
|
|
400
|
+
};
|
|
401
|
+
const monacoEditor = Object.create(isDiff ? MonacoDiffEditor.prototype : MonacoEditor.prototype);
|
|
402
|
+
if (isDiff) {
|
|
403
|
+
Object.defineProperty(monacoEditor, 'diffEditor', {
|
|
404
|
+
value: { getOriginalEditor: () => control, getModifiedEditor: () => control, updateOptions: control.updateOptions }
|
|
405
|
+
});
|
|
406
|
+
} else {
|
|
407
|
+
monacoEditor.getControl = () => control;
|
|
408
|
+
}
|
|
409
|
+
const widget = new Widget() as EditorWidget;
|
|
410
|
+
Object.defineProperty(widget, 'editor', { value: monacoEditor });
|
|
411
|
+
const entry = new DiffEntryWidget(sampleResource, new DiffEntryHeaderWidget(sampleResource, mockLabelProvider));
|
|
412
|
+
const targets = Array.from({ length: isDiff ? 2 : 1 }, () => {
|
|
413
|
+
const node = document.createElement('div');
|
|
414
|
+
widget.node.appendChild(node);
|
|
415
|
+
node.addEventListener('wheel', event => event.preventDefault());
|
|
416
|
+
return node;
|
|
417
|
+
});
|
|
418
|
+
const wheel = (target: HTMLElement, deltaY: number, options: WheelEventInit = {}) => {
|
|
419
|
+
const event = new window.WheelEvent('wheel', { deltaY, bubbles: true, cancelable: true, ...options });
|
|
420
|
+
target.dispatchEvent(event);
|
|
421
|
+
return event.defaultPrevented;
|
|
422
|
+
};
|
|
423
|
+
try {
|
|
424
|
+
entry.setEditor(widget);
|
|
425
|
+
for (const target of targets) {
|
|
426
|
+
scrollTop = 0;
|
|
427
|
+
expect(wheel(target, -100)).to.be.false;
|
|
428
|
+
expect(wheel(target, 100)).to.be.true;
|
|
429
|
+
scrollTop = 2500;
|
|
430
|
+
expect(wheel(target, -100)).to.be.true;
|
|
431
|
+
expect(wheel(target, 100)).to.be.true;
|
|
432
|
+
scrollTop = 5000;
|
|
433
|
+
expect(wheel(target, 100)).to.be.false;
|
|
434
|
+
expect(wheel(target, -100)).to.be.true;
|
|
435
|
+
expect(wheel(target, 100, { deltaX: 200 })).to.be.true;
|
|
436
|
+
expect(wheel(target, 100, { ctrlKey: true })).to.be.true;
|
|
437
|
+
expect(wheel(target, 100, { shiftKey: true })).to.be.true;
|
|
438
|
+
}
|
|
439
|
+
contentHeight = 20;
|
|
440
|
+
changed.fire();
|
|
441
|
+
expect(wheel(targets[0], 100)).to.be.true;
|
|
442
|
+
entry.dispose();
|
|
443
|
+
expect(wheel(targets[0], 100)).to.be.true;
|
|
444
|
+
} finally {
|
|
445
|
+
entry.dispose();
|
|
446
|
+
changed.dispose();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it(`should size ${isDiff ? 'diff' : 'code'} content including borders and toggle scrolling on content changes and resizes`, () => {
|
|
451
|
+
const changed = new Emitter<void>();
|
|
452
|
+
const originalChanged = new Emitter<void>();
|
|
453
|
+
let contentHeight = 20;
|
|
454
|
+
let originalHeight = 10;
|
|
455
|
+
const updateOptions = sinon.spy((_options: unknown) => changed.fire());
|
|
456
|
+
const control = {
|
|
457
|
+
getContentHeight: () => contentHeight,
|
|
458
|
+
onDidContentSizeChange: changed.event,
|
|
459
|
+
updateOptions
|
|
460
|
+
};
|
|
461
|
+
const monacoEditor = Object.create(isDiff ? MonacoDiffEditor.prototype : MonacoEditor.prototype);
|
|
462
|
+
if (isDiff) {
|
|
463
|
+
Object.defineProperty(monacoEditor, 'diffEditor', {
|
|
464
|
+
value: {
|
|
465
|
+
getOriginalEditor: () => ({ getContentHeight: () => originalHeight, onDidContentSizeChange: originalChanged.event }),
|
|
466
|
+
getModifiedEditor: () => control,
|
|
467
|
+
updateOptions
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
monacoEditor.getControl = () => control;
|
|
472
|
+
}
|
|
473
|
+
const widget = new Widget() as EditorWidget;
|
|
474
|
+
Object.defineProperty(widget, 'editor', { value: monacoEditor });
|
|
475
|
+
const entry = new DiffEntryWidget(sampleResource, new DiffEntryHeaderWidget(sampleResource, mockLabelProvider));
|
|
476
|
+
entry.node.style.border = '1px solid black';
|
|
477
|
+
Widget.attach(entry, document.body);
|
|
478
|
+
const chrome = HEADER_HEIGHT + 2;
|
|
479
|
+
const expectScrolling = (enabled: boolean) => expect(updateOptions.lastCall.args[0]).to.deep.equal({
|
|
480
|
+
scrollbar: { vertical: enabled ? 'auto' : 'hidden', handleMouseWheel: enabled, alwaysConsumeMouseWheel: false }
|
|
481
|
+
});
|
|
482
|
+
try {
|
|
483
|
+
entry.setAvailableHeight(250, false);
|
|
484
|
+
entry.setEditor(widget);
|
|
485
|
+
expect(entry.node.style.height).to.equal(`${20 + chrome}px`);
|
|
486
|
+
expectScrolling(false);
|
|
487
|
+
expect(updateOptions.callCount).to.equal(1);
|
|
488
|
+
changed.fire();
|
|
489
|
+
expect(updateOptions.callCount).to.equal(1);
|
|
490
|
+
|
|
491
|
+
contentHeight = 6000;
|
|
492
|
+
changed.fire();
|
|
493
|
+
expect(entry.node.style.height).to.equal('5000px');
|
|
494
|
+
expectScrolling(true);
|
|
495
|
+
expect(updateOptions.callCount).to.equal(2);
|
|
496
|
+
|
|
497
|
+
entry.setAvailableHeight(600, false);
|
|
498
|
+
expect(entry.node.style.height).to.equal('5000px');
|
|
499
|
+
expectScrolling(true);
|
|
500
|
+
expect(updateOptions.callCount).to.equal(2);
|
|
501
|
+
contentHeight = 400;
|
|
502
|
+
changed.fire();
|
|
503
|
+
expect(entry.node.style.height).to.equal(`${400 + chrome}px`);
|
|
504
|
+
expectScrolling(false);
|
|
505
|
+
entry.setAvailableHeight(200, false);
|
|
506
|
+
expect(entry.node.style.height).to.equal(`${400 + chrome}px`);
|
|
507
|
+
expectScrolling(false);
|
|
508
|
+
contentHeight = 5000 - chrome;
|
|
509
|
+
changed.fire();
|
|
510
|
+
expect(entry.node.style.height).to.equal('5000px');
|
|
511
|
+
expectScrolling(false);
|
|
512
|
+
contentHeight++;
|
|
513
|
+
changed.fire();
|
|
514
|
+
expect(entry.node.style.height).to.equal('5000px');
|
|
515
|
+
expectScrolling(true);
|
|
516
|
+
|
|
517
|
+
entry.setCollapsed(true);
|
|
518
|
+
expect(entry.node.style.height).to.equal(`${chrome}px`);
|
|
519
|
+
entry.setAvailableHeight(100, false);
|
|
520
|
+
contentHeight = 20;
|
|
521
|
+
changed.fire();
|
|
522
|
+
expect(entry.node.style.height).to.equal(`${chrome}px`);
|
|
523
|
+
entry.setCollapsed(false);
|
|
524
|
+
expect(entry.node.style.height).to.equal(`${20 + chrome}px`);
|
|
525
|
+
expectScrolling(false);
|
|
526
|
+
|
|
527
|
+
if (isDiff) {
|
|
528
|
+
originalHeight = 500;
|
|
529
|
+
originalChanged.fire();
|
|
530
|
+
expect(entry.node.style.height).to.equal(`${500 + chrome}px`);
|
|
531
|
+
expectScrolling(false);
|
|
532
|
+
originalHeight = 10;
|
|
533
|
+
originalChanged.fire();
|
|
534
|
+
expect(entry.node.style.height).to.equal(`${20 + chrome}px`);
|
|
535
|
+
expectScrolling(false);
|
|
536
|
+
}
|
|
537
|
+
entry.setAvailableHeight(500);
|
|
538
|
+
expect(entry.node.style.height).to.equal('500px');
|
|
539
|
+
expectScrolling(false);
|
|
540
|
+
contentHeight = 600;
|
|
541
|
+
changed.fire();
|
|
542
|
+
expect(entry.node.style.height).to.equal('500px');
|
|
543
|
+
expectScrolling(true);
|
|
544
|
+
entry.setAvailableHeight(700);
|
|
545
|
+
expect(entry.node.style.height).to.equal('700px');
|
|
546
|
+
expectScrolling(false);
|
|
547
|
+
} finally {
|
|
548
|
+
entry.dispose();
|
|
549
|
+
changed.dispose();
|
|
550
|
+
originalChanged.dispose();
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe('DiffEntryLoadingWidget / DiffEntryErrorWidget', () => {
|
|
557
|
+
|
|
558
|
+
it('should render a spinning codicon loading indicator', () => {
|
|
559
|
+
const w = new DiffEntryLoadingWidget();
|
|
560
|
+
try {
|
|
561
|
+
const spinner = w.node.querySelector('.codicon-loading.codicon-modifier-spin');
|
|
562
|
+
expect(spinner).to.exist;
|
|
563
|
+
} finally {
|
|
564
|
+
w.dispose();
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should render the error message', () => {
|
|
569
|
+
const w = new DiffEntryErrorWidget('oops');
|
|
570
|
+
try {
|
|
571
|
+
expect(w.node.textContent).to.contain('oops');
|
|
572
|
+
expect(w.node.querySelector('.codicon-error')).to.exist;
|
|
573
|
+
} finally {
|
|
574
|
+
w.dispose();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
describe('MultiDiffEditor', () => {
|
|
580
|
+
|
|
581
|
+
function createEditor(count: number, load?: () => Promise<EditorWidget>): MultiDiffEditor {
|
|
582
|
+
const container = new Container();
|
|
583
|
+
container.bind(MultiDiffEditorData).toConstantValue({ title: 'Changes', resources: Array(count).fill(sampleResource) });
|
|
584
|
+
container.bind(MultiDiffEditor).toSelf();
|
|
585
|
+
const editor = container.get(MultiDiffEditor);
|
|
586
|
+
for (let index = 0; index < count; index++) {
|
|
587
|
+
editor.addDiffEntry(sampleResource, new DiffEntryHeaderWidget(sampleResource, mockLabelProvider), load);
|
|
588
|
+
}
|
|
589
|
+
return editor;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
it('should opt out of persistence and ignore restore state', () => {
|
|
593
|
+
const editor = createEditor(1);
|
|
594
|
+
try {
|
|
595
|
+
editor.restoreState({ collapsedUris: [sampleResource.modifiedUri.toString()], scrollTop: 123 });
|
|
596
|
+
expect(editor.entries[0].isCollapsed).to.be.false;
|
|
597
|
+
expect(editor['entriesPanel'].node.scrollTop).to.equal(0);
|
|
598
|
+
expect(editor.storeState()).to.be.undefined;
|
|
599
|
+
} finally {
|
|
600
|
+
editor.dispose();
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should observe near-viewport entries, skip collapsed entries, and disconnect safely', async () => {
|
|
605
|
+
const previous = globalThis.IntersectionObserver;
|
|
606
|
+
let callback!: IntersectionObserverCallback;
|
|
607
|
+
const observe = sinon.spy();
|
|
608
|
+
const disconnect = sinon.spy();
|
|
609
|
+
let options: IntersectionObserverInit | undefined;
|
|
610
|
+
globalThis.IntersectionObserver = class {
|
|
611
|
+
constructor(cb: IntersectionObserverCallback, opts?: IntersectionObserverInit) {
|
|
612
|
+
callback = cb;
|
|
613
|
+
options = opts;
|
|
614
|
+
}
|
|
615
|
+
observe = observe;
|
|
616
|
+
disconnect = disconnect;
|
|
617
|
+
} as unknown as typeof IntersectionObserver;
|
|
618
|
+
const load = sinon.stub().callsFake(async () => new Widget() as EditorWidget);
|
|
619
|
+
const editor = createEditor(2, load);
|
|
620
|
+
const firstLoad = sinon.spy(editor.entries[0], 'loadEditor');
|
|
621
|
+
const secondLoad = sinon.spy(editor.entries[1], 'loadEditor');
|
|
622
|
+
try {
|
|
623
|
+
expect(observe.callCount).to.equal(2);
|
|
624
|
+
expect(options?.root).to.equal(editor['entriesPanel'].node);
|
|
625
|
+
expect(options?.rootMargin).to.equal('500px 0px');
|
|
626
|
+
editor.entries[1].setCollapsed(true);
|
|
627
|
+
const records = editor.entries.map(entry => ({ target: entry.node, isIntersecting: true } as unknown as IntersectionObserverEntry));
|
|
628
|
+
callback([{ ...records[0], isIntersecting: false }], editor['entryObserver']!);
|
|
629
|
+
expect(firstLoad.called).to.be.false;
|
|
630
|
+
callback(records, editor['entryObserver']!);
|
|
631
|
+
expect(firstLoad.calledOnce).to.be.true;
|
|
632
|
+
expect(editor.entries[1].editorWidget).to.be.undefined;
|
|
633
|
+
expect(load.calledOnce).to.be.true;
|
|
634
|
+
await Promise.resolve();
|
|
635
|
+
expect(editor.entries[0].editorWidget).to.exist;
|
|
636
|
+
editor.entries[1].setCollapsed(false);
|
|
637
|
+
await Promise.resolve();
|
|
638
|
+
expect(load.calledTwice).to.be.true;
|
|
639
|
+
expect(editor.entries[1].editorWidget).to.exist;
|
|
640
|
+
editor.dispose();
|
|
641
|
+
expect(disconnect.calledOnce).to.be.true;
|
|
642
|
+
callback(records, editor['entryObserver']!);
|
|
643
|
+
expect(firstLoad.calledOnce).to.be.true;
|
|
644
|
+
expect(secondLoad.calledTwice).to.be.true;
|
|
645
|
+
expect(load.calledTwice).to.be.true;
|
|
646
|
+
} finally {
|
|
647
|
+
editor.dispose();
|
|
648
|
+
globalThis.IntersectionObserver = previous;
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should expand and load explicitly revealed entries', () => {
|
|
653
|
+
const previous = globalThis.requestAnimationFrame;
|
|
654
|
+
globalThis.requestAnimationFrame = sinon.stub().returns(0);
|
|
655
|
+
const editor = createEditor(1);
|
|
656
|
+
const load = sinon.spy(editor.entries[0], 'loadEditor');
|
|
657
|
+
try {
|
|
658
|
+
editor.entries[0].setCollapsed(true);
|
|
659
|
+
editor.revealResource(sampleResource.modifiedUri);
|
|
660
|
+
expect(editor.entries[0].isCollapsed).to.be.false;
|
|
661
|
+
expect(load.called).to.be.true;
|
|
662
|
+
} finally {
|
|
663
|
+
editor.dispose();
|
|
664
|
+
globalThis.requestAnimationFrame = previous;
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('should reveal only within the entries panel and repeat after an in-flight target loads', async () => {
|
|
669
|
+
const previous = globalThis.requestAnimationFrame;
|
|
670
|
+
const frames: FrameRequestCallback[] = [];
|
|
671
|
+
globalThis.requestAnimationFrame = callback => frames.push(callback);
|
|
672
|
+
let resolve!: (widget: EditorWidget) => void;
|
|
673
|
+
const editor = createEditor(1, () => new Promise(r => resolve = r));
|
|
674
|
+
const entry = editor.entries[0];
|
|
675
|
+
const panel = editor['entriesPanel'].node;
|
|
676
|
+
const offset = sinon.stub(entry.node, 'offsetTop').get(() => 120);
|
|
677
|
+
const pending = entry.loadEditor();
|
|
678
|
+
try {
|
|
679
|
+
Widget.attach(editor, document.body);
|
|
680
|
+
editor.node.scrollTop = 42;
|
|
681
|
+
document.body.scrollTop = 17;
|
|
682
|
+
editor.revealResource(entry.modifiedUri);
|
|
683
|
+
frames.splice(0).forEach(frame => frame(0));
|
|
684
|
+
expect(panel.scrollTop).to.equal(120);
|
|
685
|
+
expect(editor.node.scrollTop).to.equal(42);
|
|
686
|
+
expect(document.body.scrollTop).to.equal(17);
|
|
687
|
+
offset.get(() => 480);
|
|
688
|
+
resolve(new Widget() as EditorWidget);
|
|
689
|
+
await pending;
|
|
690
|
+
frames.splice(0).forEach(frame => frame(0));
|
|
691
|
+
expect(panel.scrollTop).to.equal(480);
|
|
692
|
+
expect(editor.node.scrollTop).to.equal(42);
|
|
693
|
+
expect(document.body.scrollTop).to.equal(17);
|
|
694
|
+
} finally {
|
|
695
|
+
editor.dispose();
|
|
696
|
+
offset.restore();
|
|
697
|
+
document.body.scrollTop = 0;
|
|
698
|
+
globalThis.requestAnimationFrame = previous;
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('should ignore stale reveal frames and frames after disposal', () => {
|
|
703
|
+
const previous = globalThis.requestAnimationFrame;
|
|
704
|
+
const frames: FrameRequestCallback[] = [];
|
|
705
|
+
globalThis.requestAnimationFrame = callback => frames.push(callback);
|
|
706
|
+
const editor = createEditor(1);
|
|
707
|
+
const otherResource = { ...sampleResource, modifiedUri: new URI('file:///workspace/c.ts') };
|
|
708
|
+
const other = editor.addDiffEntry(otherResource, new DiffEntryHeaderWidget(otherResource, mockLabelProvider));
|
|
709
|
+
const panel = editor['entriesPanel'].node;
|
|
710
|
+
const offset = sinon.stub(other.node, 'offsetTop').get(() => 240);
|
|
711
|
+
try {
|
|
712
|
+
Widget.attach(editor, document.body);
|
|
713
|
+
editor.revealResource(sampleResource.modifiedUri);
|
|
714
|
+
editor.revealResource(otherResource.modifiedUri);
|
|
715
|
+
frames.shift()!(0);
|
|
716
|
+
expect(panel.scrollTop).to.equal(0);
|
|
717
|
+
frames.shift()!(0);
|
|
718
|
+
expect(panel.scrollTop).to.equal(240);
|
|
719
|
+
editor.revealResource(sampleResource.modifiedUri);
|
|
720
|
+
editor.dispose();
|
|
721
|
+
frames.splice(0).forEach(frame => frame(0));
|
|
722
|
+
expect(panel.scrollTop).to.equal(240);
|
|
723
|
+
} finally {
|
|
724
|
+
editor.dispose();
|
|
725
|
+
offset.restore();
|
|
726
|
+
globalThis.requestAnimationFrame = previous;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('should activate a visible editor, remember focused entries, and preserve ancestor scroll positions', () => {
|
|
731
|
+
const editor = createEditor(2);
|
|
732
|
+
const panel = editor['entriesPanel'].node;
|
|
733
|
+
sinon.stub(panel, 'getBoundingClientRect').returns({ top: 0, bottom: 500 } as DOMRect);
|
|
734
|
+
const firstBounds = sinon.stub(editor.entries[0].node, 'getBoundingClientRect').returns({ top: -400, bottom: -100 } as DOMRect);
|
|
735
|
+
sinon.stub(editor.entries[1].node, 'getBoundingClientRect').returns({ top: 100, bottom: 400 } as DOMRect);
|
|
736
|
+
class FocusWidget extends Widget {
|
|
737
|
+
protected override onActivateRequest(): void {
|
|
738
|
+
panel.scrollTop = 0;
|
|
739
|
+
editor.node.scrollTop = 0;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const first = new FocusWidget() as unknown as EditorWidget;
|
|
743
|
+
const second = new FocusWidget() as unknown as EditorWidget;
|
|
744
|
+
editor.entries[0].setEditor(first);
|
|
745
|
+
editor.entries[1].setEditor(second);
|
|
746
|
+
const firstActivate = sinon.spy(first, 'processMessage');
|
|
747
|
+
const secondActivate = sinon.spy(second, 'processMessage');
|
|
748
|
+
try {
|
|
749
|
+
panel.scrollTop = 350;
|
|
750
|
+
editor.node.scrollTop = 42;
|
|
751
|
+
MessageLoop.sendMessage(editor, Widget.Msg.ActivateRequest);
|
|
752
|
+
expect(firstActivate.calledWith(Widget.Msg.ActivateRequest)).to.be.false;
|
|
753
|
+
expect(secondActivate.calledWith(Widget.Msg.ActivateRequest)).to.be.true;
|
|
754
|
+
expect(panel.scrollTop).to.equal(350);
|
|
755
|
+
expect(editor.node.scrollTop).to.equal(42);
|
|
756
|
+
second.node.dispatchEvent(new window.Event('focusin', { bubbles: true }));
|
|
757
|
+
firstBounds.returns({ top: 0, bottom: 200 } as DOMRect);
|
|
758
|
+
secondActivate.resetHistory();
|
|
759
|
+
MessageLoop.sendMessage(editor, Widget.Msg.ActivateRequest);
|
|
760
|
+
expect(secondActivate.calledWith(Widget.Msg.ActivateRequest)).to.be.true;
|
|
761
|
+
editor.entries[1].setCollapsed(true);
|
|
762
|
+
MessageLoop.sendMessage(editor, Widget.Msg.ActivateRequest);
|
|
763
|
+
expect(panel.scrollTop).to.equal(350);
|
|
764
|
+
} finally {
|
|
765
|
+
sinon.restore();
|
|
766
|
+
editor.dispose();
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('should fill the viewport for a single entry and follow resizes', () => {
|
|
771
|
+
const editor = createEditor(1);
|
|
772
|
+
try {
|
|
773
|
+
MessageLoop.sendMessage(editor, new Widget.ResizeMessage(1000, 800));
|
|
774
|
+
expect(editor.entries[0].node.style.height).to.equal('800px');
|
|
775
|
+
MessageLoop.sendMessage(editor, new Widget.ResizeMessage(1000, 400));
|
|
776
|
+
expect(editor.entries[0].node.style.height).to.equal('400px');
|
|
777
|
+
} finally {
|
|
778
|
+
editor.dispose();
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('should retain bounded content heights for multiple entries', () => {
|
|
783
|
+
const editor = createEditor(2);
|
|
784
|
+
try {
|
|
785
|
+
MessageLoop.sendMessage(editor, new Widget.ResizeMessage(1000, 800));
|
|
786
|
+
for (const entry of editor.entries) {
|
|
787
|
+
entry.setHeight(1000);
|
|
788
|
+
expect(entry.node.style.height).to.equal('1000px');
|
|
789
|
+
entry.setHeight(10_000);
|
|
790
|
+
expect(entry.node.style.height).to.equal('5000px');
|
|
791
|
+
}
|
|
792
|
+
MessageLoop.sendMessage(editor, new Widget.ResizeMessage(1000, 300));
|
|
793
|
+
expect(editor.entries[0].node.style.height).to.equal('5000px');
|
|
794
|
+
editor.entries[1].setHeight(60);
|
|
795
|
+
expect(editor.entries[1].node.style.height).to.equal('60px');
|
|
796
|
+
} finally {
|
|
797
|
+
editor.dispose();
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
describe('MultiDiffEditorWidgetFactory', () => {
|
|
803
|
+
|
|
804
|
+
class TestFactory extends MultiDiffEditorWidgetFactory {
|
|
805
|
+
configure(widget: EditorWidget): void {
|
|
806
|
+
this.configureEmbeddedEditor(widget);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
it('should disable visibility side effects and enable unchanged-region hiding only for diff editors', () => {
|
|
811
|
+
const factory = new TestFactory(new Container(), {} as EditorManager, mockLabelProvider);
|
|
812
|
+
const diff = Object.create(MonacoDiffEditor.prototype);
|
|
813
|
+
const updateDiffOptions = sinon.spy();
|
|
814
|
+
Object.defineProperty(diff, 'diffEditor', { value: { updateOptions: updateDiffOptions } });
|
|
815
|
+
const code = Object.create(MonacoEditor.prototype);
|
|
816
|
+
const updateCodeOptions = sinon.spy();
|
|
817
|
+
code.getControl = () => ({ updateOptions: updateCodeOptions });
|
|
818
|
+
factory.configure({ editor: diff } as EditorWidget);
|
|
819
|
+
factory.configure({ editor: code } as EditorWidget);
|
|
820
|
+
for (const editor of [diff, code]) {
|
|
821
|
+
expect(editor.detachModelOnHide).to.be.false;
|
|
822
|
+
}
|
|
823
|
+
expect(updateDiffOptions.firstCall.args[0]).to.deep.equal({
|
|
824
|
+
folding: false, codeLens: false, scrollBeyondLastLine: false, stickyScroll: { enabled: false }, minimap: { enabled: false }, hideUnchangedRegions: { enabled: true }
|
|
825
|
+
});
|
|
826
|
+
expect(updateCodeOptions.firstCall.args[0]).not.to.have.property('hideUnchangedRegions');
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('should return placeholders without loading models, then configure and publish loaded editors', async () => {
|
|
830
|
+
const widget = new Widget() as EditorWidget;
|
|
831
|
+
const createByUri = sinon.stub().resolves(widget);
|
|
832
|
+
const manager = { createByUri } as unknown as EditorManager;
|
|
833
|
+
const factory = new MultiDiffEditorWidgetFactory(new Container(), manager, mockLabelProvider);
|
|
834
|
+
const editor = factory.createMultiDiffEditor(MultiDiffEditorUri.encode({ title: 'Changes', resources: [sampleResource] }));
|
|
835
|
+
const changed = sinon.spy();
|
|
836
|
+
editor.onDidChangeTrackableWidgets(changed);
|
|
837
|
+
try {
|
|
838
|
+
expect(createByUri.called).to.be.false;
|
|
839
|
+
expect(editor.entries[0].node.querySelector('.multi-diff-entry-loading')).to.exist;
|
|
840
|
+
await editor.entries[0].loadEditor();
|
|
841
|
+
expect(createByUri.calledOnce).to.be.true;
|
|
842
|
+
expect(editor.getTrackableWidgets()).to.deep.equal([widget]);
|
|
843
|
+
expect(changed.calledOnce).to.be.true;
|
|
844
|
+
} finally {
|
|
845
|
+
editor.dispose();
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
describe('MultiDiffEditorLabelProvider', () => {
|
|
851
|
+
|
|
852
|
+
it('should label synthetic URIs with the full editor title', () => {
|
|
853
|
+
const provider = new MultiDiffEditorLabelProvider();
|
|
854
|
+
const uri = MultiDiffEditorUri.encode({ title: 'Changes in feature/test', resources: [] });
|
|
855
|
+
expect(provider.canHandle(uri)).to.equal(20);
|
|
856
|
+
expect(provider.canHandle(new URI('file:///a.ts'))).to.equal(0);
|
|
857
|
+
expect(provider.getName(uri)).to.equal('Changes in feature/test');
|
|
858
|
+
expect(provider.getLongName(uri)).to.equal('Changes in feature/test');
|
|
859
|
+
expect(provider.getIcon()).to.equal('codicon codicon-diff-multiple');
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
describe('MultiDiffEditorOpenHandler', () => {
|
|
864
|
+
|
|
865
|
+
// canHandle can be exercised without DI, since it's a pure function of the URI.
|
|
866
|
+
const handler = Object.create(MultiDiffEditorOpenHandler.prototype) as MultiDiffEditorOpenHandler;
|
|
867
|
+
|
|
868
|
+
it('should return 1000 for multi-diff-editor URIs', () => {
|
|
869
|
+
const uri = MultiDiffEditorUri.encode({ title: 'x', resources: [] });
|
|
870
|
+
expect(handler.canHandle(uri)).to.equal(1000);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it('should return 0 for other URIs', () => {
|
|
874
|
+
expect(handler.canHandle(new URI('file:///a.ts'))).to.equal(0);
|
|
875
|
+
expect(handler.canHandle(new URI('diff://a/b'))).to.equal(0);
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
});
|