Skip to content

Commit

Permalink
Merge pull request #878 from krassowski/commit-on-ctrl-enter
Browse files Browse the repository at this point in the history
Allow to commit with ctrl + enter
  • Loading branch information
fcollonval authored Feb 25, 2021
2 parents 8375459 + 3a90f51 commit f60ed32
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 28 deletions.
9 changes: 8 additions & 1 deletion schema/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,12 @@
"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",
"default": false
}
}
},
"jupyter.lab.shortcuts": [
{
"command": "git:submit-commit",
"keys": ["Accel Enter"],
"selector": ".jp-git-CommitBox"
}
]
}
18 changes: 18 additions & 0 deletions src/commandsAndMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export namespace CommandIDs {
export const gitIgnoreExtension = 'git:context-ignoreExtension';
}

export const SUBMIT_COMMIT_COMMAND = 'git:submit-commit';

/**
* Add the commands for the git extension.
*/
Expand All @@ -99,6 +101,22 @@ export function addCommands(
) {
const { commands, shell } = app;

/**
* Commit using a keystroke combination when in CommitBox.
*
* This command is not accessible from the user interface (not visible),
* as it is handled by a signal listener in the CommitBox component instead.
* The label and caption are given to ensure that the command will
* show up in the shortcut editor UI with a nice description.
*/
commands.addCommand(SUBMIT_COMMIT_COMMAND, {
label: 'Commit from the Commit Box',
caption:
'Submit the commit using the summary and description from commit box',
execute: () => void 0,
isVisible: () => false
});

/**
* Add open terminal in the Git repository
*/
Expand Down
76 changes: 64 additions & 12 deletions src/components/CommitBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ import {
commitDescriptionClass,
commitButtonClass
} from '../style/CommitBox';
import { CommandRegistry } from '@lumino/commands';
import { SUBMIT_COMMIT_COMMAND } from '../commandsAndMenu';

/**
* Interface describing component properties.
*/
export interface ICommitBoxProps {
/**
* Jupyter App commands registry
*/
commands: CommandRegistry;

/**
* Boolean indicating whether files currently exist which have changes to commit.
*/
Expand Down Expand Up @@ -61,24 +68,37 @@ export class CommitBox extends React.Component<
};
}

componentDidMount(): void {
this.props.commands.commandExecuted.connect(this._handleCommand);
}

componentWillUnmount(): void {
this.props.commands.commandExecuted.disconnect(this._handleCommand);
}

/**
* Renders the component.
*
* @returns React element
*/
render(): React.ReactElement {
const disabled = !(this.props.hasFiles && this.state.summary);
const disabled = !this._canCommit();
const title = !this.props.hasFiles
? 'Disabled: No files are staged for commit'
: !this.state.summary
? 'Disabled: No commit message summary'
: 'Commit';

const shortcutHint = CommandRegistry.formatKeystroke(
this._getSubmitKeystroke()
);
const summaryPlaceholder = 'Summary (' + shortcutHint + ' to commit)';
return (
<form className={commitFormClass}>
<form className={[commitFormClass, 'jp-git-CommitBox'].join(' ')}>
<input
className={commitSummaryClass}
type="text"
placeholder="Summary (required)"
placeholder={summaryPlaceholder}
title="Enter a commit message summary (a single line, preferably less than 50 characters)"
value={this.state.summary}
onChange={this._onSummaryChange}
Expand All @@ -87,7 +107,7 @@ export class CommitBox extends React.Component<
<TextareaAutosize
className={commitDescriptionClass}
minRows={5}
placeholder="Description"
placeholder="Description (optional)"
title="Enter a commit message description"
value={this.state.description}
onChange={this._onDescriptionChange}
Expand All @@ -98,18 +118,33 @@ export class CommitBox extends React.Component<
title={title}
value="Commit"
disabled={disabled}
onClick={this._onCommitClick}
onClick={this._onCommitSubmit}
/>
</form>
);
}

/**
* Callback invoked upon clicking a commit message submit button.
*
* @param event - event object
* Whether a commit can be performed (files are staged and summary is not empty).
*/
private _canCommit(): boolean {
return !!(this.props.hasFiles && this.state.summary);
}

