@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,628 @@
|
|
|
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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
18
|
+
import { Emitter, Event, nls, URI } from '@theia/core';
|
|
19
|
+
import {
|
|
20
|
+
addEventListener, ApplicationShell, BaseWidget, BoxLayout, codicon, Key, LabelProvider, LabelProviderContribution,
|
|
21
|
+
Message, MessageLoop, Navigatable, NavigatableWidgetOpenHandler, Panel, PanelLayout, StatefulWidget,
|
|
22
|
+
Widget, WidgetOpenerOptions
|
|
23
|
+
} from '@theia/core/lib/browser';
|
|
24
|
+
import { 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 { MultiDiffEditorUri, MultiDiffEditorResourcePair, MultiDiffEditorUriData } from './multi-diff-editor-uri';
|
|
28
|
+
|
|
29
|
+
export { MultiDiffEditorUri, MultiDiffEditorResourcePair, MultiDiffEditorUriData };
|
|
30
|
+
|
|
31
|
+
export interface MultiDiffEditorOpenerOptions extends WidgetOpenerOptions {
|
|
32
|
+
reveal?: URI;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Data required to construct a {@link MultiDiffEditor} via dependency injection.
|
|
37
|
+
*/
|
|
38
|
+
export const MultiDiffEditorData = Symbol('MultiDiffEditorData');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initial height of loading and error placeholders (in pixels).
|
|
42
|
+
*/
|
|
43
|
+
export const MIN_ENTRY_HEIGHT = 120;
|
|
44
|
+
/**
|
|
45
|
+
* Fallback height limit before the viewport size is known (in pixels).
|
|
46
|
+
*/
|
|
47
|
+
export const MAX_ENTRY_HEIGHT = 500;
|
|
48
|
+
/**
|
|
49
|
+
* Height of the header bar above each embedded diff editor (in pixels).
|
|
50
|
+
* Used as the collapsed-entry height and as the sizeBasis of the BoxLayout header child.
|
|
51
|
+
*/
|
|
52
|
+
export const HEADER_HEIGHT = 28;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Header widget for a single diff entry. Displays a collapse/expand toggle, file icon,
|
|
56
|
+
* name and path. Clicking anywhere on the header fires {@link onDidToggleCollapse}.
|
|
57
|
+
*/
|
|
58
|
+
export class DiffEntryHeaderWidget extends BaseWidget {
|
|
59
|
+
|
|
60
|
+
protected readonly onDidToggleCollapseEmitter = new Emitter<void>();
|
|
61
|
+
readonly onDidToggleCollapse: Event<void> = this.onDidToggleCollapseEmitter.event;
|
|
62
|
+
|
|
63
|
+
protected readonly chevron: HTMLElement;
|
|
64
|
+
|
|
65
|
+
constructor(resource: MultiDiffEditorResourcePair, labelProvider: LabelProvider) {
|
|
66
|
+
super();
|
|
67
|
+
this.addClass('multi-diff-entry-header');
|
|
68
|
+
this.node.setAttribute('role', 'button');
|
|
69
|
+
this.node.setAttribute('aria-expanded', 'true');
|
|
70
|
+
this.node.tabIndex = 0;
|
|
71
|
+
|
|
72
|
+
this.chevron = document.createElement('span');
|
|
73
|
+
this.chevron.className = `${codicon('chevron-down')} multi-diff-entry-chevron`;
|
|
74
|
+
this.node.appendChild(this.chevron);
|
|
75
|
+
|
|
76
|
+
const icon = document.createElement('span');
|
|
77
|
+
icon.className = `${labelProvider.getIcon(resource.modifiedUri)} file-icon multi-diff-entry-icon`;
|
|
78
|
+
this.node.appendChild(icon);
|
|
79
|
+
|
|
80
|
+
const label = document.createElement('span');
|
|
81
|
+
label.classList.add('multi-diff-entry-label');
|
|
82
|
+
label.textContent = labelProvider.getName(resource.modifiedUri);
|
|
83
|
+
this.node.appendChild(label);
|
|
84
|
+
|
|
85
|
+
const description = document.createElement('span');
|
|
86
|
+
description.classList.add('multi-diff-entry-description');
|
|
87
|
+
description.textContent = labelProvider.getLongName(resource.modifiedUri);
|
|
88
|
+
this.node.appendChild(description);
|
|
89
|
+
|
|
90
|
+
this.toDispose.push(this.onDidToggleCollapseEmitter);
|
|
91
|
+
this.addEventListener(this.node, 'click', () => this.onDidToggleCollapseEmitter.fire());
|
|
92
|
+
this.addKeyListener(this.node, [Key.ENTER, Key.SPACE], () => this.onDidToggleCollapseEmitter.fire());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setCollapsed(collapsed: boolean): void {
|
|
96
|
+
this.node.setAttribute('aria-expanded', String(!collapsed));
|
|
97
|
+
this.chevron.classList.toggle('codicon-chevron-down', !collapsed);
|
|
98
|
+
this.chevron.classList.toggle('codicon-chevron-right', collapsed);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Placeholder widget shown while a diff entry's editor is being loaded.
|
|
104
|
+
*/
|
|
105
|
+
export class DiffEntryLoadingWidget extends BaseWidget {
|
|
106
|
+
|
|
107
|
+
constructor() {
|
|
108
|
+
super();
|
|
109
|
+
this.addClass('multi-diff-entry-loading');
|
|
110
|
+
const spinner = document.createElement('span');
|
|
111
|
+
spinner.className = `${codicon('loading')} codicon-modifier-spin`;
|
|
112
|
+
this.node.appendChild(spinner);
|
|
113
|
+
const text = document.createElement('span');
|
|
114
|
+
text.textContent = nls.localizeByDefault('Loading...');
|
|
115
|
+
this.node.appendChild(text);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Placeholder widget shown when a diff entry's editor fails to load.
|
|
121
|
+
*/
|
|
122
|
+
export class DiffEntryErrorWidget extends BaseWidget {
|
|
123
|
+
|
|
124
|
+
constructor(message: string, retry?: () => void) {
|
|
125
|
+
super();
|
|
126
|
+
this.addClass('multi-diff-entry-error');
|
|
127
|
+
const icon = document.createElement('span');
|
|
128
|
+
icon.className = codicon('error');
|
|
129
|
+
this.node.appendChild(icon);
|
|
130
|
+
const text = document.createElement('span');
|
|
131
|
+
text.textContent = message;
|
|
132
|
+
this.node.appendChild(text);
|
|
133
|
+
if (retry) {
|
|
134
|
+
const button = document.createElement('button');
|
|
135
|
+
button.type = 'button';
|
|
136
|
+
button.className = 'theia-button';
|
|
137
|
+
button.textContent = nls.localizeByDefault('Retry');
|
|
138
|
+
this.node.appendChild(button);
|
|
139
|
+
this.addEventListener(button, 'click', retry);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A single diff entry combining a header and a content area that transitions through
|
|
146
|
+
* loading, editor, and error states. The entry's outer height is controlled via
|
|
147
|
+
* {@link DiffEntryWidget.setHeight} and, once the editor is loaded, tracks the
|
|
148
|
+
* editor's content size up to 5000 pixels. A single entry fills the viewport.
|
|
149
|
+
* Entries can be collapsed (showing only the header) via {@link DiffEntryWidget.setCollapsed}.
|
|
150
|
+
*/
|
|
151
|
+
export class DiffEntryWidget extends BaseWidget {
|
|
152
|
+
|
|
153
|
+
readonly modifiedUri: URI;
|
|
154
|
+
|
|
155
|
+
protected readonly boxLayout: BoxLayout;
|
|
156
|
+
protected readonly header: DiffEntryHeaderWidget;
|
|
157
|
+
protected currentContent: Widget;
|
|
158
|
+
protected _editorWidget?: EditorWidget;
|
|
159
|
+
|
|
160
|
+
protected _isCollapsed = false;
|
|
161
|
+
/** Height to restore when expanding from a collapsed state. */
|
|
162
|
+
protected lastExpandedHeight = MIN_ENTRY_HEIGHT;
|
|
163
|
+
protected availableHeight?: number;
|
|
164
|
+
protected fillViewport = false;
|
|
165
|
+
protected requestedHeight = MIN_ENTRY_HEIGHT;
|
|
166
|
+
protected contentHeight?: number;
|
|
167
|
+
protected innerScrolling?: boolean;
|
|
168
|
+
protected loading = false;
|
|
169
|
+
protected loadAttempted = false;
|
|
170
|
+
|
|
171
|
+
protected readonly onDidLoadEditorEmitter = new Emitter<void>();
|
|
172
|
+
readonly onDidLoadEditor: Event<void> = this.onDidLoadEditorEmitter.event;
|
|
173
|
+
|
|
174
|
+
protected readonly onDidChangeCollapsedEmitter = new Emitter<boolean>();
|
|
175
|
+
readonly onDidChangeCollapsed: Event<boolean> = this.onDidChangeCollapsedEmitter.event;
|
|
176
|
+
|
|
177
|
+
constructor(
|
|
178
|
+
resource: MultiDiffEditorResourcePair,
|
|
179
|
+
headerWidget: DiffEntryHeaderWidget,
|
|
180
|
+
protected readonly createEditor?: () => Promise<EditorWidget>
|
|
181
|
+
) {
|
|
182
|
+
super();
|
|
183
|
+
this.addClass('multi-diff-entry');
|
|
184
|
+
this.modifiedUri = resource.modifiedUri;
|
|
185
|
+
this.header = headerWidget;
|
|
186
|
+
|
|
187
|
+
this.boxLayout = new BoxLayout({ direction: 'top-to-bottom', spacing: 0 });
|
|
188
|
+
BoxLayout.setStretch(headerWidget, 0);
|
|
189
|
+
BoxLayout.setSizeBasis(headerWidget, HEADER_HEIGHT);
|
|
190
|
+
this.boxLayout.addWidget(headerWidget);
|
|
191
|
+
|
|
192
|
+
this.currentContent = new DiffEntryLoadingWidget();
|
|
193
|
+
BoxLayout.setStretch(this.currentContent, 1);
|
|
194
|
+
this.boxLayout.addWidget(this.currentContent);
|
|
195
|
+
|
|
196
|
+
this.layout = this.boxLayout;
|
|
197
|
+
this.setHeight(MIN_ENTRY_HEIGHT);
|
|
198
|
+
|
|
199
|
+
this.toDispose.pushAll([
|
|
200
|
+
this.onDidChangeCollapsedEmitter,
|
|
201
|
+
this.onDidLoadEditorEmitter,
|
|
202
|
+
this.header.onDidToggleCollapse(() => this.setCollapsed(!this._isCollapsed))
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get editorWidget(): EditorWidget | undefined {
|
|
207
|
+
return this._editorWidget;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get isCollapsed(): boolean {
|
|
211
|
+
return this._isCollapsed;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
setCollapsed(collapsed: boolean): void {
|
|
215
|
+
if (this._isCollapsed === collapsed) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this._isCollapsed = collapsed;
|
|
219
|
+
this.header.setCollapsed(collapsed);
|
|
220
|
+
this.currentContent.setHidden(collapsed);
|
|
221
|
+
if (collapsed) {
|
|
222
|
+
this.lastExpandedHeight = this.getCurrentHeight() || MIN_ENTRY_HEIGHT;
|
|
223
|
+
this.setNodeHeight(this.getNonContentHeight());
|
|
224
|
+
} else {
|
|
225
|
+
this.setNodeHeight(this.lastExpandedHeight);
|
|
226
|
+
}
|
|
227
|
+
this.onDidChangeCollapsedEmitter.fire(collapsed);
|
|
228
|
+
if (!collapsed) {
|
|
229
|
+
this.loadEditor();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async loadEditor(retry = false): Promise<void> {
|
|
234
|
+
if (this.isDisposed || this._isCollapsed || this._editorWidget || this.loading || (!retry && this.loadAttempted) || !this.createEditor) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.loading = true;
|
|
238
|
+
this.loadAttempted = true;
|
|
239
|
+
this.replaceContent(new DiffEntryLoadingWidget());
|
|
240
|
+
try {
|
|
241
|
+
const editorWidget = await this.createEditor();
|
|
242
|
+
if (this.isDisposed) {
|
|
243
|
+
editorWidget.dispose();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.setEditor(editorWidget);
|
|
247
|
+
this.onDidLoadEditorEmitter.fire();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (!this.isDisposed) {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
this.setError(nls.localize('theia/scm/multiDiffEditor/loadFailed', 'Failed to load diff: {0}', message));
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
this.loading = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setError(message: string): void {
|
|
259
|
+
this.replaceContent(new DiffEntryErrorWidget(message, () => this.loadEditor(true)));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
setEditor(editorWidget: EditorWidget): void {
|
|
263
|
+
this._editorWidget = editorWidget;
|
|
264
|
+
this.replaceContent(editorWidget);
|
|
265
|
+
this.trackContentHeight(editorWidget);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Replace the current content widget (loading/editor/error) with a new one.
|
|
270
|
+
* Using BoxLayout directly (instead of a nested Panel) ensures that Monaco's
|
|
271
|
+
* container gets explicit `position: absolute` + `width`/`height` from Lumino's
|
|
272
|
+
* LayoutItem, which is required for Monaco to lay out correctly.
|
|
273
|
+
*/
|
|
274
|
+
protected replaceContent(widget: Widget): void {
|
|
275
|
+
const old = this.currentContent;
|
|
276
|
+
if (old === widget) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
widget.setHidden(this._isCollapsed);
|
|
280
|
+
BoxLayout.setStretch(widget, 1);
|
|
281
|
+
this.boxLayout.addWidget(widget);
|
|
282
|
+
this.currentContent = widget;
|
|
283
|
+
// eslint-disable-next-line no-null/no-null
|
|
284
|
+
old.parent = null;
|
|
285
|
+
old.dispose();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Fit multiple entries to their content, capped at 5000 pixels; a single entry fills the viewport. While collapsed,
|
|
290
|
+
* remember the requested height for expansion.
|
|
291
|
+
*/
|
|
292
|
+
setHeight(height: number): void {
|
|
293
|
+
this.requestedHeight = height;
|
|
294
|
+
const limit = this.fillViewport ? (this.availableHeight ?? MAX_ENTRY_HEIGHT) : 5000;
|
|
295
|
+
const clamped = Math.max(this.getNonContentHeight(), this.fillViewport ? limit : Math.min(limit, height));
|
|
296
|
+
this.lastExpandedHeight = clamped;
|
|
297
|
+
if (!this._isCollapsed) {
|
|
298
|
+
this.setNodeHeight(clamped);
|
|
299
|
+
}
|
|
300
|
+
this.updateInnerScrolling();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Update the viewport height, used when a single entry fills it. */
|
|
304
|
+
setAvailableHeight(height: number, fillViewport = true): void {
|
|
305
|
+
this.availableHeight = Math.max(this.getNonContentHeight(), height);
|
|
306
|
+
this.fillViewport = fillViewport;
|
|
307
|
+
this.setHeight(this.requestedHeight);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
protected getNonContentHeight(): number {
|
|
311
|
+
const style = window.getComputedStyle(this.node);
|
|
312
|
+
return HEADER_HEIGHT + (parseFloat(style.borderTopWidth) || 0) + (parseFloat(style.borderBottomWidth) || 0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
protected updateInnerScrolling(): void {
|
|
316
|
+
if (this.contentHeight === undefined || !this._editorWidget) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const scrolling = this.contentHeight > this.lastExpandedHeight - this.getNonContentHeight();
|
|
320
|
+
if (this.innerScrolling === scrolling) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Set this before updating Monaco options, which can emit content-size events synchronously.
|
|
324
|
+
this.innerScrolling = scrolling;
|
|
325
|
+
const editor = MonacoEditor.get(this._editorWidget);
|
|
326
|
+
const options = {
|
|
327
|
+
scrollbar: { vertical: scrolling ? 'auto' as const : 'hidden' as const, handleMouseWheel: scrolling, alwaysConsumeMouseWheel: false }
|
|
328
|
+
};
|
|
329
|
+
if (editor instanceof MonacoDiffEditor) {
|
|
330
|
+
editor.diffEditor.updateOptions(options);
|
|
331
|
+
} else {
|
|
332
|
+
editor?.getControl().updateOptions(options);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
protected override onAfterAttach(msg: Message): void {
|
|
337
|
+
super.onAfterAttach(msg);
|
|
338
|
+
if (this.contentHeight !== undefined) {
|
|
339
|
+
this.setHeight(this.contentHeight + this.getNonContentHeight());
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
protected getCurrentHeight(): number {
|
|
344
|
+
const parsed = parseInt(this.node.style.height, 10);
|
|
345
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
protected setNodeHeight(height: number): void {
|
|
349
|
+
if (this.node.style.height === `${height}px`) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.node.style.height = `${height}px`;
|
|
353
|
+
if (this.isAttached) {
|
|
354
|
+
MessageLoop.sendMessage(this, Widget.ResizeMessage.UnknownSize);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Subscribe to the embedded editor's content size changes and size this entry accordingly.
|
|
360
|
+
* The subscriptions are disposed together with this widget.
|
|
361
|
+
*/
|
|
362
|
+
protected trackContentHeight(editorWidget: EditorWidget): void {
|
|
363
|
+
const monacoEditor = MonacoEditor.get(editorWidget);
|
|
364
|
+
if (!monacoEditor) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const control = monacoEditor instanceof MonacoDiffEditor ? monacoEditor.diffEditor.getModifiedEditor() : monacoEditor.getControl();
|
|
368
|
+
this.toDispose.push(addEventListener(editorWidget.node, 'wheel', event => {
|
|
369
|
+
if (!this.innerScrolling || event.ctrlKey || event.shiftKey || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const scrollTop = control.getScrollTop();
|
|
373
|
+
const atBoundary = event.deltaY < 0
|
|
374
|
+
? scrollTop <= 0
|
|
375
|
+
: scrollTop >= control.getScrollHeight() - control.getLayoutInfo().height;
|
|
376
|
+
if (atBoundary) {
|
|
377
|
+
// Monaco does not apply alwaysConsumeMouseWheel changes to an existing scrollbar.
|
|
378
|
+
event.stopPropagation();
|
|
379
|
+
}
|
|
380
|
+
}, true));
|
|
381
|
+
const getContentHeight = (): number => {
|
|
382
|
+
if (monacoEditor instanceof MonacoDiffEditor) {
|
|
383
|
+
return Math.max(
|
|
384
|
+
monacoEditor.diffEditor.getOriginalEditor().getContentHeight(),
|
|
385
|
+
monacoEditor.diffEditor.getModifiedEditor().getContentHeight()
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return monacoEditor.getControl().getContentHeight();
|
|
389
|
+
};
|
|
390
|
+
const updateHeight = () => {
|
|
391
|
+
this.contentHeight = getContentHeight();
|
|
392
|
+
this.setHeight(this.contentHeight + this.getNonContentHeight());
|
|
393
|
+
};
|
|
394
|
+
if (monacoEditor instanceof MonacoDiffEditor) {
|
|
395
|
+
this.toDispose.push(monacoEditor.diffEditor.getOriginalEditor().onDidContentSizeChange(updateHeight));
|
|
396
|
+
this.toDispose.push(monacoEditor.diffEditor.getModifiedEditor().onDidContentSizeChange(updateHeight));
|
|
397
|
+
} else {
|
|
398
|
+
this.toDispose.push(monacoEditor.getControl().onDidContentSizeChange(updateHeight));
|
|
399
|
+
}
|
|
400
|
+
updateHeight();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Widget that displays a list of diff editors stacked vertically.
|
|
406
|
+
*
|
|
407
|
+
* **Async-loading contract:** the widget is returned immediately by the factory with
|
|
408
|
+
* placeholder entries (one per resource) showing a loading indicator. Each
|
|
409
|
+
* {@link DiffEntryWidget.editorWidget} is `undefined` until the embedded diff editor
|
|
410
|
+
* finishes loading, at which point the placeholder is replaced and
|
|
411
|
+
* {@link onDidChangeTrackableWidgets} fires so the {@link ApplicationShell} focus
|
|
412
|
+
* tracker picks up the new editor. Consumers must not assume that all embedded editors
|
|
413
|
+
* are present synchronously when this widget is returned.
|
|
414
|
+
*/
|
|
415
|
+
@injectable()
|
|
416
|
+
export class MultiDiffEditor extends BaseWidget implements Navigatable, ApplicationShell.TrackableWidgetProvider, StatefulWidget {
|
|
417
|
+
|
|
418
|
+
@inject(MultiDiffEditorData)
|
|
419
|
+
protected readonly data: MultiDiffEditorUriData;
|
|
420
|
+
|
|
421
|
+
protected readonly entryWidgets: DiffEntryWidget[] = [];
|
|
422
|
+
protected entriesPanel: Panel;
|
|
423
|
+
protected encodedUri: URI;
|
|
424
|
+
protected entryObserver?: IntersectionObserver;
|
|
425
|
+
protected lastFocusedEntry?: DiffEntryWidget;
|
|
426
|
+
|
|
427
|
+
protected readonly onDidChangeTrackableWidgetsEmitter = new Emitter<Widget[]>();
|
|
428
|
+
readonly onDidChangeTrackableWidgets: Event<Widget[]> = this.onDidChangeTrackableWidgetsEmitter.event;
|
|
429
|
+
|
|
430
|
+
protected pendingReveal?: DiffEntryWidget;
|
|
431
|
+
|
|
432
|
+
@postConstruct()
|
|
433
|
+
protected init(): void {
|
|
434
|
+
this.toDispose.push(this.onDidChangeTrackableWidgetsEmitter);
|
|
435
|
+
this.addClass('theia-multi-diff-editor');
|
|
436
|
+
this.encodedUri = MultiDiffEditorUri.encode(this.data);
|
|
437
|
+
this.id = MultiDiffEditorOpenHandler.ID + ':' + this.encodedUri.toString();
|
|
438
|
+
this.title.label = this.data.title;
|
|
439
|
+
this.title.caption = this.data.title;
|
|
440
|
+
this.title.closable = true;
|
|
441
|
+
this.title.iconClass = 'codicon codicon-diff-multiple file-icon';
|
|
442
|
+
|
|
443
|
+
this.entriesPanel = new Panel();
|
|
444
|
+
this.entriesPanel.addClass('multi-diff-editor-entries');
|
|
445
|
+
const layout = new PanelLayout();
|
|
446
|
+
layout.addWidget(this.entriesPanel);
|
|
447
|
+
this.layout = layout;
|
|
448
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
449
|
+
this.entryObserver = new IntersectionObserver(entries => {
|
|
450
|
+
if (!this.isDisposed) {
|
|
451
|
+
for (const observed of entries) {
|
|
452
|
+
if (observed.isIntersecting) {
|
|
453
|
+
this.entryWidgets.find(entry => entry.node === observed.target)?.loadEditor();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}, { root: this.entriesPanel.node, rootMargin: '500px 0px' });
|
|
458
|
+
this.toDispose.push({ dispose: () => this.entryObserver?.disconnect() });
|
|
459
|
+
}
|
|
460
|
+
this.addEventListener(this.entriesPanel.node, 'focusin', event => {
|
|
461
|
+
this.lastFocusedEntry = this.entryWidgets.find(entry => event.target instanceof Node && entry.node.contains(event.target));
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
get entries(): readonly DiffEntryWidget[] {
|
|
466
|
+
return this.entryWidgets;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
addDiffEntry(resource: MultiDiffEditorResourcePair, headerWidget: DiffEntryHeaderWidget, createEditor?: () => Promise<EditorWidget>): DiffEntryWidget {
|
|
470
|
+
const entry = new DiffEntryWidget(resource, headerWidget, createEditor);
|
|
471
|
+
this.entryWidgets.push(entry);
|
|
472
|
+
this.entriesPanel.addWidget(entry);
|
|
473
|
+
this.toDispose.push(entry.onDidLoadEditor(() => {
|
|
474
|
+
this.notifyTrackableWidgetsChanged();
|
|
475
|
+
if (this.pendingReveal === entry) {
|
|
476
|
+
this.scrollToEntry(entry);
|
|
477
|
+
}
|
|
478
|
+
}));
|
|
479
|
+
this.entryObserver?.observe(entry.node);
|
|
480
|
+
return entry;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Notify listeners (in particular, the {@link ApplicationShell} focus tracker) that a
|
|
485
|
+
* new editor widget has been attached to one of the entries. Called on the entry's
|
|
486
|
+
* {@link DiffEntryWidget.onDidLoadEditor} event.
|
|
487
|
+
*/
|
|
488
|
+
notifyTrackableWidgetsChanged(): void {
|
|
489
|
+
this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
revealResource(modifiedUri: URI): void {
|
|
493
|
+
const uriStr = modifiedUri.toString();
|
|
494
|
+
const entry = this.entryWidgets.find(e => e.modifiedUri.toString() === uriStr);
|
|
495
|
+
if (!entry) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.pendingReveal = entry;
|
|
499
|
+
entry.setCollapsed(false);
|
|
500
|
+
entry.loadEditor();
|
|
501
|
+
this.scrollToEntry(entry);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
protected scrollToEntry(entry: DiffEntryWidget): void {
|
|
505
|
+
// Scroll only the entries panel, and repeat after the target editor's initial layout.
|
|
506
|
+
requestAnimationFrame(() => {
|
|
507
|
+
if (!this.isDisposed && entry.isAttached && this.pendingReveal === entry) {
|
|
508
|
+
this.entriesPanel.node.scrollTop = entry.node.offsetTop;
|
|
509
|
+
if (entry.editorWidget) {
|
|
510
|
+
this.pendingReveal = undefined;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
getTrackableWidgets(): Widget[] {
|
|
517
|
+
const result: Widget[] = [];
|
|
518
|
+
for (const entry of this.entryWidgets) {
|
|
519
|
+
if (entry.editorWidget) {
|
|
520
|
+
result.push(entry.editorWidget);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return result;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
protected override onActivateRequest(msg: Message): void {
|
|
527
|
+
super.onActivateRequest(msg);
|
|
528
|
+
const viewport = this.entriesPanel.node.getBoundingClientRect();
|
|
529
|
+
const isVisible = (candidate: DiffEntryWidget): boolean => {
|
|
530
|
+
const bounds = candidate.node.getBoundingClientRect();
|
|
531
|
+
return !candidate.isCollapsed && !!candidate.editorWidget && bounds.bottom > viewport.top && bounds.top < viewport.bottom;
|
|
532
|
+
};
|
|
533
|
+
const entry = this.lastFocusedEntry && isVisible(this.lastFocusedEntry)
|
|
534
|
+
? this.lastFocusedEntry : this.entryWidgets.find(isVisible);
|
|
535
|
+
if (entry?.editorWidget) {
|
|
536
|
+
const scrollPositions: { node: HTMLElement; top: number; left: number }[] = [];
|
|
537
|
+
for (let node: HTMLElement | undefined = this.entriesPanel.node; node; node = node.parentElement ?? undefined) {
|
|
538
|
+
scrollPositions.push({ node, top: node.scrollTop, left: node.scrollLeft });
|
|
539
|
+
}
|
|
540
|
+
// Deliver activation synchronously so focus-induced ancestor scrolling can be restored.
|
|
541
|
+
MessageLoop.sendMessage(entry.editorWidget, Widget.Msg.ActivateRequest);
|
|
542
|
+
for (const position of scrollPositions) {
|
|
543
|
+
position.node.scrollTop = position.top;
|
|
544
|
+
position.node.scrollLeft = position.left;
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
this.node.tabIndex = -1;
|
|
548
|
+
this.node.focus({ preventScroll: true });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
protected override onResize(msg: Widget.ResizeMessage): void {
|
|
553
|
+
super.onResize(msg);
|
|
554
|
+
const height = msg.height < 0 ? this.node.clientHeight : msg.height;
|
|
555
|
+
if (height > 0) {
|
|
556
|
+
for (const entry of this.entryWidgets) {
|
|
557
|
+
entry.setAvailableHeight(height, this.entryWidgets.length === 1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
protected override onAfterAttach(msg: Message): void {
|
|
563
|
+
super.onAfterAttach(msg);
|
|
564
|
+
if (!this.entryObserver) {
|
|
565
|
+
for (const entry of this.entryWidgets) {
|
|
566
|
+
entry.loadEditor();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
storeState(): undefined {
|
|
572
|
+
// Resource providers may not be registered yet when the layout is restored.
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
restoreState(_state: object): void { }
|
|
577
|
+
|
|
578
|
+
getResourceUri(): URI | undefined {
|
|
579
|
+
return this.encodedUri;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
createMoveToUri(_resourceUri: URI): URI | undefined {
|
|
583
|
+
// Multi-diff editors represent a collection of resources, so they cannot be moved to a single resource.
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
@injectable()
|
|
589
|
+
export class MultiDiffEditorLabelProvider implements LabelProviderContribution {
|
|
590
|
+
|
|
591
|
+
canHandle(element: object): number {
|
|
592
|
+
return element instanceof URI && MultiDiffEditorUri.isMultiDiffEditorUri(element) ? 20 : 0;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
getName(uri: URI): string {
|
|
596
|
+
return MultiDiffEditorUri.decode(uri).title;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
getLongName(uri: URI): string {
|
|
600
|
+
return this.getName(uri);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
getIcon(): string {
|
|
604
|
+
return codicon('diff-multiple');
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
@injectable()
|
|
609
|
+
export class MultiDiffEditorOpenHandler extends NavigatableWidgetOpenHandler<MultiDiffEditor> {
|
|
610
|
+
|
|
611
|
+
static readonly ID = 'multi-diff-editor-opener';
|
|
612
|
+
|
|
613
|
+
readonly id = MultiDiffEditorOpenHandler.ID;
|
|
614
|
+
|
|
615
|
+
readonly label = nls.localizeByDefault('Multi Diff Editor');
|
|
616
|
+
|
|
617
|
+
override canHandle(uri: URI, options?: MultiDiffEditorOpenerOptions): number {
|
|
618
|
+
return MultiDiffEditorUri.isMultiDiffEditorUri(uri) ? 1000 : 0;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
override async open(uri: URI, options?: MultiDiffEditorOpenerOptions): Promise<MultiDiffEditor> {
|
|
622
|
+
const widget = await super.open(uri, options);
|
|
623
|
+
if (options?.reveal) {
|
|
624
|
+
widget.revealResource(options.reveal);
|
|
625
|
+
}
|
|
626
|
+
return widget;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
@@ -44,6 +44,7 @@ import { ColorContribution } from '@theia/core/lib/browser/color-application-con
|
|
|
44
44
|
import { LabelProviderContribution } from '@theia/core/lib/browser/label-provider';
|
|
45
45
|
import { bindScmPreferences } from '../common/scm-preferences';
|
|
46
46
|
import { bindMergeEditor } from './merge-editor/merge-editor-module';
|
|
47
|
+
import { bindMultiDiffEditor } from './multi-diff-editor/multi-diff-editor-module';
|
|
47
48
|
import { ScmRepositoriesWidget } from './scm-repositories-widget';
|
|
48
49
|
import { ScmHistoryGraphWidget } from './scm-history-graph-widget';
|
|
49
50
|
import { ScmHistoryGraphModel, ScmHistoryGraphModelProvider } from './scm-history-graph-model';
|
|
@@ -156,6 +157,8 @@ export default new ContainerModule(bind => {
|
|
|
156
157
|
bindScmPreferences(bind);
|
|
157
158
|
|
|
158
159
|
bindMergeEditor(bind);
|
|
160
|
+
|
|
161
|
+
bindMultiDiffEditor(bind);
|
|
159
162
|
});
|
|
160
163
|
|
|
161
164
|
export function createScmTreeContainer(parent: interfaces.Container): Container {
|
package/src/browser/scm-input.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
18
18
|
|
|
19
|
-
import
|
|
19
|
+
import debounce from 'p-debounce';
|
|
20
20
|
import { Disposable, DisposableCollection, Emitter } from '@theia/core/lib/common';
|
|
21
21
|
import { JSONExt, JSONObject } from '@theia/core/shared/@lumino/coreutils';
|
|
22
22
|
|
|
@@ -62,6 +62,8 @@ export interface ScmResourceGroup extends Disposable {
|
|
|
62
62
|
export interface ScmResource {
|
|
63
63
|
/** The uri of the underlying resource inside the workspace. */
|
|
64
64
|
readonly sourceUri: URI;
|
|
65
|
+
readonly multiDiffEditorOriginalUri?: URI;
|
|
66
|
+
readonly multiDiffEditorModifiedUri?: URI;
|
|
65
67
|
readonly decorations?: ScmResourceDecorations;
|
|
66
68
|
open(): Promise<void>;
|
|
67
69
|
|