diff options
Diffstat (limited to 'helix-view')
-rw-r--r-- | helix-view/Cargo.toml | 1 | ||||
-rw-r--r-- | helix-view/src/document.rs | 46 | ||||
-rw-r--r-- | helix-view/src/editor.rs | 60 | ||||
-rw-r--r-- | helix-view/src/handlers/dap.rs | 2 | ||||
-rw-r--r-- | helix-view/src/info.rs | 33 | ||||
-rw-r--r-- | helix-view/src/input.rs | 1 | ||||
-rw-r--r-- | helix-view/src/lib.rs | 19 | ||||
-rw-r--r-- | helix-view/src/theme.rs | 199 | ||||
-rw-r--r-- | helix-view/src/view.rs | 28 |
9 files changed, 299 insertions, 90 deletions
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 266a5732..b96a537d 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -17,6 +17,7 @@ term = ["crossterm"] bitflags = "1.3" anyhow = "1" helix-core = { version = "0.6", path = "../helix-core" } +helix-loader = { version = "0.6", path = "../helix-loader" } helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-dap = { version = "0.6", path = "../helix-dap" } crossterm = { version = "0.25", optional = true } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 2ef99c6a..0daa983f 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -5,6 +5,7 @@ use helix_core::auto_pairs::AutoPairs; use helix_core::Range; use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; +use std::borrow::Cow; use std::cell::Cell; use std::collections::HashMap; use std::fmt::Display; @@ -23,7 +24,7 @@ use helix_core::{ DEFAULT_LINE_ENDING, }; -use crate::{DocumentId, Editor, ViewId}; +use crate::{apply_transaction, DocumentId, Editor, View, ViewId}; /// 8kB of buffer space for encoding and decoding `Rope`s. const BUF_SIZE: usize = 8192; @@ -600,7 +601,7 @@ impl Document { } /// Reload the document from its path. - pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> { + pub fn reload(&mut self, view: &mut View) -> Result<(), Error> { let encoding = &self.encoding; let path = self.path().filter(|path| path.exists()); @@ -616,8 +617,8 @@ impl Document { // This is not considered a modification of the contents of the file regardless // of the encoding. let transaction = helix_core::diff::compare_ropes(self.text(), &rope); - self.apply(&transaction, view_id); - self.append_changes_to_history(view_id); + apply_transaction(&transaction, self, view); + self.append_changes_to_history(view.id); self.reset_modified(); self.detect_indent_and_line_ending(); @@ -809,6 +810,9 @@ impl Document { } /// Apply a [`Transaction`] to the [`Document`] to change its text. + /// Instead of calling this function directly, use [crate::apply_transaction] + /// to ensure that the transaction is applied to the appropriate [`View`] as + /// well. pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { // store the state just before any changes are made. This allows us to undo to the // state just before a transaction was applied. @@ -830,11 +834,11 @@ impl Document { success } - fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool { + fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool { let mut history = self.history.take(); let txn = if undo { history.undo() } else { history.redo() }; let success = if let Some(txn) = txn { - self.apply_impl(txn, view_id) + self.apply_impl(txn, view.id) && view.apply(txn, self) } else { false }; @@ -848,26 +852,26 @@ impl Document { } /// Undo the last modification to the [`Document`]. Returns whether the undo was successful. - pub fn undo(&mut self, view_id: ViewId) -> bool { - self.undo_redo_impl(view_id, true) + pub fn undo(&mut self, view: &mut View) -> bool { + self.undo_redo_impl(view, true) } /// Redo the last modification to the [`Document`]. Returns whether the redo was successful. - pub fn redo(&mut self, view_id: ViewId) -> bool { - self.undo_redo_impl(view_id, false) + pub fn redo(&mut self, view: &mut View) -> bool { + self.undo_redo_impl(view, false) } pub fn savepoint(&mut self) { self.savepoint = Some(Transaction::new(self.text())); } - pub fn restore(&mut self, view_id: ViewId) { + pub fn restore(&mut self, view: &mut View) { if let Some(revert) = self.savepoint.take() { - self.apply(&revert, view_id); + apply_transaction(&revert, self, view); } } - fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool { + fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool { let txns = if earlier { self.history.get_mut().earlier(uk) } else { @@ -875,7 +879,7 @@ impl Document { }; let mut success = false; for txn in txns { - if self.apply_impl(&txn, view_id) { + if self.apply_impl(&txn, view.id) && view.apply(&txn, self) { success = true; } } @@ -887,13 +891,13 @@ impl Document { } /// Undo modifications to the [`Document`] according to `uk`. - pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool { - self.earlier_later_impl(view_id, uk, true) + pub fn earlier(&mut self, view: &mut View, uk: UndoKind) -> bool { + self.earlier_later_impl(view, uk, true) } /// Redo modifications to the [`Document`] according to `uk`. - pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool { - self.earlier_later_impl(view_id, uk, false) + pub fn later(&mut self, view: &mut View, uk: UndoKind) -> bool { + self.earlier_later_impl(view, uk, false) } /// Commit pending changes to history @@ -1038,6 +1042,12 @@ impl Document { .map(helix_core::path::get_relative_path) } + pub fn display_name(&self) -> Cow<'static, str> { + self.relative_path() + .map(|path| path.to_string_lossy().to_string().into()) + .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()) + } + // transact(Fn) ? // -- LSP methods diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5eff9983..e9a3c639 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,6 +1,6 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, - document::{Mode, SCRATCH_BUFFER_NAME}, + document::Mode, graphics::{CursorKind, Rect}, info::Info, input::KeyEvent, @@ -28,7 +28,7 @@ use tokio::{ time::{sleep, Duration, Instant, Sleep}, }; -use anyhow::{bail, Error}; +use anyhow::Error; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; @@ -124,6 +124,8 @@ pub struct Config { pub line_number: LineNumber, /// Highlight the lines cursors are currently on. Defaults to false. pub cursorline: bool, + /// Highlight the columns cursors are currently on. Defaults to false. + pub cursorcolumn: bool, /// Gutters. Default ["diagnostics", "line-numbers"] pub gutters: Vec<GutterType>, /// Middle click paste support. Defaults to true. @@ -260,6 +262,7 @@ pub struct StatusLineConfig { pub center: Vec<StatusLineElement>, pub right: Vec<StatusLineElement>, pub separator: String, + pub mode: ModeConfig, } impl Default for StatusLineConfig { @@ -271,6 +274,25 @@ impl Default for StatusLineConfig { center: vec![], right: vec![E::Diagnostics, E::Selections, E::Position, E::FileEncoding], separator: String::from("│"), + mode: ModeConfig::default(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] +pub struct ModeConfig { + pub normal: String, + pub insert: String, + pub select: String, +} + +impl Default for ModeConfig { + fn default() -> Self { + Self { + normal: String::from("NOR"), + insert: String::from("INS"), + select: String::from("SEL"), } } } @@ -311,6 +333,9 @@ pub enum StatusLineElement { /// The cursor position as a percent of the total file PositionPercentage, + /// The total line numbers of the current file + TotalLineNumbers, + /// A single space Spacer, } @@ -529,15 +554,17 @@ impl Default for WhitespaceCharacters { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default)] +#[serde(default, rename_all = "kebab-case")] pub struct IndentGuidesConfig { pub render: bool, pub character: char, + pub skip_levels: u8, } impl Default for IndentGuidesConfig { fn default() -> Self { Self { + skip_levels: 0, render: false, character: '│', } @@ -557,6 +584,7 @@ impl Default for Config { }, line_number: LineNumber::Absolute, cursorline: false, + cursorcolumn: false, gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers], middle_click_paste: true, auto_pairs: AutoPairConfig::default(), @@ -643,7 +671,7 @@ pub struct Editor { /// The currently applied editor theme. While previewing a theme, the previewed theme /// is set here. pub theme: Theme, - + pub last_line_number: Option<usize>, pub status_msg: Option<(Cow<'static, str>, Severity)>, pub autoinfo: Option<Info>, @@ -652,7 +680,6 @@ pub struct Editor { pub idle_timer: Pin<Box<Sleep>>, pub last_motion: Option<Motion>, - pub pseudo_pending: Option<String>, pub last_completion: Option<CompleteAction>, @@ -686,6 +713,14 @@ pub enum Action { VerticalSplit, } +/// Error thrown on failed document closed +pub enum CloseError { + /// Document doesn't exist + DoesNotExist, + /// Buffer is modified + BufferModified(String), +} + impl Editor { pub fn new( mut area: Rect, @@ -717,6 +752,7 @@ impl Editor { syn_loader, theme_loader, last_theme: None, + last_line_number: None, registers: Registers::default(), clipboard_provider: get_clipboard_provider(), status_msg: None, @@ -724,7 +760,6 @@ impl Editor { idle_timer: Box::pin(sleep(conf.idle_timeout)), last_motion: None, last_completion: None, - pseudo_pending: None, config, auto_pairs, exit_code: 0, @@ -844,7 +879,7 @@ impl Editor { // try to find a language server based on the language name let language_server = doc.language.as_ref().and_then(|language| { - ls.get(language) + ls.get(language, doc.path()) .map_err(|e| { log::error!( "Failed to initialize the LSP for `{}` {{ {} }}", @@ -1044,19 +1079,14 @@ impl Editor { self._refresh(); } - pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> { + pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> { let doc = match self.documents.get(&doc_id) { Some(doc) => doc, - None => bail!("document does not exist"), + None => return Err(CloseError::DoesNotExist), }; if !force && doc.is_modified() { - bail!( - "buffer {:?} is modified", - doc.relative_path() - .map(|path| path.to_string_lossy().to_string()) - .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()) - ); + return Err(CloseError::BufferModified(doc.display_name().into_owned())); } if let Some(language_server) = doc.language_server() { diff --git a/helix-view/src/handlers/dap.rs b/helix-view/src/handlers/dap.rs index e39584c3..2e86871b 100644 --- a/helix-view/src/handlers/dap.rs +++ b/helix-view/src/handlers/dap.rs @@ -262,7 +262,7 @@ impl Editor { log::info!("{}", output); self.set_status(format!("{} {}", prefix, output)); } - Event::Initialized => { + Event::Initialized(_) => { // send existing breakpoints for (path, breakpoints) in &mut self.breakpoints { // TODO: call futures in parallel, await all diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 5ad6a60c..3080cf8e 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -16,7 +16,11 @@ pub struct Info { } impl Info { - pub fn new(title: &str, body: Vec<(String, String)>) -> Self { + pub fn new<T, U>(title: &str, body: &[(T, U)]) -> Self + where + T: AsRef<str>, + U: AsRef<str>, + { if body.is_empty() { return Self { title: title.to_string(), @@ -26,11 +30,21 @@ impl Info { }; } - let item_width = body.iter().map(|(item, _)| item.width()).max().unwrap(); + let item_width = body + .iter() + .map(|(item, _)| item.as_ref().width()) + .max() + .unwrap(); let mut text = String::new(); - for (item, desc) in &body { - let _ = writeln!(text, "{:width$} {}", item, desc, width = item_width); + for (item, desc) in body { + let _ = writeln!( + text, + "{:width$} {}", + item.as_ref(), + desc.as_ref(), + width = item_width + ); } Self { @@ -42,19 +56,19 @@ impl Info { } pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Self { - let body = body + let body: Vec<_> = body .into_iter() .map(|(desc, events)| { let events = events.iter().map(ToString::to_string).collect::<Vec<_>>(); - (events.join(", "), desc.to_string()) + (events.join(", "), desc) }) .collect(); - Self::new(title, body) + Self::new(title, &body) } pub fn from_registers(registers: &Registers) -> Self { - let body = registers + let body: Vec<_> = registers .inner() .iter() .map(|(ch, reg)| { @@ -62,13 +76,12 @@ impl Info { .read() .get(0) .and_then(|s| s.lines().next()) - .map(String::from) .unwrap_or_default(); (ch.to_string(), content) }) .collect(); - let mut infobox = Self::new("Registers", body); + let mut infobox = Self::new("Registers", &body); infobox.width = 30; // copied content could be very long infobox } diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 083a1e08..30fa72c4 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -14,6 +14,7 @@ pub enum Event { Mouse(MouseEvent), Paste(String), Resize(u16, u16), + IdleTimeout, } #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 788304bc..276be441 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -53,17 +53,30 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) { .cursor(doc.text().slice(..)); let line = doc.text().char_to_line(pos); - let height = view.inner_area().height as usize; + let last_line_height = view.inner_area().height.saturating_sub(1) as usize; let relative = match align { - Align::Center => height / 2, + Align::Center => last_line_height / 2, Align::Top => 0, - Align::Bottom => height, + Align::Bottom => last_line_height, }; view.offset.row = line.saturating_sub(relative); } +/// Applies a [`helix_core::Transaction`] to the given [`Document`] +/// and [`View`]. +pub fn apply_transaction( + transaction: &helix_core::Transaction, + doc: &mut Document, + view: &mut View, +) -> bool { + // This is a short function but it's easy to call `Document::apply` + // without calling `View::apply` or in the wrong order. The transaction + // must be applied to the document before the view. + doc.apply(transaction, view.id) && view.apply(transaction, doc) +} + pub use document::Document; pub use editor::Editor; pub use theme::Theme; diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index aaef28b2..302844b7 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -3,20 +3,29 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::Context; +use anyhow::{anyhow, Context, Result}; use helix_core::hashmap; +use helix_loader::merge_toml_values; use log::warn; use once_cell::sync::Lazy; use serde::{Deserialize, Deserializer}; -use toml::Value; +use toml::{map::Map, Value}; use crate::graphics::UnderlineStyle; pub use crate::graphics::{Color, Modifier, Style}; pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { + // let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml")) + // .expect("Failed to parse default theme"); + // Theme::from(raw_theme) + toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") }); pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { + // let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml")) + // .expect("Failed to parse base 16 default theme"); + // Theme::from(raw_theme) + toml::from_slice(include_bytes!("../../base16_theme.toml")) .expect("Failed to parse base 16 default theme") }); @@ -36,24 +45,51 @@ impl Loader { } /// Loads a theme first looking in the `user_dir` then in `default_dir` - pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> { + pub fn load(&self, name: &str) -> Result<Theme> { if name == "default" { return Ok(self.default()); } if name == "base16_default" { return Ok(self.base16_default()); } - let filename = format!("{}.toml", name); - let user_path = self.user_dir.join(&filename); - let path = if user_path.exists() { - user_path + self.load_theme(name, name, false).map(Theme::from) + } + + // load the theme and its parent recursively and merge them + // `base_theme_name` is the theme from the config.toml, + // used to prevent some circular loading scenarios + fn load_theme( + &self, + name: &str, + base_them_name: &str, + only_default_dir: bool, + ) -> Result<Value> { + let path = self.path(name, only_default_dir); + let theme_toml = self.load_toml(path)?; + + let inherits = theme_toml.get("inherits"); + + let theme_toml = if let Some(parent_theme_name) = inherits { + let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| { + anyhow!( + "Theme: expected 'inherits' to be a string: {}", + parent_theme_name + ) + })?; + + let parent_theme_toml = self.load_theme( + parent_theme_name, + base_them_name, + base_them_name == parent_theme_name, + )?; + + self.merge_themes(parent_theme_toml, theme_toml) } else { - self.default_dir.join(filename) + theme_toml }; - let data = std::fs::read(&path)?; - toml::from_slice(data.as_slice()).context("Failed to deserialize theme") + Ok(theme_toml) } pub fn read_names(path: &Path) -> Vec<String> { @@ -71,6 +107,53 @@ impl Loader { .unwrap_or_default() } + // merge one theme into the parent theme + fn merge_themes(&self, parent_theme_toml: Value, theme_toml: Value) -> Value { + let parent_palette = parent_theme_toml.get("palette"); + let palette = theme_toml.get("palette"); + + // handle the table seperately since it needs a `merge_depth` of 2 + // this would conflict with the rest of the theme merge strategy + let palette_values = match (parent_palette, palette) { + (Some(parent_palette), Some(palette)) => { + merge_toml_values(parent_palette.clone(), palette.clone(), 2) + } + (Some(parent_palette), None) => parent_palette.clone(), + (None, Some(palette)) => palette.clone(), + (None, None) => Map::new().into(), + }; + + // add the palette correctly as nested table + let mut palette = Map::new(); + palette.insert(String::from("palette"), palette_values); + + // merge the theme into the parent theme + let theme = merge_toml_values(parent_theme_toml, theme_toml, 1); + // merge the before specially handled palette into the theme + merge_toml_values(theme, palette.into(), 1) + } + + // Loads the theme data as `toml::Value` first from the user_dir then in default_dir + fn load_toml(&self, path: PathBuf) -> Result<Value> { + let data = std::fs::read(&path)?; + + toml::from_slice(data.as_slice()).context("Failed to deserialize theme") + } + + // Returns the path to the theme with the name + // With `only_default_dir` as false the path will first search for the user path + // disabled it ignores the user path and returns only the default path + fn path(&self, name: &str, only_default_dir: bool) -> PathBuf { + let filename = format!("{}.toml", name); + + let user_path = self.user_dir.join(&filename); + if !only_default_dir && user_path.exists() { + user_path + } else { + self.default_dir.join(filename) + } + } + /// Lists all theme names available in default and user directory pub fn names(&self) -> Vec<String> { let mut names = Self::read_names(&self.user_dir); @@ -106,52 +189,77 @@ pub struct Theme { highlights: Vec<Style>, } +impl From<Value> for Theme { + fn from(value: Value) -> Self { + let values: Result<HashMap<String, Value>> = + toml::from_str(&value.to_string()).context("Failed to load theme"); + + let (styles, scopes, highlights) = build_theme_values(values); + + Self { + styles, + scopes, + highlights, + } + } +} + impl<'de> Deserialize<'de> for Theme { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { - let mut styles = HashMap::new(); - let mut scopes = Vec::new(); - let mut highlights = Vec::new(); - - if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) { - // TODO: alert user of parsing failures in editor - let palette = colors - .remove("palette") - .map(|value| { - ThemePalette::try_from(value).unwrap_or_else(|err| { - warn!("{}", err); - ThemePalette::default() - }) - }) - .unwrap_or_default(); - - styles.reserve(colors.len()); - scopes.reserve(colors.len()); - highlights.reserve(colors.len()); + let values = HashMap::<String, Value>::deserialize(deserializer)?; - for (name, style_value) in colors { - let mut style = Style::default(); - if let Err(err) = palette.parse_style(&mut style, style_value) { - warn!("{}", err); - } - - // these are used both as UI and as highlights - styles.insert(name.clone(), style); - scopes.push(name); - highlights.push(style); - } - } + let (styles, scopes, highlights) = build_theme_values(Ok(values)); Ok(Self { - scopes, styles, + scopes, highlights, }) } } +fn build_theme_values( + values: Result<HashMap<String, Value>>, +) -> (HashMap<String, Style>, Vec<String>, Vec<Style>) { + let mut styles = HashMap::new(); + let mut scopes = Vec::new(); + let mut highlights = Vec::new(); + + if let Ok(mut colors) = values { + // TODO: alert user of parsing failures in editor + let palette = colors + .remove("palette") + .map(|value| { + ThemePalette::try_from(value).unwrap_or_else(|err| { + warn!("{}", err); + ThemePalette::default() + }) + }) + .unwrap_or_default(); + // remove inherits from value to prevent errors + let _ = colors.remove("inherits"); + styles.reserve(colors.len()); + scopes.reserve(colors.len()); + highlights.reserve(colors.len()); + for (name, style_value) in colors { + let mut style = Style::default(); + if let Err(err) = palette.parse_style(&mut style, style_value) { + warn!("{}", err); + } + + // these are used both as UI and as highlights + styles.insert(name.clone(), style); + scopes.push(name); + highlights.push(style); + } + } + + (styles, scopes, highlights) +} + impl Theme { #[inline] pub fn highlight(&self, index: usize) -> Style { @@ -170,6 +278,13 @@ impl Theme { .find_map(|s| self.styles.get(s).copied()) } + /// Get the style of a scope, without falling back to dot separated broader + /// scopes. For example if `ui.text.focus` is not defined in the theme, it + /// will return `None`, even if `ui.text` is. + pub fn try_get_exact(&self, scope: &str) -> Option<Style> { + self.styles.get(scope).copied() + } + #[inline] pub fn scopes(&self) -> &[String] { &self.scopes diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 3df533df..62984b88 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -3,7 +3,9 @@ use crate::{ gutter::{self, Gutter}, Document, DocumentId, ViewId, }; -use helix_core::{pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection}; +use helix_core::{ + pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction, +}; use std::fmt; @@ -62,6 +64,22 @@ impl JumpList { pub fn get(&self) -> &[Jump] { &self.jumps } + + /// Applies a [`Transaction`] of changes to the jumplist. + /// This is necessary to ensure that changes to documents do not leave jump-list + /// selections pointing to parts of the text which no longer exist. + fn apply(&mut self, transaction: &Transaction, doc: &Document) { + let text = doc.text().slice(..); + + for (doc_id, selection) in &mut self.jumps { + if doc.id() == *doc_id { + *selection = selection + .clone() + .map(transaction.changes()) + .ensure_invariants(text); + } + } + } } #[derive(Clone)] @@ -334,6 +352,14 @@ impl View { // (None, None) => return, // } // } + + /// Applies a [`Transaction`] to the view. + /// Instead of calling this function directly, use [crate::apply_transaction] + /// which applies a transaction to the [`Document`] and view together. + pub fn apply(&mut self, transaction: &Transaction, doc: &Document) -> bool { + self.jumps.apply(transaction, doc); + true + } } #[cfg(test)] |