Skip to content

Commit f60ed32

Browse files
authored
Merge pull request #878 from krassowski/commit-on-ctrl-enter
Allow to commit with ctrl + enter
2 parents 8375459 + 3a90f51 commit f60ed32

File tree

5 files changed

+146
-28
lines changed

5 files changed

+146
-28
lines changed

schema/plugin.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,12 @@
5959
"description": "If true, use a simplified concept of staging. Only files with changes are shown (instead of showing staged/changed/untracked), and all files with changes will be automatically staged",
6060
"default": false
6161
}
62-
}
62+
},
63+
"jupyter.lab.shortcuts": [
64+
{
65+
"command": "git:submit-commit",
66+
"keys": ["Accel Enter"],
67+
"selector": ".jp-git-CommitBox"
68+
}
69+
]
6370
}

src/commandsAndMenu.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export namespace CommandIDs {
8787
export const gitIgnoreExtension = 'git:context-ignoreExtension';
8888
}
8989

90+
export const SUBMIT_COMMIT_COMMAND = 'git:submit-commit';
91+
9092
/**
9193
* Add the commands for the git extension.
9294
*/
@@ -99,6 +101,22 @@ export function addCommands(
99101
) {
100102
const { commands, shell } = app;
101103

104+
/**
105+
* Commit using a keystroke combination when in CommitBox.
106+
*
107+
* This command is not accessible from the user interface (not visible),
108+
* as it is handled by a signal listener in the CommitBox component instead.
109+
* The label and caption are given to ensure that the command will
110+
* show up in the shortcut editor UI with a nice description.
111+
*/
112+
commands.addCommand(SUBMIT_COMMIT_COMMAND, {
113+
label: 'Commit from the Commit Box',
114+
caption:
115+
'Submit the commit using the summary and description from commit box',
116+
execute: () => void 0,
117+
isVisible: () => false
118+
});
119+
102120
/**
103121
* Add open terminal in the Git repository
104122
*/

src/components/CommitBox.tsx

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ import {
66
commitDescriptionClass,
77
commitButtonClass
88
} from '../style/CommitBox';
9+
import { CommandRegistry } from '@lumino/commands';
10+
import { SUBMIT_COMMIT_COMMAND } from '../commandsAndMenu';
911

1012
/**
1113
* Interface describing component properties.
1214
*/
1315
export interface ICommitBoxProps {
16+
/**
17+
* Jupyter App commands registry
18+
*/
19+
commands: CommandRegistry;
20+
1421
/**
1522
* Boolean indicating whether files currently exist which have changes to commit.
1623
*/
@@ -61,24 +68,37 @@ export class CommitBox extends React.Component<
6168
};
6269
}
6370

71+
componentDidMount(): void {
72+
this.props.commands.commandExecuted.connect(this._handleCommand);
73+
}
74+
75+
componentWillUnmount(): void {
76+
this.props.commands.commandExecuted.disconnect(this._handleCommand);
77+
}
78+
6479
/**
6580
* Renders the component.
6681
*
6782
* @returns React element
6883
*/
6984
render(): React.ReactElement {
70-
const disabled = !(this.props.hasFiles && this.state.summary);
85+
const disabled = !this._canCommit();
7186
const title = !this.props.hasFiles
7287
? 'Disabled: No files are staged for commit'
7388
: !this.state.summary
7489
? 'Disabled: No commit message summary'
7590
: 'Commit';
91+
92+
const shortcutHint = CommandRegistry.formatKeystroke(
93+
this._getSubmitKeystroke()
94+
);
95+
const summaryPlaceholder = 'Summary (' + shortcutHint + ' to commit)';
7696
return (
77-
<form className={commitFormClass}>
97+
<form className={[commitFormClass, 'jp-git-CommitBox'].join(' ')}>
7898
<input
7999
className={commitSummaryClass}
80100
type="text"
81-
placeholder="Summary (required)"
101+
placeholder={summaryPlaceholder}
82102
title="Enter a commit message summary (a single line, preferably less than 50 characters)"
83103
value={this.state.summary}
84104
onChange={this._onSummaryChange}
@@ -87,7 +107,7 @@ export class CommitBox extends React.Component<
87107
<TextareaAutosize
88108
className={commitDescriptionClass}
89109
minRows={5}
90-
placeholder="Description"
110+
placeholder="Description (optional)"
91111
title="Enter a commit message description"
92112
value={this.state.description}
93113
onChange={this._onDescriptionChange}
@@ -98,18 +118,33 @@ export class CommitBox extends React.Component<
98118
title={title}
99119
value="Commit"
100120
disabled={disabled}
101-
onClick={this._onCommitClick}
121+
onClick={this._onCommitSubmit}
102122
/>
103123
</form>
104124
);
105125
}
106126

