Better newline behaviour

This commit is contained in:
Joshua Barretto 2025-09-12 00:28:05 +01:00
parent 551b2816b3
commit 0912450513
4 changed files with 96 additions and 4 deletions

View file

@ -34,6 +34,7 @@ pub enum Action {
CommandStart(&'static str), // Start a new command
GotoLine(isize), // Go to the specified file line
SelectToken, // Fully select the token under the cursor
SelectAll, // Fully select the entire input
Save, // Save the current buffer
}
@ -190,6 +191,22 @@ impl RawEvent {
}
}
pub fn to_select_all(&self) -> Option<Action> {
if matches!(
&self.0,
TerminalEvent::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
})
) {
Some(Action::SelectAll)
} else {
None
}
}
pub fn to_indent(&self) -> Option<Action> {
if let TerminalEvent::Key(KeyEvent {
code: c @ (KeyCode::Tab | KeyCode::BackTab),

View file

@ -241,6 +241,14 @@ impl Buffer {
}
}
pub fn select_all_cursor(&mut self, cursor_id: CursorId) {
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return;
};
cursor.base = 0;
cursor.pos = self.text.chars().len();
}
fn indent_at(&mut self, mut pos: usize, forward: bool) {
const TAB_ALIGN: usize = 4;
@ -385,6 +393,18 @@ impl Buffer {
});
}
pub fn insert_after(&mut self, cursor_id: CursorId, chars: impl IntoIterator<Item = char>) {
let Some(cursor) = self.cursors.get(cursor_id) else {
return;
};
let old_cursor = *cursor;
self.insert(old_cursor.pos, chars);
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return;
};
*cursor = old_cursor;
}
pub fn enter(&mut self, cursor_id: CursorId, chars: impl IntoIterator<Item = char>) {
let Some(cursor) = self.cursors.get(cursor_id) else {
return;
@ -448,6 +468,47 @@ impl Buffer {
}
}
pub fn newline(&mut self, cursor_id: CursorId) {
let Some(cursor) = self.cursors.get(cursor_id) else {
return;
};
let line_start = self.text.to_pos([0, self.text.to_coord(cursor.pos)[1]]);
let is_block = if let Some(last_pos) = cursor
.selection()
.map_or(cursor.pos, |s| s.start)
.checked_sub(1)
&& let Some(last_char) = self.text.chars().get(last_pos)
&& let Some((l, r)) = [('(', ')'), ('[', ']'), ('{', '}')]
.iter()
.find(|(l, _)| l == last_char)
&& let next_pos = cursor.selection().map_or(cursor.pos, |s| s.end)
&& let next_char = self.text.chars().get(next_pos)
{
Some((*r, next_char == Some(r)))
} else {
None
};
self.enter(cursor_id, ['\n']);
// Indent to same level as last line
if let Some(chars) = self.text.chars().get(line_start..) {
let indent = chars
.iter()
.take_while(|c| [' ', '\t'].contains(c))
.copied()
.collect::<Vec<_>>();
self.enter(cursor_id, indent.iter().copied());
// If the last character was the start of a block, perform an additional indent
if let Some((r, is_complete)) = is_block {
self.indent(cursor_id, true);
// If the block was not already completed, complete it (TODO: make configurable!)
let tail = if !is_complete { Some(r) } else { None };
self.insert_after(cursor_id, core::iter::once('\n').chain(indent).chain(tail));
}
}
}
pub fn start_session(&mut self) -> CursorId {
self.cursors.insert(Cursor::default())
}

View file

@ -62,6 +62,7 @@ impl Input {
.map(Action::Char)
.or_else(|| e.to_move())
.or_else(|| e.to_select_token())
.or_else(|| e.to_select_all())
.or_else(|| e.to_indent())
}) {
Some(Action::Char(c)) => {
@ -69,6 +70,8 @@ impl Input {
buffer.backspace(cursor_id);
} else if c == '\x7F' {
buffer.delete(cursor_id);
} else if c == '\n' {
buffer.newline(cursor_id);
} else {
buffer.enter(cursor_id, [c]);
}
@ -98,6 +101,10 @@ impl Input {
buffer.select_token_cursor(cursor_id);
Ok(Resp::handled(None))
}
Some(Action::SelectAll) => {
buffer.select_all_cursor(cursor_id);
Ok(Resp::handled(None))
}
_ => Err(event),
}
}

View file

@ -103,10 +103,17 @@ impl Element<()> for Root {
self.tasks
.push(Task::Prompt(Prompt::new(&format!("{cmd} "))));
}
Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
label: Label("Are you sure you wish to quit? (y/n)".to_string()),
action: Action::Quit,
})),
Action::Cancel => {
let unsaved = state.buffers.values().filter(|b| b.unsaved).count();
if unsaved == 0 {
return Ok(Resp::end(None));
} else {
self.tasks.push(Task::Confirm(Confirm {
label: Label(format!("Are you sure you wish to quit? (y/n). Note that {} files are unsaved!", unsaved)),
action: Action::Quit,
}));
}
}
Action::Show(title, text) => self.tasks.push(Task::Show(Show {
title,
label: Label(text),