@theia/scm 1.45.0 → 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.
Files changed (94) hide show
  1. package/README.md +31 -31
  2. package/lib/browser/decorations/scm-decorations-service.d.ts +14 -14
  3. package/lib/browser/decorations/scm-decorations-service.js +101 -101
  4. package/lib/browser/decorations/scm-navigator-decorator.d.ts +25 -25
  5. package/lib/browser/decorations/scm-navigator-decorator.js +132 -132
  6. package/lib/browser/decorations/scm-tab-bar-decorator.d.ts +17 -17
  7. package/lib/browser/decorations/scm-tab-bar-decorator.js +93 -93
  8. package/lib/browser/dirty-diff/content-lines.d.ts +12 -12
  9. package/lib/browser/dirty-diff/content-lines.js +106 -106
  10. package/lib/browser/dirty-diff/content-lines.spec.d.ts +1 -1
  11. package/lib/browser/dirty-diff/content-lines.spec.js +39 -39
  12. package/lib/browser/dirty-diff/diff-computer.d.ts +29 -29
  13. package/lib/browser/dirty-diff/diff-computer.js +102 -102
  14. package/lib/browser/dirty-diff/diff-computer.spec.d.ts +1 -1
  15. package/lib/browser/dirty-diff/diff-computer.spec.js +315 -315
  16. package/lib/browser/dirty-diff/dirty-diff-decorator.d.ts +14 -14
  17. package/lib/browser/dirty-diff/dirty-diff-decorator.js +98 -98
  18. package/lib/browser/dirty-diff/dirty-diff-module.d.ts +3 -3
  19. package/lib/browser/dirty-diff/dirty-diff-module.js +24 -24
  20. package/lib/browser/scm-amend-component.d.ts +123 -123
  21. package/lib/browser/scm-amend-component.js +463 -463
  22. package/lib/browser/scm-amend-widget.d.ts +20 -20
  23. package/lib/browser/scm-amend-widget.js +101 -101
  24. package/lib/browser/scm-avatar-service.d.ts +3 -3
  25. package/lib/browser/scm-avatar-service.js +36 -36
  26. package/lib/browser/scm-commit-widget.d.ts +52 -52
  27. package/lib/browser/scm-commit-widget.js +199 -199
  28. package/lib/browser/scm-context-key-service.d.ts +10 -10
  29. package/lib/browser/scm-context-key-service.js +58 -58
  30. package/lib/browser/scm-contribution.d.ts +83 -83
  31. package/lib/browser/scm-contribution.js +356 -356
  32. package/lib/browser/scm-frontend-module.d.ts +6 -6
  33. package/lib/browser/scm-frontend-module.js +130 -130
  34. package/lib/browser/scm-groups-tree-model.d.ts +14 -14
  35. package/lib/browser/scm-groups-tree-model.js +97 -97
  36. package/lib/browser/scm-input.d.ts +53 -53
  37. package/lib/browser/scm-input.js +127 -127
  38. package/lib/browser/scm-layout-migrations.d.ts +9 -9
  39. package/lib/browser/scm-layout-migrations.js +79 -79
  40. package/lib/browser/scm-no-repository-widget.d.ts +8 -8
  41. package/lib/browser/scm-no-repository-widget.js +49 -49
  42. package/lib/browser/scm-preferences.d.ts +11 -11
  43. package/lib/browser/scm-preferences.js +51 -51
  44. package/lib/browser/scm-provider.d.ts +58 -58
  45. package/lib/browser/scm-provider.js +19 -19
  46. package/lib/browser/scm-quick-open-service.d.ts +11 -11
  47. package/lib/browser/scm-quick-open-service.js +73 -73
  48. package/lib/browser/scm-repository.d.ts +17 -17
  49. package/lib/browser/scm-repository.js +41 -41
  50. package/lib/browser/scm-service.d.ts +26 -26
  51. package/lib/browser/scm-service.js +108 -108
  52. package/lib/browser/scm-tree-label-provider.d.ts +7 -7
  53. package/lib/browser/scm-tree-label-provider.js +57 -57
  54. package/lib/browser/scm-tree-model.d.ts +74 -74
  55. package/lib/browser/scm-tree-model.js +351 -351
  56. package/lib/browser/scm-tree-widget.d.ts +208 -208
  57. package/lib/browser/scm-tree-widget.js +703 -703
  58. package/lib/browser/scm-widget.d.ts +40 -40
  59. package/lib/browser/scm-widget.js +218 -218
  60. package/package.json +6 -6
  61. package/src/browser/decorations/scm-decorations-service.ts +78 -78
  62. package/src/browser/decorations/scm-navigator-decorator.ts +121 -121
  63. package/src/browser/decorations/scm-tab-bar-decorator.ts +83 -83
  64. package/src/browser/dirty-diff/content-lines.spec.ts +42 -42
  65. package/src/browser/dirty-diff/content-lines.ts +112 -112
  66. package/src/browser/dirty-diff/diff-computer.spec.ts +387 -387
  67. package/src/browser/dirty-diff/diff-computer.ts +129 -129
  68. package/src/browser/dirty-diff/dirty-diff-decorator.ts +107 -107
  69. package/src/browser/dirty-diff/dirty-diff-module.ts +24 -24
  70. package/src/browser/scm-amend-component.tsx +600 -600
  71. package/src/browser/scm-amend-widget.tsx +77 -77
  72. package/src/browser/scm-avatar-service.ts +27 -27
  73. package/src/browser/scm-commit-widget.tsx +215 -215
  74. package/src/browser/scm-context-key-service.ts +46 -46
  75. package/src/browser/scm-contribution.ts +361 -361
  76. package/src/browser/scm-frontend-module.ts +149 -149
  77. package/src/browser/scm-groups-tree-model.ts +78 -78
  78. package/src/browser/scm-input.ts +164 -164
  79. package/src/browser/scm-layout-migrations.ts +64 -64
  80. package/src/browser/scm-no-repository-widget.tsx +41 -41
  81. package/src/browser/scm-preferences.ts +63 -63
  82. package/src/browser/scm-provider.ts +91 -91
  83. package/src/browser/scm-quick-open-service.ts +48 -48
  84. package/src/browser/scm-repository.ts +52 -52
  85. package/src/browser/scm-service.ts +108 -108
  86. package/src/browser/scm-tree-label-provider.ts +44 -44
  87. package/src/browser/scm-tree-model.ts +405 -405
  88. package/src/browser/scm-tree-widget.tsx +838 -838
  89. package/src/browser/scm-widget.tsx +204 -204
  90. package/src/browser/style/dirty-diff-decorator.css +52 -52
  91. package/src/browser/style/dirty-diff.css +50 -50
  92. package/src/browser/style/index.css +271 -271
  93. package/src/browser/style/scm-amend-component.css +94 -94
  94. package/src/browser/style/scm.svg +4 -4