107127
/**
108-
* Callback invoked upon clicking a commit message submit button.
109-
*
110-
* @param event - event object
128+
* Whether a commit can be performed (files are staged and summary is not empty).
129+
*/
130+
private _canCommit(): boolean {
131+
return !!(this.props.hasFiles && this.state.summary);
132+
}
133+
134+
/**
135+
* Get keystroke configured to act as a submit action.
111136
*/
112-
private _onCommitClick = (): void => {
137+
private _getSubmitKeystroke = (): string => {
138+
const binding = this.props.commands.keyBindings.find(
139+
binding => binding.command === SUBMIT_COMMIT_COMMAND
140+
);
141+
return binding.keys.join(' ');
142+
};
143+
144+
/**
145+
* Callback invoked upon clicking a commit message submit button or otherwise submitting the form.
146+
*/
147+
private _onCommitSubmit = (): void => {
113148
const msg = this.state.summary + '\n\n' + this.state.description + '\n';
114149
this.props.onCommit(msg);
115150

@@ -148,11 +183,28 @@ export class CommitBox extends React.Component<
148183
*
149184
* @param event - event object
150185
*/
151-
private _onSummaryKeyPress(event: any): void {
152-
if (event.which === 13) {
186+
private _onSummaryKeyPress = (event: React.KeyboardEvent): void => {
187+
if (event.key === 'Enter') {
153188
event.preventDefault();
154189
}
155-
}
190+
};
191+
192+
/**
193+
* Callback invoked upon command execution activated when entering a commit message description.
194+
*
195+
* ## Notes
196+
*
197+
* - Triggers the `'submit'` action on appropriate command (and if commit is possible)
198+
*
199+
*/
200+
private _handleCommand = (
201+
_: CommandRegistry,
202+
commandArgs: CommandRegistry.ICommandExecutedArgs
203+
): void => {
204+
if (commandArgs.id === SUBMIT_COMMIT_COMMAND && this._canCommit()) {
205+
this._onCommitSubmit();
206+
}
207+
};
156208

157209
/**
158210
* Resets component state (e.g., in order to re-initialize the commit message input box).

src/components/GitPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,13 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
373373
<CommitBox
374374
hasFiles={this._markedFiles.length > 0}
375375
onCommit={this.commitMarkedFiles}
376+
commands={this.props.commands}
376377
/>
377378
) : (
378379
<CommitBox
379380
hasFiles={this._hasStagedFile()}
380381
onCommit={this.commitStagedFiles}
382+
commands={this.props.commands}
381383
/>
382384
)}
383385
</React.Fragment>

tests/test-components/CommitBox.spec.tsx

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,43 @@
11
import * as React from 'react';
22
import 'jest';
33
import { shallow } from 'enzyme';
4-
import { CommitBox } from '../../src/components/CommitBox';
4+
import { CommitBox} from '../../src/components/CommitBox';
5+
import { CommandRegistry } from '@lumino/commands';
6+
import { SUBMIT_COMMIT_COMMAND } from '../../src/commandsAndMenu';
57

68
describe('CommitBox', () => {
9+
10+
const defaultCommands = new CommandRegistry()
11+
defaultCommands.addKeyBinding({
12+
keys: ['Accel Enter'],
13+
command: SUBMIT_COMMIT_COMMAND,
14+
selector: '.jp-git-CommitBox'
15+
})
16+
717
describe('#constructor()', () => {
818
it('should return a new instance', () => {
919
const box = new CommitBox({
1020
onCommit: async () => {},
11-
hasFiles: false
21+
hasFiles: false,
22+
commands: defaultCommands
1223
});
1324
expect(box).toBeInstanceOf(CommitBox);
1425
});
1526

1627
it('should set the default commit message summary to an empty string', () => {
1728
const box = new CommitBox({
1829
onCommit: async () => {},
19-
hasFiles: false
30+
hasFiles: false,
31+
commands: defaultCommands
2032
});
2133
expect(box.state.summary).toEqual('');
2234
});
2335

2436
it('should set the default commit message description to an empty string', () => {
2537
const box = new CommitBox({
2638
onCommit: async () => {},
27-
hasFiles: false
39+
hasFiles: false,
40+
commands: defaultCommands
2841
});
2942
expect(box.state.description).toEqual('');
3043
});
@@ -34,17 +47,36 @@ describe('CommitBox', () => {
3447
it('should display placeholder text for the commit message summary', () => {
3548
const props = {
3649
onCommit: async () => {},
37-
hasFiles: false
50+
hasFiles: false,
51+
commands: defaultCommands
52+
};
53+
const component = shallow(<CommitBox {...props} />);
54+
const node = component.find('input[type="text"]').first();
55+
expect(node.prop('placeholder')).toEqual('Summary (Ctrl+Enter to commit)');
56+
});
57+
58+
it('should adjust placeholder text for the commit message summary when keybinding changes', () => {
59+
const adjustedCommands = new CommandRegistry()
60+
adjustedCommands.addKeyBinding({
61+
keys: ['Shift Enter'],
62+
command: SUBMIT_COMMIT_COMMAND,
63+
selector: '.jp-git-CommitBox'
64+
})
65+
const props = {
66+
onCommit: async () => {},
67+
hasFiles: false,
68+
commands: adjustedCommands
3869
};
3970
const component = shallow(<CommitBox {...props} />);
4071
const node = component.find('input[type="text"]').first();
41-
expect(node.prop('placeholder')).toEqual('Summary (required)');
72+
expect(node.prop('placeholder')).toEqual('Summary (Shift+Enter to commit)');
4273
});
4374

4475
it('should set a `title` attribute on the input element to provide a commit message summary', () => {
4576
const props = {
4677
onCommit: async () => {},
47-
hasFiles: false
78+
hasFiles: false,
79+
commands: defaultCommands
4880
};
4981
const component = shallow(<CommitBox {...props} />);
5082
const node = component.find('input[type="text"]').first();
@@ -54,17 +86,19 @@ describe('CommitBox', () => {
5486
it('should display placeholder text for the commit message description', () => {
5587
const props = {
5688
onCommit: async () => {},
57-
hasFiles: false
89+
hasFiles: false,
90+
commands: defaultCommands
5891
};
5992
const component = shallow(<CommitBox {...props} />);
6093
const node = component.find('TextareaAutosize').first();
61-
expect(node.prop('placeholder')).toEqual('Description');
94+
expect(node.prop('placeholder')).toEqual('Description (optional)');
6295
});
6396

6497
it('should set a `title` attribute on the input element to provide a commit message description', () => {
6598
const props = {
6699
onCommit: async () => {},
67-
hasFiles: false
100+
hasFiles: false,
101+
commands: defaultCommands
68102
};
69103
const component = shallow(<CommitBox {...props} />);
70104
const node = component.find('TextareaAutosize').first();
@@ -74,7 +108,8 @@ describe('CommitBox', () => {
74108
it('should display a button to commit changes', () => {
75109
const props = {
76110
onCommit: async () => {},
77-
hasFiles: false
111+
hasFiles: false,
112+
commands: defaultCommands
78113
};
79114
const component = shallow(<CommitBox {...props} />);
80115
const node = component.find('input[type="button"]').first();
@@ -84,7 +119,8 @@ describe('CommitBox', () => {
84119
it('should set a `title` attribute on the button to commit changes', () => {
85120
const props = {
86121
onCommit: async () => {},
87-
hasFiles: false
122+
hasFiles: false,
123+
commands: defaultCommands
88124
};
89125
const component = shallow(<CommitBox {...props} />);
90126
const node = component.find('input[type="button"]').first();
@@ -94,7 +130,8 @@ describe('CommitBox', () => {
94130
it('should apply a class to disable the commit button when no files have changes to commit', () => {
95131
const props = {
96132
onCommit: async () => {},
97-
hasFiles: false
133+
hasFiles: false,
134+
commands: defaultCommands
98135
};
99136
const component = shallow(<CommitBox {...props} />);
100137
const node = component.find('input[type="button"]').first();
@@ -105,7 +142,8 @@ describe('CommitBox', () => {
105142
it('should apply a class to disable the commit button when files have changes to commit, but the user has not entered a commit message summary', () => {
106143
const props = {
107144
onCommit: async () => {},
108-
hasFiles: true
145+
hasFiles: true,
146+
commands: defaultCommands
109147
};
110148
const component = shallow(<CommitBox {...props} />);
111149
const node = component.find('input[type="button"]').first();
@@ -116,7 +154,8 @@ describe('CommitBox', () => {
116154
it('should not apply a class to disable the commit button when files have changes to commit and the user has entered a commit message summary', () => {
117155
const props = {
118156
onCommit: async () => {},
119-
hasFiles: true
157+
hasFiles: true,
158+
commands: defaultCommands
120159
};
121160
const component = shallow(<CommitBox {...props} />);
122161
component.setState({

0 commit comments

Comments
 (0)