@theia/scm 1.45.1 → 1.46.0-next.72
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/README.md +31 -31
- package/lib/browser/decorations/scm-decorations-service.d.ts +14 -14
- package/lib/browser/decorations/scm-decorations-service.js +101 -101
- package/lib/browser/decorations/scm-navigator-decorator.d.ts +25 -25
- package/lib/browser/decorations/scm-navigator-decorator.js +132 -132
- package/lib/browser/decorations/scm-tab-bar-decorator.d.ts +17 -17
- package/lib/browser/decorations/scm-tab-bar-decorator.js +93 -93
- package/lib/browser/dirty-diff/content-lines.d.ts +12 -12
- package/lib/browser/dirty-diff/content-lines.js +106 -106
- package/lib/browser/dirty-diff/content-lines.spec.d.ts +1 -1
- package/lib/browser/dirty-diff/content-lines.spec.js +39 -39
- package/lib/browser/dirty-diff/diff-computer.d.ts +29 -29
- package/lib/browser/dirty-diff/diff-computer.js +102 -102
- package/lib/browser/dirty-diff/diff-computer.spec.d.ts +1 -1
- package/lib/browser/dirty-diff/diff-computer.spec.js +315 -315
- package/lib/browser/dirty-diff/dirty-diff-decorator.d.ts +14 -14
- package/lib/browser/dirty-diff/dirty-diff-decorator.js +98 -98
- package/lib/browser/dirty-diff/dirty-diff-module.d.ts +3 -3
- package/lib/browser/dirty-diff/dirty-diff-module.js +24 -24
- package/lib/browser/scm-amend-component.d.ts +123 -123
- package/lib/browser/scm-amend-component.js +463 -463
- package/lib/browser/scm-amend-widget.d.ts +20 -20
- package/lib/browser/scm-amend-widget.js +101 -101
- package/lib/browser/scm-avatar-service.d.ts +3 -3
- package/lib/browser/scm-avatar-service.js +36 -36
- package/lib/browser/scm-commit-widget.d.ts +52 -52
- package/lib/browser/scm-commit-widget.js +199 -199
- package/lib/browser/scm-context-key-service.d.ts +10 -10
- package/lib/browser/scm-context-key-service.js +58 -58
- package/lib/browser/scm-contribution.d.ts +83 -83
- package/lib/browser/scm-contribution.js +356 -356
- package/lib/browser/scm-frontend-module.d.ts +6 -6
- package/lib/browser/scm-frontend-module.js +130 -130
- package/lib/browser/scm-groups-tree-model.d.ts +14 -14
- package/lib/browser/scm-groups-tree-model.js +97 -97
- package/lib/browser/scm-input.d.ts +53 -53
- package/lib/browser/scm-input.js +127 -127
- package/lib/browser/scm-layout-migrations.d.ts +9 -9
- package/lib/browser/scm-layout-migrations.js +79 -79
- package/lib/browser/scm-no-repository-widget.d.ts +8 -8
- package/lib/browser/scm-no-repository-widget.js +49 -49
- package/lib/browser/scm-preferences.d.ts +11 -11
- package/lib/browser/scm-preferences.js +51 -51
- package/lib/browser/scm-provider.d.ts +58 -58
- package/lib/browser/scm-provider.js +19 -19
- package/lib/browser/scm-quick-open-service.d.ts +11 -11
- package/lib/browser/scm-quick-open-service.js +73 -73
- package/lib/browser/scm-repository.d.ts +17 -17
- package/lib/browser/scm-repository.js +41 -41
- package/lib/browser/scm-service.d.ts +26 -26
- package/lib/browser/scm-service.js +108 -108
- package/lib/browser/scm-tree-label-provider.d.ts +7 -7
- package/lib/browser/scm-tree-label-provider.js +57 -57
- package/lib/browser/scm-tree-model.d.ts +74 -74
- package/lib/browser/scm-tree-model.js +351 -351
- package/lib/browser/scm-tree-widget.d.ts +208 -208
- package/lib/browser/scm-tree-widget.js +703 -703
- package/lib/browser/scm-widget.d.ts +40 -40
- package/lib/browser/scm-widget.js +218 -218
- package/package.json +6 -6
- package/src/browser/decorations/scm-decorations-service.ts +78 -78
- package/src/browser/decorations/scm-navigator-decorator.ts +121 -121
- package/src/browser/decorations/scm-tab-bar-decorator.ts +83 -83
- package/src/browser/dirty-diff/content-lines.spec.ts +42 -42
- package/src/browser/dirty-diff/content-lines.ts +112 -112
- package/src/browser/dirty-diff/diff-computer.spec.ts +387 -387
- package/src/browser/dirty-diff/diff-computer.ts +129 -129
- package/src/browser/dirty-diff/dirty-diff-decorator.ts +107 -107
- package/src/browser/dirty-diff/dirty-diff-module.ts +24 -24
- package/src/browser/scm-amend-component.tsx +600 -600
- package/src/browser/scm-amend-widget.tsx +77 -77
- package/src/browser/scm-avatar-service.ts +27 -27
- package/src/browser/scm-commit-widget.tsx +215 -215
- package/src/browser/scm-context-key-service.ts +46 -46
- package/src/browser/scm-contribution.ts +361 -361
- package/src/browser/scm-frontend-module.ts +149 -149
- package/src/browser/scm-groups-tree-model.ts +78 -78
- package/src/browser/scm-input.ts +164 -164
- package/src/browser/scm-layout-migrations.ts +64 -64
- package/src/browser/scm-no-repository-widget.tsx +41 -41
- package/src/browser/scm-preferences.ts +63 -63
- package/src/browser/scm-provider.ts +91 -91
- package/src/browser/scm-quick-open-service.ts +48 -48
- package/src/browser/scm-repository.ts +52 -52
- package/src/browser/scm-service.ts +108 -108
- package/src/browser/scm-tree-label-provider.ts +44 -44
- package/src/browser/scm-tree-model.ts +405 -405
- package/src/browser/scm-tree-widget.tsx +838 -838
- package/src/browser/scm-widget.tsx +204 -204
- package/src/browser/style/dirty-diff-decorator.css +52 -52
- package/src/browser/style/dirty-diff.css +50 -50
- package/src/browser/style/index.css +271 -271
- package/src/browser/style/scm-amend-component.css +94 -94
- package/src/browser/style/scm.svg +4 -4
|
@@ -1,838 +1,838 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2020 Arm 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
|
-
/* eslint-disable no-null/no-null, @typescript-eslint/no-explicit-any */
|
|
18
|
-
|
|
19
|
-
import * as React from '@theia/core/shared/react';
|
|
20
|
-
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
21
|
-
import URI from '@theia/core/lib/common/uri';
|
|
22
|
-
import { isOSX } from '@theia/core/lib/common/os';
|
|
23
|
-
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
|
|
24
|
-
import { TreeWidget, TreeNode, SelectableTreeNode, TreeModel, TreeProps, NodeProps, TREE_NODE_SEGMENT_CLASS, TREE_NODE_SEGMENT_GROW_CLASS } from '@theia/core/lib/browser/tree';
|
|
25
|
-
import { ScmTreeModel, ScmFileChangeRootNode, ScmFileChangeGroupNode, ScmFileChangeFolderNode, ScmFileChangeNode } from './scm-tree-model';
|
|
26
|
-
import { MenuCommandExecutor, MenuModelRegistry, ActionMenuNode, CompoundMenuNode, MenuPath } from '@theia/core/lib/common/menu';
|
|
27
|
-
import { ScmResource } from './scm-provider';
|
|
28
|
-
import { ContextMenuRenderer, LabelProvider, CorePreferences, DiffUris, ACTION_ITEM } from '@theia/core/lib/browser';
|
|
29
|
-
import { ScmContextKeyService } from './scm-context-key-service';
|
|
30
|
-
import { EditorWidget, EditorManager, DiffNavigatorProvider } from '@theia/editor/lib/browser';
|
|
31
|
-
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
|
|
32
|
-
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
|
33
|
-
import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service';
|
|
34
|
-
import { FileStat } from '@theia/filesystem/lib/common/files';
|
|
35
|
-
import { ThemeService } from '@theia/core/lib/browser/theming';
|
|
36
|
-
|
|
37
|
-
@injectable()
|
|
38
|
-
export class ScmTreeWidget extends TreeWidget {
|
|
39
|
-
|
|
40
|
-
static ID = 'scm-resource-widget';
|
|
41
|
-
|
|
42
|
-
static RESOURCE_GROUP_CONTEXT_MENU = ['RESOURCE_GROUP_CONTEXT_MENU'];
|
|
43
|
-
static RESOURCE_GROUP_INLINE_MENU = ['RESOURCE_GROUP_CONTEXT_MENU', 'inline'];
|
|
44
|
-
|
|
45
|
-
static RESOURCE_FOLDER_CONTEXT_MENU = ['RESOURCE_FOLDER_CONTEXT_MENU'];
|
|
46
|
-
static RESOURCE_FOLDER_INLINE_MENU = ['RESOURCE_FOLDER_CONTEXT_MENU', 'inline'];
|
|
47
|
-
|
|
48
|
-
static RESOURCE_CONTEXT_MENU = ['RESOURCE_CONTEXT_MENU'];
|
|
49
|
-
static RESOURCE_INLINE_MENU = ['RESOURCE_CONTEXT_MENU', 'inline'];
|
|
50
|
-
|
|
51
|
-
@inject(MenuCommandExecutor) protected readonly menuCommandExecutor: MenuCommandExecutor;
|
|
52
|
-
@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry;
|
|
53
|
-
@inject(ScmContextKeyService) protected readonly contextKeys: ScmContextKeyService;
|
|
54
|
-
@inject(EditorManager) protected readonly editorManager: EditorManager;
|
|
55
|
-
@inject(DiffNavigatorProvider) protected readonly diffNavigatorProvider: DiffNavigatorProvider;
|
|
56
|
-
@inject(IconThemeService) protected readonly iconThemeService: IconThemeService;
|
|
57
|
-
@inject(DecorationsService) protected readonly decorationsService: DecorationsService;
|
|
58
|
-
@inject(ColorRegistry) protected readonly colors: ColorRegistry;
|
|
59
|
-
@inject(ThemeService) protected readonly themeService: ThemeService;
|
|
60
|
-
|
|
61
|
-
// TODO: Make TreeWidget generic to better type those fields.
|
|
62
|
-
override readonly model: ScmTreeModel;
|
|
63
|
-
|
|
64
|
-
constructor(
|
|
65
|
-
@inject(TreeProps) props: TreeProps,
|
|
66
|
-
@inject(TreeModel) treeModel: ScmTreeModel,
|
|
67
|
-
@inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer,
|
|
68
|
-
) {
|
|
69
|
-
super(props, treeModel, contextMenuRenderer);
|
|
70
|
-
this.id = ScmTreeWidget.ID;
|
|
71
|
-
this.addClass('groups-outer-container');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
@postConstruct()
|
|
75
|
-
protected override init(): void {
|
|
76
|
-
super.init();
|
|
77
|
-
this.toDispose.push(this.themeService.onDidColorThemeChange(() => this.update()));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
set viewMode(id: 'tree' | 'list') {
|
|
81
|
-
// Close the search box because the structure of the tree will change dramatically
|
|
82
|
-
// and the search results will be out of date.
|
|
83
|
-
this.searchBox.hide();
|
|
84
|
-
this.model.viewMode = id;
|
|
85
|
-
}
|
|
86
|
-
get viewMode(): 'tree' | 'list' {
|
|
87
|
-
return this.model.viewMode;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Render the node given the tree node and node properties.
|
|
92
|
-
* @param node the tree node.
|
|
93
|
-
* @param props the node properties.
|
|
94
|
-
*/
|
|
95
|
-
protected override renderNode(node: TreeNode, props: NodeProps): React.ReactNode {
|
|
96
|
-
if (!TreeNode.isVisible(node)) {
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const attributes = this.createNodeAttributes(node, props);
|
|
101
|
-
const label = this.labelProvider.getName(node);
|
|
102
|
-
const searchHighlights = this.searchHighlights?.get(node.id);
|
|
103
|
-
// The group nodes should not be subject to highlighting.
|
|
104
|
-
const caption = (searchHighlights && !ScmFileChangeGroupNode.is(node)) ? this.toReactNode(label, searchHighlights) : label;
|
|
105
|
-
|
|
106
|
-
if (ScmFileChangeGroupNode.is(node)) {
|
|
107
|
-
const content = <ScmResourceGroupElement
|
|
108
|
-
key={`${node.groupId}`}
|
|
109
|
-
model={this.model}
|
|
110
|
-
treeNode={node}
|
|
111
|
-
renderExpansionToggle={() => this.renderExpansionToggle(node, props)}
|
|
112
|
-
commandExecutor={this.menuCommandExecutor}
|
|
113
|
-
contextMenuRenderer={this.contextMenuRenderer}
|
|
114
|
-
menus={this.menus}
|
|
115
|
-
contextKeys={this.contextKeys}
|
|
116
|
-
labelProvider={this.labelProvider}
|
|
117
|
-
corePreferences={this.corePreferences}
|
|
118
|
-
caption={caption}
|
|
119
|
-
/>;
|
|
120
|
-
|
|
121
|
-
return React.createElement('div', attributes, content);
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
if (ScmFileChangeFolderNode.is(node)) {
|
|
125
|
-
const content = <ScmResourceFolderElement
|
|
126
|
-
key={String(node.sourceUri)}
|
|
127
|
-
model={this.model}
|
|
128
|
-
treeNode={node}
|
|
129
|
-
sourceUri={node.sourceUri}
|
|
130
|
-
renderExpansionToggle={() => this.renderExpansionToggle(node, props)}
|
|
131
|
-
commandExecutor={this.menuCommandExecutor}
|
|
132
|
-
contextMenuRenderer={this.contextMenuRenderer}
|
|
133
|
-
menus={this.menus}
|
|
134
|
-
contextKeys={this.contextKeys}
|
|
135
|
-
labelProvider={this.labelProvider}
|
|
136
|
-
corePreferences={this.corePreferences}
|
|
137
|
-
caption={caption}
|
|
138
|
-
/>;
|
|
139
|
-
|
|
140
|
-
return React.createElement('div', attributes, content);
|
|
141
|
-
}
|
|
142
|
-
if (ScmFileChangeNode.is(node)) {
|
|
143
|
-
const parentPath =
|
|
144
|
-
(node.parent && ScmFileChangeFolderNode.is(node.parent))
|
|
145
|
-
? new URI(node.parent.sourceUri) : new URI(this.model.rootUri);
|
|
146
|
-
|
|
147
|
-
const content = <ScmResourceComponent
|
|
148
|
-
key={node.sourceUri}
|
|
149
|
-
model={this.model}
|
|
150
|
-
treeNode={node}
|
|
151
|
-
contextMenuRenderer={this.contextMenuRenderer}
|
|
152
|
-
commandExecutor={this.menuCommandExecutor}
|
|
153
|
-
menus={this.menus}
|
|
154
|
-
contextKeys={this.contextKeys}
|
|
155
|
-
labelProvider={this.labelProvider}
|
|
156
|
-
corePreferences={this.corePreferences}
|
|
157
|
-
caption={caption}
|
|
158
|
-
{...{
|
|
159
|
-
...this.props,
|
|
160
|
-
parentPath,
|
|
161
|
-
sourceUri: node.sourceUri,
|
|
162
|
-
decoration: this.decorationsService.getDecoration(new URI(node.sourceUri), true)[0],
|
|
163
|
-
colors: this.colors,
|
|
164
|
-
isLightTheme: this.isCurrentThemeLight(),
|
|
165
|
-
renderExpansionToggle: () => this.renderExpansionToggle(node, props),
|
|
166
|
-
}}
|
|
167
|
-
/>;
|
|
168
|
-
return React.createElement('div', attributes, content);
|
|
169
|
-
}
|
|
170
|
-
return super.renderNode(node, props);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
protected override createContainerAttributes(): React.HTMLAttributes<HTMLElement> {
|
|
174
|
-
if (this.model.canTabToWidget()) {
|
|
175
|
-
return {
|
|
176
|
-
...super.createContainerAttributes(),
|
|
177
|
-
tabIndex: 0
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
return super.createContainerAttributes();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The ARROW_LEFT key controls both the movement around the file tree and also
|
|
185
|
-
* the movement through the change chunks within a file.
|
|
186
|
-
*
|
|
187
|
-
* If the selected tree node is a folder then the ARROW_LEFT key behaves exactly
|
|
188
|
-
* as it does in explorer. It collapses the tree node if the folder is expanded and
|
|
189
|
-
* it moves the selection up to the parent folder if the folder is collapsed (no-op if no parent folder, as
|
|
190
|
-
* group headers are not selectable). This behavior is the default behavior implemented
|
|
191
|
-
* in the TreeWidget super class.
|
|
192
|
-
*
|
|
193
|
-
* If the selected tree node is a file then the ARROW_LEFT key moves up through the
|
|
194
|
-
* change chunks within each file. If the selected chunk is the first chunk in the file
|
|
195
|
-
* then the file selection is moved to the previous file (no-op if no previous file).
|
|
196
|
-
*
|
|
197
|
-
* Note that when cursoring through change chunks, the ARROW_LEFT key cannot be used to
|
|
198
|
-
* move up through the parent folders of the file tree. If users want to do this, using
|
|
199
|
-
* keys only, then they must press ARROW_UP repeatedly until the selected node is the folder
|
|
200
|
-
* node and then press ARROW_LEFT.
|
|
201
|
-
*/
|
|
202
|
-
protected override async handleLeft(event: KeyboardEvent): Promise<void> {
|
|
203
|
-
if (this.model.selectedNodes.length === 1) {
|
|
204
|
-
const selectedNode = this.model.selectedNodes[0];
|
|
205
|
-
if (ScmFileChangeNode.is(selectedNode)) {
|
|
206
|
-
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
207
|
-
if (!selectedResource) {
|
|
208
|
-
return super.handleLeft(event);
|
|
209
|
-
}
|
|
210
|
-
const widget = await this.openResource(selectedResource);
|
|
211
|
-
|
|
212
|
-
if (widget) {
|
|
213
|
-
const diffNavigator = this.diffNavigatorProvider(widget.editor);
|
|
214
|
-
if (diffNavigator.hasPrevious()) {
|
|
215
|
-
diffNavigator.previous();
|
|
216
|
-
} else {
|
|
217
|
-
const previousNode = this.moveToPreviousFileNode();
|
|
218
|
-
if (previousNode) {
|
|
219
|
-
const previousResource = this.model.getResourceFromNode(previousNode);
|
|
220
|
-
if (previousResource) {
|
|
221
|
-
this.openResource(previousResource);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return super.handleLeft(event);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* The ARROW_RIGHT key controls both the movement around the file tree and also
|
|
234
|
-
* the movement through the change chunks within a file.
|
|
235
|
-
*
|
|
236
|
-
* If the selected tree node is a folder then the ARROW_RIGHT key behaves exactly
|
|
237
|
-
* as it does in explorer. It expands the tree node if the folder is collapsed and
|
|
238
|
-
* it moves the selection to the first child node if the folder is expanded.
|
|
239
|
-
* This behavior is the default behavior implemented
|
|
240
|
-
* in the TreeWidget super class.
|
|
241
|
-
*
|
|
242
|
-
* If the selected tree node is a file then the ARROW_RIGHT key moves down through the
|
|
243
|
-
* change chunks within each file. If the selected chunk is the last chunk in the file
|
|
244
|
-
* then the file selection is moved to the next file (no-op if no next file).
|
|
245
|
-
*/
|
|
246
|
-
protected override async handleRight(event: KeyboardEvent): Promise<void> {
|
|
247
|
-
if (this.model.selectedNodes.length === 0) {
|
|
248
|
-
const firstNode = this.getFirstSelectableNode();
|
|
249
|
-
// Selects the first visible resource as none are selected.
|
|
250
|
-
if (!firstNode) {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
this.model.selectNode(firstNode);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (this.model.selectedNodes.length === 1) {
|
|
257
|
-
const selectedNode = this.model.selectedNodes[0];
|
|
258
|
-
if (ScmFileChangeNode.is(selectedNode)) {
|
|
259
|
-
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
260
|
-
if (!selectedResource) {
|
|
261
|
-
return super.handleRight(event);
|
|
262
|
-
}
|
|
263
|
-
const widget = await this.openResource(selectedResource);
|
|
264
|
-
|
|
265
|
-
if (widget) {
|
|
266
|
-
const diffNavigator = this.diffNavigatorProvider(widget.editor);
|
|
267
|
-
if (diffNavigator.hasNext()) {
|
|
268
|
-
diffNavigator.next();
|
|
269
|
-
} else {
|
|
270
|
-
const nextNode = this.moveToNextFileNode();
|
|
271
|
-
if (nextNode) {
|
|
272
|
-
const nextResource = this.model.getResourceFromNode(nextNode);
|
|
273
|
-
if (nextResource) {
|
|
274
|
-
this.openResource(nextResource);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return super.handleRight(event);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
protected override handleEnter(event: KeyboardEvent): void {
|
|
286
|
-
if (this.model.selectedNodes.length === 1) {
|
|
287
|
-
const selectedNode = this.model.selectedNodes[0];
|
|
288
|
-
if (ScmFileChangeNode.is(selectedNode)) {
|
|
289
|
-
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
290
|
-
if (selectedResource) {
|
|
291
|
-
this.openResource(selectedResource);
|
|
292
|
-
}
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
super.handleEnter(event);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async goToPreviousChange(): Promise<void> {
|
|
300
|
-
if (this.model.selectedNodes.length === 1) {
|
|
301
|
-
const selectedNode = this.model.selectedNodes[0];
|
|
302
|
-
if (ScmFileChangeNode.is(selectedNode)) {
|
|
303
|
-
if (ScmFileChangeNode.is(selectedNode)) {
|
|
304
|
-
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
305
|
-
if (!selectedResource) {
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
const widget = await this.openResource(selectedResource);
|
|
309
|
-
|
|
310
|
-
if (widget) {
|
|
311
|
-
const diffNavigator = this.diffNavigatorProvider(widget.editor);
|
|
312
|
-
if (diffNavigator.hasPrevious()) {
|
|
313
|
-
diffNavigator.previous();
|
|
314
|
-
} else {
|
|
315
|
-
const previousNode = this.moveToPreviousFileNode();
|
|
316
|
-
if (previousNode) {
|
|
317
|
-
const previousResource = this.model.getResourceFromNode(previousNode);
|
|
318
|
-
if (previousResource) {
|
|
319
|
-
this.openResource(previousResource);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async goToNextChange(): Promise<void> {
|
|
330
|
-
if (this.model.selectedNodes.length === 0) {
|
|
331
|
-
const firstNode = this.getFirstSelectableNode();
|
|
332
|
-
// Selects the first visible resource as none are selected.
|
|
333
|
-
if (!firstNode) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
this.model.selectNode(firstNode);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
if (this.model.selectedNodes.length === 1) {
|
|
340
|
-
const selectedNode = this.model.selectedNodes[0];
|
|
341
|
-
if (ScmFileChangeNode.is(selectedNode)) {
|
|
342
|
-
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
343
|
-
if (!selectedResource) {
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
const widget = await this.openResource(selectedResource);
|
|
347
|
-
|
|
348
|
-
if (widget) {
|
|
349
|
-
const diffNavigator = this.diffNavigatorProvider(widget.editor);
|
|
350
|
-
if (diffNavigator.hasNext()) {
|
|
351
|
-
diffNavigator.next();
|
|
352
|
-
} else {
|
|
353
|
-
const nextNode = this.moveToNextFileNode();
|
|
354
|
-
if (nextNode) {
|
|
355
|
-
const nextResource = this.model.getResourceFromNode(nextNode);
|
|
356
|
-
if (nextResource) {
|
|
357
|
-
this.openResource(nextResource);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
selectNodeByUri(uri: URI): void {
|
|
367
|
-
for (const group of this.model.groups) {
|
|
368
|
-
const sourceUri = new URI(uri.path.toString());
|
|
369
|
-
const id = `${group.id}:${sourceUri.toString()}`;
|
|
370
|
-
const node = this.model.getNode(id);
|
|
371
|
-
if (SelectableTreeNode.is(node)) {
|
|
372
|
-
this.model.selectNode(node);
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
protected getFirstSelectableNode(): SelectableTreeNode | undefined {
|
|
379
|
-
if (this.model.root) {
|
|
380
|
-
const root = this.model.root as ScmFileChangeRootNode;
|
|
381
|
-
const groupNode = root.children[0];
|
|
382
|
-
return groupNode.children[0];
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
protected moveToPreviousFileNode(): ScmFileChangeNode | undefined {
|
|
387
|
-
let previousNode = this.model.getPrevSelectableNode();
|
|
388
|
-
while (previousNode) {
|
|
389
|
-
if (ScmFileChangeNode.is(previousNode)) {
|
|
390
|
-
this.model.selectNode(previousNode);
|
|
391
|
-
return previousNode;
|
|
392
|
-
}
|
|
393
|
-
previousNode = this.model.getPrevSelectableNode(previousNode);
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
protected moveToNextFileNode(): ScmFileChangeNode | undefined {
|
|
398
|
-
let nextNode = this.model.getNextSelectableNode();
|
|
399
|
-
while (nextNode) {
|
|
400
|
-
if (ScmFileChangeNode.is(nextNode)) {
|
|
401
|
-
this.model.selectNode(nextNode);
|
|
402
|
-
return nextNode;
|
|
403
|
-
}
|
|
404
|
-
nextNode = this.model.getNextSelectableNode(nextNode);
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
protected async openResource(resource: ScmResource): Promise<EditorWidget | undefined> {
|
|
409
|
-
try {
|
|
410
|
-
await resource.open();
|
|
411
|
-
} catch (e) {
|
|
412
|
-
console.error('Failed to open a SCM resource', e);
|
|
413
|
-
return undefined;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
let standaloneEditor: EditorWidget | undefined;
|
|
417
|
-
const resourcePath = resource.sourceUri.path.toString();
|
|
418
|
-
|
|
419
|
-
for (const widget of this.editorManager.all) {
|
|
420
|
-
const resourceUri = widget.editor.document.uri;
|
|
421
|
-
const editorResourcePath = new URI(resourceUri).path.toString();
|
|
422
|
-
if (resourcePath === editorResourcePath) {
|
|
423
|
-
if (widget.editor.uri.scheme === DiffUris.DIFF_SCHEME) {
|
|
424
|
-
// prefer diff editor
|
|
425
|
-
return widget;
|
|
426
|
-
} else {
|
|
427
|
-
standaloneEditor = widget;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
if (widget.editor.uri.scheme === DiffUris.DIFF_SCHEME
|
|
431
|
-
&& resourceUri === resource.sourceUri.toString()) {
|
|
432
|
-
return widget;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
// fallback to standalone editor
|
|
436
|
-
return standaloneEditor;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
protected override getPaddingLeft(node: TreeNode, props: NodeProps): number {
|
|
440
|
-
if (this.viewMode === 'list') {
|
|
441
|
-
if (props.depth === 1) {
|
|
442
|
-
return this.props.expansionTogglePadding;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return super.getPaddingLeft(node, props);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
protected override getDepthPadding(depth: number): number {
|
|
449
|
-
return super.getDepthPadding(depth) + 5;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
protected isCurrentThemeLight(): boolean {
|
|
453
|
-
const type = this.themeService.getCurrentTheme().type;
|
|
454
|
-
return type.toLocaleLowerCase().includes('light');
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
protected override needsExpansionTogglePadding(node: TreeNode): boolean {
|
|
458
|
-
const theme = this.iconThemeService.getDefinition(this.iconThemeService.current);
|
|
459
|
-
if (theme && (theme.hidesExplorerArrows || (theme.hasFileIcons && !theme.hasFolderIcons))) {
|
|
460
|
-
return false;
|
|
461
|
-
}
|
|
462
|
-
return super.needsExpansionTogglePadding(node);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export namespace ScmTreeWidget {
|
|
468
|
-
export namespace Styles {
|
|
469
|
-
export const NO_SELECT = 'no-select';
|
|
470
|
-
}
|
|
471
|
-
// This is an 'abstract' base interface for all the element component props.
|
|
472
|
-
export interface Props {
|
|
473
|
-
treeNode: TreeNode;
|
|
474
|
-
model: ScmTreeModel;
|
|
475
|
-
menus: MenuModelRegistry;
|
|
476
|
-
contextKeys: ScmContextKeyService;
|
|
477
|
-
labelProvider: LabelProvider;
|
|
478
|
-
contextMenuRenderer: ContextMenuRenderer;
|
|
479
|
-
corePreferences?: CorePreferences;
|
|
480
|
-
caption: React.ReactNode;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
export abstract class ScmElement<P extends ScmElement.Props = ScmElement.Props> extends React.Component<P, ScmElement.State> {
|
|
485
|
-
|
|
486
|
-
constructor(props: P) {
|
|
487
|
-
super(props);
|
|
488
|
-
this.state = {
|
|
489
|
-
hover: false
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
const setState = this.setState.bind(this);
|
|
493
|
-
this.setState = newState => {
|
|
494
|
-
if (!this.toDisposeOnUnmount.disposed) {
|
|
495
|
-
setState(newState);
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
protected readonly toDisposeOnUnmount = new DisposableCollection();
|
|
501
|
-
override componentDidMount(): void {
|
|
502
|
-
this.toDisposeOnUnmount.push(Disposable.create(() => { /* mark as mounted */ }));
|
|
503
|
-
}
|
|
504
|
-
override componentWillUnmount(): void {
|
|
505
|
-
this.toDisposeOnUnmount.dispose();
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
protected detectHover = (element: HTMLElement | null) => {
|
|
509
|
-
if (element) {
|
|
510
|
-
window.requestAnimationFrame(() => {
|
|
511
|
-
const hover = element.matches(':hover');
|
|
512
|
-
this.setState({ hover });
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
};
|
|
516
|
-
protected showHover = () => this.setState({ hover: true });
|
|
517
|
-
protected hideHover = () => this.setState({ hover: false });
|
|
518
|
-
|
|
519
|
-
protected renderContextMenu = (event: React.MouseEvent<HTMLElement>) => {
|
|
520
|
-
event.preventDefault();
|
|
521
|
-
const { treeNode: node, contextMenuRenderer } = this.props;
|
|
522
|
-
this.props.model.execInNodeContext(node, () => {
|
|
523
|
-
contextMenuRenderer.render({
|
|
524
|
-
menuPath: this.contextMenuPath,
|
|
525
|
-
anchor: event.nativeEvent,
|
|
526
|
-
args: this.contextMenuArgs
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
protected abstract get contextMenuPath(): MenuPath;
|
|
532
|
-
protected abstract get contextMenuArgs(): any[];
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
export namespace ScmElement {
|
|
536
|
-
export interface Props extends ScmTreeWidget.Props {
|
|
537
|
-
renderExpansionToggle: () => React.ReactNode;
|
|
538
|
-
commandExecutor: MenuCommandExecutor;
|
|
539
|
-
}
|
|
540
|
-
export interface State {
|
|
541
|
-
hover: boolean
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
export class ScmResourceComponent extends ScmElement<ScmResourceComponent.Props> {
|
|
546
|
-
|
|
547
|
-
override render(): JSX.Element | undefined {
|
|
548
|
-
const { hover } = this.state;
|
|
549
|
-
const { model, treeNode, colors, parentPath, sourceUri, decoration, labelProvider, commandExecutor, menus, contextKeys, caption, isLightTheme } = this.props;
|
|
550
|
-
const resourceUri = new URI(sourceUri);
|
|
551
|
-
|
|
552
|
-
const decorationIcon = treeNode.decorations;
|
|
553
|
-
const themedIcon = isLightTheme ? decorationIcon?.icon : decorationIcon?.iconDark;
|
|
554
|
-
const classNames: string[] = themedIcon ? ['decoration-icon', themedIcon] : ['decoration-icon', 'status'];
|
|
555
|
-
|
|
556
|
-
const icon = labelProvider.getIcon(resourceUri);
|
|
557
|
-
const color = decoration && decoration.colorId && !themedIcon ? `var(${colors.toCssVariableName(decoration.colorId)})` : '';
|
|
558
|
-
const letter = decoration && decoration.letter && !themedIcon ? decoration.letter : '';
|
|
559
|
-
const tooltip = decoration && decoration.tooltip || '';
|
|
560
|
-
const textDecoration = treeNode.decorations?.strikeThrough === true ? 'line-through' : 'normal';
|
|
561
|
-
const relativePath = parentPath.relative(resourceUri.parent);
|
|
562
|
-
const path = relativePath ? relativePath.fsPath() : labelProvider.getLongName(resourceUri.parent);
|
|
563
|
-
const title = tooltip.length !== 0
|
|
564
|
-
? `${resourceUri.path.fsPath()} • ${tooltip}`
|
|
565
|
-
: resourceUri.path.fsPath();
|
|
566
|
-
|
|
567
|
-
return <div key={sourceUri}
|
|
568
|
-
className={`scmItem ${TREE_NODE_SEGMENT_CLASS} ${TREE_NODE_SEGMENT_GROW_CLASS}`}
|
|
569
|
-
onContextMenu={this.renderContextMenu}
|
|
570
|
-
onMouseEnter={this.showHover}
|
|
571
|
-
onMouseLeave={this.hideHover}
|
|
572
|
-
ref={this.detectHover}
|
|
573
|
-
title={title}
|
|
574
|
-
onClick={this.handleClick}
|
|
575
|
-
onDoubleClick={this.handleDoubleClick} >
|
|
576
|
-
<span className={icon + ' file-icon'} />
|
|
577
|
-
{this.props.renderExpansionToggle()}
|
|
578
|
-
<div className={`noWrapInfo ${TREE_NODE_SEGMENT_GROW_CLASS}`} >
|
|
579
|
-
<span className='name' style={{ textDecoration }}>{caption}</span>
|
|
580
|
-
<span className='path' style={{ textDecoration }}>{path}</span>
|
|
581
|
-
</div>
|
|
582
|
-
<ScmInlineActions {...{
|
|
583
|
-
hover,
|
|
584
|
-
menu: menus.getMenu(ScmTreeWidget.RESOURCE_INLINE_MENU),
|
|
585
|
-
menuPath: ScmTreeWidget.RESOURCE_INLINE_MENU,
|
|
586
|
-
commandExecutor,
|
|
587
|
-
args: this.contextMenuArgs,
|
|
588
|
-
contextKeys,
|
|
589
|
-
model,
|
|
590
|
-
treeNode
|
|
591
|
-
}}>
|
|
592
|
-
<div title={tooltip} className={classNames.join(' ')} style={{ color }}>
|
|
593
|
-
{letter}
|
|
594
|
-
</div>
|
|
595
|
-
</ScmInlineActions>
|
|
596
|
-
</div >;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
protected open = () => {
|
|
600
|
-
const resource = this.props.model.getResourceFromNode(this.props.treeNode);
|
|
601
|
-
if (resource) {
|
|
602
|
-
resource.open();
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
protected readonly contextMenuPath = ScmTreeWidget.RESOURCE_CONTEXT_MENU;
|
|
607
|
-
protected get contextMenuArgs(): any[] {
|
|
608
|
-
if (!this.props.model.selectedNodes.some(node => ScmFileChangeNode.is(node) && node.sourceUri === this.props.sourceUri)) {
|
|
609
|
-
// Clicked node is not in selection, so ignore selection and action on just clicked node
|
|
610
|
-
return this.singleNodeArgs;
|
|
611
|
-
} else {
|
|
612
|
-
return this.props.model.getSelectionArgs(this.props.model.selectedNodes);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
protected get singleNodeArgs(): any[] {
|
|
616
|
-
const selectedResource = this.props.model.getResourceFromNode(this.props.treeNode);
|
|
617
|
-
if (selectedResource) {
|
|
618
|
-
return [selectedResource];
|
|
619
|
-
} else {
|
|
620
|
-
// Repository status not yet available. Empty args disables the action.
|
|
621
|
-
return [];
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
protected hasCtrlCmdOrShiftMask(event: TreeWidget.ModifierAwareEvent): boolean {
|
|
626
|
-
const { metaKey, ctrlKey, shiftKey } = event;
|
|
627
|
-
return (isOSX && metaKey) || ctrlKey || shiftKey;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Handle the single clicking of nodes present in the widget.
|
|
632
|
-
*/
|
|
633
|
-
protected handleClick = (event: React.MouseEvent) => {
|
|
634
|
-
if (!this.hasCtrlCmdOrShiftMask(event)) {
|
|
635
|
-
// Determine the behavior based on the preference value.
|
|
636
|
-
const isSingle = this.props.corePreferences && this.props.corePreferences['workbench.list.openMode'] === 'singleClick';
|
|
637
|
-
if (isSingle) {
|
|
638
|
-
this.open();
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Handle the double clicking of nodes present in the widget.
|
|
645
|
-
*/
|
|
646
|
-
protected handleDoubleClick = () => {
|
|
647
|
-
// Determine the behavior based on the preference value.
|
|
648
|
-
const isDouble = this.props.corePreferences && this.props.corePreferences['workbench.list.openMode'] === 'doubleClick';
|
|
649
|
-
// Nodes should only be opened through double clicking if the correct preference is set.
|
|
650
|
-
if (isDouble) {
|
|
651
|
-
this.open();
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
export namespace ScmResourceComponent {
|
|
657
|
-
export interface Props extends ScmElement.Props {
|
|
658
|
-
treeNode: ScmFileChangeNode;
|
|
659
|
-
parentPath: URI;
|
|
660
|
-
sourceUri: string;
|
|
661
|
-
decoration: Decoration | undefined;
|
|
662
|
-
colors: ColorRegistry;
|
|
663
|
-
isLightTheme: boolean
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
export class ScmResourceGroupElement extends ScmElement<ScmResourceGroupComponent.Props> {
|
|
668
|
-
|
|
669
|
-
override render(): JSX.Element {
|
|
670
|
-
const { hover } = this.state;
|
|
671
|
-
const { model, treeNode, menus, commandExecutor, contextKeys, caption } = this.props;
|
|
672
|
-
return <div className={`theia-header scm-theia-header ${TREE_NODE_SEGMENT_GROW_CLASS}`}
|
|
673
|
-
onContextMenu={this.renderContextMenu}
|
|
674
|
-
onMouseEnter={this.showHover}
|
|
675
|
-
onMouseLeave={this.hideHover}
|
|
676
|
-
ref={this.detectHover}>
|
|
677
|
-
{this.props.renderExpansionToggle()}
|
|
678
|
-
<div className={`noWrapInfo ${TREE_NODE_SEGMENT_GROW_CLASS}`}>{caption}</div>
|
|
679
|
-
<ScmInlineActions {...{
|
|
680
|
-
hover,
|
|
681
|
-
args: this.contextMenuArgs,
|
|
682
|
-
menu: menus.getMenu(ScmTreeWidget.RESOURCE_GROUP_INLINE_MENU),
|
|
683
|
-
menuPath: ScmTreeWidget.RESOURCE_GROUP_INLINE_MENU,
|
|
684
|
-
commandExecutor,
|
|
685
|
-
contextKeys,
|
|
686
|
-
model,
|
|
687
|
-
treeNode
|
|
688
|
-
}}>
|
|
689
|
-
{this.renderChangeCount()}
|
|
690
|
-
</ScmInlineActions>
|
|
691
|
-
</div>;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
protected renderChangeCount(): React.ReactNode {
|
|
695
|
-
const group = this.props.model.getResourceGroupFromNode(this.props.treeNode);
|
|
696
|
-
return <div className='notification-count-container scm-change-count'>
|
|
697
|
-
<span className='notification-count'>{group ? group.resources.length : 0}</span>
|
|
698
|
-
</div>;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
protected readonly contextMenuPath = ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU;
|
|
702
|
-
protected get contextMenuArgs(): any[] {
|
|
703
|
-
const group = this.props.model.getResourceGroupFromNode(this.props.treeNode);
|
|
704
|
-
if (group) {
|
|
705
|
-
return [group];
|
|
706
|
-
} else {
|
|
707
|
-
// Repository status not yet available. Empty args disables the action.
|
|
708
|
-
return [];
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
export namespace ScmResourceGroupComponent {
|
|
713
|
-
export interface Props extends ScmElement.Props {
|
|
714
|
-
treeNode: ScmFileChangeGroupNode;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
export class ScmResourceFolderElement extends ScmElement<ScmResourceFolderElement.Props> {
|
|
719
|
-
|
|
720
|
-
override render(): JSX.Element {
|
|
721
|
-
const { hover } = this.state;
|
|
722
|
-
const { model, treeNode, sourceUri, labelProvider, commandExecutor, menus, contextKeys, caption } = this.props;
|
|
723
|
-
const sourceFileStat = FileStat.dir(sourceUri);
|
|
724
|
-
const icon = labelProvider.getIcon(sourceFileStat);
|
|
725
|
-
const title = new URI(sourceUri).path.fsPath();
|
|
726
|
-
|
|
727
|
-
return <div key={sourceUri}
|
|
728
|
-
className={`scmItem ${TREE_NODE_SEGMENT_CLASS} ${TREE_NODE_SEGMENT_GROW_CLASS} ${ScmTreeWidget.Styles.NO_SELECT}`}
|
|
729
|
-
title={title}
|
|
730
|
-
onContextMenu={this.renderContextMenu}
|
|
731
|
-
onMouseEnter={this.showHover}
|
|
732
|
-
onMouseLeave={this.hideHover}
|
|
733
|
-
ref={this.detectHover}
|
|
734
|
-
>
|
|
735
|
-
{this.props.renderExpansionToggle()}
|
|
736
|
-
<span className={icon + ' file-icon'} />
|
|
737
|
-
<div className={`noWrapInfo ${TREE_NODE_SEGMENT_GROW_CLASS}`} >
|
|
738
|
-
<span className='name'>{caption}</span>
|
|
739
|
-
</div>
|
|
740
|
-
<ScmInlineActions {...{
|
|
741
|
-
hover,
|
|
742
|
-
menu: menus.getMenu(ScmTreeWidget.RESOURCE_FOLDER_INLINE_MENU),
|
|
743
|
-
menuPath: ScmTreeWidget.RESOURCE_FOLDER_INLINE_MENU,
|
|
744
|
-
commandExecutor,
|
|
745
|
-
args: this.contextMenuArgs,
|
|
746
|
-
contextKeys,
|
|
747
|
-
model,
|
|
748
|
-
treeNode
|
|
749
|
-
}}>
|
|
750
|
-
</ScmInlineActions>
|
|
751
|
-
</div >;
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
protected readonly contextMenuPath = ScmTreeWidget.RESOURCE_FOLDER_CONTEXT_MENU;
|
|
756
|
-
protected get contextMenuArgs(): any[] {
|
|
757
|
-
if (!this.props.model.selectedNodes.some(node => ScmFileChangeFolderNode.is(node) && node.sourceUri === this.props.sourceUri)) {
|
|
758
|
-
// Clicked node is not in selection, so ignore selection and action on just clicked node
|
|
759
|
-
return this.singleNodeArgs;
|
|
760
|
-
} else {
|
|
761
|
-
return this.props.model.getSelectionArgs(this.props.model.selectedNodes);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
protected get singleNodeArgs(): any[] {
|
|
765
|
-
return this.props.model.getResourcesFromFolderNode(this.props.treeNode);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
export namespace ScmResourceFolderElement {
|
|
771
|
-
export interface Props extends ScmElement.Props {
|
|
772
|
-
treeNode: ScmFileChangeFolderNode;
|
|
773
|
-
sourceUri: string;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
export class ScmInlineActions extends React.Component<ScmInlineActions.Props> {
|
|
778
|
-
override render(): React.ReactNode {
|
|
779
|
-
const { hover, menu, menuPath, args, commandExecutor, model, treeNode, contextKeys, children } = this.props;
|
|
780
|
-
return <div className='theia-scm-inline-actions-container'>
|
|
781
|
-
<div className='theia-scm-inline-actions'>
|
|
782
|
-
{hover && menu.children
|
|
783
|
-
.map((node, index) => node instanceof ActionMenuNode &&
|
|
784
|
-
<ScmInlineAction key={index} {...{ node, menuPath, args, commandExecutor, model, treeNode, contextKeys }} />)}
|
|
785
|
-
</div>
|
|
786
|
-
{children}
|
|
787
|
-
</div>;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
export namespace ScmInlineActions {
|
|
791
|
-
export interface Props {
|
|
792
|
-
hover: boolean;
|
|
793
|
-
menu: CompoundMenuNode;
|
|
794
|
-
menuPath: MenuPath;
|
|
795
|
-
commandExecutor: MenuCommandExecutor;
|
|
796
|
-
model: ScmTreeModel;
|
|
797
|
-
treeNode: TreeNode;
|
|
798
|
-
contextKeys: ScmContextKeyService;
|
|
799
|
-
args: any[];
|
|
800
|
-
children?: React.ReactNode;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
export class ScmInlineAction extends React.Component<ScmInlineAction.Props> {
|
|
805
|
-
override render(): React.ReactNode {
|
|
806
|
-
const { node, model, treeNode, args, commandExecutor, menuPath, contextKeys } = this.props;
|
|
807
|
-
|
|
808
|
-
let isActive: boolean = false;
|
|
809
|
-
model.execInNodeContext(treeNode, () => {
|
|
810
|
-
isActive = contextKeys.match(node.when);
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
if (!commandExecutor.isVisible(menuPath, node.command, ...args) || !isActive) {
|
|
814
|
-
return false;
|
|
815
|
-
}
|
|
816
|
-
return <div className='theia-scm-inline-action'>
|
|
817
|
-
<a className={`${node.icon} ${ACTION_ITEM}`} title={node.label} onClick={this.execute} />
|
|
818
|
-
</div>;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
protected execute = (event: React.MouseEvent) => {
|
|
822
|
-
event.stopPropagation();
|
|
823
|
-
|
|
824
|
-
const { commandExecutor, menuPath, node, args } = this.props;
|
|
825
|
-
commandExecutor.executeCommand([menuPath[0]], node.command, ...args);
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
export namespace ScmInlineAction {
|
|
829
|
-
export interface Props {
|
|
830
|
-
node: ActionMenuNode;
|
|
831
|
-
commandExecutor: MenuCommandExecutor;
|
|
832
|
-
menuPath: MenuPath;
|
|
833
|
-
model: ScmTreeModel;
|
|
834
|
-
treeNode: TreeNode;
|
|
835
|
-
contextKeys: ScmContextKeyService;
|
|
836
|
-
args: any[];
|
|
837
|
-
}
|
|
838
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2020 Arm 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
|
+
/* eslint-disable no-null/no-null, @typescript-eslint/no-explicit-any */
|
|
18
|
+
|
|
19
|
+
import * as React from '@theia/core/shared/react';
|
|
20
|
+
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
21
|
+
import URI from '@theia/core/lib/common/uri';
|
|
22
|
+
import { isOSX } from '@theia/core/lib/common/os';
|
|
23
|
+
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
|
|
24
|
+
import { TreeWidget, TreeNode, SelectableTreeNode, TreeModel, TreeProps, NodeProps, TREE_NODE_SEGMENT_CLASS, TREE_NODE_SEGMENT_GROW_CLASS } from '@theia/core/lib/browser/tree';
|
|
25
|
+
import { ScmTreeModel, ScmFileChangeRootNode, ScmFileChangeGroupNode, ScmFileChangeFolderNode, ScmFileChangeNode } from './scm-tree-model';
|
|
26
|
+
import { MenuCommandExecutor, MenuModelRegistry, ActionMenuNode, CompoundMenuNode, MenuPath } from '@theia/core/lib/common/menu';
|
|
27
|
+
import { ScmResource } from './scm-provider';
|
|
28
|
+
import { ContextMenuRenderer, LabelProvider, CorePreferences, DiffUris, ACTION_ITEM } from '@theia/core/lib/browser';
|
|
29
|
+
import { ScmContextKeyService } from './scm-context-key-service';
|
|
30
|
+
import { EditorWidget, EditorManager, DiffNavigatorProvider } from '@theia/editor/lib/browser';
|
|
31
|
+
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
|
|
32
|
+
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
|
33
|
+
import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service';
|
|
34
|
+
import { FileStat } from '@theia/filesystem/lib/common/files';
|
|
35
|
+
import { ThemeService } from '@theia/core/lib/browser/theming';
|
|
36
|
+
|
|
37
|
+
@injectable()
|
|
38
|
+
export class ScmTreeWidget extends TreeWidget {
|
|
39
|
+
|
|
40
|
+
static ID = 'scm-resource-widget';
|
|
41
|
+
|
|
42
|
+
static RESOURCE_GROUP_CONTEXT_MENU = ['RESOURCE_GROUP_CONTEXT_MENU'];
|
|
43
|
+
static RESOURCE_GROUP_INLINE_MENU = ['RESOURCE_GROUP_CONTEXT_MENU', 'inline'];
|
|
44
|
+
|
|
45
|
+
static RESOURCE_FOLDER_CONTEXT_MENU = ['RESOURCE_FOLDER_CONTEXT_MENU'];
|
|
46
|
+
static RESOURCE_FOLDER_INLINE_MENU = ['RESOURCE_FOLDER_CONTEXT_MENU', 'inline'];
|
|
47
|
+
|
|
48
|
+
static RESOURCE_CONTEXT_MENU = ['RESOURCE_CONTEXT_MENU'];
|
|
49
|
+
static RESOURCE_INLINE_MENU = ['RESOURCE_CONTEXT_MENU', 'inline'];
|
|
50
|
+
|
|
51
|
+
@inject(MenuCommandExecutor) protected readonly menuCommandExecutor: MenuCommandExecutor;
|
|
52
|
+
@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry;
|
|
53
|
+
@inject(ScmContextKeyService) protected readonly contextKeys: ScmContextKeyService;
|
|
54
|
+
@inject(EditorManager) protected readonly editorManager: EditorManager;
|
|
55
|
+
@inject(DiffNavigatorProvider) protected readonly diffNavigatorProvider: DiffNavigatorProvider;
|
|
56
|
+
@inject(IconThemeService) protected readonly iconThemeService: IconThemeService;
|
|
57
|
+
@inject(DecorationsService) protected readonly decorationsService: DecorationsService;
|
|
58
|
+
@inject(ColorRegistry) protected readonly colors: ColorRegistry;
|
|
59
|
+
@inject(ThemeService) protected readonly themeService: ThemeService;
|
|
60
|
+
|
|
61
|
+
// TODO: Make TreeWidget generic to better type those fields.
|
|
62
|
+
override readonly model: ScmTreeModel;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
@inject(TreeProps) props: TreeProps,
|
|
66
|
+
@inject(TreeModel) treeModel: ScmTreeModel,
|
|
67
|
+
@inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer,
|
|
68
|
+
) {
|
|
69
|
+
super(props, treeModel, contextMenuRenderer);
|
|
70
|
+
this.id = ScmTreeWidget.ID;
|
|
71
|
+
this.addClass('groups-outer-container');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@postConstruct()
|
|
75
|
+
protected override init(): void {
|
|
76
|
+
super.init();
|
|
77
|
+
this.toDispose.push(this.themeService.onDidColorThemeChange(() => this.update()));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
set viewMode(id: 'tree' | 'list') {
|
|
81
|
+
// Close the search box because the structure of the tree will change dramatically
|
|
82
|
+
// and the search results will be out of date.
|
|
83
|
+
this.searchBox.hide();
|
|
84
|
+
this.model.viewMode = id;
|
|
85
|
+
}
|
|
86
|
+
get viewMode(): 'tree' | 'list' {
|
|
87
|
+
return this.model.viewMode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render the node given the tree node and node properties.
|
|
92
|
+
* @param node the tree node.
|
|
93
|
+
* @param props the node properties.
|
|
94
|
+
*/
|
|
95
|
+
protected override renderNode(node: TreeNode, props: NodeProps): React.ReactNode {
|
|
96
|
+
if (!TreeNode.isVisible(node)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const attributes = this.createNodeAttributes(node, props);
|
|
101
|
+
const label = this.labelProvider.getName(node);
|
|
102
|
+
const searchHighlights = this.searchHighlights?.get(node.id);
|
|
103
|
+
// The group nodes should not be subject to highlighting.
|
|
104
|
+
const caption = (searchHighlights && !ScmFileChangeGroupNode.is(node)) ? this.toReactNode(label, searchHighlights) : label;
|
|
105
|
+
|
|
106
|
+
if (ScmFileChangeGroupNode.is(node)) {
|
|
107
|
+
const content = <ScmResourceGroupElement
|
|
108
|
+
key={`${node.groupId}`}
|
|
109
|
+
model={this.model}
|
|
110
|
+
treeNode={node}
|
|
111
|
+
renderExpansionToggle={() => this.renderExpansionToggle(node, props)}
|
|
112
|
+
commandExecutor={this.menuCommandExecutor}
|
|
113
|
+
contextMenuRenderer={this.contextMenuRenderer}
|
|
114
|
+
menus={this.menus}
|
|
115
|
+
contextKeys={this.contextKeys}
|
|
116
|
+
labelProvider={this.labelProvider}
|
|
117
|
+
corePreferences={this.corePreferences}
|
|
118
|
+
caption={caption}
|
|
119
|
+
/>;
|
|
120
|
+
|
|
121
|
+
return React.createElement('div', attributes, content);
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
if (ScmFileChangeFolderNode.is(node)) {
|
|
125
|
+
const content = <ScmResourceFolderElement
|
|
126
|
+
key={String(node.sourceUri)}
|
|
127
|
+
model={this.model}
|
|
128
|
+
treeNode={node}
|
|
129
|
+
sourceUri={node.sourceUri}
|
|
130
|
+
renderExpansionToggle={() => this.renderExpansionToggle(node, props)}
|
|
131
|
+
commandExecutor={this.menuCommandExecutor}
|
|
132
|
+
contextMenuRenderer={this.contextMenuRenderer}
|
|
133
|
+
menus={this.menus}
|
|
134
|
+
contextKeys={this.contextKeys}
|
|
135
|
+
labelProvider={this.labelProvider}
|
|
136
|
+
corePreferences={this.corePreferences}
|
|
137
|
+
caption={caption}
|
|
138
|
+
/>;
|
|
139
|
+
|
|
140
|
+
return React.createElement('div', attributes, content);
|
|
141
|
+
}
|
|
142
|
+
if (ScmFileChangeNode.is(node)) {
|
|
143
|
+
const parentPath =
|
|
144
|
+
(node.parent && ScmFileChangeFolderNode.is(node.parent))
|
|
145
|
+
? new URI(node.parent.sourceUri) : new URI(this.model.rootUri);
|
|
146
|
+
|
|
147
|
+
const content = <ScmResourceComponent
|
|
148
|
+
key={node.sourceUri}
|
|
149
|
+
model={this.model}
|
|
150
|
+
treeNode={node}
|
|
151
|
+
contextMenuRenderer={this.contextMenuRenderer}
|
|
152
|
+
commandExecutor={this.menuCommandExecutor}
|
|
153
|
+
menus={this.menus}
|
|
154
|
+
contextKeys={this.contextKeys}
|
|
155
|
+
labelProvider={this.labelProvider}
|
|
156
|
+
corePreferences={this.corePreferences}
|
|
157
|
+
caption={caption}
|
|
158
|
+
{...{
|
|
159
|
+
...this.props,
|
|
160
|
+
parentPath,
|
|
161
|
+
sourceUri: node.sourceUri,
|
|
162
|
+
decoration: this.decorationsService.getDecoration(new URI(node.sourceUri), true)[0],
|
|
163
|
+
colors: this.colors,
|
|
164
|
+
isLightTheme: this.isCurrentThemeLight(),
|
|
165
|
+
renderExpansionToggle: () => this.renderExpansionToggle(node, props),
|
|
166
|
+
}}
|
|
167
|
+
/>;
|
|
168
|
+
return React.createElement('div', attributes, content);
|
|
169
|
+
}
|
|
170
|
+
return super.renderNode(node, props);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
protected override createContainerAttributes(): React.HTMLAttributes<HTMLElement> {
|
|
174
|
+
if (this.model.canTabToWidget()) {
|
|
175
|
+
return {
|
|
176
|
+
...super.createContainerAttributes(),
|
|
177
|
+
tabIndex: 0
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return super.createContainerAttributes();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The ARROW_LEFT key controls both the movement around the file tree and also
|
|
185
|
+
* the movement through the change chunks within a file.
|
|
186
|
+
*
|
|
187
|
+
* If the selected tree node is a folder then the ARROW_LEFT key behaves exactly
|
|
188
|
+
* as it does in explorer. It collapses the tree node if the folder is expanded and
|
|
189
|
+
* it moves the selection up to the parent folder if the folder is collapsed (no-op if no parent folder, as
|
|
190
|
+
* group headers are not selectable). This behavior is the default behavior implemented
|
|
191
|
+
* in the TreeWidget super class.
|
|
192
|
+
*
|
|
193
|
+
* If the selected tree node is a file then the ARROW_LEFT key moves up through the
|
|
194
|
+
* change chunks within each file. If the selected chunk is the first chunk in the file
|
|
195
|
+
* then the file selection is moved to the previous file (no-op if no previous file).
|
|
196
|
+
*
|
|
197
|
+
* Note that when cursoring through change chunks, the ARROW_LEFT key cannot be used to
|
|
198
|
+
* move up through the parent folders of the file tree. If users want to do this, using
|
|
199
|
+
* keys only, then they must press ARROW_UP repeatedly until the selected node is the folder
|
|
200
|
+
* node and then press ARROW_LEFT.
|
|
201
|
+
*/
|
|
202
|
+
protected override async handleLeft(event: KeyboardEvent): Promise<void> {
|
|
203
|
+
if (this.model.selectedNodes.length === 1) {
|
|
204
|
+
const selectedNode = this.model.selectedNodes[0];
|
|
205
|
+
if (ScmFileChangeNode.is(selectedNode)) {
|
|
206
|
+
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
207
|
+
if (!selectedResource) {
|
|
208
|
+
return super.handleLeft(event);
|
|
209
|
+
}
|
|
210
|
+
const widget = await this.openResource(selectedResource);
|
|
211
|
+
|
|
212
|
+
if (widget) {
|
|
213
|
+
const diffNavigator = this.diffNavigatorProvider(widget.editor);
|
|
214
|
+
if (diffNavigator.hasPrevious()) {
|
|
215
|
+
diffNavigator.previous();
|
|
216
|
+
} else {
|
|
217
|
+
const previousNode = this.moveToPreviousFileNode();
|
|
218
|
+
if (previousNode) {
|
|
219
|
+
const previousResource = this.model.getResourceFromNode(previousNode);
|
|
220
|
+
if (previousResource) {
|
|
221
|
+
this.openResource(previousResource);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return super.handleLeft(event);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The ARROW_RIGHT key controls both the movement around the file tree and also
|
|
234
|
+
* the movement through the change chunks within a file.
|
|
235
|
+
*
|
|
236
|
+
* If the selected tree node is a folder then the ARROW_RIGHT key behaves exactly
|
|
237
|
+
* as it does in explorer. It expands the tree node if the folder is collapsed and
|
|
238
|
+
* it moves the selection to the first child node if the folder is expanded.
|
|
239
|
+
* This behavior is the default behavior implemented
|
|
240
|
+
* in the TreeWidget super class.
|
|
241
|
+
*
|
|
242
|
+
* If the selected tree node is a file then the ARROW_RIGHT key moves down through the
|
|
243
|
+
* change chunks within each file. If the selected chunk is the last chunk in the file
|
|
244
|
+
* then the file selection is moved to the next file (no-op if no next file).
|
|
245
|
+
*/
|
|
246
|
+
protected override async handleRight(event: KeyboardEvent): Promise<void> {
|
|
247
|
+
if (this.model.selectedNodes.length === 0) {
|
|
248
|
+
const firstNode = this.getFirstSelectableNode();
|
|
249
|
+
// Selects the first visible resource as none are selected.
|
|
250
|
+
if (!firstNode) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.model.selectNode(firstNode);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (this.model.selectedNodes.length === 1) {
|
|
257
|
+
const selectedNode = this.model.selectedNodes[0];
|
|
258
|
+
if (ScmFileChangeNode.is(selectedNode)) {
|
|
259
|
+
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
260
|
+
if (!selectedResource) {
|
|
261
|
+
return super.handleRight(event);
|
|
262
|
+
}
|
|
263
|
+
const widget = await this.openResource(selectedResource);
|
|
264
|
+
|
|
265
|
+
if (widget) {
|
|
266
|
+
const diffNavigator = this.diffNavigatorProvider(widget.editor);
|
|
267
|
+
if (diffNavigator.hasNext()) {
|
|
268
|
+
diffNavigator.next();
|
|
269
|
+
} else {
|
|
270
|
+
const nextNode = this.moveToNextFileNode();
|
|
271
|
+
if (nextNode) {
|
|
272
|
+
const nextResource = this.model.getResourceFromNode(nextNode);
|
|
273
|
+
if (nextResource) {
|
|
274
|
+
this.openResource(nextResource);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return super.handleRight(event);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
protected override handleEnter(event: KeyboardEvent): void {
|
|
286
|
+
if (this.model.selectedNodes.length === 1) {
|
|
287
|
+
const selectedNode = this.model.selectedNodes[0];
|
|
288
|
+
if (ScmFileChangeNode.is(selectedNode)) {
|
|
289
|
+
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
290
|
+
if (selectedResource) {
|
|
291
|
+
this.openResource(selectedResource);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
super.handleEnter(event);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async goToPreviousChange(): Promise<void> {
|
|
300
|
+
if (this.model.selectedNodes.length === 1) {
|
|
301
|
+
const selectedNode = this.model.selectedNodes[0];
|
|
302
|
+
if (ScmFileChangeNode.is(selectedNode)) {
|
|
303
|
+
if (ScmFileChangeNode.is(selectedNode)) {
|
|
304
|
+
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
305
|
+
if (!selectedResource) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const widget = await this.openResource(selectedResource);
|
|
309
|
+
|
|
310
|
+
if (widget) {
|
|
311
|
+
const diffNavigator = this.diffNavigatorProvider(widget.editor);
|
|
312
|
+
if (diffNavigator.hasPrevious()) {
|
|
313
|
+
diffNavigator.previous();
|
|
314
|
+
} else {
|
|
315
|
+
const previousNode = this.moveToPreviousFileNode();
|
|
316
|
+
if (previousNode) {
|
|
317
|
+
const previousResource = this.model.getResourceFromNode(previousNode);
|
|
318
|
+
if (previousResource) {
|
|
319
|
+
this.openResource(previousResource);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async goToNextChange(): Promise<void> {
|
|
330
|
+
if (this.model.selectedNodes.length === 0) {
|
|
331
|
+
const firstNode = this.getFirstSelectableNode();
|
|
332
|
+
// Selects the first visible resource as none are selected.
|
|
333
|
+
if (!firstNode) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.model.selectNode(firstNode);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (this.model.selectedNodes.length === 1) {
|
|
340
|
+
const selectedNode = this.model.selectedNodes[0];
|
|
341
|
+
if (ScmFileChangeNode.is(selectedNode)) {
|
|
342
|
+
const selectedResource = this.model.getResourceFromNode(selectedNode);
|
|
343
|
+
if (!selectedResource) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const widget = await this.openResource(selectedResource);
|
|
347
|
+
|
|
348
|
+
if (widget) {
|
|
349
|
+
const diffNavigator = this.diffNavigatorProvider(widget.editor);
|
|
350
|
+
if (diffNavigator.hasNext()) {
|
|
351
|
+
diffNavigator.next();
|
|
352
|
+
} else {
|
|
353
|
+
const nextNode = this.moveToNextFileNode();
|
|
354
|
+
if (nextNode) {
|
|
355
|
+
const nextResource = this.model.getResourceFromNode(nextNode);
|
|
356
|
+
if (nextResource) {
|
|
357
|
+
this.openResource(nextResource);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
selectNodeByUri(uri: URI): void {
|
|
367
|
+
for (const group of this.model.groups) {
|
|
368
|
+
const sourceUri = new URI(uri.path.toString());
|
|
369
|
+
const id = `${group.id}:${sourceUri.toString()}`;
|
|
370
|
+
const node = this.model.getNode(id);
|
|
371
|
+
if (SelectableTreeNode.is(node)) {
|
|
372
|
+
this.model.selectNode(node);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
protected getFirstSelectableNode(): SelectableTreeNode | undefined {
|
|
379
|
+
if (this.model.root) {
|
|
380
|
+
const root = this.model.root as ScmFileChangeRootNode;
|
|
381
|
+
const groupNode = root.children[0];
|
|
382
|
+
return groupNode.children[0];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
protected moveToPreviousFileNode(): ScmFileChangeNode | undefined {
|
|
387
|
+
let previousNode = this.model.getPrevSelectableNode();
|
|
388
|
+
while (previousNode) {
|
|
389
|
+
if (ScmFileChangeNode.is(previousNode)) {
|
|
390
|
+
this.model.selectNode(previousNode);
|
|
391
|
+
return previousNode;
|
|
392
|
+
}
|
|
393
|
+
previousNode = this.model.getPrevSelectableNode(previousNode);
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
protected moveToNextFileNode(): ScmFileChangeNode | undefined {
|
|
398
|
+
let nextNode = this.model.getNextSelectableNode();
|
|
399
|
+
while (nextNode) {
|
|
400
|
+
if (ScmFileChangeNode.is(nextNode)) {
|
|
401
|
+
this.model.selectNode(nextNode);
|
|
402
|
+
return nextNode;
|
|
403
|
+
}
|
|
404
|
+
nextNode = this.model.getNextSelectableNode(nextNode);
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
protected async openResource(resource: ScmResource): Promise<EditorWidget | undefined> {
|
|
409
|
+
try {
|
|
410
|
+
await resource.open();
|
|
411
|
+
} catch (e) {
|
|
412
|
+
console.error('Failed to open a SCM resource', e);
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let standaloneEditor: EditorWidget | undefined;
|
|
417
|
+
const resourcePath = resource.sourceUri.path.toString();
|
|
418
|
+
|
|
419
|
+
for (const widget of this.editorManager.all) {
|
|
420
|
+
const resourceUri = widget.editor.document.uri;
|
|
421
|
+
const editorResourcePath = new URI(resourceUri).path.toString();
|
|
422
|
+
if (resourcePath === editorResourcePath) {
|
|
423
|
+
if (widget.editor.uri.scheme === DiffUris.DIFF_SCHEME) {
|
|
424
|
+
// prefer diff editor
|
|
425
|
+
return widget;
|
|
426
|
+
} else {
|
|
427
|
+
standaloneEditor = widget;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (widget.editor.uri.scheme === DiffUris.DIFF_SCHEME
|
|
431
|
+
&& resourceUri === resource.sourceUri.toString()) {
|
|
432
|
+
return widget;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// fallback to standalone editor
|
|
436
|
+
return standaloneEditor;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
protected override getPaddingLeft(node: TreeNode, props: NodeProps): number {
|
|
440
|
+
if (this.viewMode === 'list') {
|
|
441
|
+
if (props.depth === 1) {
|
|
442
|
+
return this.props.expansionTogglePadding;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return super.getPaddingLeft(node, props);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
protected override getDepthPadding(depth: number): number {
|
|
449
|
+
return super.getDepthPadding(depth) + 5;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
protected isCurrentThemeLight(): boolean {
|
|
453
|
+
const type = this.themeService.getCurrentTheme().type;
|
|
454
|
+
return type.toLocaleLowerCase().includes('light');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
protected override needsExpansionTogglePadding(node: TreeNode): boolean {
|
|
458
|
+
const theme = this.iconThemeService.getDefinition(this.iconThemeService.current);
|
|
459
|
+
if (theme && (theme.hidesExplorerArrows || (theme.hasFileIcons && !theme.hasFolderIcons))) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
return super.needsExpansionTogglePadding(node);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export namespace ScmTreeWidget {
|
|
468
|
+
export namespace Styles {
|
|
469
|
+
export const NO_SELECT = 'no-select';
|
|
470
|
+
}
|
|
471
|
+
// This is an 'abstract' base interface for all the element component props.
|
|
472
|
+
export interface Props {
|
|
473
|
+
treeNode: TreeNode;
|
|
474
|
+
model: ScmTreeModel;
|
|
475
|
+
menus: MenuModelRegistry;
|
|
476
|
+
contextKeys: ScmContextKeyService;
|
|
477
|
+
labelProvider: LabelProvider;
|
|
478
|
+
contextMenuRenderer: ContextMenuRenderer;
|
|
479
|
+
corePreferences?: CorePreferences;
|
|
480
|
+
caption: React.ReactNode;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export abstract class ScmElement<P extends ScmElement.Props = ScmElement.Props> extends React.Component<P, ScmElement.State> {
|
|
485
|
+
|
|
486
|
+
constructor(props: P) {
|
|
487
|
+
super(props);
|
|
488
|
+
this.state = {
|
|
489
|
+
hover: false
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const setState = this.setState.bind(this);
|
|
493
|
+
this.setState = newState => {
|
|
494
|
+
if (!this.toDisposeOnUnmount.disposed) {
|
|
495
|
+
setState(newState);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
protected readonly toDisposeOnUnmount = new DisposableCollection();
|
|
501
|
+
override componentDidMount(): void {
|
|
502
|
+
this.toDisposeOnUnmount.push(Disposable.create(() => { /* mark as mounted */ }));
|
|
503
|
+
}
|
|
504
|
+
override componentWillUnmount(): void {
|
|
505
|
+
this.toDisposeOnUnmount.dispose();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
protected detectHover = (element: HTMLElement | null) => {
|
|
509
|
+
if (element) {
|
|
510
|
+
window.requestAnimationFrame(() => {
|
|
511
|
+
const hover = element.matches(':hover');
|
|
512
|
+
this.setState({ hover });
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
protected showHover = () => this.setState({ hover: true });
|
|
517
|
+
protected hideHover = () => this.setState({ hover: false });
|
|
518
|
+
|
|
519
|
+
protected renderContextMenu = (event: React.MouseEvent<HTMLElement>) => {
|
|
520
|
+
event.preventDefault();
|
|
521
|
+
const { treeNode: node, contextMenuRenderer } = this.props;
|
|
522
|
+
this.props.model.execInNodeContext(node, () => {
|
|
523
|
+
contextMenuRenderer.render({
|
|
524
|
+
menuPath: this.contextMenuPath,
|
|
525
|
+
anchor: event.nativeEvent,
|
|
526
|
+
args: this.contextMenuArgs
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
protected abstract get contextMenuPath(): MenuPath;
|
|
532
|
+
protected abstract get contextMenuArgs(): any[];
|
|
533
|
+
|
|
534
|
+
}
|
|
535
|
+
export namespace ScmElement {
|
|
536
|
+
export interface Props extends ScmTreeWidget.Props {
|
|
537
|
+
renderExpansionToggle: () => React.ReactNode;
|
|
538
|
+
commandExecutor: MenuCommandExecutor;
|
|
539
|
+
}
|
|
540
|
+
export interface State {
|
|
541
|
+
hover: boolean
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export class ScmResourceComponent extends ScmElement<ScmResourceComponent.Props> {
|
|
546
|
+
|
|
547
|
+
override render(): JSX.Element | undefined {
|
|
548
|
+
const { hover } = this.state;
|
|
549
|
+
const { model, treeNode, colors, parentPath, sourceUri, decoration, labelProvider, commandExecutor, menus, contextKeys, caption, isLightTheme } = this.props;
|
|
550
|
+
const resourceUri = new URI(sourceUri);
|
|
551
|
+
|
|
552
|
+
const decorationIcon = treeNode.decorations;
|
|
553
|
+
const themedIcon = isLightTheme ? decorationIcon?.icon : decorationIcon?.iconDark;
|
|
554
|
+
const classNames: string[] = themedIcon ? ['decoration-icon', themedIcon] : ['decoration-icon', 'status'];
|
|
555
|
+
|
|
556
|
+
const icon = labelProvider.getIcon(resourceUri);
|
|
557
|
+
const color = decoration && decoration.colorId && !themedIcon ? `var(${colors.toCssVariableName(decoration.colorId)})` : '';
|
|
558
|
+
const letter = decoration && decoration.letter && !themedIcon ? decoration.letter : '';
|
|
559
|
+
const tooltip = decoration && decoration.tooltip || '';
|
|
560
|
+
const textDecoration = treeNode.decorations?.strikeThrough === true ? 'line-through' : 'normal';
|
|
561
|
+
const relativePath = parentPath.relative(resourceUri.parent);
|
|
562
|
+
const path = relativePath ? relativePath.fsPath() : labelProvider.getLongName(resourceUri.parent);
|
|
563
|
+
const title = tooltip.length !== 0
|
|
564
|
+
? `${resourceUri.path.fsPath()} • ${tooltip}`
|
|
565
|
+
: resourceUri.path.fsPath();
|
|
566
|
+
|
|
567
|
+
return <div key={sourceUri}
|
|
568
|
+
className={`scmItem ${TREE_NODE_SEGMENT_CLASS} ${TREE_NODE_SEGMENT_GROW_CLASS}`}
|
|
569
|
+
onContextMenu={this.renderContextMenu}
|
|
570
|
+
onMouseEnter={this.showHover}
|
|
571
|
+
onMouseLeave={this.hideHover}
|
|
572
|
+
ref={this.detectHover}
|
|
573
|
+
title={title}
|
|
574
|
+
onClick={this.handleClick}
|
|
575
|
+
onDoubleClick={this.handleDoubleClick} >
|
|
576
|
+
<span className={icon + ' file-icon'} />
|
|
577
|
+
{this.props.renderExpansionToggle()}
|
|
578
|
+
<div className={`noWrapInfo ${TREE_NODE_SEGMENT_GROW_CLASS}`} >
|
|
579
|
+
<span className='name' style={{ textDecoration }}>{caption}</span>
|
|
580
|
+
<span className='path' style={{ textDecoration }}>{path}</span>
|
|
581
|
+
</div>
|
|
582
|
+
<ScmInlineActions {...{
|
|
583
|
+
hover,
|
|
584
|
+
menu: menus.getMenu(ScmTreeWidget.RESOURCE_INLINE_MENU),
|
|
585
|
+
menuPath: ScmTreeWidget.RESOURCE_INLINE_MENU,
|
|
586
|
+
commandExecutor,
|
|
587
|
+
args: this.contextMenuArgs,
|
|
588
|
+
contextKeys,
|
|
589
|
+
model,
|
|
590
|
+
treeNode
|
|
591
|
+
}}>
|
|
592
|
+
<div title={tooltip} className={classNames.join(' ')} style={{ color }}>
|
|
593
|
+
{letter}
|
|
594
|
+
</div>
|
|
595
|
+
</ScmInlineActions>
|
|
596
|
+
</div >;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
protected open = () => {
|
|
600
|
+
const resource = this.props.model.getResourceFromNode(this.props.treeNode);
|
|
601
|
+
if (resource) {
|
|
602
|
+
resource.open();
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
protected readonly contextMenuPath = ScmTreeWidget.RESOURCE_CONTEXT_MENU;
|
|
607
|
+
protected get contextMenuArgs(): any[] {
|
|
608
|
+
if (!this.props.model.selectedNodes.some(node => ScmFileChangeNode.is(node) && node.sourceUri === this.props.sourceUri)) {
|
|
609
|
+
// Clicked node is not in selection, so ignore selection and action on just clicked node
|
|
610
|
+
return this.singleNodeArgs;
|
|
611
|
+
} else {
|
|
612
|
+
return this.props.model.getSelectionArgs(this.props.model.selectedNodes);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
protected get singleNodeArgs(): any[] {
|
|
616
|
+
const selectedResource = this.props.model.getResourceFromNode(this.props.treeNode);
|
|
617
|
+
if (selectedResource) {
|
|
618
|
+
return [selectedResource];
|
|
619
|
+
} else {
|
|
620
|
+
// Repository status not yet available. Empty args disables the action.
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
protected hasCtrlCmdOrShiftMask(event: TreeWidget.ModifierAwareEvent): boolean {
|
|
626
|
+
const { metaKey, ctrlKey, shiftKey } = event;
|
|
627
|
+
return (isOSX && metaKey) || ctrlKey || shiftKey;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Handle the single clicking of nodes present in the widget.
|
|
632
|
+
*/
|
|
633
|
+
protected handleClick = (event: React.MouseEvent) => {
|
|
634
|
+
if (!this.hasCtrlCmdOrShiftMask(event)) {
|
|
635
|
+
// Determine the behavior based on the preference value.
|
|
636
|
+
const isSingle = this.props.corePreferences && this.props.corePreferences['workbench.list.openMode'] === 'singleClick';
|
|
637
|
+
if (isSingle) {
|
|
638
|
+
this.open();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Handle the double clicking of nodes present in the widget.
|
|
645
|
+
*/
|
|
646
|
+
protected handleDoubleClick = () => {
|
|
647
|
+
// Determine the behavior based on the preference value.
|
|
648
|
+
const isDouble = this.props.corePreferences && this.props.corePreferences['workbench.list.openMode'] === 'doubleClick';
|
|
649
|
+
// Nodes should only be opened through double clicking if the correct preference is set.
|
|
650
|
+
if (isDouble) {
|
|
651
|
+
this.open();
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export namespace ScmResourceComponent {
|
|
657
|
+
export interface Props extends ScmElement.Props {
|
|
658
|
+
treeNode: ScmFileChangeNode;
|
|
659
|
+
parentPath: URI;
|
|
660
|
+
sourceUri: string;
|
|
661
|
+
decoration: Decoration | undefined;
|
|
662
|
+
colors: ColorRegistry;
|
|
663
|
+
isLightTheme: boolean
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export class ScmResourceGroupElement extends ScmElement<ScmResourceGroupComponent.Props> {
|
|
668
|
+
|
|
669
|
+
override render(): JSX.Element {
|
|
670
|
+
const { hover } = this.state;
|
|
671
|
+
const { model, treeNode, menus, commandExecutor, contextKeys, caption } = this.props;
|
|
672
|
+
return <div className={`theia-header scm-theia-header ${TREE_NODE_SEGMENT_GROW_CLASS}`}
|
|
673
|
+
onContextMenu={this.renderContextMenu}
|
|
674
|
+
onMouseEnter={this.showHover}
|
|
675
|
+
onMouseLeave={this.hideHover}
|
|
676
|
+
ref={this.detectHover}>
|
|
677
|
+
{this.props.renderExpansionToggle()}
|
|
678
|
+
<div className={`noWrapInfo ${TREE_NODE_SEGMENT_GROW_CLASS}`}>{caption}</div>
|
|
679
|
+
<ScmInlineActions {...{
|
|
680
|
+
hover,
|
|
681
|
+
args: this.contextMenuArgs,
|
|
682
|
+
menu: menus.getMenu(ScmTreeWidget.RESOURCE_GROUP_INLINE_MENU),
|
|
683
|
+
menuPath: ScmTreeWidget.RESOURCE_GROUP_INLINE_MENU,
|
|
684
|
+
commandExecutor,
|
|
685
|
+
contextKeys,
|
|
686
|
+
model,
|
|
687
|
+
treeNode
|
|
688
|
+
}}>
|
|
689
|
+
{this.renderChangeCount()}
|
|
690
|
+
</ScmInlineActions>
|
|
691
|
+
</div>;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
protected renderChangeCount(): React.ReactNode {
|
|
695
|
+
const group = this.props.model.getResourceGroupFromNode(this.props.treeNode);
|
|
696
|
+
return <div className='notification-count-container scm-change-count'>
|
|
697
|
+
<span className='notification-count'>{group ? group.resources.length : 0}</span>
|
|
698
|
+
</div>;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
protected readonly contextMenuPath = ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU;
|
|
702
|
+
protected get contextMenuArgs(): any[] {
|
|
703
|
+
const group = this.props.model.getResourceGroupFromNode(this.props.treeNode);
|
|
704
|
+
if (group) {
|
|
705
|
+
return [group];
|
|
706
|
+
} else {
|
|
707
|
+
// Repository status not yet available. Empty args disables the action.
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
export namespace ScmResourceGroupComponent {
|
|
713
|
+
export interface Props extends ScmElement.Props {
|
|
714
|
+
treeNode: ScmFileChangeGroupNode;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export class ScmResourceFolderElement extends ScmElement<ScmResourceFolderElement.Props> {
|
|
719
|
+
|
|
720
|
+
override render(): JSX.Element {
|
|
721
|
+
const { hover } = this.state;
|
|
722
|
+
const { model, treeNode, sourceUri, labelProvider, commandExecutor, menus, contextKeys, caption } = this.props;
|
|
723
|
+
const sourceFileStat = FileStat.dir(sourceUri);
|
|
724
|
+
const icon = labelProvider.getIcon(sourceFileStat);
|
|
725
|
+
const title = new URI(sourceUri).path.fsPath();
|
|
726
|
+
|
|
727
|
+
return <div key={sourceUri}
|
|
728
|
+
className={`scmItem ${TREE_NODE_SEGMENT_CLASS} ${TREE_NODE_SEGMENT_GROW_CLASS} ${ScmTreeWidget.Styles.NO_SELECT}`}
|
|
729
|
+
title={title}
|
|
730
|
+
onContextMenu={this.renderContextMenu}
|
|
731
|
+
onMouseEnter={this.showHover}
|
|
732
|
+
onMouseLeave={this.hideHover}
|
|
733
|
+
ref={this.detectHover}
|
|
734
|
+
>
|
|
735
|
+
{this.props.renderExpansionToggle()}
|
|
736
|
+
<span className={icon + ' file-icon'} />
|
|
737
|
+
<div className={`noWrapInfo ${TREE_NODE_SEGMENT_GROW_CLASS}`} >
|
|
738
|
+
<span className='name'>{caption}</span>
|
|
739
|
+
</div>
|
|
740
|
+
<ScmInlineActions {...{
|
|
741
|
+
hover,
|
|
742
|
+
menu: menus.getMenu(ScmTreeWidget.RESOURCE_FOLDER_INLINE_MENU),
|
|
743
|
+
menuPath: ScmTreeWidget.RESOURCE_FOLDER_INLINE_MENU,
|
|
744
|
+
commandExecutor,
|
|
745
|
+
args: this.contextMenuArgs,
|
|
746
|
+
contextKeys,
|
|
747
|
+
model,
|
|
748
|
+
treeNode
|
|
749
|
+
}}>
|
|
750
|
+
</ScmInlineActions>
|
|
751
|
+
</div >;
|
|
752
|
+
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
protected readonly contextMenuPath = ScmTreeWidget.RESOURCE_FOLDER_CONTEXT_MENU;
|
|
756
|
+
protected get contextMenuArgs(): any[] {
|
|
757
|
+
if (!this.props.model.selectedNodes.some(node => ScmFileChangeFolderNode.is(node) && node.sourceUri === this.props.sourceUri)) {
|
|
758
|
+
// Clicked node is not in selection, so ignore selection and action on just clicked node
|
|
759
|
+
return this.singleNodeArgs;
|
|
760
|
+
} else {
|
|
761
|
+
return this.props.model.getSelectionArgs(this.props.model.selectedNodes);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
protected get singleNodeArgs(): any[] {
|
|
765
|
+
return this.props.model.getResourcesFromFolderNode(this.props.treeNode);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export namespace ScmResourceFolderElement {
|
|
771
|
+
export interface Props extends ScmElement.Props {
|
|
772
|
+
treeNode: ScmFileChangeFolderNode;
|
|
773
|
+
sourceUri: string;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export class ScmInlineActions extends React.Component<ScmInlineActions.Props> {
|
|
778
|
+
override render(): React.ReactNode {
|
|
779
|
+
const { hover, menu, menuPath, args, commandExecutor, model, treeNode, contextKeys, children } = this.props;
|
|
780
|
+
return <div className='theia-scm-inline-actions-container'>
|
|
781
|
+
<div className='theia-scm-inline-actions'>
|
|
782
|
+
{hover && menu.children
|
|
783
|
+
.map((node, index) => node instanceof ActionMenuNode &&
|
|
784
|
+
<ScmInlineAction key={index} {...{ node, menuPath, args, commandExecutor, model, treeNode, contextKeys }} />)}
|
|
785
|
+
</div>
|
|
786
|
+
{children}
|
|
787
|
+
</div>;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
export namespace ScmInlineActions {
|
|
791
|
+
export interface Props {
|
|
792
|
+
hover: boolean;
|
|
793
|
+
menu: CompoundMenuNode;
|
|
794
|
+
menuPath: MenuPath;
|
|
795
|
+
commandExecutor: MenuCommandExecutor;
|
|
796
|
+
model: ScmTreeModel;
|
|
797
|
+
treeNode: TreeNode;
|
|
798
|
+
contextKeys: ScmContextKeyService;
|
|
799
|
+
args: any[];
|
|
800
|
+
children?: React.ReactNode;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export class ScmInlineAction extends React.Component<ScmInlineAction.Props> {
|
|
805
|
+
override render(): React.ReactNode {
|
|
806
|
+
const { node, model, treeNode, args, commandExecutor, menuPath, contextKeys } = this.props;
|
|
807
|
+
|
|
808
|
+
let isActive: boolean = false;
|
|
809
|
+
model.execInNodeContext(treeNode, () => {
|
|
810
|
+
isActive = contextKeys.match(node.when);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
if (!commandExecutor.isVisible(menuPath, node.command, ...args) || !isActive) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
return <div className='theia-scm-inline-action'>
|
|
817
|
+
<a className={`${node.icon} ${ACTION_ITEM}`} title={node.label} onClick={this.execute} />
|
|
818
|
+
</div>;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
protected execute = (event: React.MouseEvent) => {
|
|
822
|
+
event.stopPropagation();
|
|
823
|
+
|
|
824
|
+
const { commandExecutor, menuPath, node, args } = this.props;
|
|
825
|
+
commandExecutor.executeCommand([menuPath[0]], node.command, ...args);
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
export namespace ScmInlineAction {
|
|
829
|
+
export interface Props {
|
|
830
|
+
node: ActionMenuNode;
|
|
831
|
+
commandExecutor: MenuCommandExecutor;
|
|
832
|
+
menuPath: MenuPath;
|
|
833
|
+
model: ScmTreeModel;
|
|
834
|
+
treeNode: TreeNode;
|
|
835
|
+
contextKeys: ScmContextKeyService;
|
|
836
|
+
args: any[];
|
|
837
|
+
}
|
|
838
|
+
}
|