aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--helix-core/src/indent.rs111
-rw-r--r--helix-core/src/lib.rs1
-rw-r--r--helix-core/src/selection.rs1
-rw-r--r--helix-core/src/state.rs3
-rw-r--r--helix-core/src/syntax.rs4
-rw-r--r--helix-view/src/commands.rs88
-rw-r--r--helix-view/src/keymap.rs13
7 files changed, 209 insertions, 12 deletions
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
new file mode 100644
index 00000000..c1a4fd6c
--- /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<Node> {
+ 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<Node>) -> 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 4a7a2dd4..ccdc7297 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 bc677330..d3806cf3 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 fde6a866..7035b27c 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 3e5927e5..290f2652 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -146,7 +146,7 @@ pub struct Syntax {
config: Arc<HighlightConfiguration>,
- root_layer: LanguageLayer,
+ pub(crate) root_layer: LanguageLayer,
}
impl Syntax {
@@ -309,7 +309,7 @@ pub struct LanguageLayer {
// mode
// grammar
// depth
- tree: Option<Tree>,
+ pub(crate) tree: Option<Tree>,
}
use crate::state::coords_at_pos;
diff --git a/helix-view/src/commands.rs b/helix-view/src/commands.rs
index e5e3244d..56e1c5db 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<transaction>, then the higher level handles storing it?
+ // TODO: each command could simply return a Option<transaction>, 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 8c53b403..1d7505d2 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