diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs new file mode 100644 index 000000000..c1a4fd6c7 --- /dev/null +++ b/helix-core/src/indent.rs @@ -0,0 +1,111 @@ +use crate::{ + syntax::Syntax, + tree_sitter::{Node, Tree}, + Rope, RopeSlice, State, +}; + +const TAB_WIDTH: usize = 4; + +fn indent_level_for_line(line: RopeSlice) -> usize { + let mut len = 0; + for ch in line.chars() { + match ch { + '\t' => len += TAB_WIDTH, + ' ' => len += 1, + _ => break, + } + } + + len / TAB_WIDTH +} + +/// Find the highest syntax node at position. +/// This is to identify the column where this node (e.g., an HTML closing tag) ends. +fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option { + let tree = syntax.root_layer.tree.as_ref().unwrap(); + + let mut node = match tree.root_node().named_descendant_for_byte_range(pos, pos) { + Some(node) => node, + None => return None, + }; + + while let Some(parent) = node.parent() { + if parent.start_byte() == node.start_byte() { + node = parent + } else { + break; + } + } + + Some(node) +} + +fn walk(node: Option) -> usize { + let node = match node { + Some(node) => node, + None => return 0, + }; + + let parent = match node.parent() { + Some(node) => node, + None => return 0, + }; + + let mut increment = 0; + + let not_first_or_last_sibling = node.next_sibling().is_some() && node.prev_sibling().is_some(); + let is_scope = true; + + if not_first_or_last_sibling && is_scope { + increment += 1; + } + + walk(Some(parent)) + increment +} + +// for_line_at_col +fn suggested_indent_for_line(state: &State, line_num: usize) -> usize { + let line = state.doc.line(line_num); + let current = indent_level_for_line(line); + + let mut byte_start = state.doc.line_to_byte(line_num); + + // find first non-whitespace char + for ch in line.chars() { + // TODO: could use memchr with chunks? + if ch != ' ' && ch != '\t' { + break; + } + byte_start += 1; + } + + if let Some(syntax) = &state.syntax { + let node = get_highest_syntax_node_at_bytepos(state.syntax.as_ref().unwrap(), byte_start); + + // let indentation = walk() + // special case for comments + + // if preserve_leading_whitespace + + unimplemented!() + } else { + // TODO: case for non-tree sitter grammars + 0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn indent_level() { + let line = Rope::from(" fn new"); // 8 spaces + assert_eq!(indent_level_for_line(line.slice(..)), 2); + let line = Rope::from("\t\t\tfn new"); // 3 tabs + assert_eq!(indent_level_for_line(line.slice(..)), 3); + // mixed indentation + let line = Rope::from("\t \tfn new"); // 1 tab, 4 spaces, tab + assert_eq!(indent_level_for_line(line.slice(..)), 3); + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 4a7a2dd40..ccdc72979 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,6 +1,7 @@ #![allow(unused)] pub mod graphemes; mod history; +mod indent; pub mod macros; mod position; pub mod register; diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index bc6773303..d3806cf3b 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -110,7 +110,6 @@ impl Range { #[inline] pub fn fragment<'a>(&'a self, text: &'a RopeSlice) -> Cow<'a, str> { - // end inclusive Cow::from(text.slice(self.from()..self.to() + 1)) } } diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs index fde6a8666..7035b27ce 100644 --- a/helix-core/src/state.rs +++ b/helix-core/src/state.rs @@ -47,6 +47,7 @@ impl State { #[must_use] pub fn new(doc: Rope) -> Self { let changes = ChangeSet::new(&doc); + let old_state = Some((doc.clone(), Selection::single(0, 0))); Self { path: None, @@ -56,7 +57,7 @@ impl State { restore_cursor: false, syntax: None, changes, - old_state: None, + old_state, } } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 3e5927e5a..290f26524 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -146,7 +146,7 @@ pub struct Syntax { config: Arc, - root_layer: LanguageLayer, + pub(crate) root_layer: LanguageLayer, } impl Syntax { @@ -309,7 +309,7 @@ pub struct LanguageLayer { // mode // grammar // depth - tree: Option, + pub(crate) tree: Option, } use crate::state::coords_at_pos; diff --git a/helix-view/src/commands.rs b/helix-view/src/commands.rs index e5e3244d0..56e1c5dbb 100644 --- a/helix-view/src/commands.rs +++ b/helix-view/src/commands.rs @@ -241,7 +241,7 @@ pub fn select_line(view: &mut View, _count: usize) { let text = view.state.doc(); let line = text.char_to_line(pos.head); let start = text.line_to_char(line); - let end = text.line_to_char(line + 1); + let end = text.line_to_char(line + 1).saturating_sub(1); // TODO: use a transaction view.state.selection = Selection::single(start, end); @@ -249,7 +249,7 @@ pub fn select_line(view: &mut View, _count: usize) { pub fn delete_selection(view: &mut View, _count: usize) { let transaction = - Transaction::change_by_selection(&view.state, |range| (range.from(), range.to(), None)); + Transaction::change_by_selection(&view.state, |range| (range.from(), range.to() + 1, None)); transaction.apply(&mut view.state); append_changes_to_history(view); @@ -267,6 +267,13 @@ pub fn collapse_selection(view: &mut View, _count: usize) { .transform(|range| Range::new(range.head, range.head)) } +pub fn flip_selections(view: &mut View, _count: usize) { + view.state.selection = view + .state + .selection + .transform(|range| Range::new(range.head, range.anchor)) +} + fn enter_insert_mode(view: &mut View) { view.state.mode = Mode::Insert; @@ -463,7 +470,7 @@ pub fn delete_char_forward(view: &mut View, count: usize) { pub fn undo(view: &mut View, _count: usize) { view.history.undo(&mut view.state); - // TODO: each command should simply return a Option, then the higher level handles storing it? + // TODO: each command could simply return a Option, then the higher level handles storing it? } pub fn redo(view: &mut View, _count: usize) { @@ -481,11 +488,15 @@ pub fn yank(view: &mut View, _count: usize) { .map(|cow| cow.into_owned()) .collect(); - register::set('"', values); + // TODO: allow specifying reg + let reg = '"'; + register::set(reg, values); } pub fn paste(view: &mut View, _count: usize) { - if let Some(values) = register::get('"') { + // TODO: allow specifying reg + let reg = '"'; + if let Some(values) = register::get(reg) { let repeat = std::iter::repeat( values .last() @@ -493,13 +504,74 @@ pub fn paste(view: &mut View, _count: usize) { .unwrap(), ); + // TODO: if any of values ends \n it's linewise paste + // + // p => paste after + // P => paste before + // alt-p => paste every yanked selection after selected text + // alt-P => paste every yanked selection before selected text + // R => replace selected text with yanked text + // alt-R => replace selected text with every yanked text + // + // append => insert at next line + // insert => insert at start of line + // replace => replace + // default insert + + let linewise = values.iter().any(|value| value.ends_with('\n')); + let mut values = values.into_iter().map(Tendril::from).chain(repeat); - let transaction = Transaction::change_by_selection(&view.state, |range| { - (range.head + 1, range.head + 1, Some(values.next().unwrap())) - }); + let transaction = if linewise { + // paste on the next line + // TODO: can simply take a range + modifier and compute the right pos without ifs + let text = view.state.doc(); + Transaction::change_by_selection(&view.state, |range| { + let line_end = text.line_to_char(text.char_to_line(range.head) + 1); + (line_end, line_end, Some(values.next().unwrap())) + }) + } else { + Transaction::change_by_selection(&view.state, |range| { + (range.head + 1, range.head + 1, Some(values.next().unwrap())) + }) + }; transaction.apply(&mut view.state); append_changes_to_history(view); } } + +const TAB_WIDTH: usize = 4; + +pub fn indent(view: &mut View, _count: usize) { + let mut lines = Vec::new(); + + // Get all line numbers + for range in view.state.selection.ranges() { + let start = view.state.doc.char_to_line(range.from()); + let end = view.state.doc.char_to_line(range.to()); + + for line in start..=end { + lines.push(line) + } + } + lines.sort_unstable(); // sorting by usize so _unstable is preferred + lines.dedup(); + + // Indent by one level + let indent = Tendril::from(" ".repeat(TAB_WIDTH)); + + let transaction = Transaction::change( + &view.state, + lines.into_iter().map(|line| { + let pos = view.state.doc.line_to_char(line); + (pos, pos, Some(indent.clone())) + }), + ); + transaction.apply(&mut view.state); + append_changes_to_history(view); +} + +pub fn unindent(view: &mut View, _count: usize) { + unimplemented!() +} diff --git a/helix-view/src/keymap.rs b/helix-view/src/keymap.rs index 8c53b4038..1d7505d20 100644 --- a/helix-view/src/keymap.rs +++ b/helix-view/src/keymap.rs @@ -117,6 +117,15 @@ macro_rules! ctrl { }; } +macro_rules! alt { + ($ch:expr) => { + Key { + code: KeyCode::Char($ch), + modifiers: Modifiers::ALT, + } + }; +} + pub fn default() -> Keymaps { hashmap!( state::Mode::Normal => @@ -145,11 +154,15 @@ pub fn default() -> Keymaps { vec![key!('c')] => commands::change_selection, vec![key!('s')] => commands::split_selection_on_newline, vec![key!(';')] => commands::collapse_selection, + // TODO should be alt(;) + vec![key!('%')] => commands::flip_selections, vec![key!('x')] => commands::select_line, vec![key!('u')] => commands::undo, vec![shift!('U')] => commands::redo, vec![key!('y')] => commands::yank, vec![key!('p')] => commands::paste, + vec![key!('>')] => commands::indent, + vec![key!('<')] => commands::unindent, vec![Key { code: KeyCode::Esc, modifiers: Modifiers::NONE