diff --git a/rust/core-lib/src/client.rs b/rust/core-lib/src/client.rs index 63665755b..0774e66bf 100644 --- a/rust/core-lib/src/client.rs +++ b/rust/core-lib/src/client.rs @@ -235,6 +235,16 @@ impl Client { pub fn schedule_timer(&self, timeout: Instant, token: usize) { self.0.schedule_timer(timeout, token); } + + pub fn toggle_tail_config_changed(&self, view_id: ViewId, is_tail_enabled: bool) { + self.0.send_rpc_notification( + "toggle_tail_config_changed", + &json!({ + "view_id": view_id, + "is_tail_enabled": is_tail_enabled, + }), + ); + } } #[derive(Debug, Serialize)] diff --git a/rust/core-lib/src/editor.rs b/rust/core-lib/src/editor.rs index 8d17e4900..ca9db71c2 100644 --- a/rust/core-lib/src/editor.rs +++ b/rust/core-lib/src/editor.rs @@ -941,6 +941,14 @@ impl Editor { Some(GetDataResponse { chunk, offset, first_line, first_line_offset }) } + + pub(crate) fn tail_append(&mut self, tail: Rope) { + let buf_end = self.text.len(); + let mut builder = DeltaBuilder::new(buf_end); + builder.replace(buf_end..buf_end, tail); + self.add_delta(builder.build()); + self.set_pristine(); + } } #[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize)] diff --git a/rust/core-lib/src/event_context.rs b/rust/core-lib/src/event_context.rs index 8590e3632..06086a0ff 100644 --- a/rust/core-lib/src/event_context.rs +++ b/rust/core-lib/src/event_context.rs @@ -372,6 +372,18 @@ impl<'a> EventContext<'a> { ed.is_pristine(), ) } + + fn render_tail(&mut self) { + let _t = trace_block("EventContext::render_tail", &["core"]); + let ed = self.editor.borrow(); + self.view.borrow_mut().render_tail( + ed.get_buffer(), + self.client, + self.style_map, + ed.get_layers().get_merged(), + ed.is_pristine(), + ) + } } /// Helpers related to specific commands. @@ -449,6 +461,16 @@ impl<'a> EventContext<'a> { self.render(); } + pub(crate) fn reload_tail(&mut self, text: Rope) { + self.with_editor(|ed, _, _, _| ed.tail_append(text)); + self.after_edit("core"); + self.render_tail(); + } + + pub(crate) fn toggle_tail_config_changed(&mut self, is_tail_enabled: bool) { + self.client.toggle_tail_config_changed(self.view_id, is_tail_enabled); + } + pub(crate) fn plugin_info(&mut self) -> PluginBufferInfo { let ed = self.editor.borrow(); let nb_lines = ed.get_buffer().measure::() + 1; diff --git a/rust/core-lib/src/file.rs b/rust/core-lib/src/file.rs index b9518065e..23f5628bf 100644 --- a/rust/core-lib/src/file.rs +++ b/rust/core-lib/src/file.rs @@ -18,7 +18,7 @@ use std::collections::HashMap; use std::ffi::OsString; use std::fmt; use std::fs::{self, File, Permissions}; -use std::io::{self, Read, Write}; +use std::io::{self, Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::str; use std::time::SystemTime; @@ -54,6 +54,7 @@ pub struct FileInfo { pub has_changed: bool, #[cfg(target_family = "unix")] pub permissions: Option, + pub tail_details: TailDetails, } pub enum FileError { @@ -62,6 +63,13 @@ pub enum FileError { HasChanged(PathBuf), } +#[derive(Debug, Default)] +pub struct TailDetails { + pub current_position_in_tail: u64, + pub is_tail_enabled: bool, + pub is_at_bottom_of_file: bool, +} + #[derive(Debug, Clone, Copy)] pub enum CharacterEncoding { Utf8, @@ -110,7 +118,7 @@ impl FileManager { let _ = File::create(path).map_err(|e| FileError::Io(e, path.to_owned()))?; } - let (rope, info) = try_load_file(path)?; + let (rope, info) = try_load_file(self, id, path)?; self.open_files.insert(path.to_owned(), id); if self.file_info.insert(id, info).is_none() { @@ -147,6 +155,7 @@ impl FileManager { has_changed: false, #[cfg(target_family = "unix")] permissions: get_permissions(path), + tail_details: TailDetails::default(), }; self.open_files.insert(path.to_owned(), id); self.file_info.insert(id, info); @@ -172,9 +181,32 @@ impl FileManager { } Ok(()) } + + /// Toggles the **is_tail_enabled** flag in TailDetails. + /// Also when flag is enabled, it sets the cursor in TailDetails ie **current_position_in_tail** to the end of file + /// being tailed. + #[cfg(feature = "notify")] + pub fn toggle_tail(&mut self, id: BufferId, enabled: bool) -> Result<(), FileError> { + if let Some(v) = self.file_info.get_mut(&id) { + if enabled { + let path = v.path.as_path(); + let mut f = File::open(path).map_err(|e| FileError::Io(e, path.to_owned()))?; + let end_position = + f.seek(SeekFrom::End(0)).map_err(|e| FileError::Io(e, path.to_owned()))?; + + v.tail_details.current_position_in_tail = end_position; + } + v.tail_details.is_tail_enabled = enabled; + } + Ok(()) + } } -fn try_load_file

