mirror of https://github.com/helix-editor/helix
Indent draft, linewise paste
parent
4a648555ed
commit
00e661f600
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
pub mod graphemes;
|
pub mod graphemes;
|
||||||
mod history;
|
mod history;
|
||||||
|
mod indent;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
mod position;
|
mod position;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
|
|
|
@ -110,7 +110,6 @@ impl Range {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn fragment<'a>(&'a self, text: &'a RopeSlice) -> Cow<'a, str> {
|
pub fn fragment<'a>(&'a self, text: &'a RopeSlice) -> Cow<'a, str> {
|
||||||
// end inclusive
|
|
||||||
Cow::from(text.slice(self.from()..self.to() + 1))
|
Cow::from(text.slice(self.from()..self.to() + 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ impl State {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(doc: Rope) -> Self {
|
pub fn new(doc: Rope) -> Self {
|
||||||
let changes = ChangeSet::new(&doc);
|
let changes = ChangeSet::new(&doc);
|
||||||
|
let old_state = Some((doc.clone(), Selection::single(0, 0)));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
path: None,
|
path: None,
|
||||||
|
@ -56,7 +57,7 @@ impl State {
|
||||||
restore_cursor: false,
|
restore_cursor: false,
|
||||||
syntax: None,
|
syntax: None,
|
||||||
changes,
|
changes,
|
||||||
old_state: None,
|
old_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,7 @@ pub struct Syntax {
|
||||||
|
|
||||||
config: Arc<HighlightConfiguration>,
|
config: Arc<HighlightConfiguration>,
|
||||||
|
|
||||||
root_layer: LanguageLayer,
|
pub(crate) root_layer: LanguageLayer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Syntax {
|
impl Syntax {
|
||||||
|
@ -309,7 +309,7 @@ pub struct LanguageLayer {
|
||||||
// mode
|
// mode
|
||||||
// grammar
|
// grammar
|
||||||
// depth
|
// depth
|
||||||
tree: Option<Tree>,
|
pub(crate) tree: Option<Tree>,
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::state::coords_at_pos;
|
use crate::state::coords_at_pos;
|
||||||
|
|
|
@ -241,7 +241,7 @@ pub fn select_line(view: &mut View, _count: usize) {
|
||||||
let text = view.state.doc();
|
let text = view.state.doc();
|
||||||
let line = text.char_to_line(pos.head);
|
let line = text.char_to_line(pos.head);
|
||||||
let start = text.line_to_char(line);
|
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
|
// TODO: use a transaction
|
||||||
view.state.selection = Selection::single(start, end);
|
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) {
|
pub fn delete_selection(view: &mut View, _count: usize) {
|
||||||
let transaction =
|
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);
|
transaction.apply(&mut view.state);
|
||||||
|
|
||||||
append_changes_to_history(view);
|
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))
|
.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) {
|
fn enter_insert_mode(view: &mut View) {
|
||||||
view.state.mode = Mode::Insert;
|
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) {
|
pub fn undo(view: &mut View, _count: usize) {
|
||||||
view.history.undo(&mut view.state);
|
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) {
|
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())
|
.map(|cow| cow.into_owned())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
register::set('"', values);
|
// TODO: allow specifying reg
|
||||||
|
let reg = '"';
|
||||||
|
register::set(reg, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste(view: &mut View, _count: usize) {
|
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(
|
let repeat = std::iter::repeat(
|
||||||
values
|
values
|
||||||
.last()
|
.last()
|
||||||
|
@ -493,13 +504,74 @@ pub fn paste(view: &mut View, _count: usize) {
|
||||||
.unwrap(),
|
.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 mut values = values.into_iter().map(Tendril::from).chain(repeat);
|
||||||
|
|
||||||
let transaction = Transaction::change_by_selection(&view.state, |range| {
|
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()))
|
(range.head + 1, range.head + 1, Some(values.next().unwrap()))
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
transaction.apply(&mut view.state);
|
transaction.apply(&mut view.state);
|
||||||
append_changes_to_history(view);
|
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!()
|
||||||
|
}
|
||||||
|
|
|
@ -117,6 +117,15 @@ macro_rules! ctrl {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! alt {
|
||||||
|
($ch:expr) => {
|
||||||
|
Key {
|
||||||
|
code: KeyCode::Char($ch),
|
||||||
|
modifiers: Modifiers::ALT,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default() -> Keymaps {
|
pub fn default() -> Keymaps {
|
||||||
hashmap!(
|
hashmap!(
|
||||||
state::Mode::Normal =>
|
state::Mode::Normal =>
|
||||||
|
@ -145,11 +154,15 @@ pub fn default() -> Keymaps {
|
||||||
vec![key!('c')] => commands::change_selection,
|
vec![key!('c')] => commands::change_selection,
|
||||||
vec![key!('s')] => commands::split_selection_on_newline,
|
vec![key!('s')] => commands::split_selection_on_newline,
|
||||||
vec![key!(';')] => commands::collapse_selection,
|
vec![key!(';')] => commands::collapse_selection,
|
||||||
|
// TODO should be alt(;)
|
||||||
|
vec![key!('%')] => commands::flip_selections,
|
||||||
vec![key!('x')] => commands::select_line,
|
vec![key!('x')] => commands::select_line,
|
||||||
vec![key!('u')] => commands::undo,
|
vec![key!('u')] => commands::undo,
|
||||||
vec![shift!('U')] => commands::redo,
|
vec![shift!('U')] => commands::redo,
|
||||||
vec![key!('y')] => commands::yank,
|
vec![key!('y')] => commands::yank,
|
||||||
vec![key!('p')] => commands::paste,
|
vec![key!('p')] => commands::paste,
|
||||||
|
vec![key!('>')] => commands::indent,
|
||||||
|
vec![key!('<')] => commands::unindent,
|
||||||
vec![Key {
|
vec![Key {
|
||||||
code: KeyCode::Esc,
|
code: KeyCode::Esc,
|
||||||
modifiers: Modifiers::NONE
|
modifiers: Modifiers::NONE
|
||||||
|
|
Loading…
Reference in New Issue