Skip to content

Commit 2d8d659

Browse files
authored
Merge pull request #765 from fcollonval/fix/issue712
Better design based on commands
2 parents 09bf406 + 5f9d1dc commit 2d8d659

22 files changed

+573
-713
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"clean:slate": "jlpm clean:more && jlpm clean:labextension && rimraf node_modules",
2222
"contributors:generate": "jlpm run all-contributors generate",
2323
"lint": "eslint . --ext .ts,.tsx --fix",
24-
"test": "jest",
24+
"test": "jest --no-cache",
2525
"eslint-check": "eslint . --ext .ts,.tsx",
2626
"prepare": "jlpm run build",
2727
"watch": "tsc -w"

src/commandsAndMenu.ts renamed to src/commandsAndMenu.tsx

Lines changed: 232 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,27 @@ import {
33
Dialog,
44
InputDialog,
55
MainAreaWidget,
6+
ReactWidget,
67
showDialog,
78
showErrorMessage
89
} from '@jupyterlab/apputils';
10+
import { PathExt } from '@jupyterlab/coreutils';
911
import { FileBrowser } from '@jupyterlab/filebrowser';
12+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
1013
import { ISettingRegistry } from '@jupyterlab/settingregistry';
1114
import { ITerminal } from '@jupyterlab/terminal';
1215
import { CommandRegistry } from '@lumino/commands';
1316
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';
1527
import { GitCredentialsForm } from './widgets/CredentialsBox';
1628
import { doGitClone } from './widgets/gitClone';
1729
import { GitPullPushDialog, Operation } from './widgets/gitPushPull';
@@ -42,16 +54,26 @@ export namespace CommandIDs {
4254
export const gitOpenGitignore = 'git:open-gitignore';
4355
export const gitPush = 'git:push';
4456
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';
4566
}
4667

4768
/**
4869
* Add the commands for the git extension.
4970
*/
5071
export function addCommands(
5172
app: JupyterFrontEnd,
52-
model: IGitExtension,
73+
model: GitExtension,
5374
fileBrowser: FileBrowser,
54-
settings: ISettingRegistry.ISettings
75+
settings: ISettingRegistry.ISettings,
76+
renderMime: IRenderMimeRegistry
5577
) {
5678
const { commands, shell } = app;
5779

@@ -232,6 +254,212 @@ export function addCommands(
232254
);
233255
}
234256
});
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+
});
235463
}
236464

237465
/**
@@ -295,7 +523,7 @@ namespace Private {
295523
* @returns Promise for displaying a dialog
296524
*/
297525
export async function showGitOperationDialog(
298-
model: IGitExtension,
526+
model: GitExtension,
299527
operation: Operation
300528
): Promise<void> {
301529
const title = `Git ${operation}`;

src/components/FileItem.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const STATUS_CODES = {
2727

2828
export interface IFileItemProps {
2929
actions?: React.ReactElement;
30-
contextMenu?: (event: React.MouseEvent) => void;
30+
contextMenu?: (file: Git.IStatusFile, event: React.MouseEvent) => void;
3131
file: Git.IStatusFile;
3232
markBox?: boolean;
3333
model: GitExtension;
@@ -88,10 +88,7 @@ export class FileItem extends React.Component<IFileItemProps> {
8888
onContextMenu={
8989
this.props.contextMenu &&
9090
(event => {
91-
if (this.props.selectFile) {
92-
this.props.selectFile(this.props.file);
93-
}
94-
this.props.contextMenu(event);
91+
this.props.contextMenu(this.props.file, event);
9592
})
9693
}
9794
onDoubleClick={this.props.onDoubleClick}

0 commit comments

Comments
 (0)