(path: P) -> Result<(Rope, FileInfo), FileError> +fn try_load_file

( + file_manager: &FileManager, + buffer_id: BufferId, + path: P, +) -> Result<(Rope, FileInfo), FileError> where P: AsRef, { @@ -182,11 +214,45 @@ where // it's arguable that the rope crate should have file loading functionality let mut f = File::open(path.as_ref()).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?; + let mut new_tail_details = TailDetails::default(); let mut bytes = Vec::new(); - f.read_to_end(&mut bytes).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?; + + let file_info = file_manager.get_info(buffer_id); + match file_info { + Some(v) => { + let is_tail_enabled = v.tail_details.is_tail_enabled; + if is_tail_enabled { + debug!("Tailing file"); + let end_position = f + .seek(SeekFrom::End(0)) + .map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?; + let current_position = v.tail_details.current_position_in_tail; + + let diff = end_position - current_position; + bytes = vec![0; diff as usize]; + f.seek(SeekFrom::Current(-(bytes.len() as i64))).unwrap(); + f.read_exact(&mut bytes).unwrap(); + + new_tail_details = TailDetails { + current_position_in_tail: end_position, + is_tail_enabled: v.tail_details.is_tail_enabled, + is_at_bottom_of_file: v.tail_details.is_at_bottom_of_file, + }; + } else { + debug!("Tail is false, So loading entire file."); + f.read_to_end(&mut bytes) + .map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?; + } + } + None => { + debug!("Loading entire file"); + f.read_to_end(&mut bytes).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?; + } + } let encoding = CharacterEncoding::guess(&bytes); let rope = try_decode(bytes, encoding, path.as_ref())?; + let info = FileInfo { encoding, mod_time: get_mod_time(&path), @@ -194,6 +260,7 @@ where permissions: get_permissions(&path), path: path.as_ref().to_owned(), has_changed: false, + tail_details: new_tail_details, }; Ok((rope, info)) } diff --git a/rust/core-lib/src/rpc.rs b/rust/core-lib/src/rpc.rs index 84f99f3a2..7da56aafc 100644 --- a/rust/core-lib/src/rpc.rs +++ b/rust/core-lib/src/rpc.rs @@ -174,12 +174,19 @@ pub enum CoreNotification { /// ``` Plugin(PluginNotification), /// Tells `xi-core` to close the specified view. - CloseView { view_id: ViewId }, + CloseView { + view_id: ViewId, + }, /// Tells `xi-core` to save the contents of the specified view's /// buffer to the specified path. - Save { view_id: ViewId, file_path: String }, + Save { + view_id: ViewId, + file_path: String, + }, /// Tells `xi-core` to set the theme. - SetTheme { theme_name: String }, + SetTheme { + theme_name: String, + }, /// Notifies `xi-core` that the client has started. ClientStarted { #[serde(default)] @@ -197,16 +204,31 @@ pub enum CoreNotification { /// domain argument is `ConfigDomain::UserOverride(_)`, which /// represents non-persistent view-specific settings, such as when /// a user manually changes whitespace settings for a given view. - ModifyUserConfig { domain: ConfigDomainExternal, changes: Table }, + ModifyUserConfig { + domain: ConfigDomainExternal, + changes: Table, + }, /// Control whether the tracing infrastructure is enabled. /// This propagates to all peers that should respond by toggling its own /// infrastructure on/off. - TracingConfig { enabled: bool }, + TracingConfig { + enabled: bool, + }, /// Save trace data to the given path. The core will first send /// CoreRequest::CollectTrace to all peers to collect the samples. - SaveTrace { destination: PathBuf, frontend_samples: Value }, + SaveTrace { + destination: PathBuf, + frontend_samples: Value, + }, /// Tells `xi-core` to set the language id for the view. - SetLanguage { view_id: ViewId, language_id: LanguageId }, + SetLanguage { + view_id: ViewId, + language_id: LanguageId, + }, + ToggleTail { + view_id: ViewId, + enabled: bool, + }, } /// The requests which make up the base of the protocol. diff --git a/rust/core-lib/src/tabs.rs b/rust/core-lib/src/tabs.rs index 8028e8c36..f3c0f7ace 100644 --- a/rust/core-lib/src/tabs.rs +++ b/rust/core-lib/src/tabs.rs @@ -331,6 +331,7 @@ impl CoreState { // handled at the top level ClientStarted { .. } => (), SetLanguage { view_id, language_id } => self.do_set_language(view_id, language_id), + ToggleTail { view_id, enabled } => self.do_toggle_tail(view_id, enabled), } } @@ -550,6 +551,28 @@ impl CoreState { fn after_stop_plugin(&mut self, plugin: &Plugin) { self.iter_groups().for_each(|mut cx| cx.plugin_stopped(plugin)); } + + #[cfg(feature = "notify")] + fn do_toggle_tail(&mut self, view_id: ViewId, enabled: bool) { + if let Some(view) = self.views.get_mut(&view_id) { + let buffer_id = view.borrow().get_buffer_id(); + view.borrow_mut().toggle_tail(enabled); + match self.file_manager.toggle_tail(buffer_id, enabled) { + Ok(()) => { + debug!("Tail is {:?} for {:?}", enabled, view_id); + let mut context = self.make_context(view_id).unwrap(); + context.toggle_tail_config_changed(enabled); + return; + } + Err(err) => error!("Error reading file: {}", err), + } + } + } + + #[cfg(not(feature = "notify"))] + fn do_toggle_tail(&mut self, _view_id: ViewId, _enabled: bool) { + warn!("do_toggle_tail called without notify feature enabled."); + } } /// Idle, tracing, and file event handling @@ -728,7 +751,20 @@ impl CoreState { .find(|v| v.borrow().get_buffer_id() == buffer_id) .map(|v| v.borrow().get_view_id()) .unwrap(); - self.make_context(view_id).unwrap().reload(text); + + let file_info = self.file_manager.get_info(buffer_id); + + match file_info { + Some(v) => { + let tail_mode_on = v.tail_details.is_tail_enabled; + if tail_mode_on { + self.make_context(view_id).unwrap().reload_tail(text); + } else { + self.make_context(view_id).unwrap().reload(text); + } + } + None => error!("File info not found for buffer id {}", buffer_id), + }; } } } diff --git a/rust/core-lib/src/view.rs b/rust/core-lib/src/view.rs index d5cdf784b..37419fa86 100644 --- a/rust/core-lib/src/view.rs +++ b/rust/core-lib/src/view.rs @@ -98,6 +98,12 @@ pub struct View { /// Annotations provided by plugins. annotations: AnnotationStore, + + /// Tails a file if set to true. Default is false. + is_tail_enabled: bool, + + /// Flag to determine if EOF of a file is visible. Default is false. + eof_visible: bool, } /// Indicates what changed in the find state. @@ -178,6 +184,8 @@ impl View { replace: None, replace_changed: false, annotations: AnnotationStore::new(), + is_tail_enabled: false, + eof_visible: false, } } @@ -237,7 +245,7 @@ impl View { Move(movement) => self.do_move(text, movement, false), ModifySelection(movement) => self.do_move(text, movement, true), SelectAll => self.select_all(text), - Scroll(range) => self.set_scroll(range.first, range.last), + Scroll(range) => self.set_scroll(text, range.first, range.last), AddSelectionAbove => self.add_selection_by_movement(text, Movement::UpExactPosition), AddSelectionBelow => self.add_selection_by_movement(text, Movement::DownExactPosition), Gesture { line, col, ty } => self.do_gesture(text, line, col, ty), @@ -319,11 +327,13 @@ impl View { self.size = size; } - pub fn set_scroll(&mut self, first: i64, last: i64) { + pub fn set_scroll(&mut self, text: &Rope, first: i64, last: i64) { let first = max(first, 0) as usize; let last = max(last, 0) as usize; self.first_line = first; self.height = last - first; + let eof_line = self.line_of_offset(text, text.len()) + 1; + self.eof_visible = eof_line <= last; } pub fn scroll_height(&self) -> usize { @@ -853,8 +863,30 @@ impl View { let height = self.line_of_offset(text, text.len()) + 1; let plan = RenderPlan::create(height, self.first_line, self.height); self.send_update_for_plan(text, client, styles, style_spans, &plan, pristine); - if let Some(new_scroll_pos) = self.scroll_to.take() { - let (line, col) = self.offset_to_line_col(text, new_scroll_pos); + if !self.is_tail_enabled { + if let Some(new_scroll_pos) = self.scroll_to.take() { + let (line, col) = self.offset_to_line_col(text, new_scroll_pos); + client.scroll_to(self.view_id, line, col); + } + } + } + + /// Just like [`render_if_dirty`](#method.render_if_dirty) but used only when a file is being tailed. + /// When a file is being tailed and eof is visible, we keep chasing eof. + /// But when eof is not visible (for eg., on scroll), we do nothing. + pub fn render_tail( + &mut self, + text: &Rope, + client: &Client, + styles: &StyleMap, + style_spans: &Spans