@@ -1,600 +1,600 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2019 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
- import '../../src/browser/style/scm-amend-component.css';
18
-
19
- import * as React from '@theia/core/shared/react';
20
- import { ScmAvatarService } from './scm-avatar-service';
21
- import { codicon, StorageService } from '@theia/core/lib/browser';
22
- import { Disposable, DisposableCollection } from '@theia/core';
23
-
24
- import { ScmRepository } from './scm-repository';
25
- import { ScmAmendSupport, ScmCommit } from './scm-provider';
26
- import { nls } from '@theia/core/lib/common/nls';
27
-
28
- export interface ScmAmendComponentProps {
29
- style: React.CSSProperties | undefined,
30
- repository: ScmRepository,
31
- scmAmendSupport: ScmAmendSupport,
32
- setCommitMessage: (message: string) => void,
33
- avatarService: ScmAvatarService,
34
- storageService: StorageService,
35
- }
36
-
37
- interface ScmAmendComponentState {
38
- /**
39
- * This is used for transitioning. When setting up a transition, we first set to render
40
- * the elements in their starting positions. This includes creating the elements to be
41
- * transitioned in, even though those controls will not be visible when state is 'start'.
42
- * On the next frame after 'start', we render elements with their final positions and with
43
- * the transition properties.
44
- */
45
- transition: {
46
- state: 'none'
47
- } | {
48
- state: 'start' | 'transitioning',
49
- direction: 'up' | 'down',
50
- previousLastCommit: { commit: ScmCommit, avatar: string }
51
- };
52
-
53
- amendingCommits: { commit: ScmCommit, avatar: string }[];
54
- lastCommit: { commit: ScmCommit, avatar: string } | undefined;
55
- }
56
-
57
- const TRANSITION_TIME_MS = 300;
58
- const REPOSITORY_STORAGE_KEY = 'scmRepository';
59
-
60
- export class ScmAmendComponent extends React.Component<ScmAmendComponentProps, ScmAmendComponentState> {
61
-
62
- /**
63
- * a hint on how to animate an update, set by certain user action handlers
64
- * and used when updating the view based on a repository change
65
- */
66
- protected transitionHint: 'none' | 'amend' | 'unamend' = 'none';
67
-
68
- protected lastCommitHeight: number = 0;
69
- lastCommitScrollRef = (instance: HTMLDivElement) => {
70
- if (instance && this.lastCommitHeight === 0) {
71
- this.lastCommitHeight = instance.getBoundingClientRect().height;
72
- }
73
- };
74
-
75
- constructor(props: ScmAmendComponentProps) {
76
- super(props);
77
-
78
- this.state = {
79
- transition: { state: 'none' },
80
- amendingCommits: [],
81
- lastCommit: undefined
82
- };
83
-
84
- const setState = this.setState.bind(this);
85
- this.setState = newState => {
86
- if (!this.toDisposeOnUnmount.disposed) {
87
- setState(newState);
88
- }
89
- };
90
- }
91
-
92
- protected readonly toDisposeOnUnmount = new DisposableCollection();
93
-
94
- override async componentDidMount(): Promise<void> {
95
- this.toDisposeOnUnmount.push(Disposable.create(() => { /* mark as mounted */ }));
96
-
97
- const lastCommit = await this.getLastCommit();
98
- this.setState({ amendingCommits: await this.buildAmendingList(lastCommit ? lastCommit.commit : undefined), lastCommit });
99
-
100
- if (this.toDisposeOnUnmount.disposed) {
101
- return;
102
- }
103
- this.toDisposeOnUnmount.push(
104
- this.props.repository.provider.onDidChange(() => this.fetchStatusAndSetState())
105
- );
106
- }
107
-
108
- override componentWillUnmount(): void {
109
- this.toDisposeOnUnmount.dispose();
110
- }
111
-
112
- async fetchStatusAndSetState(): Promise<void> {
113
- const storageKey = this.getStorageKey();
114
-
115
- const nextCommit = await this.getLastCommit();
116
- if (nextCommit && this.state.lastCommit && nextCommit.commit.id === this.state.lastCommit.commit.id) {
117
- // No change here
118
- } else if (nextCommit === undefined && this.state.lastCommit === undefined) {
119
- // No change here
120
- } else if (this.transitionHint === 'none') {
121
- // If the 'last' commit changes, but we are not expecting an 'amend'
122
- // or 'unamend' to occur, then we clear out the list of amended commits.
123
- // This is because an unexpected change has happened to the repository,
124
- // perhaps the user committed, merged, or something. The amended commits
125
- // will no longer be valid.
126
-
127
- // Note that there may or may not have been a previous lastCommit (if the
128
- // repository was previously empty with no initial commit then lastCommit
129
- // will be undefined). Either way we clear the amending commits.
130
- await this.clearAmendingCommits();
131
-
132
- // There is a change to the last commit, but no transition hint so
133
- // the view just updates without transition.
134
- this.setState({ amendingCommits: [], lastCommit: nextCommit });
135
- } else {
136
- const amendingCommits = this.state.amendingCommits.concat([]); // copy the array
137
-
138
- const direction: 'up' | 'down' = this.transitionHint === 'amend' ? 'up' : 'down';
139
- switch (this.transitionHint) {
140
- case 'amend':
141
- if (this.state.lastCommit) {
142
- amendingCommits.push(this.state.lastCommit);
143
-
144
- const serializedState = JSON.stringify({
145
- amendingHeadCommitSha: amendingCommits[0].commit.id,
146
- latestCommitSha: nextCommit ? nextCommit.commit.id : undefined
147
- });
148
- this.props.storageService.setData<string | undefined>(storageKey, serializedState);
149
- }
150
- break;
151
- case 'unamend':
152
- amendingCommits.pop();
153
- if (amendingCommits.length === 0) {
154
- this.props.storageService.setData<string | undefined>(storageKey, undefined);
155
- } else {
156
- const serializedState = JSON.stringify({
157
- amendingHeadCommitSha: amendingCommits[0].commit.id,
158
- latestCommitSha: nextCommit ? nextCommit.commit.id : undefined
159
- });
160
- this.props.storageService.setData<string | undefined>(storageKey, serializedState);
161
- }
162
- break;
163
- }
164
-
165
- if (this.state.lastCommit && nextCommit) {
166
- const transitionData = { direction, previousLastCommit: this.state.lastCommit };
167
- this.setState({ lastCommit: nextCommit, amendingCommits, transition: { ...transitionData, state: 'start' } });
168
- this.onNextFrame(() => {
169
- this.setState({ transition: { ...transitionData, state: 'transitioning' } });
170
- });
171
-
172
- setTimeout(
173
- () => {
174
- this.setState({ transition: { state: 'none' } });
175
- },
176
- TRANSITION_TIME_MS);
177
- } else {
178
- // No previous last commit so no transition
179
- this.setState({ transition: { state: 'none' }, amendingCommits, lastCommit: nextCommit });
180
- }
181
- }
182
-
183
- this.transitionHint = 'none';
184
- }
185
-
186
- private async clearAmendingCommits(): Promise<void> {
187
- const storageKey = this.getStorageKey();
188
- await this.props.storageService.setData<string | undefined>(storageKey, undefined);
189
- }
190
-
191
- private async buildAmendingList(lastCommit: ScmCommit | undefined): Promise<{ commit: ScmCommit, avatar: string }[]> {
192
- const storageKey = this.getStorageKey();
193
- const storedState = await this.props.storageService.getData<string | undefined>(storageKey, undefined);
194
-
195
- // Restore list of commits from saved amending head commit up through parents until the
196
- // current commit. (If we don't reach the current commit, the repository has been changed in such
197
- // a way then unamending commits can no longer be done).
198
- if (storedState) {
199
- const { amendingHeadCommitSha, latestCommitSha } = JSON.parse(storedState);
200
- if (!this.commitsAreEqual(lastCommit, latestCommitSha)) {
201
- // The head commit in the repository has changed. It is not the same commit that was the
202
- // head commit after the last 'amend'.
203
- return [];
204
- }
205
- const commits = await this.props.scmAmendSupport.getInitialAmendingCommits(amendingHeadCommitSha, lastCommit ? lastCommit.id : undefined);
206
-
207
- const amendingCommitPromises = commits.map(async commit => {
208
- const avatar = await this.props.avatarService.getAvatar(commit.authorEmail);
209
- return { commit, avatar };
210
- });
211
- return Promise.all(amendingCommitPromises);
212
- } else {
213
- return [];
214
- }
215
- }
216
-
217
- private getStorageKey(): string {
218
- return REPOSITORY_STORAGE_KEY + ':' + this.props.repository.provider.rootUri;
219
- }
220
-
221
- /**
222
- * Commits are equal if the ids are equal or if both are undefined.
223
- * (If a commit is undefined, it represents the initial empty state of a repository,
224
- * before the initial commit).
225
- */
226
- private commitsAreEqual(lastCommit: ScmCommit | undefined, savedLastCommitId: string | undefined): boolean {
227
- return lastCommit
228
- ? lastCommit.id === savedLastCommitId
229
- : savedLastCommitId === undefined;
230
- }
231
-
232
- /**
233
- * This function will update the 'model' (lastCommit, amendingCommits) only
234
- * when the repository sees the last commit change.
235
- * 'render' can be called at any time, so be sure we don't update any 'model'
236
- * fields until we actually start the transition.
237
- */
238
- protected amend = async (): Promise<void> => {
239
- if (this.state.transition.state !== 'none' && this.transitionHint !== 'none') {
240
- return;
241
- }
242
-
243
- this.transitionHint = 'amend';
244
- await this.resetAndSetMessage('HEAD~', 'HEAD');
245
- };
246
-
247
- protected unamend = async (): Promise<void> => {
248
- if (this.state.transition.state !== 'none' && this.transitionHint !== 'none') {
249
- return;
250
- }
251
-
252
- const commitToRestore = (this.state.amendingCommits.length >= 1)
253
- ? this.state.amendingCommits[this.state.amendingCommits.length - 1]
254
- : undefined;
255
- const oldestAmendCommit = (this.state.amendingCommits.length >= 2)
256
- ? this.state.amendingCommits[this.state.amendingCommits.length - 2]
257
- : undefined;
258
-
259
- if (commitToRestore) {
260
- const commitToUseForMessage = oldestAmendCommit
261
- ? oldestAmendCommit.commit.id
262
- : undefined;
263
- this.transitionHint = 'unamend';
264
- await this.resetAndSetMessage(commitToRestore.commit.id, commitToUseForMessage);
265
- }
266
- };
267
-
268
- private async resetAndSetMessage(commitToRestore: string, commitToUseForMessage: string | undefined): Promise<void> {
269
- const message = commitToUseForMessage
270
- ? await this.props.scmAmendSupport.getMessage(commitToUseForMessage)
271
- : '';
272
- await this.props.scmAmendSupport.reset(commitToRestore);
273
- this.props.setCommitMessage(message);
274
- }
275
-
276
- override render(): JSX.Element {
277
- const neverShrink = this.state.amendingCommits.length <= 3;
278
-
279
- const style: React.CSSProperties = neverShrink
280
- ? {
281
- ...this.props.style,
282
- flexShrink: 0,
283
- }
284
- : {
285
- ...this.props.style,
286
- flexShrink: 1,
287
- minHeight: 240 // height with three commits
288
- };
289
-
290
- return (
291
- <div className={ScmAmendComponent.Styles.COMMIT_CONTAINER + ' no-select'} style={style}>
292
- {
293
- this.state.amendingCommits.length > 0 || (this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down')
294
- ? this.renderAmendingCommits()
295
- : ''
296
- }
297
- {
298
- this.state.lastCommit ?
299
- <div>
300
- <div id='lastCommit' className='theia-scm-amend'>
301
- <div className='theia-header scm-theia-header'>
302
- {nls.localize('theia/scm/amendHeadCommit', 'HEAD Commit')}
303
- </div>
304
- {this.renderLastCommit()}
305
- </div>
306
- </div>
307
- : ''
308
- }
309
- </div>
310
- );
311
- }
312
-
313
- protected async getLastCommit(): Promise<{ commit: ScmCommit, avatar: string } | undefined> {
314
- const commit = await this.props.scmAmendSupport.getLastCommit();
315
- if (commit) {
316
- const avatar = await this.props.avatarService.getAvatar(commit.authorEmail);
317
- return { commit, avatar };
318
- }
319
- return undefined;
320
- }
321
-
322
- protected renderAmendingCommits(): React.ReactNode {
323
- const neverShrink = this.state.amendingCommits.length <= 3;
324
-
325
- const style: React.CSSProperties = neverShrink
326
- ? {
327
- flexShrink: 0,
328
- }
329
- : {
330
- flexShrink: 1,
331
- // parent minHeight controls height, we just need any value smaller than
332
- // what the height would be when the parent is at its minHeight
333
- minHeight: 0
334
- };
335
-
336
- return <div id='amendedCommits' className='theia-scm-amend-outer-container' style={style}>
337
- <div className='theia-header scm-theia-header'>
338
- <div className='noWrapInfo'>Commits being Amended</div>
339
- {this.renderAmendCommitListButtons()}
340
- {this.renderCommitCount(this.state.amendingCommits.length)}
341
- </div>
342
- <div style={this.styleAmendedCommits()}>
343
- {this.state.amendingCommits.map((commitData, index, array) =>
344
- this.renderCommitBeingAmended(commitData, index === array.length - 1)
345
- )}
346
- {
347
- this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down'
348
- ? this.renderCommitBeingAmended(this.state.lastCommit, false)
349
- : ''
350
- }
351
- </div>
352
- </div>;
353
- }
354
-
355
- protected renderAmendCommitListButtons(): React.ReactNode {
356
- return <div className='theia-scm-inline-actions-container'>
357
- <div className='theia-scm-inline-actions'>
358
- <div className='theia-scm-inline-action'>
359
- <a className={codicon('dash')} title='Unamend All Commits' onClick={this.unamendAll} />
360
- </div>
361
- <div className='theia-scm-inline-action' >
362
- <a className={codicon('close')} title='Clear Amending Commits' onClick={this.clearAmending} />
363
- </div>
364
- </div>
365
- </div>;
366
- }
367
-
368
- protected renderLastCommit(): React.ReactNode {
369
- if (!this.state.lastCommit) {
370
- return '';
371
- }
372
-
373
- const canAmend: boolean = true;
374
- return <div className={ScmAmendComponent.Styles.COMMIT_AND_BUTTON} style={{ flexGrow: 0, flexShrink: 0 }} key={this.state.lastCommit.commit.id}>
375
- {this.renderLastCommitNoButton(this.state.lastCommit)}
376
- {
377
- canAmend
378
- ? <div className={ScmAmendComponent.Styles.FLEX_CENTER}>
379
- <button className='theia-button' title={nls.localize('theia/scm/amendLastCommit', 'Amend last commit')} onClick={this.amend}>
380
- {nls.localize('theia/scm/amend', 'Amend')}
381
- </button>
382
- </div>
383
- : ''
384
- }
385
- </div>;
386
- }
387
-
388
- protected renderLastCommitNoButton(lastCommit: { commit: ScmCommit, avatar: string }): React.ReactNode {
389
- switch (this.state.transition.state) {
390
- case 'none':
391
- return <div ref={this.lastCommitScrollRef} className='theia-scm-scrolling-container'>
392
- {this.renderCommitAvatarAndDetail(lastCommit)}
393
- </div>;
394
-
395
- case 'start':
396
- case 'transitioning':
397
- switch (this.state.transition.direction) {
398
- case 'up':
399
- return <div style={this.styleLastCommitMovingUp(this.state.transition.state)}>
400
- {this.renderCommitAvatarAndDetail(this.state.transition.previousLastCommit)}
401
- {this.renderCommitAvatarAndDetail(lastCommit)}
402
- </div>;
403
- case 'down':
404
- return <div style={this.styleLastCommitMovingDown(this.state.transition.state)}>
405
- {this.renderCommitAvatarAndDetail(lastCommit)}
406
- {this.renderCommitAvatarAndDetail(this.state.transition.previousLastCommit)}
407
- </div>;
408
- }
409
- }
410
- }
411
-
412
- /**
413
- * See https://stackoverflow.com/questions/26556436/react-after-render-code
414
- *
415
- * @param callback
416
- */
417
- protected onNextFrame(callback: FrameRequestCallback): void {
418
- setTimeout(
419
- () => window.requestAnimationFrame(callback),
420
- 0);
421
- }
422
-
423
- protected renderCommitAvatarAndDetail(commitData: { commit: ScmCommit, avatar: string }): React.ReactNode {
424
- const { commit, avatar } = commitData;
425
- return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} key={commit.id}>
426
- <div className={ScmAmendComponent.Styles.COMMIT_MESSAGE_AVATAR}>
427
- <img src={avatar} />
428
- </div>
429
- <div className={ScmAmendComponent.Styles.COMMIT_DETAILS}>
430
- <div className={ScmAmendComponent.Styles.COMMIT_MESSAGE_SUMMARY}>{commit.summary}</div>
431
- <div className={ScmAmendComponent.Styles.LAST_COMMIT_MESSAGE_TIME}>{`${commit.authorDateRelative} by ${commit.authorName}`}</div>
432
- </div>
433
- </div>;
434
- }
435
-
436
- protected renderCommitCount(commits: number): React.ReactNode {
437
- return <div className='notification-count-container scm-change-count'>
438
- <span className='notification-count'>{commits}</span>
439
- </div>;
440
- }
441
-
442
- protected renderCommitBeingAmended(commitData: { commit: ScmCommit, avatar: string }, isOldestAmendCommit: boolean): JSX.Element {
443
- if (isOldestAmendCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'up') {
444
- return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} style={{ flexGrow: 0, flexShrink: 0 }} key={commitData.commit.id}>
445
- <div className='fixed-height-commit-container'>
446
- {this.renderCommitAvatarAndDetail(commitData)}
447
- </div>
448
- </div>;
449
- } else {
450
- return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} style={{ flexGrow: 0, flexShrink: 0 }} key={commitData.commit.id}>
451
- {this.renderCommitAvatarAndDetail(commitData)}
452
- {
453
- isOldestAmendCommit
454
- ? <div className={ScmAmendComponent.Styles.FLEX_CENTER}>
455
- <button className='theia-button' title={nls.localize('theia/scm/unamendCommit', 'Unamend commit')} onClick={this.unamend}>
456
- {nls.localize('theia/scm/unamend', 'Unamend')}
457
- </button>
458
- </div>
459
- : ''
460
- }
461
- </div>;
462
- }
463
- }
464
-
465
- /*
466
- * The style for the <div> containing the list of commits being amended.
467
- * This div is scrollable.
468
- */
469
- protected styleAmendedCommits(): React.CSSProperties {
470
- const base = {
471
- display: 'flex',
472
- whitespace: 'nowrap',
473
- width: '100%',
474
- minHeight: 0,
475
- flexShrink: 1,
476
- paddingTop: '2px',
477
- };
478
-
479
- switch (this.state.transition.state) {
480
- case 'none':
481
- return {
482
- ...base,
483
- flexDirection: 'column',
484
- overflowY: 'auto',
485
- marginBottom: '0',
486
- };
487
- case 'start':
488
- case 'transitioning':
489
- let startingMargin: number = 0;
490
- let endingMargin: number = 0;
491
- switch (this.state.transition.direction) {
492
- case 'down':
493
- startingMargin = 0;
494
- endingMargin = -32;
495
- break;
496
- case 'up':
497
- startingMargin = -32;
498
- endingMargin = 0;
499
- break;
500
- }
501
-
502
- switch (this.state.transition.state) {
503
- case 'start':
504
- return {
505
- ...base,
506
- flexDirection: 'column',
507
- overflowY: 'hidden',
508
- marginBottom: `${startingMargin}px`,
509
- };
510
- case 'transitioning':
511
- return {
512
- ...base,
513
- flexDirection: 'column',
514
- overflowY: 'hidden',
515
- marginBottom: `${endingMargin}px`,
516
- transitionProperty: 'margin-bottom',
517
- transitionDuration: `${TRANSITION_TIME_MS}ms`,
518
- transitionTimingFunction: 'linear'
519
- };
520
- }
521
- }
522
- }
523
-
524
- protected styleLastCommitMovingUp(transitionState: 'start' | 'transitioning'): React.CSSProperties {
525
- return this.styleLastCommit(transitionState, 0, -28);
526
- }
527
-
528
- protected styleLastCommitMovingDown(transitionState: 'start' | 'transitioning'): React.CSSProperties {
529
- return this.styleLastCommit(transitionState, -28, 0);
530
- }
531
-
532
- protected styleLastCommit(transitionState: 'start' | 'transitioning', startingMarginTop: number, startingMarginBottom: number): React.CSSProperties {
533
- const base = {
534
- display: 'flex',
535
- width: '100%',
536
- overflow: 'hidden',
537
- paddingTop: 0,
538
- paddingBottom: 0,
539
- borderTop: 0,
540
- borderBottom: 0,
541
- height: this.lastCommitHeight * 2
542
- };
543
-
544
- // We end with top and bottom margins switched
545
- const endingMarginTop = startingMarginBottom;
546
- const endingMarginBottom = startingMarginTop;
547
-
548
- switch (transitionState) {
549
- case 'start':
550
- return {
551
- ...base,
552
- position: 'relative',
553
- flexDirection: 'column',
554
- marginTop: startingMarginTop,
555
- marginBottom: startingMarginBottom,
556
- };
557
- case 'transitioning':
558
- return {
559
- ...base,
560
- position: 'relative',
561
- flexDirection: 'column',
562
- marginTop: endingMarginTop,
563
- marginBottom: endingMarginBottom,
564
- transitionProperty: 'margin-top margin-bottom',
565
- transitionDuration: `${TRANSITION_TIME_MS}ms`,
566
- transitionTimingFunction: 'linear'
567
- };
568
- }
569
- }
570
-
571
- readonly unamendAll = () => this.doUnamendAll();
572
- protected async doUnamendAll(): Promise<void> {
573
- while (this.state.amendingCommits.length > 0) {
574
- this.unamend();
575
- await new Promise(resolve => setTimeout(resolve, TRANSITION_TIME_MS));
576
- }
577
- }
578
-
579
- readonly clearAmending = () => this.doClearAmending();
580
- protected async doClearAmending(): Promise<void> {
581
- await this.clearAmendingCommits();
582
- this.setState({ amendingCommits: [] });
583
- }
584
- }
585
-
586
- export namespace ScmAmendComponent {
587
-
588
- export namespace Styles {
589
- export const COMMIT_CONTAINER = 'theia-scm-commit-container';
590
- export const COMMIT_AND_BUTTON = 'theia-scm-commit-and-button';
591
- export const COMMIT_AVATAR_AND_TEXT = 'theia-scm-commit-avatar-and-text';
592
- export const COMMIT_DETAILS = 'theia-scm-commit-details';
593
- export const COMMIT_MESSAGE_AVATAR = 'theia-scm-commit-message-avatar';
594
- export const COMMIT_MESSAGE_SUMMARY = 'theia-scm-commit-message-summary';
595
- export const LAST_COMMIT_MESSAGE_TIME = 'theia-scm-commit-message-time';
596
-
597
- export const FLEX_CENTER = 'theia-scm-flex-container-center';
598
- }
599
-
600
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2019 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
+ import '../../src/browser/style/scm-amend-component.css';
18
+
19
+ import * as React from '@theia/core/shared/react';
20
+ import { ScmAvatarService } from './scm-avatar-service';
21
+ import { codicon, StorageService } from '@theia/core/lib/browser';
22
+ import { Disposable, DisposableCollection } from '@theia/core';
23
+
24
+ import { ScmRepository } from './scm-repository';
25
+ import { ScmAmendSupport, ScmCommit } from './scm-provider';
26
+ import { nls } from '@theia/core/lib/common/nls';
27
+
28
+ export interface ScmAmendComponentProps {
29
+ style: React.CSSProperties | undefined,
30
+ repository: ScmRepository,
31
+ scmAmendSupport: ScmAmendSupport,
32
+ setCommitMessage: (message: string) => void,
33
+ avatarService: ScmAvatarService,
34
+ storageService: StorageService,
35
+ }
36
+
37
+ interface ScmAmendComponentState {
38
+ /**
39
+ * This is used for transitioning. When setting up a transition, we first set to render
40
+ * the elements in their starting positions. This includes creating the elements to be
41
+ * transitioned in, even though those controls will not be visible when state is 'start'.
42
+ * On the next frame after 'start', we render elements with their final positions and with
43
+ * the transition properties.
44
+ */
45
+ transition: {
46
+ state: 'none'
47
+ } | {
48
+ state: 'start' | 'transitioning',
49
+ direction: 'up' | 'down',
50
+ previousLastCommit: { commit: ScmCommit, avatar: string }
51
+ };
52
+
53
+ amendingCommits: { commit: ScmCommit, avatar: string }[];
54
+ lastCommit: { commit: ScmCommit, avatar: string } | undefined;
55
+ }
56
+
57
+ const TRANSITION_TIME_MS = 300;
58
+ const REPOSITORY_STORAGE_KEY = 'scmRepository';
59
+
60
+ export class ScmAmendComponent extends React.Component<ScmAmendComponentProps, ScmAmendComponentState> {
61
+
62
+ /**
63
+ * a hint on how to animate an update, set by certain user action handlers
64
+ * and used when updating the view based on a repository change
65
+ */
66
+ protected transitionHint: 'none' | 'amend' | 'unamend' = 'none';
67
+
68
+ protected lastCommitHeight: number = 0;
69
+ lastCommitScrollRef = (instance: HTMLDivElement) => {
70
+ if (instance && this.lastCommitHeight === 0) {
71
+ this.lastCommitHeight = instance.getBoundingClientRect().height;
72
+ }
73
+ };
74
+
75
+ constructor(props: ScmAmendComponentProps) {
76
+ super(props);
77
+
78
+ this.state = {
79
+ transition: { state: 'none' },
80
+ amendingCommits: [],
81
+ lastCommit: undefined
82
+ };
83
+
84
+ const setState = this.setState.bind(this);
85
+ this.setState = newState => {
86
+ if (!this.toDisposeOnUnmount.disposed) {
87
+ setState(newState);
88
+ }
89
+ };
90
+ }
91
+
92
+ protected readonly toDisposeOnUnmount = new DisposableCollection();
93
+
94
+ override async componentDidMount(): Promise<void> {
95
+ this.toDisposeOnUnmount.push(Disposable.create(() => { /* mark as mounted */ }));
96
+
97
+ const lastCommit = await this.getLastCommit();
98
+ this.setState({ amendingCommits: await this.buildAmendingList(lastCommit ? lastCommit.commit : undefined), lastCommit });
99
+
100
+ if (this.toDisposeOnUnmount.disposed) {
101
+ return;
102
+ }
103
+ this.toDisposeOnUnmount.push(
104
+ this.props.repository.provider.onDidChange(() => this.fetchStatusAndSetState())
105
+ );
106
+ }
107
+
108
+ override componentWillUnmount(): void {
109
+ this.toDisposeOnUnmount.dispose();
110
+ }
111
+
112
+ async fetchStatusAndSetState(): Promise<void> {
113
+ const storageKey = this.getStorageKey();
114
+
115
+ const nextCommit = await this.getLastCommit();
116
+ if (nextCommit && this.state.lastCommit && nextCommit.commit.id === this.state.lastCommit.commit.id) {
117
+ // No change here
118
+ } else if (nextCommit === undefined && this.state.lastCommit === undefined) {
119
+ // No change here
120
+ } else if (this.transitionHint === 'none') {
121
+ // If the 'last' commit changes, but we are not expecting an 'amend'
122
+ // or 'unamend' to occur, then we clear out the list of amended commits.
123
+ // This is because an unexpected change has happened to the repository,
124
+ // perhaps the user committed, merged, or something. The amended commits
125
+ // will no longer be valid.
126
+
127
+ // Note that there may or may not have been a previous lastCommit (if the
128
+ // repository was previously empty with no initial commit then lastCommit
129
+ // will be undefined). Either way we clear the amending commits.
130
+ await this.clearAmendingCommits();
131
+
132
+ // There is a change to the last commit, but no transition hint so
133
+ // the view just updates without transition.
134
+ this.setState({ amendingCommits: [], lastCommit: nextCommit });
135
+ } else {
136
+ const amendingCommits = this.state.amendingCommits.concat([]); // copy the array
137
+
138
+ const direction: 'up' | 'down' = this.transitionHint === 'amend' ? 'up' : 'down';
139
+ switch (this.transitionHint) {
140
+ case 'amend':
141
+ if (this.state.lastCommit) {
142
+ amendingCommits.push(this.state.lastCommit);
143
+
144
+ const serializedState = JSON.stringify({
145
+ amendingHeadCommitSha: amendingCommits[0].commit.id,
146
+ latestCommitSha: nextCommit ? nextCommit.commit.id : undefined
147
+ });
148
+ this.props.storageService.setData<string | undefined>(storageKey, serializedState);
149
+ }
150
+ break;
151
+ case 'unamend':
152
+ amendingCommits.pop();
153
+ if (amendingCommits.length === 0) {
154
+ this.props.storageService.setData<string | undefined>(storageKey, undefined);
155
+ } else {
156
+ const serializedState = JSON.stringify({
157
+ amendingHeadCommitSha: amendingCommits[0].commit.id,
158
+ latestCommitSha: nextCommit ? nextCommit.commit.id : undefined
159
+ });
160
+ this.props.storageService.setData<string | undefined>(storageKey, serializedState);
161
+ }
162
+ break;
163
+ }
164
+
165
+ if (this.state.lastCommit && nextCommit) {
166
+ const transitionData = { direction, previousLastCommit: this.state.lastCommit };
167
+ this.setState({ lastCommit: nextCommit, amendingCommits, transition: { ...transitionData, state: 'start' } });
168
+ this.onNextFrame(() => {
169
+ this.setState({ transition: { ...transitionData, state: 'transitioning' } });
170
+ });
171
+
172
+ setTimeout(
173
+ () => {
174
+ this.setState({ transition: { state: 'none' } });
175
+ },
176
+ TRANSITION_TIME_MS);
177
+ } else {
178
+ // No previous last commit so no transition
179
+ this.setState({ transition: { state: 'none' }, amendingCommits, lastCommit: nextCommit });
180
+ }
181
+ }
182
+
183
+ this.transitionHint = 'none';
184
+ }
185
+
186
+ private async clearAmendingCommits(): Promise<void> {
187
+ const storageKey = this.getStorageKey();
188
+ await this.props.storageService.setData<string | undefined>(storageKey, undefined);
189
+ }
190
+
191
+ private async buildAmendingList(lastCommit: ScmCommit | undefined): Promise<{ commit: ScmCommit, avatar: string }[]> {
192
+ const storageKey = this.getStorageKey();
193
+ const storedState = await this.props.storageService.getData<string | undefined>(storageKey, undefined);
194
+
195
+ // Restore list of commits from saved amending head commit up through parents until the
196
+ // current commit. (If we don't reach the current commit, the repository has been changed in such
197
+ // a way then unamending commits can no longer be done).
198
+ if (storedState) {
199
+ const { amendingHeadCommitSha, latestCommitSha } = JSON.parse(storedState);
200
+ if (!this.commitsAreEqual(lastCommit, latestCommitSha)) {
201
+ // The head commit in the repository has changed. It is not the same commit that was the
202
+ // head commit after the last 'amend'.
203
+ return [];
204
+ }
205
+ const commits = await this.props.scmAmendSupport.getInitialAmendingCommits(amendingHeadCommitSha, lastCommit ? lastCommit.id : undefined);
206
+
207
+ const amendingCommitPromises = commits.map(async commit => {
208
+ const avatar = await this.props.avatarService.getAvatar(commit.authorEmail);
209
+ return { commit, avatar };
210
+ });
211
+ return Promise.all(amendingCommitPromises);
212
+ } else {
213
+ return [];
214
+ }
215
+ }
216
+
217
+ private getStorageKey(): string {
218
+ return REPOSITORY_STORAGE_KEY + ':' + this.props.repository.provider.rootUri;
219
+ }
220
+
221
+ /**
222
+ * Commits are equal if the ids are equal or if both are undefined.
223
+ * (If a commit is undefined, it represents the initial empty state of a repository,
224
+ * before the initial commit).
225
+ */
226
+ private commitsAreEqual(lastCommit: ScmCommit | undefined, savedLastCommitId: string | undefined): boolean {
227
+ return lastCommit
228
+ ? lastCommit.id === savedLastCommitId
229
+ : savedLastCommitId === undefined;
230
+ }
231
+
232
+ /**
233
+ * This function will update the 'model' (lastCommit, amendingCommits) only
234
+ * when the repository sees the last commit change.
235
+ * 'render' can be called at any time, so be sure we don't update any 'model'
236
+ * fields until we actually start the transition.
237
+ */
238
+ protected amend = async (): Promise<void> => {
239
+ if (this.state.transition.state !== 'none' && this.transitionHint !== 'none') {
240
+ return;
241
+ }
242
+
243
+ this.transitionHint = 'amend';
244
+ await this.resetAndSetMessage('HEAD~', 'HEAD');
245
+ };
246
+
247
+ protected unamend = async (): Promise<void> => {
248
+ if (this.state.transition.state !== 'none' && this.transitionHint !== 'none') {
249
+ return;
250
+ }
251
+
252
+ const commitToRestore = (this.state.amendingCommits.length >= 1)
253
+ ? this.state.amendingCommits[this.state.amendingCommits.length - 1]
254
+ : undefined;
255
+ const oldestAmendCommit = (this.state.amendingCommits.length >= 2)
256
+ ? this.state.amendingCommits[this.state.amendingCommits.length - 2]
257
+ : undefined;
258
+
259
+ if (commitToRestore) {
260
+ const commitToUseForMessage = oldestAmendCommit
261
+ ? oldestAmendCommit.commit.id
262
+ : undefined;
263
+ this.transitionHint = 'unamend';
264
+ await this.resetAndSetMessage(commitToRestore.commit.id, commitToUseForMessage);
265
+ }
266
+ };
267
+
268
+ private async resetAndSetMessage(commitToRestore: string, commitToUseForMessage: string | undefined): Promise<void> {
269
+ const message = commitToUseForMessage
270
+ ? await this.props.scmAmendSupport.getMessage(commitToUseForMessage)
271
+ : '';
272
+ await this.props.scmAmendSupport.reset(commitToRestore);
273
+ this.props.setCommitMessage(message);
274
+ }
275
+
276
+ override render(): JSX.Element {
277
+ const neverShrink = this.state.amendingCommits.length <= 3;
278
+
279
+ const style: React.CSSProperties = neverShrink
280
+ ? {
281
+ ...this.props.style,
282
+ flexShrink: 0,
283
+ }
284
+ : {
285
+ ...this.props.style,
286
+ flexShrink: 1,
287
+ minHeight: 240 // height with three commits
288
+ };
289
+
290
+ return (
291
+ <div className={ScmAmendComponent.Styles.COMMIT_CONTAINER + ' no-select'} style={style}>
292
+ {
293
+ this.state.amendingCommits.length > 0 || (this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down')
294
+ ? this.renderAmendingCommits()
295
+ : ''
296
+ }
297
+ {
298
+ this.state.lastCommit ?
299
+ <div>
300
+ <div id='lastCommit' className='theia-scm-amend'>
301
+ <div className='theia-header scm-theia-header'>
302
+ {nls.localize('theia/scm/amendHeadCommit', 'HEAD Commit')}
303
+ </div>
304
+ {this.renderLastCommit()}
305
+ </div>
306
+ </div>
307
+ : ''
308
+ }
309
+ </div>
310
+ );
311
+ }
312
+
313
+ protected async getLastCommit(): Promise<{ commit: ScmCommit, avatar: string } | undefined> {
314
+ const commit = await this.props.scmAmendSupport.getLastCommit();
315
+ if (commit) {
316
+ const avatar = await this.props.avatarService.getAvatar(commit.authorEmail);
317
+ return { commit, avatar };
318
+ }
319
+ return undefined;
320
+ }
321
+
322
+ protected renderAmendingCommits(): React.ReactNode {
323
+ const neverShrink = this.state.amendingCommits.length <= 3;
324
+
325
+ const style: React.CSSProperties = neverShrink
326
+ ? {
327
+ flexShrink: 0,
328
+ }
329
+ : {
330
+ flexShrink: 1,
331
+ // parent minHeight controls height, we just need any value smaller than
332
+ // what the height would be when the parent is at its minHeight
333
+ minHeight: 0
334
+ };
335
+
336
+ return <div id='amendedCommits' className='theia-scm-amend-outer-container' style={style}>
337
+ <div className='theia-header scm-theia-header'>
338
+ <div className='noWrapInfo'>Commits being Amended</div>
339
+ {this.renderAmendCommitListButtons()}
340
+ {this.renderCommitCount(this.state.amendingCommits.length)}
341
+ </div>
342
+ <div style={this.styleAmendedCommits()}>
343
+ {this.state.amendingCommits.map((commitData, index, array) =>
344
+ this.renderCommitBeingAmended(commitData, index === array.length - 1)
345
+ )}
346
+ {
347
+ this.state.lastCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'down'
348
+ ? this.renderCommitBeingAmended(this.state.lastCommit, false)
349
+ : ''
350
+ }
351
+ </div>
352
+ </div>;
353
+ }
354
+
355
+ protected renderAmendCommitListButtons(): React.ReactNode {
356
+ return <div className='theia-scm-inline-actions-container'>
357
+ <div className='theia-scm-inline-actions'>
358
+ <div className='theia-scm-inline-action'>
359
+ <a className={codicon('dash')} title='Unamend All Commits' onClick={this.unamendAll} />
360
+ </div>
361
+ <div className='theia-scm-inline-action' >
362
+ <a className={codicon('close')} title='Clear Amending Commits' onClick={this.clearAmending} />
363
+ </div>
364
+ </div>
365
+ </div>;
366
+ }
367
+
368
+ protected renderLastCommit(): React.ReactNode {
369
+ if (!this.state.lastCommit) {
370
+ return '';
371
+ }
372
+
373
+ const canAmend: boolean = true;
374
+ return <div className={ScmAmendComponent.Styles.COMMIT_AND_BUTTON} style={{ flexGrow: 0, flexShrink: 0 }} key={this.state.lastCommit.commit.id}>
375
+ {this.renderLastCommitNoButton(this.state.lastCommit)}
376
+ {
377
+ canAmend
378
+ ? <div className={ScmAmendComponent.Styles.FLEX_CENTER}>
379
+ <button className='theia-button' title={nls.localize('theia/scm/amendLastCommit', 'Amend last commit')} onClick={this.amend}>
380
+ {nls.localize('theia/scm/amend', 'Amend')}
381
+ </button>
382
+ </div>
383
+ : ''
384
+ }
385
+ </div>;
386
+ }
387
+
388
+ protected renderLastCommitNoButton(lastCommit: { commit: ScmCommit, avatar: string }): React.ReactNode {
389
+ switch (this.state.transition.state) {
390
+ case 'none':
391
+ return <div ref={this.lastCommitScrollRef} className='theia-scm-scrolling-container'>
392
+ {this.renderCommitAvatarAndDetail(lastCommit)}
393
+ </div>;
394
+
395
+ case 'start':
396
+ case 'transitioning':
397
+ switch (this.state.transition.direction) {
398
+ case 'up':
399
+ return <div style={this.styleLastCommitMovingUp(this.state.transition.state)}>
400
+ {this.renderCommitAvatarAndDetail(this.state.transition.previousLastCommit)}
401
+ {this.renderCommitAvatarAndDetail(lastCommit)}
402
+ </div>;
403
+ case 'down':
404
+ return <div style={this.styleLastCommitMovingDown(this.state.transition.state)}>
405
+ {this.renderCommitAvatarAndDetail(lastCommit)}
406
+ {this.renderCommitAvatarAndDetail(this.state.transition.previousLastCommit)}
407
+ </div>;
408
+ }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * See https://stackoverflow.com/questions/26556436/react-after-render-code
414
+ *
415
+ * @param callback
416
+ */
417
+ protected onNextFrame(callback: FrameRequestCallback): void {
418
+ setTimeout(
419
+ () => window.requestAnimationFrame(callback),
420
+ 0);
421
+ }
422
+
423
+ protected renderCommitAvatarAndDetail(commitData: { commit: ScmCommit, avatar: string }): React.ReactNode {
424
+ const { commit, avatar } = commitData;
425
+ return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} key={commit.id}>
426
+ <div className={ScmAmendComponent.Styles.COMMIT_MESSAGE_AVATAR}>
427
+ <img src={avatar} />
428
+ </div>
429
+ <div className={ScmAmendComponent.Styles.COMMIT_DETAILS}>
430
+ <div className={ScmAmendComponent.Styles.COMMIT_MESSAGE_SUMMARY}>{commit.summary}</div>
431
+ <div className={ScmAmendComponent.Styles.LAST_COMMIT_MESSAGE_TIME}>{`${commit.authorDateRelative} by ${commit.authorName}`}</div>
432
+ </div>
433
+ </div>;
434
+ }
435
+
436
+ protected renderCommitCount(commits: number): React.ReactNode {
437
+ return <div className='notification-count-container scm-change-count'>
438
+ <span className='notification-count'>{commits}</span>
439
+ </div>;
440
+ }
441
+
442
+ protected renderCommitBeingAmended(commitData: { commit: ScmCommit, avatar: string }, isOldestAmendCommit: boolean): JSX.Element {
443
+ if (isOldestAmendCommit && this.state.transition.state !== 'none' && this.state.transition.direction === 'up') {
444
+ return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} style={{ flexGrow: 0, flexShrink: 0 }} key={commitData.commit.id}>
445
+ <div className='fixed-height-commit-container'>
446
+ {this.renderCommitAvatarAndDetail(commitData)}
447
+ </div>
448
+ </div>;
449
+ } else {
450
+ return <div className={ScmAmendComponent.Styles.COMMIT_AVATAR_AND_TEXT} style={{ flexGrow: 0, flexShrink: 0 }} key={commitData.commit.id}>
451
+ {this.renderCommitAvatarAndDetail(commitData)}
452
+ {
453
+ isOldestAmendCommit
454
+ ? <div className={ScmAmendComponent.Styles.FLEX_CENTER}>
455
+ <button className='theia-button' title={nls.localize('theia/scm/unamendCommit', 'Unamend commit')} onClick={this.unamend}>
456
+ {nls.localize('theia/scm/unamend', 'Unamend')}
457
+ </button>
458
+ </div>
459
+ : ''
460
+ }
461
+ </div>;
462
+ }
463
+ }
464
+
465
+ /*
466
+ * The style for the <div> containing the list of commits being amended.
467
+ * This div is scrollable.
468
+ */
469
+ protected styleAmendedCommits(): React.CSSProperties {
470
+ const base = {
471
+ display: 'flex',
472
+ whitespace: 'nowrap',
473
+ width: '100%',
474
+ minHeight: 0,
475
+ flexShrink: 1,
476
+ paddingTop: '2px',
477
+ };
478
+
479
+ switch (this.state.transition.state) {
480
+ case 'none':
481
+ return {
482
+ ...base,
483
+ flexDirection: 'column',
484
+ overflowY: 'auto',
485
+ marginBottom: '0',
486
+ };
487
+ case 'start':
488
+ case 'transitioning':
489
+ let startingMargin: number = 0;
490
+ let endingMargin: number = 0;
491
+ switch (this.state.transition.direction) {
492
+ case 'down':
493
+ startingMargin = 0;
494
+ endingMargin = -32;
495
+ break;
496
+ case 'up':
497
+ startingMargin = -32;
498
+ endingMargin = 0;
499
+ break;
500
+ }
501
+
502
+ switch (this.state.transition.state) {
503
+ case 'start':
504
+ return {
505
+ ...base,
506
+ flexDirection: 'column',
507
+ overflowY: 'hidden',
508
+ marginBottom: `${startingMargin}px`,
509
+ };
510
+ case 'transitioning':
511
+ return {
512
+ ...base,
513
+ flexDirection: 'column',
514
+ overflowY: 'hidden',
515
+ marginBottom: `${endingMargin}px`,
516
+ transitionProperty: 'margin-bottom',
517
+ transitionDuration: `${TRANSITION_TIME_MS}ms`,
518
+ transitionTimingFunction: 'linear'
519
+ };
520
+ }
521
+ }
522
+ }
523
+
524
+ protected styleLastCommitMovingUp(transitionState: 'start' | 'transitioning'): React.CSSProperties {
525
+ return this.styleLastCommit(transitionState, 0, -28);
526
+ }
527
+
528
+ protected styleLastCommitMovingDown(transitionState: 'start' | 'transitioning'): React.CSSProperties {
529
+ return this.styleLastCommit(transitionState, -28, 0);
530
+ }
531
+
532
+ protected styleLastCommit(transitionState: 'start' | 'transitioning', startingMarginTop: number, startingMarginBottom: number): React.CSSProperties {
533
+ const base = {
534
+ display: 'flex',
535
+ width: '100%',
536
+ overflow: 'hidden',
537
+ paddingTop: 0,
538
+ paddingBottom: 0,
539
+ borderTop: 0,
540
+ borderBottom: 0,
541
+ height: this.lastCommitHeight * 2
542
+ };
543
+
544
+ // We end with top and bottom margins switched
545
+ const endingMarginTop = startingMarginBottom;
546
+ const endingMarginBottom = startingMarginTop;
547
+
548
+ switch (transitionState) {
549
+ case 'start':
550
+ return {
551
+ ...base,
552
+ position: 'relative',
553
+ flexDirection: 'column',
554
+ marginTop: startingMarginTop,
555
+ marginBottom: startingMarginBottom,
556
+ };
557
+ case 'transitioning':
558
+ return {
559
+ ...base,
560
+ position: 'relative',
561
+ flexDirection: 'column',
562
+ marginTop: endingMarginTop,
563
+ marginBottom: endingMarginBottom,
564
+ transitionProperty: 'margin-top margin-bottom',
565
+ transitionDuration: `${TRANSITION_TIME_MS}ms`,
566
+ transitionTimingFunction: 'linear'
567
+ };
568
+ }
569
+ }
570
+
571
+ readonly unamendAll = () => this.doUnamendAll();
572
+ protected async doUnamendAll(): Promise<void> {
573
+ while (this.state.amendingCommits.length > 0) {
574
+ this.unamend();
575
+ await new Promise(resolve => setTimeout(resolve, TRANSITION_TIME_MS));
576
+ }
577
+ }
578
+
579
+ readonly clearAmending = () => this.doClearAmending();
580
+ protected async doClearAmending(): Promise<void> {
581
+ await this.clearAmendingCommits();
582
+ this.setState({ amendingCommits: [] });
583
+ }
584
+ }
585
+
586
+ export namespace ScmAmendComponent {
587
+
588
+ export namespace Styles {
589
+ export const COMMIT_CONTAINER = 'theia-scm-commit-container';
590
+ export const COMMIT_AND_BUTTON = 'theia-scm-commit-and-button';
591
+ export const COMMIT_AVATAR_AND_TEXT = 'theia-scm-commit-avatar-and-text';
592
+ export const COMMIT_DETAILS = 'theia-scm-commit-details';
593
+ export const COMMIT_MESSAGE_AVATAR = 'theia-scm-commit-message-avatar';
594
+ export const COMMIT_MESSAGE_SUMMARY = 'theia-scm-commit-message-summary';
595
+ export const LAST_COMMIT_MESSAGE_TIME = 'theia-scm-commit-message-time';
596
+
597
+ export const FLEX_CENTER = 'theia-scm-flex-container-center';
598
+ }
599
+
600
+ }