mirror of https://github.com/microsoft/edit
Move lines with Alt+Up/Down (#230)
Closes #27 Co-authored-by: Leonard Hecker <leonard@hecker.io>
This commit is contained in:
parent
b277a1e67b
commit
091b74240c
|
|
@ -91,6 +91,8 @@ struct HistoryEntry {
|
||||||
/// [`TextBuffer::stats`] before the change was made.
|
/// [`TextBuffer::stats`] before the change was made.
|
||||||
stats_before: TextBufferStatistics,
|
stats_before: TextBufferStatistics,
|
||||||
/// [`GapBuffer::generation`] before the change was made.
|
/// [`GapBuffer::generation`] before the change was made.
|
||||||
|
///
|
||||||
|
/// **NOTE:** Entries with the same generation are grouped together.
|
||||||
generation_before: u32,
|
generation_before: u32,
|
||||||
/// Logical cursor position where the change took place.
|
/// Logical cursor position where the change took place.
|
||||||
/// The position is at the start of the changed range.
|
/// The position is at the start of the changed range.
|
||||||
|
|
@ -154,12 +156,36 @@ struct ActiveEditLineInfo {
|
||||||
distance_next_line_start: usize,
|
distance_next_line_start: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Undo/redo grouping works by recording a set of "overrides",
|
||||||
|
/// which are then applied in [`TextBuffer::edit_begin()`].
|
||||||
|
/// This allows us to create a group of edits that all share a
|
||||||
|
/// common `generation_before` and can be undone/redone together.
|
||||||
|
/// This struct stores those overrides.
|
||||||
|
struct ActiveEditGroupInfo {
|
||||||
|
/// [`TextBuffer::cursor`] position before the change was made.
|
||||||
|
cursor_before: Point,
|
||||||
|
/// [`TextBuffer::selection`] before the change was made.
|
||||||
|
selection_before: Option<TextBufferSelection>,
|
||||||
|
/// [`TextBuffer::stats`] before the change was made.
|
||||||
|
stats_before: TextBufferStatistics,
|
||||||
|
/// [`GapBuffer::generation`] before the change was made.
|
||||||
|
///
|
||||||
|
/// **NOTE:** Entries with the same generation are grouped together.
|
||||||
|
generation_before: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Char- or word-wise navigation? Your choice.
|
/// Char- or word-wise navigation? Your choice.
|
||||||
pub enum CursorMovement {
|
pub enum CursorMovement {
|
||||||
Grapheme,
|
Grapheme,
|
||||||
Word,
|
Word,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See [`TextBuffer::move_selected_lines`].
|
||||||
|
pub enum MoveLineDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
/// The result of a call to [`TextBuffer::render()`].
|
/// The result of a call to [`TextBuffer::render()`].
|
||||||
pub struct RenderResult {
|
pub struct RenderResult {
|
||||||
/// The maximum visual X position we encountered during rendering.
|
/// The maximum visual X position we encountered during rendering.
|
||||||
|
|
@ -184,6 +210,7 @@ pub struct TextBuffer {
|
||||||
last_history_type: HistoryType,
|
last_history_type: HistoryType,
|
||||||
last_save_generation: u32,
|
last_save_generation: u32,
|
||||||
|
|
||||||
|
active_edit_group: Option<ActiveEditGroupInfo>,
|
||||||
active_edit_line_info: Option<ActiveEditLineInfo>,
|
active_edit_line_info: Option<ActiveEditLineInfo>,
|
||||||
active_edit_depth: i32,
|
active_edit_depth: i32,
|
||||||
active_edit_off: usize,
|
active_edit_off: usize,
|
||||||
|
|
@ -235,6 +262,7 @@ impl TextBuffer {
|
||||||
last_history_type: HistoryType::Other,
|
last_history_type: HistoryType::Other,
|
||||||
last_save_generation: 0,
|
last_save_generation: 0,
|
||||||
|
|
||||||
|
active_edit_group: None,
|
||||||
active_edit_line_info: None,
|
active_edit_line_info: None,
|
||||||
active_edit_depth: 0,
|
active_edit_depth: 0,
|
||||||
active_edit_off: 0,
|
active_edit_off: 0,
|
||||||
|
|
@ -2010,6 +2038,7 @@ impl TextBuffer {
|
||||||
|
|
||||||
fn write(&mut self, text: &[u8], at: Cursor, raw: bool) {
|
fn write(&mut self, text: &[u8], at: Cursor, raw: bool) {
|
||||||
let history_type = if raw { HistoryType::Other } else { HistoryType::Write };
|
let history_type = if raw { HistoryType::Other } else { HistoryType::Write };
|
||||||
|
let mut edit_begun = false;
|
||||||
|
|
||||||
// If we have an active selection, writing an empty `text`
|
// If we have an active selection, writing an empty `text`
|
||||||
// will still delete the selection. As such, we check this first.
|
// will still delete the selection. As such, we check this first.
|
||||||
|
|
@ -2017,19 +2046,20 @@ impl TextBuffer {
|
||||||
self.edit_begin(history_type, beg);
|
self.edit_begin(history_type, beg);
|
||||||
self.edit_delete(end);
|
self.edit_delete(end);
|
||||||
self.set_selection(None);
|
self.set_selection(None);
|
||||||
|
edit_begun = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the text is empty the remaining code won't do anything,
|
// If the text is empty the remaining code won't do anything,
|
||||||
// allowing us to exit early.
|
// allowing us to exit early.
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
// ...we still need to end any active edit session though.
|
// ...we still need to end any active edit session though.
|
||||||
if self.active_edit_depth > 0 {
|
if edit_begun {
|
||||||
self.edit_end();
|
self.edit_end();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.active_edit_depth <= 0 {
|
if !edit_begun {
|
||||||
self.edit_begin(history_type, at);
|
self.edit_begin(history_type, at);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2238,73 +2268,142 @@ impl TextBuffer {
|
||||||
/// * The cursor movement at the end is rather costly, but at least without word wrap
|
/// * The cursor movement at the end is rather costly, but at least without word wrap
|
||||||
/// it should be possible to calculate it directly from the removed amount.
|
/// it should be possible to calculate it directly from the removed amount.
|
||||||
pub fn unindent(&mut self) {
|
pub fn unindent(&mut self) {
|
||||||
|
let selection = self.selection;
|
||||||
let mut selection_beg = self.cursor.logical_pos;
|
let mut selection_beg = self.cursor.logical_pos;
|
||||||
let mut selection_end = selection_beg;
|
let mut selection_end = selection_beg;
|
||||||
|
|
||||||
if let Some(TextBufferSelection { beg, end }) = self.selection {
|
if let Some(TextBufferSelection { beg, end }) = &selection {
|
||||||
selection_beg = beg;
|
selection_beg = *beg;
|
||||||
selection_end = end;
|
selection_end = *end;
|
||||||
}
|
}
|
||||||
|
|
||||||
let [beg, end] = minmax(selection_beg, selection_end);
|
self.edit_begin_grouping();
|
||||||
let beg = self.cursor_move_to_logical_internal(self.cursor, Point { x: 0, y: beg.y });
|
|
||||||
let end = self.cursor_move_to_logical_internal(beg, Point { x: CoordType::MAX, y: end.y });
|
|
||||||
|
|
||||||
let mut replacement = Vec::new();
|
for y in selection_beg.y.min(selection_end.y)..=selection_beg.y.max(selection_end.y) {
|
||||||
self.buffer.extract_raw(beg.offset..end.offset, &mut replacement, 0);
|
self.cursor_move_to_logical(Point { x: 0, y });
|
||||||
|
|
||||||
let initial_len = replacement.len();
|
let mut offset = self.cursor.offset;
|
||||||
let mut offset = 0;
|
let mut width = 0;
|
||||||
let mut y = beg.logical_pos.y;
|
let mut remove = 0;
|
||||||
|
|
||||||
loop {
|
// Figure out how many characters to `remove`.
|
||||||
if offset >= replacement.len() {
|
'outer: loop {
|
||||||
|
let chunk = self.read_forward(offset);
|
||||||
|
if chunk.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut remove = 0;
|
for &c in chunk {
|
||||||
|
width += match c {
|
||||||
if replacement[offset] == b'\t' {
|
b' ' => 1,
|
||||||
remove = 1;
|
b'\t' => self.tab_size,
|
||||||
} else {
|
_ => COORD_TYPE_SAFE_MAX,
|
||||||
while remove < self.tab_size as usize
|
};
|
||||||
&& offset + remove < replacement.len()
|
if width > self.tab_size {
|
||||||
&& replacement[offset + remove] == b' '
|
break 'outer;
|
||||||
{
|
}
|
||||||
remove += 1;
|
remove += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offset += chunk.len();
|
||||||
|
|
||||||
|
// No need to do another round if we
|
||||||
|
// already got the exact right amount.
|
||||||
|
if width >= self.tab_size {
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if remove > 0 {
|
if remove > 0 {
|
||||||
replacement.drain(offset..offset + remove);
|
// As the lines get unindented, the selection should shift with them.
|
||||||
}
|
|
||||||
|
|
||||||
if y == selection_beg.y {
|
if y == selection_beg.y {
|
||||||
selection_beg.x -= remove as CoordType;
|
selection_beg.x -= remove;
|
||||||
}
|
}
|
||||||
if y == selection_end.y {
|
if y == selection_end.y {
|
||||||
selection_end.x -= remove as CoordType;
|
selection_end.x -= remove;
|
||||||
}
|
}
|
||||||
|
|
||||||
(offset, y) = simd::lines_fwd(&replacement, offset, y, y + 1);
|
self.delete(CursorMovement::Grapheme, remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.edit_end_grouping();
|
||||||
|
|
||||||
|
// Move the cursor to the new end of the selection.
|
||||||
|
self.set_cursor_internal(self.cursor_move_to_logical_internal(self.cursor, selection_end));
|
||||||
|
|
||||||
|
// NOTE: If the selection was previously `None`,
|
||||||
|
// it should continue to be `None` after this.
|
||||||
|
self.set_selection(
|
||||||
|
selection.map(|_| TextBufferSelection { beg: selection_beg, end: selection_end }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if replacement.len() == initial_len {
|
/// Displaces the current, cursor or the selection, line(s) in the given direction.
|
||||||
// Nothing to do.
|
pub fn move_selected_lines(&mut self, direction: MoveLineDirection) {
|
||||||
|
let selection = self.selection;
|
||||||
|
let cursor = self.cursor;
|
||||||
|
|
||||||
|
// If there's no selection, we move the line the cursor is on instead.
|
||||||
|
let [beg, end] = match self.selection {
|
||||||
|
Some(s) => minmax(s.beg.y, s.end.y),
|
||||||
|
None => [cursor.logical_pos.y, cursor.logical_pos.y],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if this would be a no-op.
|
||||||
|
if match direction {
|
||||||
|
MoveLineDirection::Up => beg <= 0,
|
||||||
|
MoveLineDirection::Down => end >= self.stats.logical_lines - 1,
|
||||||
|
} {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.edit_begin(HistoryType::Other, beg);
|
let delta = match direction {
|
||||||
self.edit_delete(end);
|
MoveLineDirection::Up => -1,
|
||||||
self.edit_write(&replacement);
|
MoveLineDirection::Down => 1,
|
||||||
self.edit_end();
|
};
|
||||||
|
let (cut, paste) = match direction {
|
||||||
|
MoveLineDirection::Up => (beg - 1, end),
|
||||||
|
MoveLineDirection::Down => (end + 1, beg),
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(TextBufferSelection { beg, end }) = &mut self.selection {
|
self.edit_begin_grouping();
|
||||||
*beg = selection_beg;
|
{
|
||||||
*end = selection_end;
|
// Let's say this is `MoveLineDirection::Up`.
|
||||||
|
// In that case, we'll cut (remove) the line above the selection here...
|
||||||
|
self.cursor_move_to_logical(Point { x: 0, y: cut });
|
||||||
|
let line = self.extract_selection(true);
|
||||||
|
|
||||||
|
// ...and paste it below the selection. This will then
|
||||||
|
// appear to the user as if the selection was moved up.
|
||||||
|
self.cursor_move_to_logical(Point { x: 0, y: paste });
|
||||||
|
self.edit_begin(HistoryType::Write, self.cursor);
|
||||||
|
// The `extract_selection` call can return an empty `Vec`),
|
||||||
|
// if the `cut` line was at the end of the file. Since we want to
|
||||||
|
// paste the line somewhere it needs a trailing newline at the minimum.
|
||||||
|
//
|
||||||
|
// Similarly, if the `paste` line is at the end of the file
|
||||||
|
// and there's no trailing newline, we'll have failed to reach
|
||||||
|
// that end in which case `logical_pos.y != past`.
|
||||||
|
if line.is_empty() || self.cursor.logical_pos.y != paste {
|
||||||
|
self.write_canon(b"\n");
|
||||||
}
|
}
|
||||||
|
if !line.is_empty() {
|
||||||
|
self.write_raw(&line);
|
||||||
|
}
|
||||||
|
self.edit_end();
|
||||||
|
}
|
||||||
|
self.edit_end_grouping();
|
||||||
|
|
||||||
self.set_cursor_internal(self.cursor_move_to_logical_internal(self.cursor, selection_end));
|
// Shift the cursor and selection together with the moved lines.
|
||||||
|
self.cursor_move_to_logical(Point {
|
||||||
|
x: cursor.logical_pos.x,
|
||||||
|
y: cursor.logical_pos.y + delta,
|
||||||
|
});
|
||||||
|
self.set_selection(selection.map(|mut s| {
|
||||||
|
s.beg.y += delta;
|
||||||
|
s.end.y += delta;
|
||||||
|
s
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the contents of the current selection.
|
/// Extracts the contents of the current selection.
|
||||||
|
|
@ -2378,6 +2477,19 @@ impl TextBuffer {
|
||||||
if beg.offset < end.offset { Some((beg, end)) } else { None }
|
if beg.offset < end.offset { Some((beg, end)) } else { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn edit_begin_grouping(&mut self) {
|
||||||
|
self.active_edit_group = Some(ActiveEditGroupInfo {
|
||||||
|
cursor_before: self.cursor.logical_pos,
|
||||||
|
selection_before: self.selection,
|
||||||
|
stats_before: self.stats,
|
||||||
|
generation_before: self.buffer.generation(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_end_grouping(&mut self) {
|
||||||
|
self.active_edit_group = None;
|
||||||
|
}
|
||||||
|
|
||||||
/// Starts a new edit operation.
|
/// Starts a new edit operation.
|
||||||
/// This is used for tracking the undo/redo history.
|
/// This is used for tracking the undo/redo history.
|
||||||
fn edit_begin(&mut self, history_type: HistoryType, cursor: Cursor) {
|
fn edit_begin(&mut self, history_type: HistoryType, cursor: Cursor) {
|
||||||
|
|
@ -2390,7 +2502,8 @@ impl TextBuffer {
|
||||||
self.set_cursor_internal(cursor);
|
self.set_cursor_internal(cursor);
|
||||||
|
|
||||||
// If both the last and this are a Write/Delete operation, we skip allocating a new undo history item.
|
// If both the last and this are a Write/Delete operation, we skip allocating a new undo history item.
|
||||||
if history_type != self.last_history_type
|
if cursor_before.offset != cursor.offset
|
||||||
|
|| history_type != self.last_history_type
|
||||||
|| !matches!(history_type, HistoryType::Write | HistoryType::Delete)
|
|| !matches!(history_type, HistoryType::Write | HistoryType::Delete)
|
||||||
{
|
{
|
||||||
self.redo_stack.clear();
|
self.redo_stack.clear();
|
||||||
|
|
@ -2408,6 +2521,16 @@ impl TextBuffer {
|
||||||
deleted: Vec::new(),
|
deleted: Vec::new(),
|
||||||
added: Vec::new(),
|
added: Vec::new(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if let Some(info) = &self.active_edit_group
|
||||||
|
&& let Some(entry) = self.undo_stack.back()
|
||||||
|
{
|
||||||
|
let mut entry = entry.borrow_mut();
|
||||||
|
entry.cursor_before = info.cursor_before;
|
||||||
|
entry.selection_before = info.selection_before;
|
||||||
|
entry.stats_before = info.stats_before;
|
||||||
|
entry.generation_before = info.generation_before;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.active_edit_off = cursor.offset;
|
self.active_edit_off = cursor.offset;
|
||||||
|
|
@ -2461,9 +2584,12 @@ impl TextBuffer {
|
||||||
let mut out_off = usize::MAX;
|
let mut out_off = usize::MAX;
|
||||||
|
|
||||||
let mut undo = self.undo_stack.back_mut().unwrap().borrow_mut();
|
let mut undo = self.undo_stack.back_mut().unwrap().borrow_mut();
|
||||||
|
|
||||||
|
// If this is a continued backspace operation,
|
||||||
|
// we need to prepend the deleted portion to the undo entry.
|
||||||
if self.cursor.logical_pos < undo.cursor {
|
if self.cursor.logical_pos < undo.cursor {
|
||||||
out_off = 0; // Prepend the deleted portion.
|
out_off = 0;
|
||||||
undo.cursor = self.cursor.logical_pos; // Note the start of the deleted portion.
|
undo.cursor = self.cursor.logical_pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the deleted portion into the undo entry.
|
// Copy the deleted portion into the undo entry.
|
||||||
|
|
@ -2481,7 +2607,7 @@ impl TextBuffer {
|
||||||
/// and recalculates the line statistics.
|
/// and recalculates the line statistics.
|
||||||
fn edit_end(&mut self) {
|
fn edit_end(&mut self) {
|
||||||
self.active_edit_depth -= 1;
|
self.active_edit_depth -= 1;
|
||||||
assert!(self.active_edit_depth >= 0);
|
debug_assert!(self.active_edit_depth >= 0);
|
||||||
if self.active_edit_depth > 0 {
|
if self.active_edit_depth > 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2536,6 +2662,10 @@ impl TextBuffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn undo_redo(&mut self, undo: bool) {
|
fn undo_redo(&mut self, undo: bool) {
|
||||||
|
let buffer_generation = self.buffer.generation();
|
||||||
|
let mut entry_buffer_generation = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
// Transfer the last entry from the undo stack to the redo stack or vice versa.
|
// Transfer the last entry from the undo stack to the redo stack or vice versa.
|
||||||
{
|
{
|
||||||
let (from, to) = if undo {
|
let (from, to) = if undo {
|
||||||
|
|
@ -2544,8 +2674,14 @@ impl TextBuffer {
|
||||||
(&mut self.redo_stack, &mut self.undo_stack)
|
(&mut self.redo_stack, &mut self.undo_stack)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(g) = entry_buffer_generation
|
||||||
|
&& from.back().is_none_or(|c| c.borrow().generation_before != g)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(list) = from.cursor_back_mut().remove_current_as_list() else {
|
let Some(list) = from.cursor_back_mut().remove_current_as_list() else {
|
||||||
return;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
to.cursor_back_mut().splice_after(list);
|
to.cursor_back_mut().splice_after(list);
|
||||||
|
|
@ -2556,8 +2692,13 @@ impl TextBuffer {
|
||||||
to.back().unwrap()
|
to.back().unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Move to the point where the modification took place.
|
// Remember the buffer generation of the change so we can stop popping undos/redos.
|
||||||
let cursor = self.cursor_move_to_logical_internal(self.cursor, change.borrow().cursor);
|
// Also, move to the point where the modification took place.
|
||||||
|
let cursor = {
|
||||||
|
let change = change.borrow();
|
||||||
|
entry_buffer_generation = Some(change.generation_before);
|
||||||
|
self.cursor_move_to_logical_internal(self.cursor, change.cursor)
|
||||||
|
};
|
||||||
|
|
||||||
let safe_cursor = if self.word_wrap_column > 0 {
|
let safe_cursor = if self.word_wrap_column > 0 {
|
||||||
// If word-wrap is enabled, we need to move the cursor to the beginning of the line.
|
// If word-wrap is enabled, we need to move the cursor to the beginning of the line.
|
||||||
|
|
@ -2568,7 +2709,6 @@ impl TextBuffer {
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let buffer_generation = self.buffer.generation();
|
|
||||||
let mut change = change.borrow_mut();
|
let mut change = change.borrow_mut();
|
||||||
let change = &mut *change;
|
let change = &mut *change;
|
||||||
|
|
||||||
|
|
@ -2635,9 +2775,12 @@ impl TextBuffer {
|
||||||
self.last_history_type = HistoryType::Other;
|
self.last_history_type = HistoryType::Other;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry_buffer_generation.is_some() {
|
||||||
self.recalc_after_content_changed();
|
self.recalc_after_content_changed();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// For interfacing with ICU.
|
/// For interfacing with ICU.
|
||||||
pub(crate) fn read_backward(&self, off: usize) -> &[u8] {
|
pub(crate) fn read_backward(&self, off: usize) -> &[u8] {
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ use std::fmt::Write as _;
|
||||||
use std::{iter, mem, ptr, time};
|
use std::{iter, mem, ptr, time};
|
||||||
|
|
||||||
use crate::arena::{Arena, ArenaString, scratch_arena};
|
use crate::arena::{Arena, ArenaString, scratch_arena};
|
||||||
use crate::buffer::{CursorMovement, RcTextBuffer, TextBuffer, TextBufferCell};
|
use crate::buffer::{CursorMovement, MoveLineDirection, RcTextBuffer, TextBuffer, TextBufferCell};
|
||||||
use crate::cell::*;
|
use crate::cell::*;
|
||||||
use crate::clipboard::Clipboard;
|
use crate::clipboard::Clipboard;
|
||||||
use crate::document::WriteableDocument;
|
use crate::document::WriteableDocument;
|
||||||
|
|
@ -2547,6 +2547,7 @@ impl<'a> Context<'a, '_> {
|
||||||
y: tb.cursor_visual_pos().y - 1,
|
y: tb.cursor_visual_pos().y - 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
kbmod::ALT => tb.move_selected_lines(MoveLineDirection::Up),
|
||||||
kbmod::CTRL_ALT => {
|
kbmod::CTRL_ALT => {
|
||||||
// TODO: Add cursor above
|
// TODO: Add cursor above
|
||||||
}
|
}
|
||||||
|
|
@ -2617,6 +2618,7 @@ impl<'a> Context<'a, '_> {
|
||||||
tc.preferred_column = tb.cursor_visual_pos().x;
|
tc.preferred_column = tb.cursor_visual_pos().x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
kbmod::ALT => tb.move_selected_lines(MoveLineDirection::Down),
|
||||||
kbmod::CTRL_ALT => {
|
kbmod::CTRL_ALT => {
|
||||||
// TODO: Add cursor above
|
// TODO: Add cursor above
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue