Skip to content

Commit 34d9ae1

Browse files
committed
implement multiline commit messages
1 parent d92d43a commit 34d9ae1

File tree

12 files changed

+241
-11
lines changed

12 files changed

+241
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
* customizable `cmdbar_bg` theme color & screen spanning selected line bg [[@gigitsu](https://github.com/gigitsu)] ([#1299](https://github.com/extrawurst/gitui/pull/1299))
2020
* word motions to text input [[@Rodrigodd](https://github.com/Rodrigodd)] ([#1256](https://github.com/extrawurst/gitui/issues/1256))
2121
* file blame at right revision from commit-details [[@heiskane](https://github.com/heiskane)] ([#1122](https://github.com/extrawurst/gitui/issues/1122))
22+
* Multiline commit messages [[@heiskane](https://github.com/heiskane)] ([#1171](https://github.com/extrawurst/gitui/issues/1171))
2223
* dedicated selection foreground theme color `selection_fg` ([#1365](https://github.com/extrawurst/gitui/issues/1365))
2324
* add `regex-fancy` and `regex-onig` features to allow building Syntect with Onigumara regex engine instead of the default engine based on fancy-regex [[@jirutka](https://github.com/jirutka)]
2425
* add `vendor-openssl` feature to allow building without vendored openssl [[@jirutka](https://github.com/jirutka)]

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/commit.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ impl Component for CommitComponent {
359359
}
360360

361361
if let Event::Key(e) = ev {
362-
if key_match(e, self.key_config.keys.enter)
362+
if key_match(e, self.key_config.keys.confirm_commit)
363363
&& self.can_commit()
364364
{
365365
try_or_popup!(

src/components/create_branch.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ impl CreateBranchComponent {
109109
&strings::create_branch_popup_title(&key_config),
110110
&strings::create_branch_popup_msg(&key_config),
111111
true,
112-
),
112+
)
113+
.with_input_type(super::InputType::Singleline),
113114
theme,
114115
key_config,
115116
repo,

src/components/file_find_popup.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ impl FileFindPopup {
4848
"",
4949
"start typing..",
5050
false,
51-
);
51+
)
52+
.with_input_type(super::InputType::Singleline);
5253
find_text.embed();
5354

5455
Self {

src/components/rename_branch.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ impl RenameBranchComponent {
104104
&strings::rename_branch_popup_title(&key_config),
105105
&strings::rename_branch_popup_msg(&key_config),
106106
true,
107-
),
107+
)
108+
.with_input_type(super::InputType::Singleline),
108109
branch_ref: None,
109110
key_config,
110111
}

src/components/stashmsg.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ impl StashMsgComponent {
139139
&strings::stash_popup_title(&key_config),
140140
&strings::stash_popup_msg(&key_config),
141141
true,
142-
),
142+
)
143+
.with_input_type(super::InputType::Singleline),
143144
key_config,
144145
repo,
145146
}

src/components/tag_commit.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ impl TagCommitComponent {
138138
&strings::tag_popup_name_title(),
139139
&strings::tag_popup_name_msg(),
140140
true,
141-
),
141+
)
142+
.with_input_type(super::InputType::Singleline),
142143
commit_id: None,
143144
key_config,
144145
repo,

src/components/textinput.rs

Lines changed: 222 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,161 @@ impl TextInputComponent {
199199
Some(0)
200200
}
201201

202+
fn previous_line_start(&self) -> Option<usize> {
203+
let mut index = self.cursor_position;
204+
let mut newline_count = 0;
205+
while index > 0 {
206+
index -= 1;
207+
let is_newline =
208+
matches!(self.msg.as_bytes()[index] as char, '\n');
209+
210+
if is_newline {
211+
newline_count += 1;
212+
}
213+
if is_newline && newline_count == 2 {
214+
return Some(index);
215+
}
216+
}
217+
None
218+
}
219+
220+
fn line_start(&self) -> Option<usize> {
221+
let mut index = self.cursor_position;
222+
while index > 0 {
223+
index -= 1;
224+
if self.msg.as_bytes()[index] as char == '\n' {
225+
return Some(index);
226+
}
227+
}
228+
None
229+
}
230+
231+
fn cursor_up(&mut self) {
232+
if self.cursor_position == 0 {
233+
return;
234+
}
235+
236+
let prev_line_start = self.previous_line_start().unwrap_or(0);
237+
let line_start = self.line_start().unwrap_or(0);
238+
239+
if line_start == prev_line_start {
240+
self.cursor_position = line_start;
241+
return;
242+
}
243+
244+
let mut dist =
245+
self.get_real_distance(line_start, self.cursor_position);
246+
self.cursor_position = prev_line_start;
247+
248+
if prev_line_start == 0 && dist > 1 {
249+
dist -= 1;
250+
}
251+
252+
self.cursor_forward(dist);
253+
}
254+
255+
fn line_end(&self) -> Option<usize> {
256+
let mut index = self.cursor_position;
257+
while index < self.msg.len() - 1 {
258+
if self.msg.as_bytes()[index] as char == '\n' {
259+
return Some(index);
260+
}
261+
index += 1;
262+
}
263+
None
264+
}
265+
266+
fn next_line_end(&self) -> Option<usize> {
267+
let mut index = self.cursor_position;
268+
let mut newline_count = 0;
269+
while index < self.msg.len() - 1 {
270+
if !self.msg.is_char_boundary(index) {
271+
index += 1;
272+
continue;
273+
}
274+
let is_newline =
275+
matches!(self.msg.as_bytes()[index] as char, '\n');
276+
277+
if is_newline {
278+
newline_count += 1;
279+
}
280+
if is_newline && newline_count == 2 {
281+
return Some(index);
282+
}
283+
index += 1;
284+
}
285+
None
286+
}
287+
288+
fn cursor_down(&mut self) {
289+
let line_start = self.line_start().unwrap_or(0);
290+
let line_end = self.line_end().unwrap_or(self.msg.len());
291+
let next_end = self.next_line_end().unwrap_or(self.msg.len());
292+
293+
if line_end == next_end {
294+
self.cursor_position = next_end;
295+
return;
296+
}
297+
298+
if self.cursor_position - line_start > next_end {
299+
self.cursor_position = next_end;
300+
return;
301+
}
302+
303+
let mut dist =
304+
self.get_real_distance(line_start, self.cursor_position);
305+
306+
self.cursor_position = line_end;
307+
308+
if line_start != 0 {
309+
dist -= 1;
310+
}
311+
312+
self.cursor_forward(dist + 1);
313+
}
314+
315+
fn get_real_distance(&self, start: usize, end: usize) -> usize {
316+
let mut index = start;
317+
let mut dist = 0;
318+
while index <= end {
319+
if self.msg.is_char_boundary(index) {
320+
dist += 1;
321+
}
322+
index += 1;
323+
}
324+
dist
325+
}
326+
327+
/// Move forward `distance` amount of characters stopping at a newline
328+
fn cursor_forward(&mut self, distance: usize) {
329+
let mut travelled = 0;
330+
let mut index = self.cursor_position;
331+
while index < self.msg.len() + 1 {
332+
if self.msg.is_char_boundary(index) {
333+
travelled += 1;
334+
}
335+
336+
if travelled == distance {
337+
self.cursor_position = index;
338+
break;
339+
}
340+
341+
if index == self.msg.len() - 1 {
342+
self.cursor_position = index + 1;
343+
break;
344+
}
345+
346+
index += 1;
347+
348+
if index != self.msg.len() - 1
349+
&& self.msg.as_bytes()[index] as char == '\n'
350+
{
351+
self.cursor_position = index;
352+
break;
353+
}
354+
}
355+
}
356+
202357
fn backspace(&mut self) {
203358
if self.cursor_position > 0 {
204359
self.decr_cursor();
@@ -303,7 +458,7 @@ impl TextInputComponent {
303458
}
304459

305460
fn draw_char_count<B: Backend>(&self, f: &mut Frame<B>, r: Rect) {
306-
let count = self.msg.len();
461+
let count = self.msg.chars().count();
307462
if count > 0 {
308463
let w = Paragraph::new(format!("[{count} chars]"))
309464
.alignment(Alignment::Right);
@@ -374,7 +529,7 @@ impl DrawableComponent for TextInputComponent {
374529
area,
375530
)
376531
}
377-
_ => ui::centered_rect_absolute(32, 3, f.size()),
532+
_ => ui::centered_rect_absolute(64, 3, f.size()),
378533
}
379534
};
380535

@@ -430,6 +585,14 @@ impl Component for TextInputComponent {
430585
e.modifiers.contains(KeyModifiers::CONTROL);
431586

432587
match e.code {
588+
KeyCode::Enter
589+
if self.input_type
590+
== InputType::Multiline && !is_ctrl =>
591+
{
592+
self.msg.insert(self.cursor_position, '\n');
593+
self.incr_cursor();
594+
return Ok(EventState::Consumed);
595+
}
433596
KeyCode::Char(c) if !is_ctrl => {
434597
self.msg.insert(self.cursor_position, c);
435598
self.incr_cursor();
@@ -490,6 +653,14 @@ impl Component for TextInputComponent {
490653
self.incr_cursor();
491654
return Ok(EventState::Consumed);
492655
}
656+
KeyCode::Up => {
657+
self.cursor_up();
658+
return Ok(EventState::Consumed);
659+
}
660+
KeyCode::Down => {
661+
self.cursor_down();
662+
return Ok(EventState::Consumed);
663+
}
493664
KeyCode::Home => {
494665
self.cursor_position = 0;
495666
return Ok(EventState::Consumed);
@@ -717,6 +888,55 @@ mod tests {
717888
assert_eq!(comp.previous_word_position(), None);
718889
}
719890

891+
#[test]
892+
fn test_line_change() {
893+
let mut comp = TextInputComponent::new(
894+
SharedTheme::default(),
895+
SharedKeyConfig::default(),
896+
"",
897+
"",
898+
false,
899+
);
900+
901+
comp.set_text(String::from("aaaaa\näaa\naaa\naaa"));
902+
903+
comp.cursor_position = 0;
904+
comp.cursor_down();
905+
assert_eq!(comp.cursor_position, 6);
906+
907+
comp.cursor_position = 2;
908+
comp.cursor_down();
909+
assert_eq!(comp.cursor_position, 9);
910+
911+
comp.cursor_position = 10;
912+
comp.cursor_down();
913+
assert_eq!(comp.cursor_position, 14);
914+
915+
comp.cursor_position = 8;
916+
comp.cursor_down();
917+
assert_eq!(comp.cursor_position, 12);
918+
919+
comp.cursor_position = 13;
920+
comp.cursor_down();
921+
assert_eq!(comp.cursor_position, 17);
922+
923+
comp.cursor_position = 6;
924+
comp.cursor_up();
925+
assert_eq!(comp.cursor_position, 0);
926+
927+
comp.cursor_position = 9;
928+
comp.cursor_up();
929+
assert_eq!(comp.cursor_position, 2);
930+
931+
comp.cursor_position = 0;
932+
comp.cursor_up();
933+
assert_eq!(comp.cursor_position, 0);
934+
935+
comp.cursor_position = 4;
936+
comp.cursor_up();
937+
assert_eq!(comp.cursor_position, 0);
938+
}
939+
720940
#[test]
721941
fn test_next_word_multibyte() {
722942
let mut comp = TextInputComponent::new(

src/keys/key_list.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub struct KeysList {
5252
pub quit: GituiKeyEvent,
5353
pub exit_popup: GituiKeyEvent,
5454
pub open_commit: GituiKeyEvent,
55+
pub confirm_commit: GituiKeyEvent,
5556
pub open_commit_editor: GituiKeyEvent,
5657
pub open_help: GituiKeyEvent,
5758
pub open_options: GituiKeyEvent,
@@ -134,6 +135,7 @@ impl Default for KeysList {
134135
quit: GituiKeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()),
135136
exit_popup: GituiKeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
136137
open_commit: GituiKeyEvent::new(KeyCode::Char('c'), KeyModifiers::empty()),
138+
confirm_commit: GituiKeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL),
137139
open_commit_editor: GituiKeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL),
138140
open_help: GituiKeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty()),
139141
open_options: GituiKeyEvent::new(KeyCode::Char('o'), KeyModifiers::empty()),

src/keys/key_list_file.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct KeysListFile {
2323
pub quit: Option<GituiKeyEvent>,
2424
pub exit_popup: Option<GituiKeyEvent>,
2525
pub open_commit: Option<GituiKeyEvent>,
26+
pub confirm_commit: Option<GituiKeyEvent>,
2627
pub open_commit_editor: Option<GituiKeyEvent>,
2728
pub open_help: Option<GituiKeyEvent>,
2829
pub open_options: Option<GituiKeyEvent>,
@@ -114,6 +115,7 @@ impl KeysListFile {
114115
quit: self.quit.unwrap_or(default.quit),
115116
exit_popup: self.exit_popup.unwrap_or(default.exit_popup),
116117
open_commit: self.open_commit.unwrap_or(default.open_commit),
118+
confirm_commit: self.commit_amend.unwrap_or(default.confirm_commit),
117119
open_commit_editor: self.open_commit_editor.unwrap_or(default.open_commit_editor),
118120
open_help: self.open_help.unwrap_or(default.open_help),
119121
open_options: self.open_options.unwrap_or(default.open_options),

src/strings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ pub mod commands {
874874
CommandText::new(
875875
format!(
876876
"Commit [{}]",
877-
key_config.get_hint(key_config.keys.enter),
877+
key_config.get_hint(key_config.keys.confirm_commit),
878878
),
879879
"commit (available when commit message is non-empty)",
880880
CMD_GROUP_COMMIT,

0 commit comments

Comments
 (0)