@theia/scm 1.48.3 → 1.49.1
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-decorations-service.d.ts +9 -3
- package/lib/browser/decorations/scm-decorations-service.d.ts.map +1 -1
- package/lib/browser/decorations/scm-decorations-service.js +50 -30
- package/lib/browser/decorations/scm-decorations-service.js.map +1 -1
- package/lib/browser/dirty-diff/content-lines.d.ts +2 -0
- package/lib/browser/dirty-diff/content-lines.d.ts.map +1 -1
- package/lib/browser/dirty-diff/content-lines.js +7 -0
- package/lib/browser/dirty-diff/content-lines.js.map +1 -1
- package/lib/browser/dirty-diff/diff-computer.d.ts +23 -14
- package/lib/browser/dirty-diff/diff-computer.d.ts.map +1 -1
- package/lib/browser/dirty-diff/diff-computer.js +91 -27
- package/lib/browser/dirty-diff/diff-computer.js.map +1 -1
- package/lib/browser/dirty-diff/diff-computer.spec.js +120 -52
- package/lib/browser/dirty-diff/diff-computer.spec.js.map +1 -1
- package/lib/browser/dirty-diff/dirty-diff-decorator.d.ts +5 -3
- package/lib/browser/dirty-diff/dirty-diff-decorator.d.ts.map +1 -1
- package/lib/browser/dirty-diff/dirty-diff-decorator.js +14 -7
- package/lib/browser/dirty-diff/dirty-diff-decorator.js.map +1 -1
- package/lib/browser/dirty-diff/dirty-diff-module.d.ts.map +1 -1
- package/lib/browser/dirty-diff/dirty-diff-module.js +9 -0
- package/lib/browser/dirty-diff/dirty-diff-module.js.map +1 -1
- package/lib/browser/dirty-diff/dirty-diff-navigator.d.ts +49 -0
- package/lib/browser/dirty-diff/dirty-diff-navigator.d.ts.map +1 -0
- package/lib/browser/dirty-diff/dirty-diff-navigator.js +292 -0
- package/lib/browser/dirty-diff/dirty-diff-navigator.js.map +1 -0
- package/lib/browser/dirty-diff/dirty-diff-widget.d.ts +47 -0
- package/lib/browser/dirty-diff/dirty-diff-widget.d.ts.map +1 -0
- package/lib/browser/dirty-diff/dirty-diff-widget.js +320 -0
- package/lib/browser/dirty-diff/dirty-diff-widget.js.map +1 -0
- package/lib/browser/scm-colors.d.ts +6 -0
- package/lib/browser/scm-colors.d.ts.map +1 -0
- package/lib/browser/scm-colors.js +25 -0
- package/lib/browser/scm-colors.js.map +1 -0
- package/lib/browser/scm-contribution.d.ts +20 -6
- package/lib/browser/scm-contribution.d.ts.map +1 -1
- package/lib/browser/scm-contribution.js +112 -22
- package/lib/browser/scm-contribution.js.map +1 -1
- package/lib/browser/scm-tree-widget.js +1 -1
- package/lib/browser/scm-tree-widget.js.map +1 -1
- package/package.json +8 -6
- package/src/browser/decorations/scm-decorations-service.ts +60 -36
- package/src/browser/dirty-diff/content-lines.ts +9 -0
- package/src/browser/dirty-diff/diff-computer.spec.ts +120 -52
- package/src/browser/dirty-diff/diff-computer.ts +88 -40
- package/src/browser/dirty-diff/dirty-diff-decorator.ts +17 -10
- package/src/browser/dirty-diff/dirty-diff-module.ts +9 -0
- package/src/browser/dirty-diff/dirty-diff-navigator.ts +288 -0
- package/src/browser/dirty-diff/dirty-diff-widget.ts +364 -0
- package/src/browser/scm-colors.ts +21 -0
- package/src/browser/scm-contribution.ts +97 -6
- package/src/browser/scm-tree-widget.tsx +1 -1
- package/src/browser/style/dirty-diff-decorator.css +2 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2023 1C-Soft LLC 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 { Disposable, DisposableCollection, URI } from '@theia/core';
|
|
19
|
+
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
|
20
|
+
import { EditorManager, EditorMouseEvent, MouseTargetType, TextEditor } from '@theia/editor/lib/browser';
|
|
21
|
+
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
|
22
|
+
import { Change, LineRange } from './diff-computer';
|
|
23
|
+
import { DirtyDiffUpdate } from './dirty-diff-decorator';
|
|
24
|
+
import { DirtyDiffWidget, DirtyDiffWidgetFactory } from './dirty-diff-widget';
|
|
25
|
+
|
|
26
|
+
@injectable()
|
|
27
|
+
export class DirtyDiffNavigator {
|
|
28
|
+
|
|
29
|
+
protected readonly controllers = new Map<TextEditor, DirtyDiffController>();
|
|
30
|
+
|
|
31
|
+
@inject(ContextKeyService)
|
|
32
|
+
protected readonly contextKeyService: ContextKeyService;
|
|
33
|
+
|
|
34
|
+
@inject(EditorManager)
|
|
35
|
+
protected readonly editorManager: EditorManager;
|
|
36
|
+
|
|
37
|
+
@inject(DirtyDiffWidgetFactory)
|
|
38
|
+
protected readonly widgetFactory: DirtyDiffWidgetFactory;
|
|
39
|
+
|
|
40
|
+
@postConstruct()
|
|
41
|
+
protected init(): void {
|
|
42
|
+
const dirtyDiffVisible: ContextKey<boolean> = this.contextKeyService.createKey('dirtyDiffVisible', false);
|
|
43
|
+
this.editorManager.onActiveEditorChanged(editorWidget => {
|
|
44
|
+
dirtyDiffVisible.set(editorWidget && this.controllers.get(editorWidget.editor)?.isShowingChange());
|
|
45
|
+
});
|
|
46
|
+
this.editorManager.onCreated(editorWidget => {
|
|
47
|
+
const { editor } = editorWidget;
|
|
48
|
+
if (editor.uri.scheme !== 'file') {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const controller = this.createController(editor);
|
|
52
|
+
controller.widgetFactory = props => {
|
|
53
|
+
const widget = this.widgetFactory(props);
|
|
54
|
+
if (widget.editor === this.editorManager.activeEditor?.editor) {
|
|
55
|
+
dirtyDiffVisible.set(true);
|
|
56
|
+
}
|
|
57
|
+
widget.onDidClose(() => {
|
|
58
|
+
if (widget.editor === this.editorManager.activeEditor?.editor) {
|
|
59
|
+
dirtyDiffVisible.set(false);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return widget;
|
|
63
|
+
};
|
|
64
|
+
this.controllers.set(editor, controller);
|
|
65
|
+
editorWidget.disposed.connect(() => {
|
|
66
|
+
this.controllers.delete(editor);
|
|
67
|
+
controller.dispose();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handleDirtyDiffUpdate(update: DirtyDiffUpdate): void {
|
|
73
|
+
const controller = this.controllers.get(update.editor);
|
|
74
|
+
controller?.handleDirtyDiffUpdate(update);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
canNavigate(): boolean {
|
|
78
|
+
return !!this.activeController?.canNavigate();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
gotoNextChange(): void {
|
|
82
|
+
this.activeController?.gotoNextChange();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
gotoPreviousChange(): void {
|
|
86
|
+
this.activeController?.gotoPreviousChange();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
canShowChange(): boolean {
|
|
90
|
+
return !!this.activeController?.canShowChange();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
showNextChange(): void {
|
|
94
|
+
this.activeController?.showNextChange();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
showPreviousChange(): void {
|
|
98
|
+
this.activeController?.showPreviousChange();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
isShowingChange(): boolean {
|
|
102
|
+
return !!this.activeController?.isShowingChange();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
closeChangePeekView(): void {
|
|
106
|
+
this.activeController?.closeWidget();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
protected get activeController(): DirtyDiffController | undefined {
|
|
110
|
+
const editor = this.editorManager.activeEditor?.editor;
|
|
111
|
+
return editor && this.controllers.get(editor);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected createController(editor: TextEditor): DirtyDiffController {
|
|
115
|
+
return new DirtyDiffController(editor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class DirtyDiffController implements Disposable {
|
|
120
|
+
|
|
121
|
+
protected readonly toDispose = new DisposableCollection();
|
|
122
|
+
|
|
123
|
+
widgetFactory?: DirtyDiffWidgetFactory;
|
|
124
|
+
protected widget?: DirtyDiffWidget;
|
|
125
|
+
protected dirtyDiff?: DirtyDiffUpdate;
|
|
126
|
+
|
|
127
|
+
constructor(protected readonly editor: TextEditor) {
|
|
128
|
+
editor.onMouseDown(this.handleEditorMouseDown, this, this.toDispose);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
dispose(): void {
|
|
132
|
+
this.closeWidget();
|
|
133
|
+
this.toDispose.dispose();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
handleDirtyDiffUpdate(dirtyDiff: DirtyDiffUpdate): void {
|
|
137
|
+
if (dirtyDiff.editor === this.editor) {
|
|
138
|
+
this.closeWidget();
|
|
139
|
+
this.dirtyDiff = dirtyDiff;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
canNavigate(): boolean {
|
|
144
|
+
return !!this.changes?.length;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
gotoNextChange(): void {
|
|
148
|
+
const { editor } = this;
|
|
149
|
+
const index = this.findNextClosestChange(editor.cursor.line, false);
|
|
150
|
+
const change = this.changes?.[index];
|
|
151
|
+
if (change) {
|
|
152
|
+
const position = LineRange.getStartPosition(change.currentRange);
|
|
153
|
+
editor.cursor = position;
|
|
154
|
+
editor.revealPosition(position, { vertical: 'auto' });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
gotoPreviousChange(): void {
|
|
159
|
+
const { editor } = this;
|
|
160
|
+
const index = this.findPreviousClosestChange(editor.cursor.line, false);
|
|
161
|
+
const change = this.changes?.[index];
|
|
162
|
+
if (change) {
|
|
163
|
+
const position = LineRange.getStartPosition(change.currentRange);
|
|
164
|
+
editor.cursor = position;
|
|
165
|
+
editor.revealPosition(position, { vertical: 'auto' });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
canShowChange(): boolean {
|
|
170
|
+
return !!(this.widget || this.widgetFactory && this.editor instanceof MonacoEditor && this.changes?.length && this.previousRevisionUri);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
showNextChange(): void {
|
|
174
|
+
if (this.widget) {
|
|
175
|
+
this.widget.showNextChange();
|
|
176
|
+
} else {
|
|
177
|
+
(this.widget = this.createWidget())?.showChange(
|
|
178
|
+
this.findNextClosestChange(this.editor.cursor.line, true));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
showPreviousChange(): void {
|
|
183
|
+
if (this.widget) {
|
|
184
|
+
this.widget.showPreviousChange();
|
|
185
|
+
} else {
|
|
186
|
+
(this.widget = this.createWidget())?.showChange(
|
|
187
|
+
this.findPreviousClosestChange(this.editor.cursor.line, true));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
isShowingChange(): boolean {
|
|
192
|
+
return !!this.widget;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
closeWidget(): void {
|
|
196
|
+
if (this.widget) {
|
|
197
|
+
this.widget.dispose();
|
|
198
|
+
this.widget = undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
protected get changes(): readonly Change[] | undefined {
|
|
203
|
+
return this.dirtyDiff?.changes;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
protected get previousRevisionUri(): URI | undefined {
|
|
207
|
+
return this.dirtyDiff?.previousRevisionUri;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
protected createWidget(): DirtyDiffWidget | undefined {
|
|
211
|
+
const { widgetFactory, editor, changes, previousRevisionUri } = this;
|
|
212
|
+
if (widgetFactory && editor instanceof MonacoEditor && changes?.length && previousRevisionUri) {
|
|
213
|
+
const widget = widgetFactory({ editor, previousRevisionUri, changes });
|
|
214
|
+
widget.onDidClose(() => {
|
|
215
|
+
this.widget = undefined;
|
|
216
|
+
});
|
|
217
|
+
return widget;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
protected findNextClosestChange(line: number, inclusive: boolean): number {
|
|
222
|
+
const length = this.changes?.length;
|
|
223
|
+
if (!length) {
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
for (let i = 0; i < length; i++) {
|
|
227
|
+
const { currentRange } = this.changes![i];
|
|
228
|
+
|
|
229
|
+
if (inclusive) {
|
|
230
|
+
if (LineRange.getEndPosition(currentRange).line >= line) {
|
|
231
|
+
return i;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
if (LineRange.getStartPosition(currentRange).line > line) {
|
|
235
|
+
return i;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
protected findPreviousClosestChange(line: number, inclusive: boolean): number {
|
|
243
|
+
const length = this.changes?.length;
|
|
244
|
+
if (!length) {
|
|
245
|
+
return -1;
|
|
246
|
+
}
|
|
247
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
248
|
+
const { currentRange } = this.changes![i];
|
|
249
|
+
|
|
250
|
+
if (inclusive) {
|
|
251
|
+
if (LineRange.getStartPosition(currentRange).line <= line) {
|
|
252
|
+
return i;
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
if (LineRange.getEndPosition(currentRange).line < line) {
|
|
256
|
+
return i;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return length - 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
protected handleEditorMouseDown({ event, target }: EditorMouseEvent): void {
|
|
264
|
+
if (event.button !== 0) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const { range, type, element } = target;
|
|
268
|
+
if (!range || type !== MouseTargetType.GUTTER_LINE_DECORATIONS || !element || element.className.indexOf('dirty-diff-glyph') < 0) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const gutterOffsetX = target.detail.offsetX - (element as HTMLElement).offsetLeft;
|
|
272
|
+
if (gutterOffsetX < -3 || gutterOffsetX > 3) { // dirty diff decoration on hover is 6px wide
|
|
273
|
+
return; // to avoid colliding with folding
|
|
274
|
+
}
|
|
275
|
+
const index = this.findNextClosestChange(range.start.line, true);
|
|
276
|
+
if (index < 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (index === this.widget?.currentChangeIndex) {
|
|
280
|
+
this.closeWidget();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!this.widget) {
|
|
284
|
+
this.widget = this.createWidget();
|
|
285
|
+
}
|
|
286
|
+
this.widget?.showChange(index);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2023 1C-Soft LLC 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 { Position, Range } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
19
|
+
import { ActionMenuNode, Disposable, Emitter, Event, MenuCommandExecutor, MenuModelRegistry, MenuPath, URI, nls } from '@theia/core';
|
|
20
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
21
|
+
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
|
22
|
+
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
|
23
|
+
import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor';
|
|
24
|
+
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
|
25
|
+
import { MonacoEditorPeekViewWidget, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground }
|
|
26
|
+
from '@theia/monaco/lib/browser/monaco-editor-peek-view-widget';
|
|
27
|
+
import { Change, LineRange } from './diff-computer';
|
|
28
|
+
import { ScmColors } from '../scm-colors';
|
|
29
|
+
import * as monaco from '@theia/monaco-editor-core';
|
|
30
|
+
|
|
31
|
+
export const SCM_CHANGE_TITLE_MENU: MenuPath = ['scm-change-title-menu'];
|
|
32
|
+
/** Reserved for plugin contributions, corresponds to contribution point 'scm/change/title'. */
|
|
33
|
+
export const PLUGIN_SCM_CHANGE_TITLE_MENU: MenuPath = ['plugin-scm-change-title-menu'];
|
|
34
|
+
|
|
35
|
+
export const DirtyDiffWidgetProps = Symbol('DirtyDiffWidgetProps');
|
|
36
|
+
export interface DirtyDiffWidgetProps {
|
|
37
|
+
readonly editor: MonacoEditor;
|
|
38
|
+
readonly previousRevisionUri: URI;
|
|
39
|
+
readonly changes: readonly Change[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DirtyDiffWidgetFactory = Symbol('DirtyDiffWidgetFactory');
|
|
43
|
+
export type DirtyDiffWidgetFactory = (props: DirtyDiffWidgetProps) => DirtyDiffWidget;
|
|
44
|
+
|
|
45
|
+
@injectable()
|
|
46
|
+
export class DirtyDiffWidget implements Disposable {
|
|
47
|
+
|
|
48
|
+
private readonly onDidCloseEmitter = new Emitter<unknown>();
|
|
49
|
+
readonly onDidClose: Event<unknown> = this.onDidCloseEmitter.event;
|
|
50
|
+
protected index: number = -1;
|
|
51
|
+
private peekView?: DirtyDiffPeekView;
|
|
52
|
+
private diffEditorPromise?: Promise<MonacoDiffEditor>;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
@inject(DirtyDiffWidgetProps) protected readonly props: DirtyDiffWidgetProps,
|
|
56
|
+
@inject(MonacoEditorProvider) readonly editorProvider: MonacoEditorProvider,
|
|
57
|
+
@inject(ContextKeyService) readonly contextKeyService: ContextKeyService,
|
|
58
|
+
@inject(MenuModelRegistry) readonly menuModelRegistry: MenuModelRegistry,
|
|
59
|
+
@inject(MenuCommandExecutor) readonly menuCommandExecutor: MenuCommandExecutor
|
|
60
|
+
) { }
|
|
61
|
+
|
|
62
|
+
@postConstruct()
|
|
63
|
+
create(): void {
|
|
64
|
+
this.peekView = new DirtyDiffPeekView(this);
|
|
65
|
+
this.peekView.onDidClose(e => this.onDidCloseEmitter.fire(e));
|
|
66
|
+
this.diffEditorPromise = this.peekView.create();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get editor(): MonacoEditor {
|
|
70
|
+
return this.props.editor;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get uri(): URI {
|
|
74
|
+
return this.editor.uri;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get previousRevisionUri(): URI {
|
|
78
|
+
return this.props.previousRevisionUri;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get changes(): readonly Change[] {
|
|
82
|
+
return this.props.changes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get currentChange(): Change | undefined {
|
|
86
|
+
return this.changes[this.index];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get currentChangeIndex(): number {
|
|
90
|
+
return this.index;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
showChange(index: number): void {
|
|
94
|
+
this.checkCreated();
|
|
95
|
+
if (index >= 0 && index < this.changes.length) {
|
|
96
|
+
this.index = index;
|
|
97
|
+
this.showCurrentChange();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
showNextChange(): void {
|
|
102
|
+
this.checkCreated();
|
|
103
|
+
const index = this.index;
|
|
104
|
+
const length = this.changes.length;
|
|
105
|
+
if (length > 0 && (index < 0 || length > 1)) {
|
|
106
|
+
this.index = index < 0 ? 0 : cycle(index, 1, length);
|
|
107
|
+
this.showCurrentChange();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
showPreviousChange(): void {
|
|
112
|
+
this.checkCreated();
|
|
113
|
+
const index = this.index;
|
|
114
|
+
const length = this.changes.length;
|
|
115
|
+
if (length > 0 && (index < 0 || length > 1)) {
|
|
116
|
+
this.index = index < 0 ? length - 1 : cycle(index, -1, length);
|
|
117
|
+
this.showCurrentChange();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getContentWithSelectedChanges(predicate: (change: Change, index: number, changes: readonly Change[]) => boolean): Promise<string> {
|
|
122
|
+
this.checkCreated();
|
|
123
|
+
const changes = this.changes.filter(predicate);
|
|
124
|
+
const { diffEditor } = await this.diffEditorPromise!;
|
|
125
|
+
const diffEditorModel = diffEditor.getModel()!;
|
|
126
|
+
return applyChanges(changes, diffEditorModel.original, diffEditorModel.modified);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
dispose(): void {
|
|
130
|
+
this.peekView?.dispose();
|
|
131
|
+
this.onDidCloseEmitter.dispose();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected showCurrentChange(): void {
|
|
135
|
+
this.peekView!.setTitle(this.computePrimaryHeading(), this.computeSecondaryHeading());
|
|
136
|
+
const { previousRange, currentRange } = this.changes[this.index];
|
|
137
|
+
this.peekView!.show(Position.create(LineRange.getEndPosition(currentRange).line, 0),
|
|
138
|
+
this.computeHeightInLines());
|
|
139
|
+
this.diffEditorPromise!.then(({ diffEditor }) => {
|
|
140
|
+
let startLine = LineRange.getStartPosition(currentRange).line;
|
|
141
|
+
let endLine = LineRange.getEndPosition(currentRange).line;
|
|
142
|
+
if (LineRange.isEmpty(currentRange)) { // the change is a removal
|
|
143
|
+
++endLine;
|
|
144
|
+
} else if (!LineRange.isEmpty(previousRange)) { // the change is a modification
|
|
145
|
+
--startLine;
|
|
146
|
+
++endLine;
|
|
147
|
+
}
|
|
148
|
+
diffEditor.revealLinesInCenter(startLine + 1, endLine + 1, // monaco line numbers are 1-based
|
|
149
|
+
monaco.editor.ScrollType.Immediate);
|
|
150
|
+
});
|
|
151
|
+
this.editor.focus();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected computePrimaryHeading(): string {
|
|
155
|
+
return this.uri.path.base;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected computeSecondaryHeading(): string {
|
|
159
|
+
const index = this.index + 1;
|
|
160
|
+
const length = this.changes.length;
|
|
161
|
+
return length > 1 ? nls.localizeByDefault('{0} of {1} changes', index, length) :
|
|
162
|
+
nls.localizeByDefault('{0} of {1} change', index, length);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected computeHeightInLines(): number {
|
|
166
|
+
const editor = this.editor.getControl();
|
|
167
|
+
const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
|
|
168
|
+
const editorHeight = editor.getLayoutInfo().height;
|
|
169
|
+
const editorHeightInLines = Math.floor(editorHeight / lineHeight);
|
|
170
|
+
|
|
171
|
+
const { previousRange, currentRange } = this.changes[this.index];
|
|
172
|
+
const changeHeightInLines = LineRange.getLineCount(currentRange) + LineRange.getLineCount(previousRange);
|
|
173
|
+
|
|
174
|
+
return Math.min(changeHeightInLines + /* padding */ 8, Math.floor(editorHeightInLines / 3));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
protected checkCreated(): void {
|
|
178
|
+
if (!this.peekView) {
|
|
179
|
+
throw new Error('create() method needs to be called first.');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function cycle(index: number, offset: -1 | 1, length: number): number {
|
|
185
|
+
return (index + offset + length) % length;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// adapted from https://github.com/microsoft/vscode/blob/823d54f86ee13eb357bc6e8e562e89d793f3c43b/extensions/git/src/staging.ts
|
|
189
|
+
function applyChanges(changes: readonly Change[], original: monaco.editor.ITextModel, modified: monaco.editor.ITextModel): string {
|
|
190
|
+
const result: string[] = [];
|
|
191
|
+
let currentLine = 1;
|
|
192
|
+
|
|
193
|
+
for (const change of changes) {
|
|
194
|
+
const { previousRange, currentRange } = change;
|
|
195
|
+
|
|
196
|
+
const isInsertion = LineRange.isEmpty(previousRange);
|
|
197
|
+
const isDeletion = LineRange.isEmpty(currentRange);
|
|
198
|
+
|
|
199
|
+
const convert = (range: LineRange): [number, number] => {
|
|
200
|
+
let startLineNumber;
|
|
201
|
+
let endLineNumber;
|
|
202
|
+
if (!LineRange.isEmpty(range)) {
|
|
203
|
+
startLineNumber = range.start + 1;
|
|
204
|
+
endLineNumber = range.end;
|
|
205
|
+
} else {
|
|
206
|
+
startLineNumber = range.start;
|
|
207
|
+
endLineNumber = 0;
|
|
208
|
+
}
|
|
209
|
+
return [startLineNumber, endLineNumber];
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const [originalStartLineNumber, originalEndLineNumber] = convert(previousRange);
|
|
213
|
+
const [modifiedStartLineNumber, modifiedEndLineNumber] = convert(currentRange);
|
|
214
|
+
|
|
215
|
+
let toLine = isInsertion ? originalStartLineNumber + 1 : originalStartLineNumber;
|
|
216
|
+
let toCharacter = 1;
|
|
217
|
+
|
|
218
|
+
// if this is a deletion at the very end of the document,
|
|
219
|
+
// we need to account for a newline at the end of the last line,
|
|
220
|
+
// which may have been deleted
|
|
221
|
+
if (isDeletion && originalEndLineNumber === original.getLineCount()) {
|
|
222
|
+
toLine--;
|
|
223
|
+
toCharacter = original.getLineMaxColumn(toLine);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
result.push(original.getValueInRange(new monaco.Range(currentLine, 1, toLine, toCharacter)));
|
|
227
|
+
|
|
228
|
+
if (!isDeletion) {
|
|
229
|
+
let fromLine = modifiedStartLineNumber;
|
|
230
|
+
let fromCharacter = 1;
|
|
231
|
+
|
|
232
|
+
// if this is an insertion at the very end of the document,
|
|
233
|
+
// we must start the next range after the last character of the previous line,
|
|
234
|
+
// in order to take the correct eol
|
|
235
|
+
if (isInsertion && originalStartLineNumber === original.getLineCount()) {
|
|
236
|
+
fromLine--;
|
|
237
|
+
fromCharacter = modified.getLineMaxColumn(fromLine);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
result.push(modified.getValueInRange(new monaco.Range(fromLine, fromCharacter, modifiedEndLineNumber + 1, 1)));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
currentLine = isInsertion ? originalStartLineNumber + 1 : originalEndLineNumber + 1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
result.push(original.getValueInRange(new monaco.Range(currentLine, 1, original.getLineCount() + 1, 1)));
|
|
247
|
+
|
|
248
|
+
return result.join('');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
class DirtyDiffPeekView extends MonacoEditorPeekViewWidget {
|
|
252
|
+
|
|
253
|
+
private diffEditorPromise?: Promise<MonacoDiffEditor>;
|
|
254
|
+
private height?: number;
|
|
255
|
+
|
|
256
|
+
constructor(readonly widget: DirtyDiffWidget) {
|
|
257
|
+
super(widget.editor, { isResizeable: true, showArrow: true, frameWidth: 1, keepEditorSelection: true, className: 'dirty-diff' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
override async create(): Promise<MonacoDiffEditor> {
|
|
261
|
+
try {
|
|
262
|
+
super.create();
|
|
263
|
+
const diffEditor = await this.diffEditorPromise!;
|
|
264
|
+
return new Promise(resolve => {
|
|
265
|
+
// setTimeout is needed here because the non-side-by-side diff editor might still not have created the view zones;
|
|
266
|
+
// otherwise, the first change shown might not be properly revealed in the diff editor.
|
|
267
|
+
// see also https://github.com/microsoft/vscode/blob/b30900b56c4b3ca6c65d7ab92032651f4cb23f15/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts#L248
|
|
268
|
+
const disposable = diffEditor.diffEditor.onDidUpdateDiff(() => setTimeout(() => {
|
|
269
|
+
resolve(diffEditor);
|
|
270
|
+
disposable.dispose();
|
|
271
|
+
}));
|
|
272
|
+
});
|
|
273
|
+
} catch (e) {
|
|
274
|
+
this.dispose();
|
|
275
|
+
throw e;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
override show(rangeOrPos: Range | Position, heightInLines: number): void {
|
|
280
|
+
const borderColor = this.getBorderColor();
|
|
281
|
+
this.style({
|
|
282
|
+
arrowColor: borderColor,
|
|
283
|
+
frameColor: borderColor,
|
|
284
|
+
headerBackgroundColor: peekViewTitleBackground,
|
|
285
|
+
primaryHeadingColor: peekViewTitleForeground,
|
|
286
|
+
secondaryHeadingColor: peekViewTitleInfoForeground
|
|
287
|
+
});
|
|
288
|
+
this.updateActions();
|
|
289
|
+
super.show(rangeOrPos, heightInLines);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private getBorderColor(): string {
|
|
293
|
+
const { currentChange } = this.widget;
|
|
294
|
+
if (!currentChange) {
|
|
295
|
+
return peekViewBorder;
|
|
296
|
+
}
|
|
297
|
+
if (Change.isAddition(currentChange)) {
|
|
298
|
+
return ScmColors.editorGutterAddedBackground;
|
|
299
|
+
} else if (Change.isRemoval(currentChange)) {
|
|
300
|
+
return ScmColors.editorGutterDeletedBackground;
|
|
301
|
+
} else {
|
|
302
|
+
return ScmColors.editorGutterModifiedBackground;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private updateActions(): void {
|
|
307
|
+
this.clearActions();
|
|
308
|
+
const { contextKeyService, menuModelRegistry, menuCommandExecutor } = this.widget;
|
|
309
|
+
contextKeyService.with({ originalResourceScheme: this.widget.previousRevisionUri.scheme }, () => {
|
|
310
|
+
for (const menuPath of [SCM_CHANGE_TITLE_MENU, PLUGIN_SCM_CHANGE_TITLE_MENU]) {
|
|
311
|
+
const menu = menuModelRegistry.getMenu(menuPath);
|
|
312
|
+
for (const item of menu.children) {
|
|
313
|
+
if (item instanceof ActionMenuNode) {
|
|
314
|
+
const { command, id, label, icon, when } = item;
|
|
315
|
+
if (icon && menuCommandExecutor.isVisible(menuPath, command, this.widget) && (!when || contextKeyService.match(when))) {
|
|
316
|
+
this.addAction(id, label, icon, menuCommandExecutor.isEnabled(menuPath, command, this.widget), () => {
|
|
317
|
+
menuCommandExecutor.executeCommand(menuPath, command, this.widget);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
this.addAction('dirtydiff.next', nls.localizeByDefault('Show Next Change'), codicon('arrow-down'), true,
|
|
325
|
+
() => this.widget.showNextChange());
|
|
326
|
+
this.addAction('dirtydiff.previous', nls.localizeByDefault('Show Previous Change'), codicon('arrow-up'), true,
|
|
327
|
+
() => this.widget.showPreviousChange());
|
|
328
|
+
this.addAction('peekview.close', nls.localizeByDefault('Close'), codicon('close'), true,
|
|
329
|
+
() => this.dispose());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
protected override fillHead(container: HTMLElement): void {
|
|
333
|
+
super.fillHead(container, true);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
protected override fillBody(container: HTMLElement): void {
|
|
337
|
+
this.diffEditorPromise = this.widget.editorProvider.createEmbeddedDiffEditor(this.editor, container, this.widget.previousRevisionUri).then(diffEditor => {
|
|
338
|
+
this.toDispose.push(diffEditor);
|
|
339
|
+
return diffEditor;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
protected override doLayoutBody(height: number, width: number): void {
|
|
344
|
+
super.doLayoutBody(height, width);
|
|
345
|
+
this.layout(height, width);
|
|
346
|
+
this.height = height;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
protected override onWidth(width: number): void {
|
|
350
|
+
super.onWidth(width);
|
|
351
|
+
const { height } = this;
|
|
352
|
+
if (height !== undefined) {
|
|
353
|
+
this.layout(height, width);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private layout(height: number, width: number): void {
|
|
358
|
+
this.diffEditorPromise?.then(({ diffEditor }) => diffEditor.layout({ height, width }));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
protected override doRevealRange(range: Range): void {
|
|
362
|
+
this.editor.revealPosition(Position.create(range.end.line, 0), { vertical: 'centerIfOutsideViewport' });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2019 Red Hat, Inc. 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
|
+
export namespace ScmColors {
|
|
18
|
+
export const editorGutterModifiedBackground = 'editorGutter.modifiedBackground';
|
|
19
|
+
export const editorGutterAddedBackground = 'editorGutter.addedBackground';
|
|
20
|
+
export const editorGutterDeletedBackground = 'editorGutter.deletedBackground';
|
|
21
|
+
}
|