@@ -3,15 +3,27 @@ import {
3
3
Dialog ,
4
4
InputDialog ,
5
5
MainAreaWidget ,
6
+ ReactWidget ,
6
7
showDialog ,
7
8
showErrorMessage
8
9
} from '@jupyterlab/apputils' ;
10
+ import { PathExt } from '@jupyterlab/coreutils' ;
9
11
import { FileBrowser } from '@jupyterlab/filebrowser' ;
12
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime' ;
10
13
import { ISettingRegistry } from '@jupyterlab/settingregistry' ;
11
14
import { ITerminal } from '@jupyterlab/terminal' ;
12
15
import { CommandRegistry } from '@lumino/commands' ;
13
16
import { Menu } from '@lumino/widgets' ;
14
- import { IGitExtension } from './tokens' ;
17
+ import * as React from 'react' ;
18
+ import {
19
+ Diff ,
20
+ isDiffSupported ,
21
+ RenderMimeProvider
22
+ } from './components/diff/Diff' ;
23
+ import { getRefValue , IDiffContext } from './components/diff/model' ;
24
+ import { GitExtension } from './model' ;
25
+ import { diffIcon } from './style/icons' ;
26
+ import { Git } from './tokens' ;
15
27
import { GitCredentialsForm } from './widgets/CredentialsBox' ;
16
28
import { doGitClone } from './widgets/gitClone' ;
17
29
import { GitPullPushDialog , Operation } from './widgets/gitPushPull' ;
@@ -42,16 +54,26 @@ export namespace CommandIDs {
42
54
export const gitOpenGitignore = 'git:open-gitignore' ;
43
55
export const gitPush = 'git:push' ;
44
56
export const gitPull = 'git:pull' ;
57
+ // Context menu commands
58
+ export const gitFileDiff = 'git:context-diff' ;
59
+ export const gitFileDiscard = 'git:context-discard' ;
60
+ export const gitFileOpen = 'git:context-open' ;
61
+ export const gitFileUnstage = 'git:context-unstage' ;
62
+ export const gitFileStage = 'git:context-stage' ;
63
+ export const gitFileTrack = 'git:context-track' ;
64
+ export const gitIgnore = 'git:context-ignore' ;
65
+ export const gitIgnoreExtension = 'git:context-ignoreExtension' ;
45
66
}
46
67
47
68
/**
48
69
* Add the commands for the git extension.
49
70
*/
50
71
export function addCommands (
51
72
app : JupyterFrontEnd ,
52
- model : IGitExtension ,
73
+ model : GitExtension ,
53
74
fileBrowser : FileBrowser ,
54
- settings : ISettingRegistry . ISettings
75
+ settings : ISettingRegistry . ISettings ,
76
+ renderMime : IRenderMimeRegistry
55
77
) {
56
78
const { commands, shell } = app ;
57
79
@@ -232,6 +254,212 @@ export function addCommands(
232
254
) ;
233
255
}
234
256
} ) ;
257
+
258
+ /* Context menu commands */
259
+ commands . addCommand ( CommandIDs . gitFileOpen , {
260
+ label : 'Open' ,
261
+ caption : 'Open selected file' ,
262
+ execute : async args => {
263
+ const file : Git . IStatusFileResult = args as any ;
264
+
265
+ const { x, y, to } = file ;
266
+ if ( x === 'D' || y === 'D' ) {
267
+ await showErrorMessage (
268
+ 'Open File Failed' ,
269
+ 'This file has been deleted!'
270
+ ) ;
271
+ return ;
272
+ }
273
+ try {
274
+ if ( to [ to . length - 1 ] !== '/' ) {
275
+ commands . execute ( 'docmanager:open' , {
276
+ path : model . getRelativeFilePath ( to )
277
+ } ) ;
278
+ } else {
279
+ console . log ( 'Cannot open a folder here' ) ;
280
+ }
281
+ } catch ( err ) {
282
+ console . error ( `Fail to open ${ to } .` ) ;
283
+ }
284
+ }
285
+ } ) ;
286
+
287
+ commands . addCommand ( CommandIDs . gitFileDiff , {
288
+ label : 'Diff' ,
289
+ caption : 'Diff selected file' ,
290
+ execute : args => {
291
+ const { context, filePath, isText, status } = ( args as any ) as {
292
+ context ?: IDiffContext ;
293
+ filePath : string ;
294
+ isText : boolean ;
295
+ status ?: Git . Status ;
296
+ } ;
297
+
298
+ let diffContext = context ;
299
+ if ( ! diffContext ) {
300
+ const specialRef = status === 'staged' ? 'INDEX' : 'WORKING' ;
301
+ diffContext = {
302
+ currentRef : { specialRef } ,
303
+ previousRef : { gitRef : 'HEAD' }
304
+ } ;
305
+ }
306
+
307
+ if ( isDiffSupported ( filePath ) || isText ) {
308
+ const id = `nbdiff-${ filePath } -${ getRefValue ( diffContext . currentRef ) } ` ;
309
+ const mainAreaItems = shell . widgets ( 'main' ) ;
310
+ let mainAreaItem = mainAreaItems . next ( ) ;
311
+ while ( mainAreaItem ) {
312
+ if ( mainAreaItem . id === id ) {
313
+ shell . activateById ( id ) ;
314
+ break ;
315
+ }
316
+ mainAreaItem = mainAreaItems . next ( ) ;
317
+ }
318
+
319
+ if ( ! mainAreaItem ) {
320
+ const serverRepoPath = model . getRelativeFilePath ( ) ;
321
+ const nbDiffWidget = ReactWidget . create (
322
+ < RenderMimeProvider value = { renderMime } >
323
+ < Diff
324
+ path = { filePath }
325
+ diffContext = { diffContext }
326
+ topRepoPath = { serverRepoPath }
327
+ />
328
+ </ RenderMimeProvider >
329
+ ) ;
330
+ nbDiffWidget . id = id ;
331
+ nbDiffWidget . title . label = PathExt . basename ( filePath ) ;
332
+ nbDiffWidget . title . icon = diffIcon ;
333
+ nbDiffWidget . title . closable = true ;
334
+ nbDiffWidget . addClass ( 'jp-git-diff-parent-diff-widget' ) ;
335
+
336
+ shell . add ( nbDiffWidget , 'main' ) ;
337
+ shell . activateById ( nbDiffWidget . id ) ;
338
+ }
339
+ } else {
340
+ showErrorMessage (
341
+ 'Diff Not Supported' ,
342
+ `Diff is not supported for ${ PathExt . extname (
343
+ filePath
344
+ ) . toLocaleLowerCase ( ) } files.`
345
+ ) ;
346
+ }
347
+ }
348
+ } ) ;
349
+
350
+ commands . addCommand ( CommandIDs . gitFileStage , {
351
+ label : 'Stage' ,
352
+ caption : 'Stage the changes of selected file' ,
353
+ execute : async args => {
354
+ const selectedFile : Git . IStatusFile = args as any ;
355
+ await model . add ( selectedFile . to ) ;
356
+ }
357
+ } ) ;
358
+
359
+ commands . addCommand ( CommandIDs . gitFileTrack , {
360
+ label : 'Track' ,
361
+ caption : 'Start tracking selected file' ,
362
+ execute : async args => {
363
+ const selectedFile : Git . IStatusFile = args as any ;
364
+ await model . add ( selectedFile . to ) ;
365
+ }
366
+ } ) ;
367
+
368
+ commands . addCommand ( CommandIDs . gitFileUnstage , {
369
+ label : 'Unstage' ,
370
+ caption : 'Unstage the changes of selected file' ,
371
+ execute : async args => {
372
+ const selectedFile : Git . IStatusFile = args as any ;
373
+ if ( selectedFile . x !== 'D' ) {
374
+ await model . reset ( selectedFile . to ) ;
375
+ }
376
+ }
377
+ } ) ;
378
+
379
+ commands . addCommand ( CommandIDs . gitFileDiscard , {
380
+ label : 'Discard' ,
381
+ caption : 'Discard recent changes of selected file' ,
382
+ execute : async args => {
383
+ const file : Git . IStatusFile = args as any ;
384
+
385
+ const result = await showDialog ( {
386
+ title : 'Discard changes' ,
387
+ body : (
388
+ < span >
389
+ Are you sure you want to permanently discard changes to{ ' ' }
390
+ < b > { file . to } </ b > ? This action cannot be undone.
391
+ </ span >
392
+ ) ,
393
+ buttons : [
394
+ Dialog . cancelButton ( ) ,
395
+ Dialog . warnButton ( { label : 'Discard' } )
396
+ ]
397
+ } ) ;
398
+ if ( result . button . accept ) {
399
+ try {
400
+ if ( file . status === 'staged' || file . status === 'partially-staged' ) {
401
+ await model . reset ( file . to ) ;
402
+ }
403
+ if (
404
+ file . status === 'unstaged' ||
405
+ ( file . status === 'partially-staged' && file . x !== 'A' )
406
+ ) {
407
+ // resetting an added file moves it to untracked category => checkout will fail
408
+ await model . checkout ( { filename : file . to } ) ;
409
+ }
410
+ } catch ( reason ) {
411
+ showErrorMessage ( `Discard changes for ${ file . to } failed.` , reason , [
412
+ Dialog . warnButton ( { label : 'DISMISS' } )
413
+ ] ) ;
414
+ }
415
+ }
416
+ }
417
+ } ) ;
418
+
419
+ commands . addCommand ( CommandIDs . gitIgnore , {
420
+ label : ( ) => 'Ignore this file (add to .gitignore)' ,
421
+ caption : ( ) => 'Ignore this file (add to .gitignore)' ,
422
+ execute : async args => {
423
+ const selectedFile : Git . IStatusFile = args as any ;
424
+ if ( selectedFile ) {
425
+ await model . ignore ( selectedFile . to , false ) ;
426
+ }
427
+ }
428
+ } ) ;
429
+
430
+ commands . addCommand ( CommandIDs . gitIgnoreExtension , {
431
+ label : args => {
432
+ const selectedFile : Git . IStatusFile = args as any ;
433
+ return `Ignore ${ PathExt . extname (
434
+ selectedFile . to
435
+ ) } extension (add to .gitignore)`;
436
+ } ,
437
+ caption : 'Ignore this file extension (add to .gitignore)' ,
438
+ execute : async args => {
439
+ const selectedFile : Git . IStatusFile = args as any ;
440
+ if ( selectedFile ) {
441
+ const extension = PathExt . extname ( selectedFile . to ) ;
442
+ if ( extension . length > 0 ) {
443
+ const result = await showDialog ( {
444
+ title : 'Ignore file extension' ,
445
+ body : `Are you sure you want to ignore all ${ extension } files within this git repository?` ,
446
+ buttons : [
447
+ Dialog . cancelButton ( ) ,
448
+ Dialog . okButton ( { label : 'Ignore' } )
449
+ ]
450
+ } ) ;
451
+ if ( result . button . label === 'Ignore' ) {
452
+ await model . ignore ( selectedFile . to , true ) ;
453
+ }
454
+ }
455
+ }
456
+ } ,
457
+ isVisible : args => {
458
+ const selectedFile : Git . IStatusFile = args as any ;
459
+ const extension = PathExt . extname ( selectedFile . to ) ;
460
+ return extension . length > 0 ;
461
+ }
462
+ } ) ;
235
463
}
236
464
237
465
/**
@@ -295,7 +523,7 @@ namespace Private {
295
523
* @returns Promise for displaying a dialog
296
524
*/
297
525
export async function showGitOperationDialog (
298
- model : IGitExtension ,
526
+ model : GitExtension ,
299
527
operation : Operation
300
528
) : Promise < void > {
301
529
const title = `Git ${ operation } ` ;
0 commit comments