mirror of
https://github.com/enso-org/enso.git
synced 2024-12-28 15:33:23 +03:00
Moving cursors after each edit (https://github.com/enso-org/ide/pull/160)
Before, all edits did not adjust the cursor positions, but they remained with their selection in the same location.
Original commit: 57cdbbf588
This commit is contained in:
parent
81d0bb555b
commit
d6d59d224d
@ -2,6 +2,7 @@
|
||||
|
||||
pub mod content;
|
||||
pub mod cursor;
|
||||
pub mod location;
|
||||
pub mod render;
|
||||
|
||||
use crate::prelude::*;
|
||||
@ -9,9 +10,12 @@ use crate::prelude::*;
|
||||
use crate::display::object::DisplayObjectData;
|
||||
use crate::display::shape::text::text_field::content::TextFieldContent;
|
||||
use crate::display::shape::text::text_field::content::TextChange;
|
||||
use crate::display::shape::text::text_field::cursor::{Cursors, Cursor};
|
||||
use crate::display::shape::text::text_field::cursor::Cursors;
|
||||
use crate::display::shape::text::text_field::cursor::Cursor;
|
||||
use crate::display::shape::text::text_field::cursor::Step;
|
||||
use crate::display::shape::text::text_field::cursor::CursorNavigation;
|
||||
use crate::display::shape::text::text_field::location::TextLocation;
|
||||
use crate::display::shape::text::text_field::location::TextLocationChange;
|
||||
use crate::display::shape::text::glyph::font::FontId;
|
||||
use crate::display::shape::text::glyph::font::FontRegistry;
|
||||
use crate::display::shape::text::text_field::render::TextFieldSprites;
|
||||
@ -112,6 +116,12 @@ shared! { TextField
|
||||
self.rendered.display_object.position().xy()
|
||||
}
|
||||
|
||||
/// Add cursor.
|
||||
pub fn add_cursor(&mut self, position:TextLocation, fonts:&mut FontRegistry) {
|
||||
self.cursors.add_cursor(position);
|
||||
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
|
||||
}
|
||||
|
||||
/// Move all cursors by given step.
|
||||
pub fn navigate_cursors(&mut self, step:Step, selecting:bool, fonts:&mut FontRegistry) {
|
||||
let content = self.content.full_info(fonts);
|
||||
@ -153,19 +163,18 @@ shared! { TextField
|
||||
pub fn edit(&mut self, insertion:&str, fonts:&mut FontRegistry) {
|
||||
let trimmed = insertion.trim_end_matches('\n');
|
||||
let is_line_per_cursor_edit = trimmed.contains('\n') && self.cursors.cursors.len() > 1;
|
||||
let selection = self.cursors.cursors.iter().map(Cursor::selection_range);
|
||||
let cursor_ids = self.cursors.sorted_cursor_indices();
|
||||
|
||||
if is_line_per_cursor_edit {
|
||||
let range_with_line = selection.zip(trimmed.split('\n'));
|
||||
let changes = range_with_line.map(|(r,l)| TextChange::replace(r,l));
|
||||
self.content.apply_changes(changes);
|
||||
let cursor_with_line = cursor_ids.iter().cloned().zip(trimmed.split('\n'));
|
||||
self.edit_per_cursor(cursor_with_line);
|
||||
} else {
|
||||
let changes = selection.map(|range| TextChange::replace(range,insertion));
|
||||
self.content.apply_changes(changes);
|
||||
let cursor_with_line = cursor_ids.iter().map(|cursor_id| (*cursor_id,insertion));
|
||||
self.edit_per_cursor(cursor_with_line);
|
||||
};
|
||||
// TODO[ao]: fix cursor positions after applying changes.
|
||||
self.assignment_update(fonts).update_after_text_edit();
|
||||
self.rendered.update_glyphs(&mut self.content,fonts);
|
||||
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content.full_info(fonts));
|
||||
}
|
||||
|
||||
/// Update underlying Display Object.
|
||||
@ -194,6 +203,19 @@ impl TextFieldData {
|
||||
view_size : self.properties.size,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_per_cursor<'a,It>(&mut self, cursor_id_with_text_to_insert:It)
|
||||
where It : Iterator<Item=(usize,&'a str)> {
|
||||
let mut location_change = TextLocationChange::default();
|
||||
for (cursor_id,to_insert) in cursor_id_with_text_to_insert {
|
||||
let cursor = &mut self.cursors.cursors[cursor_id];
|
||||
let replaced = location_change.apply_to_range(cursor.selection_range());
|
||||
let change = TextChange::replace(replaced,to_insert);
|
||||
location_change.add_change(&change);
|
||||
*cursor = Cursor::new(change.inserted_text_range().end);
|
||||
self.content.apply_change(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@ use crate::display::shape::text::glyph::font::FontRegistry;
|
||||
use crate::display::shape::text::glyph::font::FontRenderInfo;
|
||||
use crate::display::shape::text::text_field::content::line::Line;
|
||||
use crate::display::shape::text::text_field::content::line::LineFullInfo;
|
||||
use crate::display::shape::text::text_field::location::TextLocation;
|
||||
use crate::display::shape::text::text_field::TextFieldProperties;
|
||||
|
||||
use nalgebra::Vector2;
|
||||
@ -93,8 +94,12 @@ pub enum ChangeType {
|
||||
/// A structure describing a text operation in one place.
|
||||
#[derive(Debug)]
|
||||
pub struct TextChange {
|
||||
replaced : Range<TextLocation>,
|
||||
lines : Vec<Vec<char>>,
|
||||
/// Text fragment to be replaced. If we don't mean to remove any text, this should be an empty
|
||||
/// range with start set at position there `lines` will be inserted (see `TextChange::insert`
|
||||
/// definition).
|
||||
pub replaced : Range<TextLocation>,
|
||||
/// Lines to insert instead of replaced fragment.
|
||||
pub lines : Vec<Vec<char>>,
|
||||
}
|
||||
|
||||
impl TextChange {
|
||||
@ -135,6 +140,19 @@ impl TextChange {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns text location range where the inserted text will appear after making this change.
|
||||
pub fn inserted_text_range(&self) -> Range<TextLocation> {
|
||||
let start = self.replaced.start;
|
||||
let end_line = start.line + self.lines.len().saturating_sub(1);
|
||||
let last_line_len = self.lines.last().map_or(0, |l| l.len());
|
||||
let end_column = if start.line == end_line {
|
||||
start.column + last_line_len
|
||||
} else {
|
||||
last_line_len
|
||||
};
|
||||
start..TextLocation{line:end_line, column:end_column}
|
||||
}
|
||||
|
||||
fn mk_lines_as_char_vector(text:&str) -> Vec<Vec<char>> {
|
||||
TextFieldContent::split_to_lines(text).map(|s| s.chars().collect_vec()).collect()
|
||||
}
|
||||
@ -142,39 +160,6 @@ impl TextChange {
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === TextLocation ===
|
||||
// ====================
|
||||
|
||||
/// A position of character in a multiline text.
|
||||
#[derive(Copy,Clone,Debug,PartialEq,Eq,PartialOrd,Ord)]
|
||||
pub struct TextLocation {
|
||||
/// Line index.
|
||||
pub line: usize,
|
||||
/// Column is a index of char in given line.
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl TextLocation {
|
||||
/// Create location at begin of given line.
|
||||
pub fn at_line_begin(line_index:usize) -> TextLocation {
|
||||
TextLocation {
|
||||
line : line_index,
|
||||
column : 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create location at begin of the whole document.
|
||||
pub fn at_document_begin() -> TextLocation {
|
||||
TextLocation {
|
||||
line : 0,
|
||||
column : 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================
|
||||
// === TextFieldContent ===
|
||||
// ============================
|
||||
@ -523,6 +508,21 @@ mod test {
|
||||
assert_eq!(text , content.copy_fragment(whole_content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_inserted_text_location_of_change() {
|
||||
let one_line = "One line";
|
||||
let two_lines = "Two\nlines";
|
||||
let replaced_range = TextLocation{line:1,column:2}..TextLocation{line:2,column:2};
|
||||
|
||||
let one_line_change = TextChange::replace(replaced_range.clone(),one_line);
|
||||
let two_lines_change = TextChange::replace(replaced_range.clone(),two_lines);
|
||||
|
||||
let one_line_expected = replaced_range.start..TextLocation{line:1, column:10};
|
||||
let two_lines_expected = replaced_range.start..TextLocation{line:2, column:5 };
|
||||
assert_eq!(one_line_expected , one_line_change .inserted_text_range());
|
||||
assert_eq!(two_lines_expected, two_lines_change.inserted_text_range());
|
||||
}
|
||||
|
||||
fn get_lines_as_strings(content:&TextFieldContent) -> Vec<String> {
|
||||
content.lines.iter().map(|l| l.chars().iter().collect()).collect()
|
||||
}
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::display::shape::text::text_field::content::TextLocation;
|
||||
use crate::display::shape::text::text_field::content::TextFieldContentFullInfo;
|
||||
use crate::display::shape::text::text_field::content::line::LineFullInfo;
|
||||
use crate::display::shape::text::text_field::location::TextLocation;
|
||||
|
||||
use nalgebra::Vector2;
|
||||
use std::cmp::Ordering;
|
||||
@ -298,6 +298,11 @@ impl Cursors {
|
||||
self.merge_overlapping_cursors();
|
||||
}
|
||||
|
||||
/// Returns cursor indices sorted by cursors' position in text.
|
||||
pub fn sorted_cursor_indices(&self) -> Vec<usize> {
|
||||
self.cursors.iter().enumerate().sorted_by_key(|(_,c)| c.position).map(|(i,_)| i).collect()
|
||||
}
|
||||
|
||||
/// Merge overlapping cursors
|
||||
///
|
||||
/// This function checks all cursors, and merge each pair where cursors are at the same position
|
||||
@ -307,20 +312,24 @@ impl Cursors {
|
||||
/// removed cursors.
|
||||
fn merge_overlapping_cursors(&mut self) {
|
||||
if !self.cursors.is_empty() {
|
||||
self.cursors.sort_by_key(|c| c.position);
|
||||
let mut i = 1;
|
||||
while i < self.cursors.len() {
|
||||
let merged = self.merged_selection_range(i - 1,i);
|
||||
let sorted = self.sorted_cursor_indices();
|
||||
let mut to_remove = Vec::new();
|
||||
let mut last_cursor_id = sorted[0];
|
||||
for id in sorted.iter().skip(1) {
|
||||
let merged = self.merged_selection_range(last_cursor_id,*id);
|
||||
match merged {
|
||||
Some(merged_range) => {
|
||||
self.cursors[i-1].extend_selection(&merged_range);
|
||||
self.cursors.remove(i);
|
||||
self.cursors[last_cursor_id].extend_selection(&merged_range);
|
||||
to_remove.push(*id);
|
||||
},
|
||||
None => {
|
||||
i += 1
|
||||
last_cursor_id = *id;
|
||||
}
|
||||
};
|
||||
}
|
||||
for id in to_remove.iter().sorted().rev() {
|
||||
self.cursors.remove(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,7 +480,7 @@ mod test {
|
||||
fn merging_selection_after_moving_case<F>(convert:F)
|
||||
where F : FnMut(&Range<(usize,usize)>) -> Cursor + Clone {
|
||||
let ranges = vec![(1,4)..(1,5), (0,0)..(0,5), (0,2)..(1,0), (1,5)..(2,0)];
|
||||
let expected_ranges = vec![(0,0)..(1,0), (1,4)..(1,5), (1,5)..(2,0)];
|
||||
let expected_ranges = vec![(1,4)..(1,5), (0,0)..(1,0), (1,5)..(2,0)];
|
||||
let initial_cursors = ranges.iter().map(convert.clone()).collect_vec();
|
||||
let expected_cursors = expected_ranges.iter().map(convert).collect_vec();
|
||||
let mut cursors = Cursors::mock(initial_cursors);
|
||||
|
173
gui/lib/core/src/display/shape/text/text_field/location.rs
Normal file
173
gui/lib/core/src/display/shape/text/text_field/location.rs
Normal file
@ -0,0 +1,173 @@
|
||||
//! Module with content regarding location in text (e.g. location of cursors or changes etc.)
|
||||
|
||||
use crate::display::shape::text::text_field::content::TextChange;
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
// ====================
|
||||
// === TextLocation ===
|
||||
// ====================
|
||||
|
||||
/// A position of character in a multiline text.
|
||||
#[derive(Copy,Clone,Debug,PartialEq,Eq,PartialOrd,Ord)]
|
||||
pub struct TextLocation {
|
||||
/// Line index.
|
||||
pub line: usize,
|
||||
/// Column is a index of char in given line.
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl TextLocation {
|
||||
/// Create location at begin of given line.
|
||||
pub fn at_line_begin(line_index:usize) -> TextLocation {
|
||||
TextLocation {
|
||||
line : line_index,
|
||||
column : 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create location at begin of the whole document.
|
||||
pub fn at_document_begin() -> TextLocation {
|
||||
TextLocation {
|
||||
line : 0,
|
||||
column : 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==========================
|
||||
// === TextLocationChange ===
|
||||
// ==========================
|
||||
|
||||
/// A locations change after some edit.
|
||||
///
|
||||
/// After on or many changes to text content, the following locations are altered (e.g. when you
|
||||
/// remove one line, all the next location are shifted one line up). This structure keeps history
|
||||
/// of sequence of text changes and can tell how the further TextLocations should change to mark
|
||||
/// the same place in the text.
|
||||
#[derive(Copy,Clone,Debug,Default)]
|
||||
pub struct TextLocationChange {
|
||||
/// A current line offset for all next locations;
|
||||
pub line_offset : isize,
|
||||
/// Last line altered by considered text changes (in the current numeration).
|
||||
pub last_changed_line : usize,
|
||||
/// The column offset for all next locations in `last_changed_line`.
|
||||
pub column_offset : isize,
|
||||
}
|
||||
|
||||
impl TextLocationChange {
|
||||
/// Return the new location after considering text changes so far.
|
||||
pub fn apply_to(&self, location:TextLocation) -> TextLocation {
|
||||
let line = (location.line as isize + self.line_offset) as usize;
|
||||
let column = if line == self.last_changed_line {
|
||||
(location.column as isize + self.column_offset) as usize
|
||||
} else {
|
||||
location.column
|
||||
};
|
||||
TextLocation{line,column}
|
||||
}
|
||||
|
||||
/// Return the new location range after considering text changes so far.
|
||||
pub fn apply_to_range(&self, range:Range<TextLocation>) -> Range<TextLocation> {
|
||||
self.apply_to(range.start)..self.apply_to(range.end)
|
||||
}
|
||||
|
||||
/// Add a new text change into consideration.
|
||||
pub fn add_change(&mut self, change:&TextChange) {
|
||||
let removed = &change.replaced;
|
||||
let inserted = change.inserted_text_range();
|
||||
let lines_removed = (removed.end.line - removed.start.line) as isize;
|
||||
let lines_added = (inserted.end.line - inserted.start.line) as isize;
|
||||
let change_in_last_line = removed.start.line == self.last_changed_line;
|
||||
let drop_column_offset = !change_in_last_line || lines_removed > 0;
|
||||
if drop_column_offset {
|
||||
self.column_offset = 0;
|
||||
}
|
||||
self.line_offset += lines_added - lines_removed;
|
||||
self.column_offset += inserted.end.column as isize - removed.end.column as isize;
|
||||
self.last_changed_line = inserted.end.line;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn applying_change_to_location() {
|
||||
// positive offsets
|
||||
let change = TextLocationChange {
|
||||
line_offset: 1,
|
||||
last_changed_line: 5,
|
||||
column_offset: 3
|
||||
};
|
||||
let in_last_changed_line = TextLocation{line:4, column:2};
|
||||
let not_in_last_changed_line = TextLocation{line:5, column:2};
|
||||
assert_eq!(TextLocation{line:5, column:5}, change.apply_to(in_last_changed_line));
|
||||
assert_eq!(TextLocation{line:6, column:2}, change.apply_to(not_in_last_changed_line));
|
||||
|
||||
// negative offsets
|
||||
let change = TextLocationChange {
|
||||
line_offset: -1,
|
||||
last_changed_line: 5,
|
||||
column_offset: -2
|
||||
};
|
||||
let in_last_changed_line = TextLocation{line:6, column:2};
|
||||
let not_in_last_changed_line = TextLocation{line:5, column:2};
|
||||
assert_eq!(TextLocation{line:5, column:0}, change.apply_to(in_last_changed_line));
|
||||
assert_eq!(TextLocation{line:4, column:2}, change.apply_to(not_in_last_changed_line));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_changes() {
|
||||
let one_line = "One line";
|
||||
let two_lines = "Two\nLines";
|
||||
let mut location_change = TextLocationChange::default();
|
||||
|
||||
// initial change
|
||||
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 2};
|
||||
location_change.add_change(&TextChange::replace(replaced,one_line));
|
||||
assert_eq!(0, location_change.line_offset);
|
||||
assert_eq!(2, location_change.last_changed_line);
|
||||
assert_eq!(8, location_change.column_offset);
|
||||
|
||||
// single line in the same line as previous
|
||||
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 13};
|
||||
location_change.add_change(&TextChange::replace(replaced,one_line));
|
||||
assert_eq!(0, location_change.line_offset);
|
||||
assert_eq!(2, location_change.last_changed_line);
|
||||
assert_eq!(5, location_change.column_offset);
|
||||
|
||||
// single -> multiple lines in the same line as previous
|
||||
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 8};
|
||||
location_change.add_change(&TextChange::replace(replaced,two_lines));
|
||||
assert_eq!(1, location_change.line_offset);
|
||||
assert_eq!(3, location_change.last_changed_line);
|
||||
assert_eq!(2, location_change.column_offset);
|
||||
|
||||
// multiple -> multiple lines
|
||||
let replaced = TextLocation{line:3, column:2}..TextLocation{line:7, column: 3};
|
||||
location_change.add_change(&TextChange::replace(replaced,two_lines));
|
||||
assert_eq!(-2, location_change.line_offset);
|
||||
assert_eq!(4 , location_change.last_changed_line);
|
||||
assert_eq!(2 , location_change.column_offset);
|
||||
|
||||
// multiple -> single lines in the same line as previous
|
||||
let replaced = TextLocation{line:4, column:2}..TextLocation{line:5, column: 11};
|
||||
location_change.add_change(&TextChange::replace(replaced,one_line));
|
||||
assert_eq!(-3, location_change.line_offset);
|
||||
assert_eq!(4 , location_change.last_changed_line);
|
||||
assert_eq!(-1, location_change.column_offset);
|
||||
|
||||
// single line in other line than previous
|
||||
let replaced = TextLocation{line:5, column:2}..TextLocation{line:5, column: 12};
|
||||
location_change.add_change(&TextChange::replace(replaced,one_line));
|
||||
assert_eq!(-3, location_change.line_offset);
|
||||
assert_eq!(5 , location_change.last_changed_line);
|
||||
assert_eq!(-2, location_change.column_offset);
|
||||
}
|
||||
}
|
@ -2,8 +2,8 @@
|
||||
|
||||
use crate::display::shape::primitive::system::ShapeSystem;
|
||||
use crate::display::shape::text::text_field::content::TextFieldContentFullInfo;
|
||||
use crate::display::shape::text::text_field::content::TextLocation;
|
||||
use crate::display::shape::text::text_field::cursor::Cursor;
|
||||
use crate::display::shape::text::text_field::location::TextLocation;
|
||||
use crate::display::symbol::geometry::compound::sprite::Sprite;
|
||||
|
||||
use nalgebra::Vector2;
|
||||
|
@ -21,6 +21,7 @@ use web_sys::MouseEvent;
|
||||
use failure::_core::cell::RefCell;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use crate::system::web::text_input::KeyboardBinding;
|
||||
use crate::display::shape::text::text_field::location::TextLocation;
|
||||
|
||||
|
||||
const TEXT:&str =
|
||||
@ -58,6 +59,7 @@ pub fn run_example_text_selecting() {
|
||||
let text_field = TextField::new(&world,TEXT,properties,&mut fonts);
|
||||
text_field.set_position(Vector3::new(10.0, 600.0, 0.0));
|
||||
text_field.jump_cursor(Vector2::new(50.0, -40.0),false,&mut fonts);
|
||||
text_field.add_cursor(TextLocation{line:1, column:0}, &mut fonts);
|
||||
world.add_child(&text_field);
|
||||
text_field.update();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user