/**
* Get keystroke configured to act as a submit action.
*/
private _onCommitClick = (): void => {
private _getSubmitKeystroke = (): string => {
const binding = this.props.commands.keyBindings.find(
binding => binding.command === SUBMIT_COMMIT_COMMAND
);
return binding.keys.join(' ');
};

/**
* Callback invoked upon clicking a commit message submit button or otherwise submitting the form.
*/
private _onCommitSubmit = (): void => {
const msg = this.state.summary + '\n\n' + this.state.description + '\n';
this.props.onCommit(msg);

Expand Down Expand Up @@ -148,11 +183,28 @@ export class CommitBox extends React.Component<
*
* @param event - event object
*/
private _onSummaryKeyPress(event: any): void {
if (event.which === 13) {
private _onSummaryKeyPress = (event: React.KeyboardEvent): void => {
if (event.key === 'Enter') {
event.preventDefault();
}
}
};

/**
* Callback invoked upon command execution activated when entering a commit message description.
*
* ## Notes
*
* - Triggers the `'submit'` action on appropriate command (and if commit is possible)
*
*/
private _handleCommand = (
_: CommandRegistry,
commandArgs: CommandRegistry.ICommandExecutedArgs
): void => {
if (commandArgs.id === SUBMIT_COMMIT_COMMAND && this._canCommit()) {
this._onCommitSubmit();
}
};

/**
* Resets component state (e.g., in order to re-initialize the commit message input box).
Expand Down
2 changes: 2 additions & 0 deletions src/components/GitPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,13 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
<CommitBox
hasFiles={this._markedFiles.length > 0}
onCommit={this.commitMarkedFiles}
commands={this.props.commands}
/>
) : (
<CommitBox
hasFiles={this._hasStagedFile()}
onCommit={this.commitStagedFiles}
commands={this.props.commands}
/>
)}
</React.Fragment>
Expand Down
69 changes: 54 additions & 15 deletions tests/test-components/CommitBox.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
import * as React from 'react';
import 'jest';
import { shallow } from 'enzyme';
import { CommitBox } from '../../src/components/CommitBox';
import { CommitBox} from '../../src/components/CommitBox';
import { CommandRegistry } from '@lumino/commands';
import { SUBMIT_COMMIT_COMMAND } from '../../src/commandsAndMenu';

describe('CommitBox', () => {

const defaultCommands = new CommandRegistry()
defaultCommands.addKeyBinding({
keys: ['Accel Enter'],
command: SUBMIT_COMMIT_COMMAND,
selector: '.jp-git-CommitBox'
})

describe('#constructor()', () => {
it('should return a new instance', () => {
const box = new CommitBox({
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
});
expect(box).toBeInstanceOf(CommitBox);
});

it('should set the default commit message summary to an empty string', () => {
const box = new CommitBox({
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
});
expect(box.state.summary).toEqual('');
});

it('should set the default commit message description to an empty string', () => {
const box = new CommitBox({
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
});
expect(box.state.description).toEqual('');
});
Expand All @@ -34,17 +47,36 @@ describe('CommitBox', () => {
it('should display placeholder text for the commit message summary', () => {
const props = {
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('input[type="text"]').first();
expect(node.prop('placeholder')).toEqual('Summary (Ctrl+Enter to commit)');
});

it('should adjust placeholder text for the commit message summary when keybinding changes', () => {
const adjustedCommands = new CommandRegistry()
adjustedCommands.addKeyBinding({
keys: ['Shift Enter'],
command: SUBMIT_COMMIT_COMMAND,
selector: '.jp-git-CommitBox'
})
const props = {
onCommit: async () => {},
hasFiles: false,
commands: adjustedCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('input[type="text"]').first();
expect(node.prop('placeholder')).toEqual('Summary (required)');
expect(node.prop('placeholder')).toEqual('Summary (Shift+Enter to commit)');
});

it('should set a `title` attribute on the input element to provide a commit message summary', () => {
const props = {
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('input[type="text"]').first();
Expand All @@ -54,17 +86,19 @@ describe('CommitBox', () => {
it('should display placeholder text for the commit message description', () => {
const props = {
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('TextareaAutosize').first();
expect(node.prop('placeholder')).toEqual('Description');
expect(node.prop('placeholder')).toEqual('Description (optional)');
});

it('should set a `title` attribute on the input element to provide a commit message description', () => {
const props = {
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('TextareaAutosize').first();
Expand All @@ -74,7 +108,8 @@ describe('CommitBox', () => {
it('should display a button to commit changes', () => {
const props = {
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('input[type="button"]').first();
Expand All @@ -84,7 +119,8 @@ describe('CommitBox', () => {
it('should set a `title` attribute on the button to commit changes', () => {
const props = {
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('input[type="button"]').first();
Expand All @@ -94,7 +130,8 @@ describe('CommitBox', () => {
it('should apply a class to disable the commit button when no files have changes to commit', () => {
const props = {
onCommit: async () => {},
hasFiles: false
hasFiles: false,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('input[type="button"]').first();
Expand All @@ -105,7 +142,8 @@ describe('CommitBox', () => {
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', () => {
const props = {
onCommit: async () => {},
hasFiles: true
hasFiles: true,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
const node = component.find('input[type="button"]').first();
Expand All @@ -116,7 +154,8 @@ describe('CommitBox', () => {
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', () => {
const props = {
onCommit: async () => {},
hasFiles: true
hasFiles: true,
commands: defaultCommands
};
const component = shallow(<CommitBox {...props} />);
component.setState({
Expand Down

0 comments on commit f60ed32

Please sign in to comment.