diff --git a/Cargo.toml b/Cargo.toml index 4479d9e..9c4a4cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = ["held_core", "test/test_render_plugin"] + [features] dragonos = [] @@ -26,3 +29,20 @@ serde_yaml = "0.9" # 定义标志位 bitflags = "2.4.2" + +walkdir = "2.5.0" + +held_core = { path = "./held_core" } +unicode-segmentation = "1.12.0" +syntect = "5.2.0" +error-chain = "0.12.4" +yaml-rust = "0.4.5" +app_dirs2 = "2.5.5" +linked-hash-map = "0.5.6" +strum = { version = "^0.26.3", features = ["std","derive"] } +smallvec = "1.13.2" +dlopen2 = "0.7.0" + +[build-dependencies] +regex = "1.10" + diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..3ee93d8 --- /dev/null +++ b/build.rs @@ -0,0 +1,73 @@ +use std::{ + env, + fs::{read_dir, read_to_string, File}, + io::Write, + path::PathBuf, +}; + +use regex::Regex; + +const COMMAND_REGEX: &str = r"pub fn (.*)\(app: &mut Application\) -> Result<\(\)>"; + +fn main() { + generate_handler(); +} + +fn generate_handler() { + let out_dir = env::var("OUT_DIR").unwrap(); + let out_file_pathbuf = PathBuf::new().join(out_dir).join("handle_map"); + + let mut out_file = File::create(out_file_pathbuf).unwrap(); + out_file + .write( + r"{ + let mut handles: HashMap<&'static str, fn(&mut Application) -> Result<()>> = HashMap::new(); +" + .as_bytes(), + ) + .unwrap(); + + let expression = Regex::new(COMMAND_REGEX).expect("Failed to compile command matching regex"); + let readdir = read_dir("./src/application/handler/").unwrap(); + + for entry in readdir { + if let Ok(entry) = entry { + let path = entry.path(); + let module_name = entry + .file_name() + .into_string() + .unwrap() + .split('.') + .next() + .unwrap() + .to_owned(); + + let content = read_to_string(path).unwrap(); + for captures in expression.captures_iter(&content) { + let function_name = captures.get(1).unwrap().as_str(); + write(&mut out_file, &module_name, function_name); + } + } + } + + out_file + .write( + r" + handles +}" + .as_bytes(), + ) + .unwrap(); +} + +fn write(output: &mut File, module_name: &str, function_name: &str) { + output + .write( + format!( + " handles.insert(\"{}::{}\", {}::{});\n", + module_name, function_name, module_name, function_name + ) + .as_bytes(), + ) + .unwrap(); +} diff --git a/held_core/Cargo.toml b/held_core/Cargo.toml new file mode 100644 index 0000000..19f9879 --- /dev/null +++ b/held_core/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "held_core" +version = "0.1.0" +edition = "2021" + +[dependencies] +crossterm = "0.27" \ No newline at end of file diff --git a/held_core/src/control.rs b/held_core/src/control.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/held_core/src/control.rs @@ -0,0 +1 @@ + diff --git a/held_core/src/interface/app.rs b/held_core/src/interface/app.rs new file mode 100644 index 0000000..b7c9a75 --- /dev/null +++ b/held_core/src/interface/app.rs @@ -0,0 +1,21 @@ +use super::{get_application, APPLICATION}; + +pub trait App { + fn exit(&mut self); + + fn to_insert_mode(&mut self); + + fn to_normal_mode(&mut self); +} + +pub fn exit() { + get_application().exit(); +} + +pub fn to_insert_mode() { + get_application().to_insert_mode(); +} + +pub fn to_normal_mode() { + get_application().to_normal_mode(); +} diff --git a/held_core/src/interface/buffer.rs b/held_core/src/interface/buffer.rs new file mode 100644 index 0000000..95a45d6 --- /dev/null +++ b/held_core/src/interface/buffer.rs @@ -0,0 +1,7 @@ +pub trait Buffer { + fn insert_char(&mut self); + + fn new_line(&mut self); + + fn insert_tab(&mut self); +} diff --git a/held_core/src/interface/cursor.rs b/held_core/src/interface/cursor.rs new file mode 100644 index 0000000..004ae4f --- /dev/null +++ b/held_core/src/interface/cursor.rs @@ -0,0 +1,41 @@ +use crate::utils::position::Position; + +use super::get_application; + +pub trait Cursor { + fn move_left(&mut self); + + fn move_right(&mut self); + + fn move_up(&mut self); + + fn move_down(&mut self); + + fn move_to_start_of_line(&mut self); + + fn screen_cursor_position(&self) -> Position; +} + +pub fn screen_cursor_position() -> Position { + get_application().screen_cursor_position() +} + +pub fn move_down() { + get_application().move_down() +} + +pub fn move_up() { + get_application().move_up() +} + +pub fn move_left() { + get_application().move_left() +} + +pub fn move_right() { + get_application().move_right() +} + +pub fn move_to_start_of_line() { + get_application().move_to_start_of_line() +} diff --git a/held_core/src/interface/mod.rs b/held_core/src/interface/mod.rs new file mode 100644 index 0000000..4071289 --- /dev/null +++ b/held_core/src/interface/mod.rs @@ -0,0 +1,24 @@ +use app::App; +use buffer::Buffer; +use cursor::Cursor; +use monitor::Monitor; +use workspace::Workspace; + +pub mod app; +pub mod buffer; +pub mod cursor; +pub mod monitor; +pub mod render; +pub mod terminal; +pub mod workspace; + +pub trait ApplicationInterface: App + Buffer + Cursor + Monitor + Workspace {} +pub static mut APPLICATION: Option<&'static mut dyn ApplicationInterface> = None; + +pub(crate) fn get_application() -> &'static mut &'static mut dyn ApplicationInterface { + unsafe { + APPLICATION + .as_mut() + .expect("The application has not been initialized!") + } +} diff --git a/held_core/src/interface/monitor.rs b/held_core/src/interface/monitor.rs new file mode 100644 index 0000000..440fbe2 --- /dev/null +++ b/held_core/src/interface/monitor.rs @@ -0,0 +1,5 @@ +pub trait Monitor { + fn scroll_to_cursor(&mut self); + + fn scroll_to_center(&mut self); +} diff --git a/held_core/src/interface/render.rs b/held_core/src/interface/render.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/held_core/src/interface/render.rs @@ -0,0 +1 @@ + diff --git a/held_core/src/interface/terminal.rs b/held_core/src/interface/terminal.rs new file mode 100644 index 0000000..61ad73e --- /dev/null +++ b/held_core/src/interface/terminal.rs @@ -0,0 +1,4 @@ +pub trait TerminalInfo { + fn width() -> usize; + fn height() -> usize; +} diff --git a/held_core/src/interface/workspace.rs b/held_core/src/interface/workspace.rs new file mode 100644 index 0000000..307ec65 --- /dev/null +++ b/held_core/src/interface/workspace.rs @@ -0,0 +1,5 @@ +pub trait Workspace { + fn save_file(&mut self); + + fn undo(&mut self); +} diff --git a/held_core/src/lib.rs b/held_core/src/lib.rs new file mode 100644 index 0000000..5970e75 --- /dev/null +++ b/held_core/src/lib.rs @@ -0,0 +1,30 @@ +pub mod control; +pub mod interface; +pub mod plugin; +pub mod theme; +pub mod utils; +pub mod view; + +#[macro_export] +macro_rules! declare_plugin { + ($app:ty, $constructor:path) => { + use held_core::interface::ApplicationInterface; + use held_core::interface::APPLICATION; + + #[no_mangle] + pub unsafe extern "C" fn init_plugin_application( + app: &'static mut dyn ApplicationInterface, + ) { + APPLICATION = Some(app); + } + + #[no_mangle] + pub extern "C" fn plugin_create() -> *mut dyn $crate::plugin::Plugin { + // 确保构造器正确,所以做了这一步骤,来显示声明签名 + let constructor: fn() -> $app = $constructor; + let object = constructor(); + let boxed: Box = Box::new(object); + Box::into_raw(boxed) + } + }; +} diff --git a/held_core/src/plugin.rs b/held_core/src/plugin.rs new file mode 100644 index 0000000..4f6d8f9 --- /dev/null +++ b/held_core/src/plugin.rs @@ -0,0 +1,14 @@ +use crate::view::render::ContentRenderBuffer; + +pub trait Plugin { + fn name(&self) -> &'static str; + + fn init(&self); + + fn deinit(&self); + + // 渲染文本内容部分时会触发该回调,可以返回想要在content中渲染的buffer + fn on_render_content(&self) -> Vec { + vec![] + } +} diff --git a/held_core/src/theme.rs b/held_core/src/theme.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/held_core/src/theme.rs @@ -0,0 +1 @@ + diff --git a/held_core/src/utils/distance.rs b/held_core/src/utils/distance.rs new file mode 100644 index 0000000..82638ab --- /dev/null +++ b/held_core/src/utils/distance.rs @@ -0,0 +1,27 @@ +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Distance { + pub lines: usize, + pub offset: usize, +} + +impl Distance { + /// 计算字符串覆盖的距离 + /// + /// /// # Examples + /// + /// ``` + /// use crate::buffer::distance::Distance; + /// + /// let data = "data\ndistance"; + /// assert_eq!(Distance::of_str(data), Distance{ + /// lines: 1, + /// offset: 8 + /// }); + /// ``` + pub fn of_str(from: &str) -> Distance { + Distance { + lines: from.chars().filter(|&c| c == '\n').count(), + offset: from.split('\n').last().map(|l| l.len()).unwrap_or(0), + } + } +} diff --git a/held_core/src/utils/mod.rs b/held_core/src/utils/mod.rs new file mode 100644 index 0000000..76554d0 --- /dev/null +++ b/held_core/src/utils/mod.rs @@ -0,0 +1,4 @@ +pub mod distance; +pub mod position; +pub mod range; +pub mod rectangle; diff --git a/held_core/src/utils/position.rs b/held_core/src/utils/position.rs new file mode 100644 index 0000000..e7a2222 --- /dev/null +++ b/held_core/src/utils/position.rs @@ -0,0 +1,69 @@ +use std::{ + cmp::Ordering, + ops::{Add, AddAssign}, +}; + +use super::distance::Distance; + +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct Position { + pub line: usize, + pub offset: usize, +} + +impl Position { + pub fn new(line: usize, offset: usize) -> Position { + Position { line, offset } + } +} + +impl PartialOrd for Position { + fn partial_cmp(&self, other: &Position) -> Option { + Some(if self.line < other.line { + Ordering::Less + } else if self.line > other.line { + Ordering::Greater + } else if self.offset < other.offset { + Ordering::Less + } else if self.offset > other.offset { + Ordering::Greater + } else { + Ordering::Equal + }) + } +} + +impl Add for Position { + type Output = Position; + + fn add(self, distance: Distance) -> Self::Output { + let offset = if distance.lines > 0 { + distance.offset + } else { + self.offset + distance.offset + }; + + Position { + line: self.line + distance.lines, + offset, + } + } +} + +impl AddAssign for Position { + fn add_assign(&mut self, distance: Distance) { + self.line += distance.lines; + self.offset = if distance.lines > 0 { + distance.offset + } else { + self.offset + distance.offset + }; + } +} + +impl From<(usize, usize)> for Position { + fn from(tuple: (usize, usize)) -> Self { + let (line, offset) = tuple; + Position::new(line, offset) + } +} diff --git a/held_core/src/utils/range.rs b/held_core/src/utils/range.rs new file mode 100644 index 0000000..b1406fc --- /dev/null +++ b/held_core/src/utils/range.rs @@ -0,0 +1,59 @@ +use crate::utils::position::Position; + +/// Range: 左开右闭区间 +#[derive(Clone, Debug, PartialEq, Default)] +pub struct Range { + start: Position, + end: Position, +} + +impl Range { + pub fn new(start: Position, end: Position) -> Range { + if start > end { + Range { + start: end, + end: start, + } + } else { + Range { start, end } + } + } + + pub fn start(&self) -> Position { + self.start + } + + pub fn end(&self) -> Position { + self.end + } + + /// Whether or not the range includes the specified position. + /// The range is exclusive, such that its ending position is not included. + /// + /// # Examples + /// + /// ``` + /// use scribe::buffer::{Position, Range}; + /// + /// // Builder a range. + /// let range = Range::new( + /// Position{ line: 0, offset: 0 }, + /// Position{ line: 1, offset: 5 } + /// ); + /// + /// assert!(range.includes( + /// &Position{ line: 1, offset: 0 } + /// )); + /// + /// assert!(range.includes( + /// &Position{ line: 1, offset: 4 } + /// )); + /// + /// assert!(!range.includes( + /// &Position{ line: 1, offset: 5 } + /// )); + /// ``` + pub fn includes(&self, position: &Position) -> bool { + position >= &self.start() && position < &self.end() + } +} diff --git a/held_core/src/utils/rectangle.rs b/held_core/src/utils/rectangle.rs new file mode 100644 index 0000000..afa1d21 --- /dev/null +++ b/held_core/src/utils/rectangle.rs @@ -0,0 +1,7 @@ +use super::position::Position; + +pub struct Rectangle { + pub position: Position, + pub width: usize, + pub height: usize, +} diff --git a/held_core/src/view/colors.rs b/held_core/src/view/colors.rs new file mode 100644 index 0000000..30333a4 --- /dev/null +++ b/held_core/src/view/colors.rs @@ -0,0 +1,20 @@ +use crossterm::style::Color; + +/// A convenience type used to represent a foreground/background +/// color combination. Provides generic/convenience variants to +/// discourage color selection outside of the theme, whenever possible. +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub enum Colors { + #[default] + Default, // default/background + Focused, // default/alt background + Inverted, // background/default + Insert, // white/green + Warning, // white/yellow + PathMode, // white/pink + SearchMode, // white/purple + SelectMode, // white/blue + CustomForeground(Color), + CustomFocusedForeground(Color), + Custom(Color, Color), +} diff --git a/held_core/src/view/mod.rs b/held_core/src/view/mod.rs new file mode 100644 index 0000000..dd0d4de --- /dev/null +++ b/held_core/src/view/mod.rs @@ -0,0 +1,3 @@ +pub mod colors; +pub mod render; +pub mod style; diff --git a/held_core/src/view/render/cell.rs b/held_core/src/view/render/cell.rs new file mode 100644 index 0000000..b4ca794 --- /dev/null +++ b/held_core/src/view/render/cell.rs @@ -0,0 +1,18 @@ +use crate::view::{colors::Colors, style::CharStyle}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Cell { + pub content: char, + pub colors: Colors, + pub style: CharStyle, +} + +impl Cell { + pub fn new(content: char, colors: Colors, style: CharStyle) -> Cell { + Cell { + content, + colors, + style, + } + } +} diff --git a/held_core/src/view/render/mod.rs b/held_core/src/view/render/mod.rs new file mode 100644 index 0000000..5d8ec3c --- /dev/null +++ b/held_core/src/view/render/mod.rs @@ -0,0 +1,56 @@ +use std::str::Chars; + +use cell::Cell; + +use crate::utils::{position::Position, rectangle::Rectangle}; + +use super::{colors::Colors, style::CharStyle}; + +pub mod cell; +pub struct ContentRenderBuffer { + pub rectangle: Rectangle, + pub cells: Vec>, +} + +impl ContentRenderBuffer { + pub fn new(rectangle: Rectangle) -> ContentRenderBuffer { + let cells = vec![None; rectangle.height * rectangle.width]; + ContentRenderBuffer { rectangle, cells } + } + + pub fn set_cell(&mut self, position: Position, cell: Option) { + let index = position.line * self.rectangle.width + position.offset; + if index < self.cells.len() { + self.cells[index] = cell; + } + } + + pub fn put_buffer( + &mut self, + position: Position, + buffer: String, + style: CharStyle, + colors: Colors, + ) { + let mut line = position.line; + let mut offset = position.offset; + for c in buffer.chars() { + let index = line * self.rectangle.width + offset; + if index < self.cells.len() { + let cell = Cell { + content: c, + colors, + style, + }; + self.cells[index] = Some(cell); + offset += 1; + if offset == self.rectangle.width { + line += 1; + offset = 0; + } + } else { + break; + } + } + } +} diff --git a/held_core/src/view/style.rs b/held_core/src/view/style.rs new file mode 100644 index 0000000..7aa76f8 --- /dev/null +++ b/held_core/src/view/style.rs @@ -0,0 +1,8 @@ +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub enum CharStyle { + #[default] + Default, + Bold, + Reverse, + Italic, +} diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index a3a8ae0..0000000 --- a/src/app.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::{io, sync::Arc}; - -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; - -use crate::{ - config::appconfig::AppSetting, - utils::{file::FileManager, ui::uicore::Ui}, -}; - -pub struct Application { - file_manager: FileManager, - bak: bool, - ui: Arc, -} - -impl Application { - pub fn new(file_path: Option, setting: AppSetting) -> io::Result { - let bak; - let mut file = if file_path.is_some() { - bak = true; - FileManager::new(file_path.unwrap())? - } else { - bak = false; - FileManager::new("held.tmp".to_string())? - }; - - // 将文件数据读入buf - let buf = file.init(bak)?; - - Ok(Self { - file_manager: file, - bak, - ui: Ui::new(Arc::new(buf), setting), - }) - } - - fn init(&mut self) -> io::Result<()> { - Ui::init_ui()?; - - if !self.bak { - self.ui.start_page_ui()?; - } - - Ok(()) - } - - pub fn run(&mut self) -> io::Result<()> { - enable_raw_mode()?; - self.init()?; - match self.ui.ui_loop() { - Ok(store) => { - if store { - let buffer = &self.ui.core.lock().unwrap().buffer; - self.file_manager.store(buffer)? - } else if self.file_manager.is_first_open() { - self.file_manager.delete_files()?; - } - } - Err(_) => { - // 补救措施:恢复备份文件 - todo!() - } - } - disable_raw_mode()?; - Ok(()) - } -} diff --git a/src/application/handler/app.rs b/src/application/handler/app.rs new file mode 100644 index 0000000..0dce3d8 --- /dev/null +++ b/src/application/handler/app.rs @@ -0,0 +1,59 @@ +use crate::application::mode::command::CommandData; +use crate::application::mode::{ModeData, ModeKey}; +use crate::application::Application; +use crate::errors::*; + +pub fn exit(app: &mut Application) -> Result<()> { + app.switch_mode(ModeKey::Exit); + Ok(()) +} + +pub fn exit_with_check(app: &mut Application) -> Result<()> { + if let Some(ref buf) = app.workspace.current_buffer { + if buf.modified() { + // 输出提示(todo) + if let ModeData::Command(ref mut command_data) = app.mode { + command_data.reset(); + } + app.switch_mode(ModeKey::Normal); + } else { + app.switch_mode(ModeKey::Exit); + } + } + Ok(()) +} + +pub fn to_insert_mode(app: &mut Application) -> Result<()> { + app.switch_mode(ModeKey::Insert); + Ok(()) +} + +pub fn to_normal_mode(app: &mut Application) -> Result<()> { + app.switch_mode(ModeKey::Normal); + Ok(()) +} + +pub fn to_workspace_mode(app: &mut Application) -> Result<()> { + app.switch_mode(ModeKey::Workspace); + Ok(()) +} + +pub fn to_command_mode(app: &mut Application) -> Result<()> { + app.switch_mode(ModeKey::Command); + Ok(()) +} + +pub fn to_search_mode(app: &mut Application) -> Result<()> { + app.switch_mode(ModeKey::Search); + Ok(()) +} + +pub fn to_delete_mode(app: &mut Application) -> Result<()> { + app.switch_mode(ModeKey::Delete); + Ok(()) +} + +pub fn to_replace_mode(app: &mut Application) -> Result<()> { + app.switch_mode(ModeKey::Replace); + Ok(()) +} diff --git a/src/application/handler/buffer.rs b/src/application/handler/buffer.rs new file mode 100644 index 0000000..db32530 --- /dev/null +++ b/src/application/handler/buffer.rs @@ -0,0 +1,81 @@ +use crossterm::event::KeyCode; +use held_core::utils::position::Position; + +use crate::application::Application; +use crate::errors::*; + +use super::cursor; + +pub fn insert_char(app: &mut Application) -> Result<()> { + if let Some(key) = app.monitor.last_key { + if let KeyCode::Char(c) = key.code { + app.workspace.current_buffer.as_mut().unwrap().insert(c); + cursor::move_right(app)?; + } + } + Ok(()) +} + +pub fn insert_char_on_replace(app: &mut Application) -> Result<()> { + if let Some(key) = app.monitor.last_key { + if let KeyCode::Char(c) = key.code { + app.workspace + .current_buffer + .as_mut() + .unwrap() + .replace_on_cursor(c.to_string()); + cursor::move_right(app)?; + } + } + Ok(()) +} + +pub fn new_line(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.insert('\n'); + } + Ok(()) +} + +pub fn insert_tab(app: &mut Application) -> Result<()> { + if let Some(buffer) = app.workspace.current_buffer.as_mut() { + let tab_len = app.perferences.borrow().tab_width(); + let width = tab_len - (buffer.cursor.offset) % tab_len; + if app.perferences.borrow().soft_tab() { + let tab_str = " ".repeat(width); + buffer.insert(tab_str); + buffer.cursor.move_to(Position { + line: buffer.cursor.line, + offset: buffer.cursor.offset + width, + }); + } else { + buffer.insert("\t"); + buffer.cursor.move_to(Position { + line: buffer.cursor.line, + offset: buffer.cursor.offset + width, + }); + } + } + Ok(()) +} + +pub fn redo(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.redo(); + } + Ok(()) +} + +pub fn save_file(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.save()?; + } + Ok(()) +} + +pub fn undo(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.undo(); + } + Ok(()) +} diff --git a/src/application/handler/command.rs b/src/application/handler/command.rs new file mode 100644 index 0000000..8b55a65 --- /dev/null +++ b/src/application/handler/command.rs @@ -0,0 +1,106 @@ +use std::collections::HashMap; + +use crossterm::event::KeyCode; +use smallvec::SmallVec; + +use crate::application::handler::{app, buffer}; +use crate::application::mode::{ModeData, ModeKey}; +use crate::application::Application; +use crate::errors::*; + +use lazy_static::lazy_static; + +lazy_static! { + static ref COMMAND: HashMap Result<()>; 4]>> = { + let mut cmd_map = + HashMap:: Result<()>; 4]>>::new(); + + cmd_map.insert( + "q".to_string(), + SmallVec::from_vec(vec![ + app::exit_with_check as fn(&mut Application) -> Result<()>, + ]), + ); + cmd_map.insert( + "q!".to_string(), + SmallVec::from_vec(vec![app::exit as fn(&mut Application) -> Result<()>]), + ); + cmd_map.insert( + "w".to_string(), + SmallVec::from_vec(vec![ + buffer::save_file as fn(&mut Application) -> Result<()>, + ]), + ); + cmd_map.insert( + "wq".to_string(), + SmallVec::from_vec(vec![ + buffer::save_file as fn(&mut Application) -> Result<()>, + app::exit as fn(&mut Application) -> Result<()>, + ]), + ); + cmd_map + }; +} + +pub fn commit_and_execute(app: &mut Application) -> Result<()> { + let cmd = match app.mode { + ModeData::Command(ref mut command_data) => command_data.input.clone(), + _ => String::new(), + }; + // 匹配命令执行 + match COMMAND.get(&cmd).cloned() { + Some(fucs) => { + for fuc in fucs { + fuc(app)?; + } + // 执行完函数清空数据 + if let ModeData::Command(ref mut command_data) = app.mode { + command_data.reset(); + } + if app.mode_key != ModeKey::Exit { + app::to_normal_mode(app)?; + } + } + None => { + if let ModeData::Command(ref mut command_data) = app.mode { + command_data.reset(); + } + app::to_normal_mode(app)?; + } + } + // 匹配完reset + if let ModeData::Command(ref mut command_data) = app.mode { + command_data.reset(); + } + Ok(()) +} + +pub fn insert_command(app: &mut Application) -> Result<()> { + if let Some(key) = app.monitor.last_key { + if let KeyCode::Char(c) = key.code { + if let ModeData::Command(ref mut command_data) = app.mode { + command_data.input.insert(command_data.input.len(), c); + } + } + } + Ok(()) +} + +pub fn backspace(app: &mut Application) -> Result<()> { + if let ModeData::Command(ref mut command_data) = app.mode { + if command_data.input.is_empty() { + return app::to_normal_mode(app); + } else { + command_data.input.remove(command_data.input.len() - 1); + } + } + Ok(()) +} + +pub fn to_normal_mode(app: &mut Application) -> Result<()> { + if let ModeData::Command(ref mut command_data) = app.mode { + command_data.reset(); + } + app.switch_mode(ModeKey::Normal); + Ok(()) +} diff --git a/src/application/handler/cursor.rs b/src/application/handler/cursor.rs new file mode 100644 index 0000000..33478d0 --- /dev/null +++ b/src/application/handler/cursor.rs @@ -0,0 +1,48 @@ +use crate::application::Application; +use crate::errors::*; + +pub fn move_left(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.cursor.move_left(); + app.monitor.scroll_to_cursor(buffer)?; + } + Ok(()) +} + +pub fn move_right(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.cursor.move_right(); + app.monitor.scroll_to_cursor(buffer)?; + } + Ok(()) +} + +pub fn move_up(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.cursor.move_up(); + app.monitor.scroll_to_cursor(buffer)?; + } + Ok(()) +} + +pub fn move_down(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.cursor.move_down(); + app.monitor.scroll_to_cursor(buffer)?; + } + Ok(()) +} + +pub fn move_to_start_of_line(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.cursor.move_to_start_of_line(); + } + Ok(()) +} + +pub fn move_to_end_of_line(app: &mut Application) -> Result<()> { + if let Some(buffer) = &mut app.workspace.current_buffer { + buffer.cursor.move_to_end_of_line(); + } + Ok(()) +} diff --git a/src/application/handler/delete.rs b/src/application/handler/delete.rs new file mode 100644 index 0000000..b6914a4 --- /dev/null +++ b/src/application/handler/delete.rs @@ -0,0 +1,37 @@ +use held_core::utils::position::Position; +use held_core::utils::range::Range; + +use crate::application::mode::motion::locate_next_words_begin; +use crate::application::Application; +use crate::errors::*; + +use super::normal::{self}; + +pub fn delete_words(app: &mut Application) -> Result<()> { + let count = app.cmd_counter.max(1); + let buf = app.workspace.current_buffer.as_mut().unwrap(); + let current_pos = &buf.cursor.position; + let search_range = if let Some(str) = buf.read_rest(¤t_pos) { + str + } else { + return Ok(()); + }; + let next_words_pos = locate_next_words_begin(count, &search_range, current_pos); + let del_range = Range::new(*current_pos, next_words_pos); + buf.delete_range(del_range); + normal::reset(app)?; + Ok(()) +} + +pub fn delete_lines(app: &mut Application) -> Result<()> { + let count = app.cmd_counter; + let buf = app.workspace.current_buffer.as_mut().unwrap(); + let start_pos = Position::new(buf.cursor.line, 0); + let end_pos = Position::new( + start_pos.line + count.min(buf.line_count() - start_pos.line).max(1), + 0, + ); + buf.delete_range(Range::new(start_pos, end_pos)); + normal::reset(app)?; + Ok(()) +} diff --git a/src/application/handler/insert.rs b/src/application/handler/insert.rs new file mode 100644 index 0000000..1e4ad10 --- /dev/null +++ b/src/application/handler/insert.rs @@ -0,0 +1,15 @@ +use held_core::utils::position::Position; + +use crate::application::Application; +use crate::errors::*; + +pub fn backspace(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + // 在第一行第一列时不执行删除操作 + if buffer.cursor.position != Position::new(0, 0) { + buffer.cursor.move_left(); + buffer.delete(); + } + } + Ok(()) +} diff --git a/src/application/handler/mod.rs b/src/application/handler/mod.rs new file mode 100644 index 0000000..65ffb92 --- /dev/null +++ b/src/application/handler/mod.rs @@ -0,0 +1,18 @@ +use std::collections::HashMap; + +use super::Application; +use crate::errors::*; +mod app; +mod buffer; +mod command; +mod cursor; +mod delete; +mod insert; +mod monitor; +mod normal; +mod search; +mod workspace; + +pub fn handle_map() -> HashMap<&'static str, fn(&mut Application) -> Result<()>> { + include!(concat!(env!("OUT_DIR"), "/handle_map")) +} diff --git a/src/application/handler/monitor.rs b/src/application/handler/monitor.rs new file mode 100644 index 0000000..2f7ae1f --- /dev/null +++ b/src/application/handler/monitor.rs @@ -0,0 +1,32 @@ +use crate::application::Application; +use crate::errors::*; + +pub fn scroll_to_cursor(app: &mut Application) -> Result<()> { + if let Some(ref buffer) = app.workspace.current_buffer { + app.monitor.scroll_to_cursor(buffer)?; + } + Ok(()) +} + +pub fn scroll_to_center(app: &mut Application) -> Result<()> { + if let Some(ref buffer) = app.workspace.current_buffer { + app.monitor.scroll_to_center(buffer)?; + } + Ok(()) +} + +pub fn scroll_to_first_line(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.cursor.move_to_first_line(); + app.monitor.scroll_to_cursor(buffer)?; + } + Ok(()) +} + +pub fn scroll_to_last_line(app: &mut Application) -> Result<()> { + if let Some(ref mut buffer) = app.workspace.current_buffer { + buffer.cursor.move_to_last_line(); + app.monitor.scroll_to_cursor(buffer)?; + } + Ok(()) +} diff --git a/src/application/handler/normal.rs b/src/application/handler/normal.rs new file mode 100644 index 0000000..0330ebd --- /dev/null +++ b/src/application/handler/normal.rs @@ -0,0 +1,177 @@ +use crossterm::event::KeyCode; +use held_core::utils::position::Position; +use held_core::utils::range::Range; +use unicode_segmentation::UnicodeSegmentation; + +use crate::application::mode::motion; +use crate::application::Application; +use crate::errors::*; + +pub fn count_cmd(app: &mut Application) -> Result<()> { + if let Some(key) = app.monitor.last_key { + if let KeyCode::Char(ch) = key.code { + if let Some(digit) = ch.to_digit(10) { + let count = &mut app.cmd_counter; + *count *= 10; + *count += digit as usize; + } + } + } + Ok(()) +} + +// 可以指定执行次数的命令,数字必须先于命令字符;而命令字符可以在配置文件指定 + +pub fn move_down_n(app: &mut Application) -> Result<()> { + let count = app.cmd_counter.max(1); + if let Some(buffer) = &mut app.workspace.current_buffer { + for _ in 0..count.min(buffer.line_count() - buffer.cursor.line) { + buffer.cursor.move_down(); + } + app.monitor.scroll_to_cursor(buffer).unwrap(); + reset(app)?; + } + Ok(()) +} + +pub fn move_up_n(app: &mut Application) -> Result<()> { + let count = app.cmd_counter.max(1); + if let Some(buffer) = &mut app.workspace.current_buffer { + for _ in 0..count.min(buffer.cursor.line) { + buffer.cursor.move_up(); + } + app.monitor.scroll_to_cursor(buffer).unwrap(); + reset(app)?; + } + Ok(()) +} + +pub fn move_to_target_line(app: &mut Application) -> Result<()> { + if let Some(buffer) = &mut app.workspace.current_buffer { + let count = app.cmd_counter; + if count > 0 { + let target_line = count.min(buffer.line_count()); + let offset = buffer.cursor.offset; + if !buffer + .cursor + .move_to(Position::new(target_line - 1, offset)) + { + let target_offset = buffer + .data() + .lines() + .nth(target_line - 1) + .unwrap() + .graphemes(true) + .count(); + buffer + .cursor + .move_to(Position::new(target_line - 1, target_offset)); + } + } + app.monitor.scroll_to_cursor(buffer).unwrap(); + reset(app)?; + } + Ok(()) +} + +pub fn move_left_n(app: &mut Application) -> Result<()> { + let mut count = app.cmd_counter.max(1); + if let Some(buffer) = &mut app.workspace.current_buffer { + let offset = buffer.cursor.offset; + count = count.min(offset); + for _ in 0..count { + buffer.cursor.move_left(); + } + app.monitor.scroll_to_cursor(buffer)?; + reset(app)?; + } + Ok(()) +} + +pub fn move_right_n(app: &mut Application) -> Result<()> { + let mut count = app.cmd_counter.max(1); + if let Some(buffer) = &mut app.workspace.current_buffer { + let max_offset = buffer + .data() + .lines() + .nth(buffer.cursor.line) + .unwrap() + .graphemes(true) + .count(); + let offset = buffer.cursor.offset; + count = count.min(max_offset - offset); + for _ in 0..count { + buffer.cursor.move_right(); + } + app.monitor.scroll_to_cursor(buffer)?; + reset(app)?; + } + Ok(()) +} + +pub fn reset(app: &mut Application) -> Result<()> { + app.cmd_counter = 0; + Ok(()) +} + +pub fn move_to_next_words(app: &mut Application) -> Result<()> { + if let Some(buffer) = &mut app.workspace.current_buffer { + let current_pos = buffer.cursor.position; + // 从当前位置向后搜索 + let search_range = if let Some(str) = buffer.read_rest(¤t_pos) { + str + } else { + return Ok(()); + }; + let count = app.cmd_counter; + let next_words_pos = + motion::locate_next_words_begin(count.max(1), &search_range, ¤t_pos); + buffer.cursor.move_to(next_words_pos); + app.monitor.scroll_to_cursor(buffer)?; + reset(app)?; + } + Ok(()) +} + +pub fn move_to_prev_words(app: &mut Application) -> Result<()> { + if let Some(buffer) = &mut app.workspace.current_buffer { + let current_pos = buffer.cursor.position; + // + Distance { + // lines: 0, + // offset: 1, + // }; // 由于是左闭右开区间,所以需要向后移动一个字符 + // 从当前位置向前搜索 + let search_range = + if let Some(str) = buffer.read(&Range::new(Position::new(0, 0), current_pos)) { + str + } else { + return Ok(()); + }; + let count = app.cmd_counter; + let prev_words_pos = + motion::locate_previous_words(count.max(1), &search_range, ¤t_pos); + buffer.cursor.move_to(prev_words_pos); + app.monitor.scroll_to_cursor(buffer)?; + reset(app)?; + } + Ok(()) +} + +pub fn move_to_next_words_end(app: &mut Application) -> Result<()> { + if let Some(buffer) = &mut app.workspace.current_buffer { + let current_pos = buffer.cursor.position; + // 从当前位置向后搜索 + let search_range = if let Some(str) = buffer.read_rest(¤t_pos) { + str + } else { + return Ok(()); + }; + let count = app.cmd_counter; + let next_words_pos = + motion::locate_next_words_end(count.max(1), &search_range, ¤t_pos); + buffer.cursor.move_to(next_words_pos); + app.monitor.scroll_to_cursor(buffer)?; + reset(app)?; + } + Ok(()) +} diff --git a/src/application/handler/search.rs b/src/application/handler/search.rs new file mode 100644 index 0000000..08661dd --- /dev/null +++ b/src/application/handler/search.rs @@ -0,0 +1,84 @@ +use crate::application::mode::ModeData; +use crate::application::Application; +use crate::errors::*; +use crossterm::event::KeyCode; +use held_core::utils::{position::Position, range::Range}; + +pub fn exec_search(app: &mut Application) -> Result<()> { + if let ModeData::Search(ref mut search_data) = app.mode { + search_data.is_exec_search = true; + if let Some(ref mut buffer) = app.workspace.current_buffer { + let search_string = search_data.search_string.clone(); + let search_result = buffer.search(&search_string); + + let fixed_offset = search_string.len(); + let ranges: Vec = search_result + .into_iter() + .map(|pos| { + let end_position = Position { + line: pos.line, + offset: pos.offset + fixed_offset, + }; + Range::new(pos, end_position) + }) + .collect(); + + search_data.search_result = ranges; + } + } + Ok(()) +} + +pub fn input_search_data(app: &mut Application) -> Result<()> { + if let Some(key) = app.monitor.last_key { + if let KeyCode::Char(c) = key.code { + if let ModeData::Search(ref mut search_data) = app.mode { + if search_data.is_exec_search == false { + search_data + .search_string + .insert(search_data.search_string.len(), c); + } + } + } + } + Ok(()) +} + +pub fn backspace(app: &mut Application) -> Result<()> { + if let ModeData::Search(ref mut search_data) = app.mode { + if search_data.is_exec_search == false && search_data.search_string.len() > 0 { + search_data + .search_string + .remove(search_data.search_string.len() - 1); + } + } + Ok(()) +} + +pub fn last_result(app: &mut Application) -> Result<()> { + if let ModeData::Search(ref mut search_data) = app.mode { + if search_data.is_exec_search == true && search_data.search_result.len() != 1 { + search_data.search_result_index = + (search_data.search_result_index + search_data.search_result.len() - 2) + % (search_data.search_result.len() - 1); + } + } + Ok(()) +} + +pub fn next_result(app: &mut Application) -> Result<()> { + if let ModeData::Search(ref mut search_data) = app.mode { + if search_data.is_exec_search == true && search_data.search_result.len() != 1 { + search_data.search_result_index = + (search_data.search_result_index + 1) % (search_data.search_result.len() - 1); + } + } + Ok(()) +} + +pub fn clear(app: &mut Application) -> Result<()> { + if let ModeData::Search(ref mut search_data) = app.mode { + search_data.clear(); + } + Ok(()) +} diff --git a/src/application/handler/workspace.rs b/src/application/handler/workspace.rs new file mode 100644 index 0000000..ef88548 --- /dev/null +++ b/src/application/handler/workspace.rs @@ -0,0 +1,34 @@ +use crate::application::mode::{ModeData, ModeKey}; +use crate::application::Application; +use crate::errors::*; + +pub fn to_normal_mode(app: &mut Application) -> Result<()> { + if let ModeData::Workspace(ref mode) = app.mode { + app.workspace.select_buffer(mode.prev_buffer_id); + } + app.switch_mode(ModeKey::Normal); + Ok(()) +} + +pub fn move_down(app: &mut Application) -> Result<()> { + if let ModeData::Workspace(ref mut mode) = app.mode { + mode.move_down(); + } + Ok(()) +} + +pub fn move_up(app: &mut Application) -> Result<()> { + if let ModeData::Workspace(ref mut mode) = app.mode { + mode.move_up(); + } + Ok(()) +} + +pub fn enter(app: &mut Application) -> Result<()> { + if let ModeData::Workspace(ref mut mode) = app.mode { + if mode.open(&mut app.workspace, &mut app.monitor)? { + to_normal_mode(app)?; + } + } + Ok(()) +} diff --git a/src/application/mod.rs b/src/application/mod.rs new file mode 100644 index 0000000..53eb73a --- /dev/null +++ b/src/application/mod.rs @@ -0,0 +1,217 @@ +use crate::{ + errors::*, + modules::input::{InputLoader, InputMapper}, + plugin::system::PluginSystem, +}; +use crossterm::{event::Event, terminal::disable_raw_mode}; +use held_core::plugin::Plugin; +use mode::{ + command::CommandData, error::ErrorRenderer, search::SearchData, workspace::WorkspaceModeData, + ModeData, ModeKey, ModeRenderer, ModeRouter, +}; +use smallvec::SmallVec; +use state::ApplicationStateData; + +use std::{cell::RefCell, collections::HashMap, mem, rc::Rc, sync::Arc}; + +use crate::{ + config::appconfig::AppSetting, + modules::perferences::{Perferences, PerferencesManager}, + utils::{file::FileManager, ui::uicore::Ui}, + view::monitor::Monitor, + workspace::Workspace, +}; + +mod handler; +pub mod mode; +pub mod plugin_interafce; +pub mod state; + +pub struct Application { + file_manager: FileManager, + bak: bool, + ui: Arc, + pub workspace: Workspace, + pub monitor: Monitor, + pub perferences: Rc>, + pub mode: ModeData, + mode_key: ModeKey, + mode_history: HashMap, + input_map: HashMap< + String, + HashMap< + String, + SmallVec<[fn(&mut crate::Application) -> std::result::Result<(), Error>; 4]>, + >, + >, + plugin_system: Rc>, + pub state_data: ApplicationStateData, + pub cmd_counter: usize, +} + +impl Application { + pub fn new(file_path: Option, setting: AppSetting, args: &[String]) -> Result { + let bak; + let mut file = if file_path.is_some() { + bak = true; + FileManager::new(file_path.unwrap())? + } else { + bak = false; + FileManager::new("held.tmp".to_string())? + }; + + // 将文件数据读入buf + let buf = file.init(bak)?; + + let perferences = PerferencesManager::load()?; + + let plugin_system = Rc::new(RefCell::new(PluginSystem::init_system( + perferences.borrow().plugins_path()?, + ))); + + let input_map = InputLoader::load(perferences.borrow().input_config_path()?)?; + let mut monitor = Monitor::new(perferences.clone(), plugin_system.clone())?; + let workspace = Workspace::create_workspace(&mut monitor, perferences.borrow(), args)?; + Ok(Self { + file_manager: file, + bak, + ui: Ui::new(Arc::new(buf), setting), + workspace, + monitor, + perferences, + mode: ModeData::Normal, + mode_key: ModeKey::Normal, + mode_history: HashMap::new(), + input_map, + plugin_system, + state_data: ApplicationStateData::default(), + cmd_counter: 0, + }) + } + + fn init(&mut self) -> Result<()> { + // Ui::init_ui()?; + // PluginSystem::init_system(); + // self.monitor.terminal.clear().unwrap(); + self.init_modes()?; + self.plugin_system.borrow().init(); + // if !self.bak { + // self.ui.start_page_ui()?; + // } + + Ok(()) + } + + fn init_modes(&mut self) -> Result<()> { + self.mode_history.insert(ModeKey::Normal, ModeData::Normal); + self.mode_history.insert(ModeKey::Insert, ModeData::Insert); + self.mode_history + .insert(ModeKey::Command, ModeData::Command(CommandData::new())); + self.mode_history + .insert(ModeKey::Replace, ModeData::Replace); + self.mode_history + .insert(ModeKey::Error, ModeData::Error(Error::default())); + self.mode_history.insert(ModeKey::Exit, ModeData::Exit); + self.mode_history.insert( + ModeKey::Workspace, + ModeData::Workspace(WorkspaceModeData::new( + &mut self.workspace, + &mut self.monitor, + )?), + ); + self.mode_history.insert(ModeKey::Delete, ModeData::Delete); + self.mode_history + .insert(ModeKey::Search, ModeData::Search(SearchData::new())); + Ok(()) + } + + pub fn run(&mut self) -> Result<()> { + self.init()?; + + loop { + self.render()?; + self.listen_event()?; + + if let ModeKey::Exit = &self.mode_key { + disable_raw_mode()?; + return Ok(()); + } + } + + // 主线程 + match self.ui.ui_loop() { + Ok(store) => { + if store { + let buffer = &self.ui.core.lock().unwrap().buffer; + self.file_manager.store(buffer)? + } else if self.file_manager.is_first_open() { + self.file_manager.delete_files()?; + } + } + Err(_) => { + // 补救措施:恢复备份文件 + todo!() + } + } + disable_raw_mode()?; + Ok(()) + } + + fn listen_event(&mut self) -> Result<()> { + let event = self.monitor.listen()?; + self.handle_input(event)?; + Ok(()) + } + + fn render(&mut self) -> Result<()> { + if let Err(err) = ModeRouter::render(&mut self.workspace, &mut self.monitor, &mut self.mode) + { + ErrorRenderer::render( + &mut self.workspace, + &mut self.monitor, + &mut ModeData::Error(err), + )?; + } + Ok(()) + } + + pub fn switch_mode(&mut self, mode_key: ModeKey) { + if self.mode_key == mode_key { + return; + } + + let mut mode = self.mode_history.remove(&mode_key).unwrap(); + + mem::swap(&mut self.mode, &mut mode); + + self.mode_history.insert(self.mode_key, mode); + + self.mode_key = mode_key; + } + + fn handle_input(&mut self, event: Event) -> Result<()> { + let key = InputMapper::event_map_str(event); + if key.is_none() { + return Ok(()); + } + + let key = key.unwrap(); + if let Some(mode_key) = self.mode_key.to_string() { + if let Some(mapper) = self.input_map.get(&mode_key) { + if let Some(commands) = mapper.get(&key).cloned() { + for command in commands { + command(self)?; + } + } else { + if let Some(commands) = mapper.get("_").cloned() { + for command in commands { + command(self)?; + } + } + } + } + } + + Ok(()) + } +} diff --git a/src/application/mode/command.rs b/src/application/mode/command.rs new file mode 100644 index 0000000..f8b4b99 --- /dev/null +++ b/src/application/mode/command.rs @@ -0,0 +1,76 @@ +use std::{collections::HashMap, process::CommandArgs}; + +use held_core::{ + utils::position::Position, + view::{colors::Colors, style::CharStyle}, +}; + +use crate::view::status_data::StatusLineData; + +use super::{ModeData, ModeRenderer}; + +const EDITED_NO_STORE: &'static str = "Changes have not been saved"; +const NOT_FOUNT_CMD: &'static str = "Command Not Fount"; + +pub(super) struct CommandRenderer; + +impl ModeRenderer for CommandRenderer { + fn render( + workspace: &mut crate::workspace::Workspace, + monitor: &mut crate::view::monitor::Monitor, + mode: &mut super::ModeData, + ) -> super::Result<()> { + let line = monitor.height()? - 1; + let mut presenter = monitor.build_presenter()?; + + if let Some(buffer) = &workspace.current_buffer { + let data = buffer.data(); + presenter.print_buffer(buffer, &data, &workspace.syntax_set, None, None)?; + + let mode_name_data = StatusLineData { + content: " COMMAND ".to_string(), + color: Colors::Inverted, + style: CharStyle::Bold, + }; + + let cmd_str = if let ModeData::Command(command_data) = mode { + command_data.input.clone() + } else { + String::new() + }; + let command_line_str = ":".to_owned() + &cmd_str; + let command_data = StatusLineData { + content: command_line_str.clone(), + color: Colors::Default, + style: CharStyle::Default, + }; + + presenter.print_status_line(&[mode_name_data, command_data])?; + + let offset = " COMMAND ".len() + command_line_str.len(); + presenter.set_cursor(Position { line, offset }); + + presenter.present()?; + } else { + } + + Ok(()) + } +} + +#[derive(Debug)] +pub struct CommandData { + pub input: String, +} + +impl CommandData { + pub fn new() -> Self { + CommandData { + input: String::new(), + } + } + + pub fn reset(&mut self) { + self.input.clear(); + } +} diff --git a/src/application/mode/delete.rs b/src/application/mode/delete.rs new file mode 100644 index 0000000..57fa23c --- /dev/null +++ b/src/application/mode/delete.rs @@ -0,0 +1,39 @@ +use held_core::view::{colors::Colors, style::CharStyle}; + +use super::ModeRenderer; +use crate::{ + errors::*, + view::status_data::{buffer_status_data, StatusLineData}, +}; +pub(super) struct DeleteRenderer; + +impl ModeRenderer for DeleteRenderer { + fn render( + workspace: &mut crate::workspace::Workspace, + monitor: &mut crate::view::monitor::Monitor, + _mode: &mut super::ModeData, + ) -> Result<()> { + let mut presenter = monitor.build_presenter()?; + + if let Some(buffer) = &workspace.current_buffer { + warn!("Delete buffer id: {}", buffer.id.unwrap()); + let data = buffer.data(); + presenter.print_buffer(buffer, &data, &workspace.syntax_set, None, None)?; + + let mode_name_data = StatusLineData { + content: " DELETE ".to_string(), + color: Colors::Inverted, + style: CharStyle::Bold, + }; + presenter.print_status_line(&[ + mode_name_data, + buffer_status_data(&workspace.current_buffer), + ])?; + + presenter.present()?; + } else { + } + + Ok(()) + } +} diff --git a/src/application/mode/error.rs b/src/application/mode/error.rs new file mode 100644 index 0000000..cccbd75 --- /dev/null +++ b/src/application/mode/error.rs @@ -0,0 +1,16 @@ +use super::ModeRenderer; +use crate::{application::mode::ModeData, errors::*}; +pub struct ErrorRenderer; + +impl ModeRenderer for ErrorRenderer { + fn render( + _workspace: &mut crate::workspace::Workspace, + _monitor: &mut crate::view::monitor::Monitor, + mode: &mut super::ModeData, + ) -> Result<()> { + if let ModeData::Error(e) = mode { + panic!("{e:?}"); + } + todo!() + } +} diff --git a/src/application/mode/insert.rs b/src/application/mode/insert.rs new file mode 100644 index 0000000..3d31d8f --- /dev/null +++ b/src/application/mode/insert.rs @@ -0,0 +1,37 @@ +use held_core::view::{colors::Colors, style::CharStyle}; + +use crate::view::status_data::{buffer_status_data, StatusLineData}; + +use super::ModeRenderer; + +pub(super) struct InsertRenderer; + +impl ModeRenderer for InsertRenderer { + fn render( + workspace: &mut crate::workspace::Workspace, + monitor: &mut crate::view::monitor::Monitor, + _mode: &mut super::ModeData, + ) -> super::Result<()> { + let mut presenter = monitor.build_presenter()?; + + if let Some(buffer) = &workspace.current_buffer { + let data = buffer.data(); + presenter.print_buffer(buffer, &data, &workspace.syntax_set, None, None)?; + + let mode_name_data = StatusLineData { + content: " INSERT ".to_string(), + color: Colors::Inverted, + style: CharStyle::Bold, + }; + presenter.print_status_line(&[ + mode_name_data, + buffer_status_data(&workspace.current_buffer), + ])?; + + presenter.present()?; + } else { + } + + Ok(()) + } +} diff --git a/src/application/mode/mod.rs b/src/application/mode/mod.rs new file mode 100644 index 0000000..d15957b --- /dev/null +++ b/src/application/mode/mod.rs @@ -0,0 +1,170 @@ +use std::collections::HashMap; + +use crate::errors::*; +use crate::{view::monitor::Monitor, workspace::Workspace}; +use command::{CommandData, CommandRenderer}; +use delete::DeleteRenderer; +use error::ErrorRenderer; +use error_chain::bail; +use insert::InsertRenderer; +use linked_hash_map::LinkedHashMap; +use normal::NormalRenderer; +use replace::ReplaceRenderer; +use search::{SearchData, SearchRenderer}; +use smallvec::SmallVec; +use strum::EnumIter; +use workspace::{WorkspaceModeData, WorkspaceRender}; +use yaml_rust::Yaml; + +use super::handler::handle_map; +use super::Application; + +pub mod command; +pub mod delete; +pub mod error; +mod insert; +pub mod motion; +pub mod normal; +mod replace; +pub mod search; +pub mod workspace; + +pub enum ModeData { + Normal, + Error(Error), + Exit, + Insert, + Command(CommandData), + Workspace(WorkspaceModeData), + Search(SearchData), + Delete, + Replace, // Other(OtherData) +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, EnumIter)] +pub enum ModeKey { + Normal, + Error, + Exit, + Insert, + Command, + Workspace, + Search, + Delete, + Replace, +} + +impl ModeKey { + pub fn to_string(&self) -> Option { + match self { + ModeKey::Normal => Some("normal".into()), + ModeKey::Insert => Some("insert".into()), + ModeKey::Command => Some("command".into()), + ModeKey::Workspace => Some("workspace".into()), + ModeKey::Search => Some("search".into()), + ModeKey::Delete => Some("delete".into()), + ModeKey::Replace => Some("replace".into()), + _ => None, + } + } + + pub fn generate_handle_map( + &self, + mode_map: &mut HashMap< + String, + HashMap Result<()>; 4]>>, + >, + extra: Option<&LinkedHashMap>, + default: &LinkedHashMap, + ) -> Result<()> { + let handle_map = handle_map(); + let mut command_map = + HashMap:: Result<()>; 4]>>::new(); + if let Some(mode) = self.to_string() { + if let Some(yaml) = default.get(&Yaml::String(mode.clone())) { + if let Some(keys) = yaml.as_hash() { + self.parse_mode_keybindings(keys, &handle_map, &mut command_map)?; + } + } + + if let Some(extra) = extra { + if let Some(yaml) = extra.get(&Yaml::String(mode.clone())) { + if let Some(keys) = yaml.as_hash() { + self.parse_mode_keybindings(keys, &handle_map, &mut command_map)?; + } + } + } + mode_map.insert(mode, command_map); + } + + Ok(()) + } + + fn parse_mode_keybindings( + &self, + keybindings: &LinkedHashMap, + handle_map: &HashMap<&str, fn(&mut Application) -> Result<()>>, + result: &mut HashMap Result<()>; 4]>>, + ) -> Result<()> { + for (key, handle) in keybindings { + if let Some(key) = key.as_str() { + let mut closures = SmallVec::new(); + + match handle { + Yaml::String(command_key) => { + closures.push( + *handle_map + .get(command_key.as_str()) + .ok_or_else(|| format!("command \"{command_key:?}\" not found"))?, + ); + } + Yaml::Array(commands) => { + for command in commands { + let command_key = command.as_str().ok_or_else(|| { + format!( + "Keymap command \"{:?}\" couldn't be parsed as a string", + command + ) + })?; + + closures.push( + *handle_map.get(command_key).ok_or_else(|| { + format!("command \"{command_key:?}\" not found") + })?, + ); + } + } + _ => { + bail!(format!("conmand: \"{handle:?}\" couldn't be parsed")); + } + } + + result.insert(key.to_string(), closures); + } + } + + Ok(()) + } +} + +pub trait ModeRenderer { + fn render(workspace: &mut Workspace, monitor: &mut Monitor, mode: &mut ModeData) -> Result<()>; +} + +pub struct ModeRouter; + +impl ModeRenderer for ModeRouter { + fn render(workspace: &mut Workspace, monitor: &mut Monitor, mode: &mut ModeData) -> Result<()> { + match mode { + ModeData::Normal => NormalRenderer::render(workspace, monitor, mode), + ModeData::Error(_) => ErrorRenderer::render(workspace, monitor, mode), + ModeData::Insert => InsertRenderer::render(workspace, monitor, mode), + ModeData::Command(_) => CommandRenderer::render(workspace, monitor, mode), + ModeData::Workspace(_) => WorkspaceRender::render(workspace, monitor, mode), + ModeData::Search(_) => SearchRenderer::render(workspace, monitor, mode), + ModeData::Replace => ReplaceRenderer::render(workspace, monitor, mode), + ModeData::Exit => todo!(), + ModeData::Delete => DeleteRenderer::render(workspace, monitor, mode), + } + } +} diff --git a/src/application/mode/motion.rs b/src/application/mode/motion.rs new file mode 100644 index 0000000..28108a6 --- /dev/null +++ b/src/application/mode/motion.rs @@ -0,0 +1,161 @@ +use held_core::utils::position::Position; + +pub fn locate_next_words_begin(count: usize, str: &str, current_pos: &Position) -> Position { + let s = str.as_bytes(); + let mut left = 0; + let mut right = left; + for _ in 0..count { + while left <= right && right < s.len() { + let lchar = s[left] as char; + let rchar = s[right] as char; + if lchar.is_alphanumeric() { + left += 1; + right += 1; + continue; + } + if rchar.is_alphanumeric() { + left = right; + break; + } + right += 1; + } + } + right = right.min(s.len() - 1); + // 向下移动的行数 + let down_line = str[..right].matches('\n').count(); + + let new_offset = if let Some(idx) = str[..right].rfind('\n') { + // 即目标位置到行首的距离 + right - idx - 1 + } else { + // 新旧行之间没有换行符 + current_pos.offset + right + }; + let pos = Position::new(current_pos.line + down_line, new_offset); + return pos; +} + +pub fn locate_previous_words(count: usize, str: &str, current_pos: &Position) -> Position { + if str.len() == 0 { + return *current_pos; + } + let s = str.as_bytes(); + let mut left = s.len() - 1; + let mut right = left; + for _ in 0..count { + while left <= right && left > 0 { + let lchar = s[left] as char; + let rchar = s[right] as char; + if !rchar.is_alphanumeric() { + left -= 1; + right -= 1; + continue; + } + if !lchar.is_alphanumeric() { + right = left; + break; + } + left -= 1; + } + } + let up_line = str[left..].matches('\n').count(); + let new_line = current_pos.line - up_line; + let new_line_len = str.lines().nth(new_line).unwrap().len(); + let new_offset = if let Some(back_offset) = str[left..].find('\n') { + // back_offset为目标位置到行尾的距离 + // new_line_len - back_offset为目标位置到行首的距离 + new_line_len - back_offset + 1 + } else { + // 新旧行之间没有换行符 + current_pos.offset - (s.len() - 1 - left) + }; + return Position::new(new_line, new_offset); +} + +pub fn locate_next_words_end(count: usize, str: &str, current_pos: &Position) -> Position { + let s = str.as_bytes(); + let mut left = 0; + let mut right = left; + let mut tmp_pos = right; + for _ in 0..count { + while left <= right && right < s.len() { + let lchar = s[left] as char; + let rchar = s[right] as char; + if !lchar.is_alphanumeric() { + left += 1; + right += 1; + continue; + } + if !rchar.is_alphanumeric() { + if right == tmp_pos + 1 { + left = right; + continue; + } + right -= 1; + left = right; + tmp_pos = right; + break; + } + right += 1; + } + } + right = right.min(s.len() - 1); + // 向下移动的行数 + let down_line = str[..right].matches('\n').count(); + + let new_offset = if let Some(idx) = str[..right].rfind('\n') { + // 即目标位置到行首的距离 + right - idx - 1 + } else { + // 新旧行之间没有换行符 + current_pos.offset + right + }; + let pos = Position::new(current_pos.line + down_line, new_offset); + return pos; +} +#[cfg(test)] +mod tests { + #[test] + fn next_word_test() { + let mut v = Vec::new(); + v.push("pub fn locate_next_words(count: usize, str\n: &str) -> Position {"); + v.push(stringify!(let new_offset = if let Some(idx) = str[..mt + ch.start()].rfind('\n'))); + v.push(";\nmod utils;\nmod view;\nmod workspace;\n"); + + assert_eq!(next_word_search(1, v[0], 0), 4); + assert_eq!(next_word_search(1, v[0], 4), 7); + + assert_eq!(next_word_search(1, v[1], 0), 4); + assert_eq!(next_word_search(1, v[1], 3), 4); + + assert_eq!(next_word_search(1, v[2], 0), 2); + assert_eq!(next_word_search(2, v[2], 0), 6); + } + + fn next_word_search(count: usize, str: &str, at: usize) -> usize { + let s = str.as_bytes(); + let mut left = at; + let mut right = left; + for _ in 0..count { + while left <= right && right < s.len() { + let lchar = s[left] as char; + let rchar = s[right] as char; + if rchar.is_ascii_punctuation() && right != at { + break; + } + if lchar.is_alphanumeric() { + left += 1; + right += 1; + continue; + } + if rchar.is_alphanumeric() { + left = right; + break; + } + right += 1; + } + } + return right; + } +} diff --git a/src/application/mode/normal.rs b/src/application/mode/normal.rs new file mode 100644 index 0000000..83aeb8d --- /dev/null +++ b/src/application/mode/normal.rs @@ -0,0 +1,39 @@ +use held_core::view::{colors::Colors, style::CharStyle}; + +use super::ModeRenderer; +use crate::{ + errors::*, + view::status_data::{buffer_status_data, StatusLineData}, +}; +pub(super) struct NormalRenderer; + +impl ModeRenderer for NormalRenderer { + fn render( + workspace: &mut crate::workspace::Workspace, + monitor: &mut crate::view::monitor::Monitor, + _mode: &mut super::ModeData, + ) -> Result<()> { + let mut presenter = monitor.build_presenter()?; + + if let Some(buffer) = &workspace.current_buffer { + warn!("normal buffer id: {}", buffer.id.unwrap()); + let data = buffer.data(); + presenter.print_buffer(buffer, &data, &workspace.syntax_set, None, None)?; + + let mode_name_data = StatusLineData { + content: " NORMAL ".to_string(), + color: Colors::Inverted, + style: CharStyle::Bold, + }; + presenter.print_status_line(&[ + mode_name_data, + buffer_status_data(&workspace.current_buffer), + ])?; + + presenter.present()?; + } else { + } + + Ok(()) + } +} diff --git a/src/application/mode/replace.rs b/src/application/mode/replace.rs new file mode 100644 index 0000000..52c7218 --- /dev/null +++ b/src/application/mode/replace.rs @@ -0,0 +1,37 @@ +use crate::view::status_data::{buffer_status_data, StatusLineData}; +use held_core::view::colors::Colors; +use held_core::view::style::CharStyle; + +use super::ModeRenderer; + +pub(super) struct ReplaceRenderer; + +impl ModeRenderer for ReplaceRenderer { + fn render( + workspace: &mut crate::workspace::Workspace, + monitor: &mut crate::view::monitor::Monitor, + _mode: &mut super::ModeData, + ) -> super::Result<()> { + let mut presenter = monitor.build_presenter()?; + + if let Some(buffer) = &workspace.current_buffer { + let data = buffer.data(); + presenter.print_buffer(buffer, &data, &workspace.syntax_set, None, None)?; + + let mode_name_data = StatusLineData { + content: " REPLACE ".to_string(), + color: Colors::Inverted, + style: CharStyle::Bold, + }; + presenter.print_status_line(&[ + mode_name_data, + buffer_status_data(&workspace.current_buffer), + ])?; + + presenter.present()?; + } else { + } + + Ok(()) + } +} diff --git a/src/application/mode/search.rs b/src/application/mode/search.rs new file mode 100644 index 0000000..852cd49 --- /dev/null +++ b/src/application/mode/search.rs @@ -0,0 +1,101 @@ +use super::ModeRenderer; +use crate::{ + errors::*, + view::status_data::{buffer_status_data, StatusLineData}, +}; +use held_core::{ + utils::range::Range, + view::{colors::Colors, style::CharStyle}, +}; +pub(super) struct SearchRenderer; + +impl ModeRenderer for SearchRenderer { + fn render( + workspace: &mut crate::workspace::Workspace, + monitor: &mut crate::view::monitor::Monitor, + _mode: &mut super::ModeData, + ) -> Result<()> { + let mut presenter = monitor.build_presenter()?; + + if let Some(buffer) = &workspace.current_buffer { + let data = buffer.data(); + + if let super::ModeData::Search(ref search_data) = _mode { + let highlight_search_string = search_data.search_result.clone(); + + let collected_ranges: Vec<(Range, CharStyle, Colors)> = + if !highlight_search_string.is_empty() { + highlight_search_string + .iter() + .map(|range| (range.clone(), CharStyle::Bold, Colors::Inverted)) + .collect() + } else { + Vec::new() + }; + + let highlight_search_string_slice: Option<&[(Range, CharStyle, Colors)]> = + if !collected_ranges.is_empty() { + Some(collected_ranges.as_slice()) + } else { + None + }; + + presenter.print_buffer( + buffer, + &data, + &workspace.syntax_set, + highlight_search_string_slice, + None, + )?; + + let mode_name_data = StatusLineData { + content: " Search/".to_string(), + color: Colors::Inverted, + style: CharStyle::Bold, + }; + + let search_data = StatusLineData { + content: search_data.search_string.clone(), + color: Colors::Default, + style: CharStyle::Default, + }; + + presenter.print_status_line(&[ + mode_name_data, + search_data, + buffer_status_data(&workspace.current_buffer), + ])?; + + presenter.present()?; + } else { + } + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct SearchData { + pub search_string: String, + pub is_exec_search: bool, + pub search_result_index: usize, + pub search_result: Vec, +} + +impl SearchData { + pub fn new() -> Self { + Self { + search_string: String::new(), + is_exec_search: false, + search_result_index: 0, + search_result: Vec::new(), + } + } + + pub fn clear(&mut self) { + self.search_string.clear(); + self.is_exec_search = false; + self.search_result_index = 0; + self.search_result.clear(); + } +} diff --git a/src/application/mode/workspace.rs b/src/application/mode/workspace.rs new file mode 100644 index 0000000..3eddb1e --- /dev/null +++ b/src/application/mode/workspace.rs @@ -0,0 +1,316 @@ +use std::{collections::HashSet, os::unix::fs::MetadataExt, path::PathBuf}; + +use crossterm::style::Color; +use error_chain::bail; +use held_core::{ + utils::{position::Position, range::Range}, + view::{colors::Colors, style::CharStyle}, +}; +use unicode_segmentation::UnicodeSegmentation; +use walkdir::{DirEntry, DirEntryExt, WalkDir}; + +use super::{ModeData, ModeRenderer}; +use crate::{ + buffer::Buffer, + errors::*, + view::{monitor::Monitor, status_data::StatusLineData}, + workspace::Workspace, +}; +pub struct WorkspaceModeData { + path: PathBuf, + selected_index: usize, + selected_path: PathBuf, + current_render_index: usize, + max_index: usize, + opened_dir_inos: HashSet, + buffer_id: usize, + pub prev_buffer_id: usize, + highlight_ranges: Vec<(Range, CharStyle, Colors)>, +} + +impl WorkspaceModeData { + pub fn new(workspace: &mut Workspace, monitor: &mut Monitor) -> Result { + if !workspace.path.is_dir() { + bail!("The workspace must be a directory!"); + } + + let mut opened_dir_inos = HashSet::new(); + opened_dir_inos.insert(workspace.path.metadata()?.ino()); + + let prev_buffer_id = workspace.current_buffer.as_ref().unwrap().id()?; + + let buffer = Buffer::new(); + let buffer_id = workspace.add_buffer(buffer); + monitor.init_buffer(workspace.current_buffer.as_mut().unwrap())?; + + workspace.select_buffer(prev_buffer_id); + + Ok(WorkspaceModeData { + path: workspace.path.clone(), + selected_index: 0, + opened_dir_inos, + buffer_id: buffer_id, + prev_buffer_id, + highlight_ranges: Vec::new(), + current_render_index: 0, + max_index: 0, + selected_path: workspace.path.clone(), + }) + } + + fn update_max_index(&mut self) { + self.max_index = self.current_render_index - 1; + } + + pub(super) fn render_workspace_tree( + &mut self, + workspace: &mut crate::workspace::Workspace, + monitor: &mut crate::view::monitor::Monitor, + ) -> Result<()> { + if !workspace.select_buffer(self.buffer_id) { + bail!("Not Workspace Buffer!"); + } + + self.current_render_index = 0; + + if let Some(ref mut buffer) = workspace.current_buffer { + buffer.delete_range(Range::new( + Position { line: 0, offset: 0 }, + Position { + line: buffer.line_count(), + offset: usize::MAX, + }, + )); + buffer.cursor.move_to(Position { line: 0, offset: 0 }); + self.highlight_ranges.resize(0, Default::default()); + } + + let mut depth = 0; + let root = self.path.clone(); + self.render_dir(workspace, &root, &mut depth); + + if let Some(ref mut buffer) = workspace.current_buffer { + buffer.cursor.move_to(Position { + line: self.selected_index, + offset: 0, + }); + monitor.scroll_to_cursor(buffer)?; + } + + let mut presenter = monitor.build_presenter()?; + + let buffer = workspace.current_buffer.as_ref().unwrap(); + let buffer_data = buffer.data(); + presenter.print_buffer( + buffer, + &buffer_data, + &workspace.syntax_set, + Some(&self.highlight_ranges), + None, + )?; + + let mode_name_data = StatusLineData { + content: " WORKSPACE ".to_string(), + color: Colors::Inverted, + style: CharStyle::Bold, + }; + let workspace_path_data = StatusLineData { + content: format!(" {}", self.path.display()), + color: Colors::Focused, + style: CharStyle::Bold, + }; + presenter.print_status_line(&[mode_name_data, workspace_path_data])?; + presenter.present()?; + + monitor.terminal.set_cursor(None)?; + monitor.terminal.present()?; + + self.update_max_index(); + Ok(()) + } + + fn entry_sort(a: &DirEntry, b: &DirEntry) -> std::cmp::Ordering { + if a.file_type().is_dir() && b.file_type().is_dir() { + return a.file_name().cmp(b.file_name()); + } + + if a.file_type().is_dir() { + return std::cmp::Ordering::Less; + } else if b.file_type().is_dir() { + return std::cmp::Ordering::Greater; + } + + a.file_name().cmp(b.file_name()) + } + + // 需要当前buffer为workspace buffer才能调用该方法 + fn render_dir(&mut self, workspace: &mut Workspace, root_path: &PathBuf, depth: &mut usize) { + let walkdir = WalkDir::new(root_path) + .max_depth(1) + .sort_by(Self::entry_sort); + + let mut iter = walkdir.into_iter(); + + if let Some(entry) = iter.next() { + if let Ok(entry) = entry { + let target_modified = workspace + .get_buffer_with_ino(entry.ino()) + .map(|x| x.modified()); + + let buffer = workspace.current_buffer.as_mut().unwrap(); + let ino = entry.ino(); + buffer.cursor.move_down(); + self.print_entry( + buffer, + entry, + *depth, + self.opened_dir_inos.contains(&ino), + target_modified, + ); + *depth += 1; + if !self.opened_dir_inos.contains(&ino) { + return; + } + } + } + + for entry in iter { + if let Ok(entry) = entry { + self.render_entry(workspace, entry, depth); + } + } + } + + fn render_entry(&mut self, workspace: &mut Workspace, entry: DirEntry, depth: &mut usize) { + let target_modified = workspace + .get_buffer_with_ino(entry.ino()) + .map(|x| x.modified()); + let buffer = workspace.current_buffer.as_mut().unwrap(); + if entry.file_type().is_dir() { + if self.opened_dir_inos.contains(&entry.ino()) { + self.render_dir(workspace, &entry.path().to_path_buf(), depth); + *depth -= 1; + } else { + self.print_entry(buffer, entry, *depth, false, target_modified); + } + } else { + self.print_entry(buffer, entry, *depth, false, target_modified); + } + } + + fn print_entry( + &mut self, + buffer: &mut Buffer, + entry: DirEntry, + depth: usize, + is_open: bool, + target_buffer_modified: Option, + ) { + let prefix = " ".repeat(depth) + + if entry.file_type().is_dir() { + if is_open { + "- " + } else { + "+ " + } + } else { + "| " + }; + + buffer.insert(&prefix); + buffer.cursor.move_to(Position { + line: buffer.cursor.line, + offset: buffer.cursor.offset + prefix.graphemes(true).count(), + }); + + let start = buffer.cursor.position; + let file_name = entry.file_name().to_str().unwrap_or_default(); + + buffer.insert(file_name); + buffer.cursor.move_to(Position { + line: buffer.cursor.line, + offset: buffer.cursor.offset + file_name.graphemes(true).count(), + }); + let end = buffer.cursor.position; + + buffer.insert("\n"); + buffer.cursor.move_down(); + + if self.selected_index == self.current_render_index { + self.highlight_ranges.push(( + Range::new(start, end), + CharStyle::Bold, + Colors::CustomForeground(Color::Cyan), + )); + + self.selected_path = entry.into_path(); + } else if let Some(modified) = target_buffer_modified { + if modified { + self.highlight_ranges.push(( + Range::new(start, end), + CharStyle::Bold, + Colors::CustomForeground(Color::Yellow), + )); + } else { + self.highlight_ranges.push(( + Range::new(start, end), + CharStyle::Bold, + Colors::CustomForeground(Color::Green), + )); + } + } + + self.current_render_index += 1; + } + + /// 打开当前选择的节点,若返回true则表示打开的是文件而非dir,需要回退normal模式 + pub fn open(&mut self, workspace: &mut Workspace, monitor: &mut Monitor) -> Result { + if self.selected_path.is_dir() { + let ino = self.selected_path.metadata()?.ino(); + if self.opened_dir_inos.contains(&ino) { + self.opened_dir_inos.remove(&ino); + } else { + self.opened_dir_inos.insert(ino); + } + + Ok(false) + } else { + let buffer = Buffer::from_file(&self.selected_path)?; + let id = workspace.add_buffer(buffer); + workspace.select_buffer(id); + monitor.init_buffer(workspace.current_buffer.as_mut().unwrap())?; + self.prev_buffer_id = id; + Ok(true) + } + } + + pub fn move_down(&mut self) { + if self.selected_index == self.max_index { + return; + } + self.selected_index += 1; + } + + pub fn move_up(&mut self) { + if self.selected_index == 0 { + return; + } + self.selected_index -= 1; + } +} + +pub struct WorkspaceRender; + +impl ModeRenderer for WorkspaceRender { + fn render( + workspace: &mut crate::workspace::Workspace, + monitor: &mut crate::view::monitor::Monitor, + mode: &mut super::ModeData, + ) -> super::Result<()> { + if let ModeData::Workspace(mode_data) = mode { + return mode_data.render_workspace_tree(workspace, monitor); + } else { + bail!("Workspace mode cannot receive data other than WorkspaceModeData") + } + } +} diff --git a/src/application/plugin_interafce/app.rs b/src/application/plugin_interafce/app.rs new file mode 100644 index 0000000..c8a1eb4 --- /dev/null +++ b/src/application/plugin_interafce/app.rs @@ -0,0 +1,17 @@ +use held_core::interface::app::App; + +use crate::application::Application; + +impl App for Application { + fn exit(&mut self) { + todo!() + } + + fn to_insert_mode(&mut self) { + todo!() + } + + fn to_normal_mode(&mut self) { + todo!() + } +} diff --git a/src/application/plugin_interafce/buffer.rs b/src/application/plugin_interafce/buffer.rs new file mode 100644 index 0000000..56a1893 --- /dev/null +++ b/src/application/plugin_interafce/buffer.rs @@ -0,0 +1,17 @@ +use held_core::interface; + +use crate::application::Application; + +impl interface::buffer::Buffer for Application { + fn insert_char(&mut self) { + todo!() + } + + fn new_line(&mut self) { + todo!() + } + + fn insert_tab(&mut self) { + todo!() + } +} diff --git a/src/application/plugin_interafce/cursor.rs b/src/application/plugin_interafce/cursor.rs new file mode 100644 index 0000000..458872c --- /dev/null +++ b/src/application/plugin_interafce/cursor.rs @@ -0,0 +1,29 @@ +use held_core::interface; + +use crate::application::Application; + +impl interface::cursor::Cursor for Application { + fn move_left(&mut self) { + todo!() + } + + fn move_right(&mut self) { + todo!() + } + + fn move_up(&mut self) { + todo!() + } + + fn move_down(&mut self) { + todo!() + } + + fn move_to_start_of_line(&mut self) { + todo!() + } + + fn screen_cursor_position(&self) -> held_core::utils::position::Position { + self.state_data.cursor_state.screen_position + } +} diff --git a/src/application/plugin_interafce/mod.rs b/src/application/plugin_interafce/mod.rs new file mode 100644 index 0000000..2bd81a8 --- /dev/null +++ b/src/application/plugin_interafce/mod.rs @@ -0,0 +1,11 @@ +use held_core::interface::{app::App, ApplicationInterface}; + +use super::Application; + +pub mod app; +pub mod buffer; +pub mod cursor; +pub mod monitor; +pub mod workspace; + +impl ApplicationInterface for Application {} diff --git a/src/application/plugin_interafce/monitor.rs b/src/application/plugin_interafce/monitor.rs new file mode 100644 index 0000000..3d97342 --- /dev/null +++ b/src/application/plugin_interafce/monitor.rs @@ -0,0 +1,13 @@ +use held_core::interface; + +use crate::application::Application; + +impl interface::monitor::Monitor for Application { + fn scroll_to_cursor(&mut self) { + todo!() + } + + fn scroll_to_center(&mut self) { + todo!() + } +} diff --git a/src/application/plugin_interafce/workspace.rs b/src/application/plugin_interafce/workspace.rs new file mode 100644 index 0000000..69d8b69 --- /dev/null +++ b/src/application/plugin_interafce/workspace.rs @@ -0,0 +1,13 @@ +use held_core::interface; + +use crate::application::Application; + +impl interface::workspace::Workspace for Application { + fn save_file(&mut self) { + todo!() + } + + fn undo(&mut self) { + todo!() + } +} diff --git a/src/application/state.rs b/src/application/state.rs new file mode 100644 index 0000000..d756f69 --- /dev/null +++ b/src/application/state.rs @@ -0,0 +1,13 @@ +use held_core::utils::position::Position; + +/// 用于记录当前实时更新的状态信息 +/// 因为某些插件可能需要获取实时的状态信息,所以所有实时的状态都可能需要更新到这里 +#[derive(Debug, Default)] +pub struct ApplicationStateData { + pub cursor_state: CursorStateData, +} + +#[derive(Debug, Default)] +pub struct CursorStateData { + pub screen_position: Position, +} diff --git a/src/buffer/cursor.rs b/src/buffer/cursor.rs new file mode 100644 index 0000000..9602453 --- /dev/null +++ b/src/buffer/cursor.rs @@ -0,0 +1,502 @@ +use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, + rc::Rc, +}; + +use unicode_segmentation::UnicodeSegmentation; + +use super::{GapBuffer, Position}; + +#[derive(Clone)] +pub struct Cursor { + pub data: Rc>, + pub position: Position, + /// 限制上下移动时,offset不会溢出 + sticky_offset: usize, +} + +impl Deref for Cursor { + type Target = Position; + + fn deref(&self) -> &Position { + &self.position + } +} + +impl DerefMut for Cursor { + fn deref_mut(&mut self) -> &mut Position { + &mut self.position + } +} + +impl Cursor { + pub fn new(data: Rc>, position: Position) -> Cursor { + Cursor { + data, + position, + sticky_offset: position.offset, + } + } + + pub fn move_to(&mut self, position: Position) -> bool { + if self.data.borrow().in_bounds(&position) { + self.position = position; + + // 缓冲当前offset + self.sticky_offset = position.offset; + + return true; + } + false + } + + pub fn move_up(&mut self) { + if self.line == 0 { + return; + } + + let target_line = self.line - 1; + let new_position = Position { + line: target_line, + offset: self.sticky_offset, + }; + + if !self.move_to(new_position) { + let mut target_offset = 0; + for (line_number, line) in self.data.borrow().to_string().lines().enumerate() { + if line_number == target_line { + target_offset = line.graphemes(true).count(); + break; + } + } + self.move_to(Position { + line: target_line, + offset: target_offset, + }); + + self.sticky_offset = new_position.offset; + } + } + + pub fn move_down(&mut self) { + let target_line = self.line + 1; + let new_position = Position { + line: target_line, + offset: self.sticky_offset, + }; + + if !self.move_to(new_position) { + let mut target_offset = 0; + for (line_number, line) in self.data.borrow().to_string().lines().enumerate() { + if line_number == target_line { + target_offset = line.graphemes(true).count(); + } + } + self.move_to(Position { + line: target_line, + offset: target_offset, + }); + + self.sticky_offset = new_position.offset; + } + } + + pub fn move_left(&mut self) { + if self.offset == 0 { + if self.line == 0 { + return; + } + let offset = self + .data + .borrow() + .to_string() + .lines() + .nth(self.line - 1) + .unwrap() + .graphemes(true) + .count(); + + let new_position = Position { + line: self.line - 1, + offset, + }; + self.move_to(new_position); + } else { + let new_position = Position { + line: self.line, + offset: self.offset - 1, + }; + self.move_to(new_position); + } + } + + pub fn move_right(&mut self) { + let max_offset = self + .data + .borrow() + .to_string() + .lines() + .nth(self.line) + .unwrap_or_default() + .graphemes(true) + .count(); + + if max_offset == 0 { + return; + } + + if self.offset + 1 > max_offset + && self.line + 1 <= self.data.borrow().to_string().lines().count() + { + let new_position = Position { + line: self.line + 1, + offset: 0, + }; + self.move_to(new_position); + } else { + let new_position = Position { + line: self.line, + offset: self.offset + 1, + }; + self.move_to(new_position); + } + } + + pub fn move_to_start_of_line(&mut self) { + let new_position = Position { + line: self.line, + offset: 0, + }; + self.move_to(new_position); + } + + pub fn move_to_end_of_line(&mut self) { + let data = self.data.borrow().to_string(); + let current_line = data.lines().nth(self.line); + if let Some(line) = current_line { + let new_position = Position { + line: self.line, + offset: line.graphemes(true).count(), + }; + self.move_to(new_position); + } + } + + pub fn move_to_last_line(&mut self) { + // Figure out the number and length of the last line. + let mut line = 0; + let mut length = 0; + for c in self.data.borrow().to_string().graphemes(true) { + if c == "\n" { + line += 1; + length = 0; + } else { + length += 1; + } + } + + let target_position = if length < self.sticky_offset { + // Current offset is beyond the last line's length; move to the end of it. + Position { + line, + offset: length, + } + } else { + // Current offset is available on the last line; go there. + Position { + line, + offset: self.sticky_offset, + } + }; + self.move_to(target_position); + } + + pub fn move_to_first_line(&mut self) { + // Figure out the length of the first line. + let length = self + .data + .borrow() + .to_string() + .lines() + .nth(0) + .map(|line| line.graphemes(true).count()) + .unwrap_or(0); + + let target_position = if length < self.sticky_offset { + // Current offset is beyond the first line's length; move to the end of it. + Position { + line: 0, + offset: length, + } + } else { + // Current offset is available on the first line; go there. + Position { + line: 0, + offset: self.sticky_offset, + } + }; + self.move_to(target_position); + } +} + +#[cfg(test)] +mod tests { + use crate::buffer::{Cursor, GapBuffer, Position}; + use std::cell::RefCell; + use std::rc::Rc; + + #[test] + fn move_up_goes_to_eol_if_offset_would_be_out_of_range() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "This is a test.\nAnother line that is longer.".to_string(), + ))); + let mut cursor = Cursor::new( + buffer, + Position { + line: 1, + offset: 20, + }, + ); + cursor.move_up(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 15); + } + + #[test] + fn move_down_goes_to_eol_if_offset_would_be_out_of_range() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "Another line that is longer.\nThis is a test.".to_string(), + ))); + let mut cursor = Cursor::new( + buffer, + Position { + line: 0, + offset: 20, + }, + ); + cursor.move_down(); + assert_eq!(cursor.line, 1); + assert_eq!(cursor.offset, 15); + } + + #[test] + fn move_up_counts_graphemes_as_a_single_offset() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "First नी\nSecond line".to_string(), + ))); + let mut cursor = Cursor::new( + buffer, + Position { + line: 1, + offset: 11, + }, + ); + cursor.move_up(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 7); + } + + #[test] + fn move_down_counts_graphemes_as_a_single_offset() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "First line\nSecond नी".to_string(), + ))); + let mut cursor = Cursor::new( + buffer, + Position { + line: 0, + offset: 10, + }, + ); + cursor.move_down(); + assert_eq!(cursor.line, 1); + assert_eq!(cursor.offset, 8); + } + + #[test] + fn move_up_persists_offset_across_shorter_lines() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "First line that is longer.\nThis is a test.\nAnother line that is longer.".to_string(), + ))); + let mut cursor = Cursor::new( + buffer, + Position { + line: 2, + offset: 20, + }, + ); + cursor.move_up(); + cursor.move_up(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 20); + } + + #[test] + fn move_down_persists_offset_across_shorter_lines() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "First line that is longer.\nThis is a test.\nAnother line that is longer.".to_string(), + ))); + let mut cursor = Cursor::new( + buffer, + Position { + line: 0, + offset: 20, + }, + ); + cursor.move_down(); + cursor.move_down(); + assert_eq!(cursor.line, 2); + assert_eq!(cursor.offset, 20); + } + + #[test] + fn move_to_sets_persisted_offset() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "First line that is longer.\nThis is a test.\nAnother line that is longer.".to_string(), + ))); + let mut cursor = Cursor::new( + buffer, + Position { + line: 0, + offset: 20, + }, + ); + cursor.move_to(Position { line: 1, offset: 5 }); + cursor.move_down(); + assert_eq!(cursor.line, 2); + assert_eq!(cursor.offset, 5); + } + + #[test] + fn move_to_start_of_line_sets_offset_to_zero() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "This is a test.\nAnother line.".to_string(), + ))); + let mut cursor = Cursor::new(buffer, Position { line: 1, offset: 5 }); + cursor.move_to_start_of_line(); + assert_eq!(cursor.line, 1); + assert_eq!(cursor.offset, 0); + } + + #[test] + fn move_to_end_of_line_counts_graphemes_as_a_single_offset() { + let buffer = Rc::new(RefCell::new(GapBuffer::new("First नी".to_string()))); + let mut cursor = Cursor::new(buffer, Position { line: 0, offset: 0 }); + cursor.move_to_end_of_line(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 7); + } + + #[test] + fn move_to_end_of_line_sets_offset_the_line_length() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "This is a test.\nAnother line.".to_string(), + ))); + let mut cursor = Cursor::new(buffer, Position { line: 0, offset: 5 }); + cursor.move_to_end_of_line(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 15); + } + + #[test] + fn move_up_does_nothing_if_at_the_start_of_line() { + let buffer = Rc::new(RefCell::new(GapBuffer::new("This is a test.".to_string()))); + let mut cursor = Cursor::new(buffer, Position { line: 0, offset: 0 }); + cursor.move_up(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 0); + } + + #[test] + fn move_left_does_nothing_if_at_the_start_of_line() { + let buffer = Rc::new(RefCell::new(GapBuffer::new("This is a test.".to_string()))); + let mut cursor = Cursor::new(buffer, Position { line: 0, offset: 0 }); + cursor.move_left(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 0); + } + + #[test] + fn move_to_last_line_counts_graphemes_as_a_single_offset() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "First line\nLast नी".to_string(), + ))); + let mut cursor = Cursor::new( + buffer, + Position { + line: 0, + offset: 10, + }, + ); + cursor.move_to_last_line(); + assert_eq!(cursor.line, 1); + assert_eq!(cursor.offset, 6); + } + + #[test] + fn move_to_last_line_moves_to_same_offset_on_last_line() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "first\nsecond\nlast".to_string(), + ))); + let mut cursor = Cursor::new(buffer, Position { line: 0, offset: 2 }); + cursor.move_to_last_line(); + assert_eq!(cursor.line, 2); + assert_eq!(cursor.offset, 2); + } + + #[test] + fn move_to_last_line_moves_to_end_of_last_line_if_offset_would_be_out_of_range() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "first\nsecond\nlast".to_string(), + ))); + let mut cursor = Cursor::new(buffer, Position { line: 0, offset: 5 }); + cursor.move_to_last_line(); + assert_eq!(cursor.line, 2); + assert_eq!(cursor.offset, 4); + } + + #[test] + fn move_to_last_line_moves_last_line_when_it_is_a_trailing_newline() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "first\nsecond\nlast\n".to_string(), + ))); + let mut cursor = Cursor::new(buffer, Position { line: 0, offset: 2 }); + cursor.move_to_last_line(); + assert_eq!(cursor.line, 3); + assert_eq!(cursor.offset, 0); + } + + #[test] + fn move_to_first_line_counts_graphemes_as_a_single_offset() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "First नी\nLast line".to_string(), + ))); + let mut cursor = Cursor::new(buffer, Position { line: 0, offset: 9 }); + cursor.move_to_first_line(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 7); + } + + #[test] + fn move_to_first_line_moves_to_same_offset_on_first_line() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "first\nsecond\nlast".to_string(), + ))); + let mut cursor = Cursor::new(buffer, Position { line: 1, offset: 2 }); + cursor.move_to_first_line(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 2); + } + + #[test] + fn move_to_first_line_moves_to_end_of_first_line_if_offset_would_be_out_of_range() { + let buffer = Rc::new(RefCell::new(GapBuffer::new( + "first\nsecond\nlast".to_string(), + ))); + let mut cursor = Cursor::new(buffer, Position { line: 1, offset: 6 }); + cursor.move_to_first_line(); + assert_eq!(cursor.line, 0); + assert_eq!(cursor.offset, 5); + } +} diff --git a/src/buffer/gap_buffer.rs b/src/buffer/gap_buffer.rs new file mode 100644 index 0000000..8b5bae9 --- /dev/null +++ b/src/buffer/gap_buffer.rs @@ -0,0 +1,463 @@ +use std::{borrow::Borrow, fmt}; + +use held_core::utils::position::Position; +use unicode_segmentation::UnicodeSegmentation; + +use held_core::utils::range::Range; + +/// GapBuffer 增加减少重分配的buffer +/// 在一整块buffer中有一段gap(空闲空间)将整块buffer分为两段,以实现插入删除等操作的高效率(减少重分配) +/// 数据结构: +/// | data | gap | data | +/// | gap_start------>gap_length<-----| | +pub struct GapBuffer { + data: Vec, + gap_start: usize, + gap_length: usize, +} + +impl GapBuffer { + pub fn new>(data: T) -> GapBuffer { + let mut buf = data.as_ref().as_bytes().to_owned(); + let capacity = buf.capacity(); + let gap_start = buf.len(); + let gap_length = capacity - gap_start; + unsafe { + buf.set_len(capacity); + } + + GapBuffer { + data: buf, + gap_start: gap_start, + gap_length: gap_length, + } + } + + pub fn insert(&mut self, data: &str, position: &Position) { + if data.len() > self.gap_length { + // 先将gap区域移动到最后,不然会出现两段gap + let offset = self.data.capacity(); + self.move_gap(offset); + // 扩容 + self.data.reserve(data.len()); + + let capacity = self.data.capacity(); + self.gap_length = capacity - self.gap_start; + unsafe { + self.data.set_len(capacity); + } + } + + let offset = match self.find_offset(position) { + Some(offset) => offset, + None => return, + }; + // println!("{:?}", self.data); + self.move_gap(offset); + // println!("{:?}", self.data); + self.write_to_gap(data); + // println!("{:?}", self.data); + // println!("start {} length {}", self.gap_start, self.gap_length); + } + + pub fn read(&self, range: &Range) -> Option { + let start_offset = match self.find_offset(&range.start()) { + Some(offset) => offset, + None => return None, + }; + + let end_offset = match self.find_offset(&range.end()) { + Some(offset) => offset, + None => return None, + }; + + let data = if start_offset < self.gap_start && end_offset > self.gap_start { + let mut data = + String::from_utf8_lossy(&self.data[start_offset..self.gap_start]).into_owned(); + data.push_str( + String::from_utf8_lossy(&self.data[self.gap_start + self.gap_length..end_offset]) + .borrow(), + ); + data + } else { + String::from_utf8_lossy(&self.data[start_offset..end_offset]).into_owned() + }; + + Some(data) + } + + pub fn read_rest(&self, position: &Position) -> Option { + let offset = match self.find_offset(position) { + Some(offset) => offset, + None => return None, + }; + + let data = if offset < self.gap_start { + let mut data = String::from_utf8_lossy(&self.data[offset..self.gap_start]).into_owned(); + data.push_str( + String::from_utf8_lossy(&self.data[self.gap_start + self.gap_length..]).borrow(), + ); + data + } else { + String::from_utf8_lossy(&self.data[offset..]).into_owned() + }; + + Some(data) + } + + // | data | gap | data | + pub fn delete(&mut self, range: &Range) { + let start_offset = match self.find_offset(&range.start()) { + Some(offset) => offset, + None => return, + }; + + self.move_gap(start_offset); + + match self.find_offset(&range.end()) { + Some(offset) => { + self.gap_length = offset - self.gap_start; + } + None => { + // 确定delete后gap长度 + + // 尝试跳过一行查找,若找到,则end在原gap区域中,若找不到,则end超出data范围或者在末尾 + let start_of_next_line = Position { + line: range.end().line + 1, + offset: 0, + }; + + match self.find_offset(&start_of_next_line) { + Some(offset) => { + self.gap_length = offset - self.gap_start; + } + None => { + self.gap_length = self.data.len() - self.gap_start; + } + } + } + } + } + + pub fn in_bounds(&self, position: &Position) -> bool { + return self.find_offset(position).is_some(); + } + + /// 将对应的position映射为具体在buffer中的offset + pub fn find_offset(&self, position: &Position) -> Option { + let first = String::from_utf8_lossy(&self.data[..self.gap_start]); + let mut line = 0; + let mut line_offset = 0; + for (offset, grapheme) in (*first).grapheme_indices(true) { + if line == position.line && line_offset == position.offset { + return Some(offset); + } + + if grapheme == "\n" { + line += 1; + line_offset = 0; + } else { + line_offset += 1; + } + } + + // | data1 | gap | data2 | + // 当data1最后个字符为'\n'时,若刚好匹配到,则实际offset为data2的开头 + if line == position.line && line_offset == position.offset { + return Some(self.gap_start + self.gap_length); + } + + let second = String::from_utf8_lossy(&self.data[self.gap_start + self.gap_length..]); + for (offset, grapheme) in (*second).grapheme_indices(true) { + if line == position.line && line_offset == position.offset { + return Some(self.gap_start + self.gap_length + offset); + } + + if grapheme == "\n" { + line += 1; + line_offset = 0; + } else { + line_offset += 1; + } + } + + // | data1 | gap | data2 | + // 当data2最后个字符为'\n'时,若刚好匹配到,则实际offset为data2的结尾 + if line == position.line && line_offset == position.offset { + return Some(self.data.len()); + } + + None + } + + pub fn move_gap(&mut self, offset: usize) { + if self.gap_length == 0 { + self.gap_start = offset; + return; + } + + match offset.cmp(&self.gap_start) { + std::cmp::Ordering::Less => { + for index in (offset..self.gap_start).rev() { + self.data[index + self.gap_length] = self.data[index]; + } + + self.gap_start = offset; + } + std::cmp::Ordering::Equal => {} + std::cmp::Ordering::Greater => { + for index in self.gap_start + self.gap_length..offset { + self.data[index - self.gap_length] = self.data[index]; + } + + self.gap_start = offset - self.gap_length; + } + } + } + + // 写gap区域 + fn write_to_gap(&mut self, data: &str) { + assert!(self.gap_length >= data.bytes().len()); + for byte in data.bytes() { + self.data[self.gap_start] = byte; + self.gap_start += 1; + self.gap_length -= 1; + } + } +} + +impl fmt::Display for GapBuffer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let first_half = String::from_utf8_lossy(&self.data[..self.gap_start]); + let second_half = String::from_utf8_lossy(&self.data[self.gap_start + self.gap_length..]); + + write!(f, "{}{}", first_half, second_half) + } +} + +#[cfg(test)] +mod tests { + use held_core::utils::range::Range; + + use crate::buffer::{GapBuffer, Position}; + + #[test] + fn move_gap_works() { + let mut gb = GapBuffer::new("This is a test."); + gb.move_gap(0); + assert_eq!(gb.to_string(), "This is a test."); + } + + #[test] + fn inserting_at_the_start_works() { + let mut gb = GapBuffer::new("toolkit"); + + // This insert serves to move the gap to the start of the buffer. + gb.insert(" ", &Position { line: 0, offset: 0 }); + assert_eq!(gb.to_string(), " toolkit"); + + // We insert enough data (more than twice original capacity) to force + // a re-allocation, which, with the gap at the start and when handled + // incorrectly, will create a split/two-segment gap. Bad news. + gb.insert("scribe text", &Position { line: 0, offset: 0 }); + assert_eq!(gb.to_string(), "scribe text toolkit"); + } + + #[test] + fn inserting_in_the_middle_works() { + let mut gb = GapBuffer::new(" editor"); + + // Same deal as above "at the start" test, where we want to move + // the gap into the middle and then force a reallocation to check + // that pre-allocation gap shifting is working correctly. + gb.insert(" ", &Position { line: 0, offset: 4 }); + gb.insert("scribe", &Position { line: 0, offset: 4 }); + assert_eq!(gb.to_string(), " scribe editor"); + } + + #[test] + fn inserting_at_the_end_works() { + let mut gb = GapBuffer::new("This is a test."); + gb.insert( + " Seriously.", + &Position { + line: 0, + offset: 15, + }, + ); + assert_eq!(gb.to_string(), "This is a test. Seriously."); + } + + #[test] + fn inserting_in_different_spots_twice_works() { + let mut gb = GapBuffer::new("This is a test."); + gb.insert("Hi. ", &Position { line: 0, offset: 0 }); + gb.insert( + " Thank you.", + &Position { + line: 0, + offset: 19, + }, + ); + assert_eq!(gb.to_string(), "Hi. This is a test. Thank you."); + } + + #[test] + fn inserting_at_an_invalid_position_does_nothing() { + let mut gb = GapBuffer::new("This is a test."); + gb.insert( + " Seriously.", + &Position { + line: 0, + offset: 35, + }, + ); + assert_eq!(gb.to_string(), "This is a test."); + } + + #[test] + fn inserting_after_a_grapheme_cluster_works() { + let mut gb = GapBuffer::new("scribe नी"); + gb.insert(" library", &Position { line: 0, offset: 8 }); + assert_eq!(gb.to_string(), "scribe नी library"); + } + + #[test] + fn deleting_works() { + let mut gb = GapBuffer::new("This is a test.\nSee what happens."); + let start = Position { line: 0, offset: 8 }; + let end = Position { line: 1, offset: 4 }; + gb.delete(&Range::new(start, end)); + assert_eq!(gb.to_string(), "This is what happens."); + } + + #[test] + fn inserting_then_deleting_at_the_start_works() { + let mut gb = GapBuffer::new(""); + gb.insert("This is a test.", &Position { line: 0, offset: 0 }); + let start = Position { line: 0, offset: 0 }; + let end = Position { line: 0, offset: 1 }; + gb.delete(&Range::new(start, end)); + assert_eq!(gb.to_string(), "his is a test."); + } + + #[test] + fn deleting_immediately_after_the_end_of_the_gap_widens_the_gap() { + let mut gb = GapBuffer::new("This is a test."); + let mut start = Position { line: 0, offset: 8 }; + let mut end = Position { line: 0, offset: 9 }; + gb.delete(&Range::new(start, end)); + assert_eq!(gb.to_string(), "This is test."); + assert_eq!(gb.gap_length, 1); + + start = Position { line: 0, offset: 9 }; + end = Position { + line: 0, + offset: 10, + }; + gb.delete(&Range::new(start, end)); + assert_eq!(gb.to_string(), "This is est."); + assert_eq!(gb.gap_length, 2); + } + + #[test] + fn deleting_to_an_out_of_range_line_deletes_to_the_end_of_the_buffer() { + let mut gb = GapBuffer::new("scribe\nlibrary"); + let start = Position { line: 0, offset: 6 }; + let end = Position { + line: 2, + offset: 10, + }; + gb.delete(&Range::new(start, end)); + assert_eq!(gb.to_string(), "scribe"); + } + + #[test] + fn deleting_to_an_out_of_range_column_deletes_to_the_end_of_the_buffer() { + let mut gb = GapBuffer::new("scribe\nlibrary"); + let start = Position { line: 0, offset: 0 }; + let end = Position { + line: 0, + offset: 100, + }; + gb.delete(&Range::new(start, end)); + assert_eq!(gb.to_string(), "library"); + } + + #[test] + fn deleting_after_a_grapheme_cluster_works() { + let mut gb = GapBuffer::new("scribe नी library"); + let start = Position { line: 0, offset: 8 }; + let end = Position { + line: 0, + offset: 16, + }; + gb.delete(&Range::new(start, end)); + assert_eq!(gb.to_string(), "scribe नी"); + } + + #[test] + fn read_does_not_include_gap_contents_when_gap_is_at_start_of_range() { + // Create a buffer and a range that captures the first character. + let mut gb = GapBuffer::new("scribe"); + let range = Range::new( + Position { line: 0, offset: 0 }, + Position { line: 0, offset: 1 }, + ); + + // Delete the first character, which will move the gap buffer to the start. + gb.delete(&range); + assert_eq!(gb.to_string(), "cribe"); + + // Ask for the first character, which would include the deleted + // value, if the read function isn't smart enough to skip it. + assert_eq!(gb.read(&range).unwrap(), "c"); + } + + #[test] + fn read_does_not_include_gap_contents_when_gap_is_in_middle_of_range() { + let mut gb = GapBuffer::new("scribe"); + + // Delete data from the middle of the buffer, which will move the gap there. + gb.delete(&Range::new( + Position { line: 0, offset: 2 }, + Position { line: 0, offset: 4 }, + )); + assert_eq!(gb.to_string(), "scbe"); + + // Request a range that extends from the start to the finish. + let range = Range::new( + Position { line: 0, offset: 0 }, + Position { line: 0, offset: 4 }, + ); + assert_eq!(gb.read(&range).unwrap(), "scbe"); + } + + #[test] + fn reading_after_a_grapheme_cluster_works() { + let gb = GapBuffer::new("scribe नी library"); + let range = Range::new( + Position { line: 0, offset: 8 }, + Position { + line: 0, + offset: 16, + }, + ); + assert_eq!(gb.read(&range).unwrap(), " library"); + } + + #[test] + fn in_bounds_considers_grapheme_clusters() { + let gb = GapBuffer::new("scribe नी library"); + let in_bounds = Position { + line: 0, + offset: 16, + }; + let out_of_bounds = Position { + line: 0, + offset: 17, + }; + assert!(gb.in_bounds(&in_bounds)); + assert!(!gb.in_bounds(&out_of_bounds)); + } +} diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs new file mode 100644 index 0000000..cc945e9 --- /dev/null +++ b/src/buffer/mod.rs @@ -0,0 +1,201 @@ +use crate::errors::*; +use std::cell::RefCell; +use std::fs::File; +use std::io::{ErrorKind, Write}; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::{fs, io}; + +use cursor::Cursor; +use held_core::utils::position::Position; +use operation::history::History; +use operation::{Operation, OperationGroup}; +use syntect::parsing::SyntaxReference; + +use crate::errors::Error; +use held_core::utils::range::Range; + +// Published API +pub use self::gap_buffer::GapBuffer; + +mod cursor; +mod gap_buffer; +mod operation; + +pub struct Buffer { + pub id: Option, + data: Rc>, + pub path: Option, + pub cursor: Cursor, + history: History, + operation_group: Option, + pub syntax_definition: Option, + pub change_callback: Option>, +} + +impl Default for Buffer { + fn default() -> Self { + let data = Rc::new(RefCell::new(GapBuffer::new(String::new()))); + let cursor = Cursor::new(data.clone(), Position { line: 0, offset: 0 }); + let mut history = History::new(); + history.mark(); + + Buffer { + id: None, + data: data.clone(), + path: None, + cursor, + history: History::new(), + operation_group: None, + syntax_definition: None, + change_callback: None, + } + } +} + +impl Buffer { + pub fn new() -> Buffer { + Buffer::default() + } + + pub fn from_file(path: &Path) -> io::Result { + let content = fs::read_to_string(path)?; + + let data = Rc::new(RefCell::new(GapBuffer::new(content))); + let cursor = Cursor::new(data.clone(), Position { line: 0, offset: 0 }); + + let mut buffer = Buffer { + id: None, + data: data.clone(), + path: Some(path.canonicalize()?), + cursor, + history: History::new(), + operation_group: None, + syntax_definition: None, + change_callback: None, + }; + + buffer.history.mark(); + + Ok(buffer) + } + + pub fn data(&self) -> String { + self.data.borrow().to_string() + } + + pub fn save(&mut self) -> io::Result<()> { + let mut file = if let Some(ref path) = self.path { + File::create(path)? + } else { + File::create(PathBuf::new())? + }; + + file.write_all(self.data().to_string().as_bytes())?; + + self.history.mark(); + + Ok(()) + } + + pub fn file_name(&self) -> Option { + self.path.as_ref().and_then(|p| { + p.file_name() + .and_then(|f| f.to_str().map(|s| s.to_string())) + }) + } + + pub fn undo(&mut self) { + // Look for an operation to undo. First, check if there's an open, non-empty + // operation group. If not, try taking the last operation from the buffer history. + let operation: Option> = match self.operation_group.take() { + Some(group) => { + if group.is_empty() { + self.history.previous() + } else { + Some(Box::new(group)) + } + } + None => self.history.previous(), + }; + + // If we found an eligible operation, reverse it. + if let Some(mut op) = operation { + op.reverse(self); + } + } + + pub fn redo(&mut self) { + // Look for an operation to apply. + if let Some(mut op) = self.history.next() { + op.run(self); + } + } + + pub fn read(&self, range: &Range) -> Option { + self.data.borrow().read(range) + } + + pub fn read_rest(&self, position: &Position) -> Option { + self.data.borrow().read_rest(position) + } + + pub fn search(&self, needle: &str) -> Vec { + let mut results = Vec::new(); + + for (line, data) in self.data().lines().enumerate() { + for (offset, _) in data.char_indices() { + let haystack = &data[offset..]; + + // Check haystack length before slicing it and comparing bytes with needle. + if haystack.len() >= needle.len() + && needle.as_bytes() == &haystack.as_bytes()[..needle.len()] + { + results.push(Position { line, offset }); + } + } + } + + results + } + + pub fn modified(&self) -> bool { + !self.history.at_mark() + } + + pub fn line_count(&self) -> usize { + self.data().chars().filter(|&c| c == '\n').count() + 1 + } + + pub fn reload(&mut self) -> io::Result<()> { + // Load content from disk. + let path = self.path.as_ref().ok_or(ErrorKind::NotFound)?; + let content = fs::read_to_string(path)?; + + self.replace(content); + + // We mark the history at points where the + // buffer is in sync with its file equivalent. + self.history.mark(); + + Ok(()) + } + + /// 文件拓展名 + pub fn file_extension(&self) -> Option { + self.path.as_ref().and_then(|p| { + p.extension().and_then(|e| { + if !e.is_empty() { + return Some(e.to_string_lossy().into_owned()); + } + + None + }) + }) + } + + pub fn id(&self) -> Result { + self.id + .ok_or_else(|| Error::from("Buffer ID doesn't exist")) + } +} diff --git a/src/buffer/operation/delete.rs b/src/buffer/operation/delete.rs new file mode 100644 index 0000000..422aeeb --- /dev/null +++ b/src/buffer/operation/delete.rs @@ -0,0 +1,81 @@ +use crate::buffer::{Buffer, Position, Range}; + +use super::Operation; + +#[derive(Clone)] +pub struct Delete { + content: Option, + range: Range, +} + +impl Operation for Delete { + fn run(&mut self, buffer: &mut crate::buffer::Buffer) { + self.content = buffer.data.borrow().read(&self.range); + + buffer.data.borrow_mut().delete(&self.range); + + if let Some(ref callback) = buffer.change_callback { + callback(self.range.start()); + } + } + + fn reverse(&mut self, buffer: &mut crate::buffer::Buffer) { + if let Some(ref content) = self.content { + buffer + .data + .borrow_mut() + .insert(content, &self.range.start()); + + // Run the change callback, if present. + if let Some(ref callback) = buffer.change_callback { + callback(self.range.start()) + } + } + } + + fn clone_operation(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Delete { + /// Creates a new empty delete operation. + pub fn new(range: Range) -> Delete { + Delete { + content: None, + range, + } + } +} + +impl Buffer { + // 删除当前cursor指向的字符 + pub fn delete(&mut self) { + let mut end = Position { + line: self.cursor.line, + offset: self.cursor.offset + 1, + }; + + // 下一行的行首 + if !self.data.borrow().in_bounds(&end) { + end.line += 1; + end.offset = 0; + } + + let start = self.cursor.position; + self.delete_range(Range::new(start, end)); + } + + pub fn delete_range(&mut self, range: Range) { + // Build and run a delete operation. + let mut op = Delete::new(range); + op.run(self); + + // Store the operation in the history + // object so that it can be undone. + match self.operation_group { + Some(ref mut group) => group.add(Box::new(op)), + None => self.history.add(Box::new(op)), + }; + } +} diff --git a/src/buffer/operation/group.rs b/src/buffer/operation/group.rs new file mode 100644 index 0000000..7ab01b6 --- /dev/null +++ b/src/buffer/operation/group.rs @@ -0,0 +1,69 @@ +use crate::buffer::Buffer; + +use super::Operation; + +pub struct OperationGroup { + operations: Vec>, +} + +impl Operation for OperationGroup { + fn run(&mut self, buffer: &mut Buffer) { + for operation in &mut self.operations { + operation.run(buffer); + } + } + + fn reverse(&mut self, buffer: &mut Buffer) { + for operation in &mut self.operations.iter_mut().rev() { + operation.reverse(buffer); + } + } + + fn clone_operation(&self) -> Box { + Box::new(OperationGroup { + operations: self + .operations + .iter() + .map(|o| (*o).clone_operation()) + .collect(), + }) + } +} + +impl OperationGroup { + pub fn new() -> OperationGroup { + OperationGroup { + operations: Vec::new(), + } + } + + pub fn add(&mut self, operation: Box) { + self.operations.push(operation); + } + + pub fn is_empty(&self) -> bool { + self.operations.is_empty() + } +} + +impl Buffer { + // 开启一个操作组 + pub fn start_operation_group(&mut self) { + // Create an operation group, if one doesn't already exist. + match self.operation_group { + Some(_) => (), + None => { + self.operation_group = Some(OperationGroup::new()); + } + } + } + + pub fn end_operation_group(&mut self) { + // Push an open operation group on to the history stack, if one exists. + if let Some(group) = self.operation_group.take() { + if !group.is_empty() { + self.history.add(Box::new(group)) + } + } + } +} diff --git a/src/buffer/operation/history.rs b/src/buffer/operation/history.rs new file mode 100644 index 0000000..a9c7488 --- /dev/null +++ b/src/buffer/operation/history.rs @@ -0,0 +1,69 @@ +use super::Operation; + +pub struct History { + previous: Vec>, + next: Vec>, + marked_position: Option, +} + +impl History { + /// Creates a new empty operation history. + pub fn new() -> History { + History { + previous: Vec::new(), + next: Vec::new(), + marked_position: None, + } + } + + /// Store an operation that has already been run. + pub fn add(&mut self, operation: Box) { + self.previous.push(operation); + self.next.clear(); + + // Clear marked position if we've replaced a prior operation. + if let Some(position) = self.marked_position { + if position >= self.previous.len() { + self.marked_position = None + } + } + } + + /// Navigate the history backwards. + pub fn previous(&mut self) -> Option> { + match self.previous.pop() { + Some(operation) => { + // We've found a previous operation. Before we return it, store a + // clone of it so that it can be re-applied as a redo operation. + self.next.push(operation.clone_operation()); + Some(operation) + } + None => None, + } + } + + /// Navigate the history forwards. + pub fn next(&mut self) -> Option> { + match self.next.pop() { + Some(operation) => { + // We've found a subsequent operation. Before we return it, store a + // clone of it so that it can be re-applied as an undo operation, again. + self.previous.push(operation.clone_operation()); + Some(operation) + } + None => None, + } + } + + pub fn mark(&mut self) { + self.marked_position = Some(self.previous.len()) + } + + pub fn at_mark(&self) -> bool { + if let Some(position) = self.marked_position { + self.previous.len() == position + } else { + false + } + } +} diff --git a/src/buffer/operation/insert.rs b/src/buffer/operation/insert.rs new file mode 100644 index 0000000..be8f219 --- /dev/null +++ b/src/buffer/operation/insert.rs @@ -0,0 +1,99 @@ +use unicode_segmentation::UnicodeSegmentation; + +use crate::buffer::{Buffer, Position, Range}; + +use super::Operation; + +#[derive(Clone)] +pub struct Insert { + content: String, + position: Position, +} + +impl Operation for Insert { + fn run(&mut self, buffer: &mut Buffer) { + buffer + .data + .borrow_mut() + .insert(&self.content, &self.position); + + // Run the change callback, if present. + if let Some(ref callback) = buffer.change_callback { + callback(self.position) + } + } + + fn reverse(&mut self, buffer: &mut Buffer) { + // The line count of the content tells us the line number for the end of the + // range (just add the number of new lines to the starting line). + let line_count = self.content.chars().filter(|&c| c == '\n').count() + 1; + let end_line = self.position.line + line_count - 1; + + let end_offset = if line_count == 1 { + // If there's only one line, the range starts and ends on the same line, and so its + // offset needs to take the original insertion location into consideration. + self.position.offset + self.content.graphemes(true).count() + } else { + // If there are multiple lines, the end of the range doesn't + // need to consider the original insertion location. + match self.content.split('\n').last() { + Some(line) => line.graphemes(true).count(), + None => return, + } + }; + + // Now that we have the required info, + // build the end position and total range. + let end_position = Position { + line: end_line, + offset: end_offset, + }; + let range = Range::new(self.position, end_position); + + // Remove the content we'd previously inserted. + buffer.data.borrow_mut().delete(&range); + + // Run the change callback, if present. + if let Some(ref callback) = buffer.change_callback { + callback(self.position) + } + } + + fn clone_operation(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Insert { + /// Creates a new empty insert operation. + pub fn new(content: String, position: Position) -> Insert { + Insert { content, position } + } +} + +impl Buffer { + /// Inserts `data` into the buffer at the cursor position. + /// + /// # Examples + /// + /// ``` + /// use scribe::Buffer; + /// + /// let mut buffer = Buffer::new(); + /// buffer.insert("scribe"); + /// assert_eq!(buffer.data(), "scribe"); + /// ``` + pub fn insert>(&mut self, data: T) { + // Build and run an insert operation. + let mut op = Insert::new(data.into(), self.cursor.position); + + op.run(self); + + // Store the operation in the history + // object so that it can be undone. + match self.operation_group { + Some(ref mut group) => group.add(Box::new(op)), + None => self.history.add(Box::new(op)), + }; + } +} diff --git a/src/buffer/operation/mod.rs b/src/buffer/operation/mod.rs new file mode 100644 index 0000000..2140077 --- /dev/null +++ b/src/buffer/operation/mod.rs @@ -0,0 +1,14 @@ +pub use self::group::OperationGroup; +use crate::buffer::Buffer; + +mod delete; +pub mod group; +pub mod history; +mod insert; +mod replace; + +pub trait Operation { + fn run(&mut self, buffer: &mut Buffer); + fn reverse(&mut self, buffer: &mut Buffer); + fn clone_operation(&self) -> Box; +} diff --git a/src/buffer/operation/replace.rs b/src/buffer/operation/replace.rs new file mode 100644 index 0000000..acfa2f6 --- /dev/null +++ b/src/buffer/operation/replace.rs @@ -0,0 +1,135 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::buffer::{cursor::Cursor, Buffer, GapBuffer, Position}; + +use super::Operation; + +#[derive(Clone)] +pub struct Replace { + old_content: String, + new_content: String, +} + +impl Operation for Replace { + fn run(&mut self, buffer: &mut Buffer) { + replace_content(self.new_content.clone(), buffer); + } + + fn reverse(&mut self, buffer: &mut Buffer) { + replace_content(self.old_content.clone(), buffer); + } + + fn clone_operation(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Replace { + /// Creates a new empty insert operation. + pub fn new(old_content: String, new_content: String) -> Replace { + Replace { + old_content, + new_content, + } + } +} + +impl Buffer { + /// Replaces the buffer's contents with the provided data. This method will + /// make best efforts to retain the full cursor position, then cursor line, + /// and will ultimately fall back to resetting the cursor to its initial + /// (0,0) position if these fail. The buffer's ID, syntax definition, and + /// change callback are always persisted. + /// + ///
+ /// As this is a reversible operation, both the before and after buffer + /// contents are kept in-memory, which for large buffers may be relatively + /// expensive. To help avoid needless replacements, this method will + /// ignore requests that don't actually change content. Despite this, use + /// this operation judiciously; it is designed for wholesale replacements + /// (e.g. external formatting tools) that cannot be broken down into + /// selective delete/insert operations. + ///
+ /// + /// # Examples + /// + /// ``` + /// use scribe::buffer::{Buffer, Position}; + /// + /// let mut buffer = Buffer::new(); + /// buffer.insert("scribe\nlibrary\n"); + /// buffer.cursor.move_to(Position { line: 1, offset: 1 }); + /// buffer.replace("new\ncontent"); + /// + /// assert_eq!(buffer.data(), "new\ncontent"); + /// assert_eq!(*buffer.cursor, Position{ line: 1, offset: 1 }); + /// ``` + pub fn replace + AsRef>(&mut self, content: T) { + let old_content = self.data(); + + // Ignore replacements that don't change content. + if content.as_ref() == old_content { + return; + } + + // Build and run an insert operation. + let mut op = Replace::new(self.data(), content.into()); + op.run(self); + + // Store the operation in the history object so that it can be undone. + match self.operation_group { + Some(ref mut group) => group.add(Box::new(op)), + None => self.history.add(Box::new(op)), + }; + } + + pub fn replace_on_cursor + AsRef>(&mut self, content: T) { + let old_content = self.data(); + let offset = self.cursor.offset; + let line_number = self.cursor.line; + let lines: Vec<&str> = old_content.lines().collect(); + let mut new_content = old_content.clone(); + + // 计算目标行的起始和结束位置 + let mut line_start = 0; + let mut line_count = 0; + + for (_, line) in lines.iter().enumerate() { + if line_count == line_number { + let line_end = line_start + offset; + if new_content.remove(line_end) == '\n' { + new_content.insert(line_end, '\n'); + } + new_content.insert_str(line_end, &content.into()); + break; + } + line_start += line.len() + 1; // +1 是为了加上换行符的长度 + line_count += 1; + } + + self.replace(new_content); + } +} + +fn replace_content(content: String, buffer: &mut Buffer) { + // Create a new gap buffer and associated cursor with the new content. + let data = Rc::new(RefCell::new(GapBuffer::new(content))); + let mut cursor = Cursor::new(data.clone(), Position { line: 0, offset: 0 }); + + // Try to retain cursor position or line of the current gap buffer. + if !cursor.move_to(*buffer.cursor) { + cursor.move_to(Position { + line: buffer.cursor.line, + offset: 0, + }); + } + + // Do the replacement. + buffer.data = data; + buffer.cursor = cursor; + + // Run the change callback, if present. + if let Some(ref callback) = buffer.change_callback { + callback(Position::default()) + } +} diff --git a/src/config/lastline_cmd.rs b/src/config/lastline_cmd.rs index 35a2b1c..15e520a 100644 --- a/src/config/lastline_cmd.rs +++ b/src/config/lastline_cmd.rs @@ -6,7 +6,7 @@ use crate::utils::{ buffer::LineState, ui::{ event::WarpUiCallBackType, - uicore::{UiCore, APP_INFO, CONTENT_WINSIZE}, + uicore::{UiCore, APP_INTERNAL_INFOMATION, CONTENT_WINSIZE}, InfoLevel, }, }; @@ -63,7 +63,7 @@ impl LastLineCommand { ret } else { - let mut info = APP_INFO.lock().unwrap(); + let mut info = APP_INTERNAL_INFOMATION.lock().unwrap(); info.level = InfoLevel::Info; info.info = NOT_FOUNT_CMD.to_string(); return WarpUiCallBackType::None; @@ -82,7 +82,7 @@ impl LastLineCommand { if ui.edited() { // 编辑过但不保存? // 更新警示信息 - let mut info = APP_INFO.lock().unwrap(); + let mut info = APP_INTERNAL_INFOMATION.lock().unwrap(); info.level = InfoLevel::Warn; info.info = EDITED_NO_STORE.to_string(); return WarpUiCallBackType::None; @@ -96,7 +96,7 @@ impl LastLineCommand { fn goto(ui: &mut MutexGuard, args: &str) -> WarpUiCallBackType { if args.is_empty() { - let mut info = APP_INFO.lock().unwrap(); + let mut info = APP_INTERNAL_INFOMATION.lock().unwrap(); info.level = InfoLevel::Info; info.info = "Useage: {goto}|{gt} {row}{' '|','|';'|':'|'/'}{col}".to_string(); return WarpUiCallBackType::None; @@ -112,7 +112,7 @@ impl LastLineCommand { }; if y.is_err() { - let mut info = APP_INFO.lock().unwrap(); + let mut info = APP_INTERNAL_INFOMATION.lock().unwrap(); info.level = InfoLevel::Info; info.info = "Useage: goto {row}({' '|','|';'|':'|'/'}{col})".to_string(); return WarpUiCallBackType::None; @@ -170,7 +170,7 @@ impl LastLineCommand { for s in args { let line = usize::from_str_radix(s, 10); if line.is_err() { - APP_INFO.lock().unwrap().info = format!("\"{s}\" is not a number"); + APP_INTERNAL_INFOMATION.lock().unwrap().info = format!("\"{s}\" is not a number"); return WarpUiCallBackType::None; } @@ -196,7 +196,8 @@ impl LastLineCommand { for arg in args { let line = usize::from_str_radix(arg, 10); if line.is_err() { - APP_INFO.lock().unwrap().info = format!("\"{arg}\" is not a number"); + APP_INTERNAL_INFOMATION.lock().unwrap().info = + format!("\"{arg}\" is not a number"); return WarpUiCallBackType::None; } @@ -224,7 +225,8 @@ impl LastLineCommand { for arg in args { let line = usize::from_str_radix(arg, 10); if line.is_err() { - APP_INFO.lock().unwrap().info = format!("\"{arg}\" is not a number"); + APP_INTERNAL_INFOMATION.lock().unwrap().info = + format!("\"{arg}\" is not a number"); return WarpUiCallBackType::None; } @@ -252,7 +254,8 @@ impl LastLineCommand { for arg in args { let line = usize::from_str_radix(arg, 10); if line.is_err() { - APP_INFO.lock().unwrap().info = format!("\"{arg}\" is not a number"); + APP_INTERNAL_INFOMATION.lock().unwrap().info = + format!("\"{arg}\" is not a number"); return WarpUiCallBackType::None; } @@ -273,7 +276,8 @@ impl LastLineCommand { let offset = ui.buffer.offset() + ui.cursor.y() as usize; let count = ui.buffer.delete_lines(offset, offset + 1); if count != 0 { - APP_INFO.lock().unwrap().info = format!("Successfully deleted {count} row"); + APP_INTERNAL_INFOMATION.lock().unwrap().info = + format!("Successfully deleted {count} row"); } ui.render_content(0, CONTENT_WINSIZE.read().unwrap().rows as usize) .unwrap(); @@ -282,7 +286,8 @@ impl LastLineCommand { 1 => { let line = usize::from_str_radix(args[0], 10); if line.is_err() { - APP_INFO.lock().unwrap().info = format!("\"{}\" is not a number", args[0]); + APP_INTERNAL_INFOMATION.lock().unwrap().info = + format!("\"{}\" is not a number", args[0]); return WarpUiCallBackType::None; } @@ -291,7 +296,8 @@ impl LastLineCommand { let offset = ui.buffer.offset() + line; let count = ui.buffer.delete_lines(offset, offset + 1); if count != 0 { - APP_INFO.lock().unwrap().info = format!("Successfully deleted {count} row"); + APP_INTERNAL_INFOMATION.lock().unwrap().info = + format!("Successfully deleted {count} row"); } ui.render_content(0, CONTENT_WINSIZE.read().unwrap().rows as usize) .unwrap(); @@ -302,14 +308,15 @@ impl LastLineCommand { let end = usize::from_str_radix(args[1], 10); if start.is_err() || end.is_err() { - APP_INFO.lock().unwrap().info = + APP_INTERNAL_INFOMATION.lock().unwrap().info = "Useage: (dl)|(delete) {start}({'-'}{end})".to_string(); return WarpUiCallBackType::None; } let count = ui.buffer.delete_lines(start.unwrap() - 1, end.unwrap() - 1); if count != 0 { - APP_INFO.lock().unwrap().info = format!("Successfully deleted {count} row"); + APP_INTERNAL_INFOMATION.lock().unwrap().info = + format!("Successfully deleted {count} row"); } ui.render_content(0, CONTENT_WINSIZE.read().unwrap().rows as usize) diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..c371892 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,40 @@ +use error_chain::error_chain; + +impl Default for Error { + fn default() -> Self { + Self(ErrorKind::Unreachable, Default::default()) + } +} + +error_chain! { + errors { + EmptyWorkspace { + description("the workspace is empty") + display("the workspace is empty") + } + MissingPath { + description("buffer doesn't have a path") + display("buffer doesn't have a path") + } + MissingScope { + description("couldn't find any scopes at the cursor position") + display("couldn't find any scopes at the cursor position") + } + MissingSyntax { + description("no syntax definition for the current buffer") + display("no syntax definition for the current buffer") + } + Unreachable { + description("Unreachable error") + display("Unreachable error") + } + } + + foreign_links { + Io(std::io::Error) #[cfg(unix)]; + ParsingError(syntect::parsing::ParsingError); + ScopeError(syntect::parsing::ScopeError); + SyntaxLoadingError(syntect::LoadingError); + DlopenError(dlopen2::Error); + } +} diff --git a/src/main.rs b/src/main.rs index 58a24da..63dc707 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,37 @@ -use std::{fs::File, io}; +#![feature(duration_millis_float)] -use app::Application; +use std::{env, fs::File}; + +use application::Application; use clap::Parser; use config::{appconfig::DeserializeAppOption, cmd::CmdConfig}; use utils::log_util::Log; -mod app; +mod application; +mod buffer; mod config; +mod errors; +mod modules; +mod plugin; +mod util; mod utils; +mod view; +mod workspace; #[macro_use] extern crate log; extern crate simplelog; -fn main() -> io::Result<()> { +use crate::errors::*; + +pub static mut APPLICATION: Option = None; + +pub fn get_application() -> &'static mut Application { + unsafe { APPLICATION.as_mut().unwrap() } +} + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); let config = CmdConfig::parse(); Log::init(config.level)?; @@ -26,5 +44,11 @@ fn main() -> io::Result<()> { setting = serde_yaml::from_reader::(file?).unwrap_or_default(); } - Application::new(config.file, setting.to_app_setting())?.run() + let application = Application::new(config.file, setting.to_app_setting(), &args)?; + + unsafe { + APPLICATION = Some(application); + } + + get_application().run() } diff --git a/src/modules/input/default.yaml b/src/modules/input/default.yaml new file mode 100644 index 0000000..20b97d1 --- /dev/null +++ b/src/modules/input/default.yaml @@ -0,0 +1,116 @@ +normal: + left: cursor::move_left + right: cursor::move_right + up: cursor::move_up + down: cursor::move_down + ctrl-c: app::exit + i: app::to_insert_mode + ':': app::to_command_mode + a: + - app::to_insert_mode + - cursor::move_right + shift-A: + - app::to_insert_mode + - cursor::move_to_end_of_line + shift-I: + - app::to_insert_mode + - cursor::move_to_start_of_line + backspace: cursor::move_left + escape: normal::reset + shift-L: cursor::move_to_end_of_line + shift-H: cursor::move_to_start_of_line + shift-T: monitor::scroll_to_first_line + shift-B: monitor::scroll_to_last_line + shift-G: normal::move_to_target_line + shift-O: + - cursor::move_to_start_of_line + - buffer::new_line + o: + - cursor::move_to_end_of_line + - buffer::new_line + - cursor::move_down + j: normal::move_down_n + k: normal::move_up_n + h: normal::move_left_n + l: normal::move_right_n + d: app::to_delete_mode + w: app::to_workspace_mode + /: app::to_search_mode + n: normal::move_to_next_words + b: normal::move_to_prev_words + e: normal::move_to_next_words_end + u: buffer::undo + ctrl-r: buffer::redo + num: normal::count_cmd + shift-R: app::to_replace_mode +insert: + escape: app::to_normal_mode + left: cursor::move_left + right: cursor::move_right + up: cursor::move_up + down: cursor::move_down + ctrl-c: app::exit + ctrl-s: buffer::save_file + ctrl-z: buffer::undo + enter: + - buffer::new_line + - cursor::move_down + - cursor::move_to_start_of_line + backspace: insert::backspace + tab: buffer::insert_tab + _: + - buffer::insert_char +command: + escape: command::to_normal_mode + backspace: command::backspace + enter: command::commit_and_execute + _: + - command::insert_command + +workspace: + up: workspace::move_up + down: workspace::move_down + enter: workspace::enter + escape: workspace::to_normal_mode + ctrl-c: app::exit +search: + /: + - search::clear + - app::to_search_mode + backspace: search::backspace + escape: + - search::clear + - app::to_normal_mode + enter: search::exec_search + up: search::last_result + down: search::next_result + ctrl-c: app::exit + _: search::input_search_data +delete: + ctrl-c: app::exit + escape: app::to_normal_mode + w: + - delete::delete_words + - app::to_normal_mode + d: + - delete::delete_lines + - cursor::move_to_start_of_line + - app::to_normal_mode + +replace: + escape: app::to_normal_mode + left: cursor::move_left + right: cursor::move_right + up: cursor::move_up + down: cursor::move_down + ctrl-c: app::exit + ctrl-s: buffer::save_file + ctrl-z: buffer::undo + enter: + - buffer::new_line + - cursor::move_down + - cursor::move_to_start_of_line + backspace: insert::backspace + tab: buffer::insert_tab + _: + - buffer::insert_char_on_replace \ No newline at end of file diff --git a/src/modules/input/mod.rs b/src/modules/input/mod.rs new file mode 100644 index 0000000..96776fe --- /dev/null +++ b/src/modules/input/mod.rs @@ -0,0 +1,168 @@ +use std::{collections::HashMap, ffi::OsStr, fs::read_to_string, path::PathBuf}; + +use crate::{ + application::{mode::ModeKey, Application}, + errors::*, +}; +use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers}; +use linked_hash_map::LinkedHashMap; + +use smallvec::SmallVec; +use strum::IntoEnumIterator; +use yaml_rust::{Yaml, YamlLoader}; + +const INPUT_CONFIG_NAME: &str = "input.yaml"; +pub struct InputLoader; + +impl InputLoader { + pub fn load( + path: PathBuf, + ) -> Result Result<()>; 4]>>>> + { + #[cfg(not(feature = "dragonos"))] + let data = Self::load_user(path)?; + #[cfg(feature = "dragonos")] + let data = None; + let default = Self::load_default()?; + let handle_map = Self::generate_handle_map( + data, + default + .as_hash() + .ok_or_else(|| "default input config didn't return a hash of key bindings")?, + )?; + Ok(handle_map) + } + + fn generate_handle_map( + extra_data: Option>, + default: &LinkedHashMap, + ) -> Result Result<()>; 4]>>>> + { + let mut handle_map = HashMap::new(); + for mode_key in ModeKey::iter() { + mode_key.generate_handle_map(&mut handle_map, extra_data.as_ref(), default)?; + } + Ok(handle_map) + } + + fn load_user(path: PathBuf) -> Result>> { + let readdir = path.read_dir()?; + let mut entries = readdir + .filter_map(|f| f.ok()) + .map(|f| f.path()) + .filter(|f| f.is_file()) + .filter(|f| { + f.file_name().is_some() && f.file_name().unwrap() == OsStr::new(INPUT_CONFIG_NAME) + }); + + let entry = entries.next(); + if let Some(entry) = entry { + let yaml = YamlLoader::load_from_str(&read_to_string(entry.clone())?) + .chain_err(|| format!("Couldn't parse input config file: {:?}", entry))? + .into_iter() + .next() + .chain_err(|| "No input document found")?; + let yaml_hash = yaml + .as_hash() + .ok_or_else(|| "extra input config didn't return a hash of key bindings")?; + + return Ok(Some(yaml_hash.clone())); + } + + Ok(None) + } + + fn load_default() -> Result { + YamlLoader::load_from_str(include_str!("default.yaml")) + .chain_err(|| "Couldn't parse default input config file")? + .into_iter() + .next() + .chain_err(|| "No default input document found") + } +} + +pub struct InputMapper; + +impl InputMapper { + pub fn event_map_str(event: Event) -> Option { + match event { + Event::FocusGained => None, + Event::FocusLost => None, + Event::Key(key_event) => { + return Some(Self::key_event_map_str(key_event)); + } + Event::Mouse(_) => None, + Event::Paste(_) => None, + Event::Resize(_, _) => None, + } + } + + fn key_event_map_str(event: KeyEvent) -> String { + if let KeyEventKind::Press = event.kind { + let mut modifier = String::new(); + if event.modifiers.contains(KeyModifiers::CONTROL) { + modifier.push_str("ctrl-"); + } + if event.modifiers.contains(KeyModifiers::ALT) { + modifier.push_str("alt-"); + } + if event.modifiers.contains(KeyModifiers::SHIFT) { + modifier.push_str("shift-"); + } + let keycode_str = match event.code { + crossterm::event::KeyCode::Backspace => "backspace".into(), + crossterm::event::KeyCode::Enter => "enter".into(), + crossterm::event::KeyCode::Left => "left".into(), + crossterm::event::KeyCode::Right => "right".into(), + crossterm::event::KeyCode::Up => "up".into(), + crossterm::event::KeyCode::Down => "down".into(), + crossterm::event::KeyCode::Home => "home".into(), + crossterm::event::KeyCode::End => "end".into(), + crossterm::event::KeyCode::PageUp => "pageup".into(), + crossterm::event::KeyCode::PageDown => "pagedown".into(), + crossterm::event::KeyCode::Tab => "tab".into(), + crossterm::event::KeyCode::BackTab => "backtab".into(), + crossterm::event::KeyCode::Delete => "delete".into(), + crossterm::event::KeyCode::Insert => "insert".into(), + crossterm::event::KeyCode::F(f) => format!("f{f}"), + crossterm::event::KeyCode::Char(c) => { + if c.is_digit(10) { + "num".to_string() + } else { + c.into() + } + } + crossterm::event::KeyCode::Null => "".into(), + crossterm::event::KeyCode::Esc => "escape".into(), + crossterm::event::KeyCode::CapsLock => "caps_lock".into(), + crossterm::event::KeyCode::ScrollLock => "scroll_lock".into(), + crossterm::event::KeyCode::NumLock => "num_lock".into(), + crossterm::event::KeyCode::PrintScreen => "print_screen".into(), + crossterm::event::KeyCode::Pause => "pause".into(), + crossterm::event::KeyCode::Menu => "menu".into(), + crossterm::event::KeyCode::KeypadBegin => "keypad_begin".into(), + crossterm::event::KeyCode::Media(_) => "".into(), + crossterm::event::KeyCode::Modifier(modifier_key_code) => match modifier_key_code { + crossterm::event::ModifierKeyCode::LeftShift + | crossterm::event::ModifierKeyCode::IsoLevel3Shift + | crossterm::event::ModifierKeyCode::IsoLevel5Shift + | crossterm::event::ModifierKeyCode::RightShift => "shift".into(), + crossterm::event::ModifierKeyCode::LeftControl + | crossterm::event::ModifierKeyCode::RightControl => "ctrl".into(), + crossterm::event::ModifierKeyCode::LeftAlt + | crossterm::event::ModifierKeyCode::RightAlt => "alt".into(), + crossterm::event::ModifierKeyCode::RightSuper + | crossterm::event::ModifierKeyCode::LeftSuper => "super".into(), + crossterm::event::ModifierKeyCode::RightHyper + | crossterm::event::ModifierKeyCode::LeftHyper => "hyper".into(), + crossterm::event::ModifierKeyCode::RightMeta + | crossterm::event::ModifierKeyCode::LeftMeta => "meta".into(), + }, + }; + + format!("{}{}", modifier, keycode_str) + } else { + "".into() + } + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..489d608 --- /dev/null +++ b/src/modules/mod.rs @@ -0,0 +1,8 @@ +use app_dirs2::AppInfo; + +pub mod input; +pub mod perferences; +const APP_INFO: AppInfo = AppInfo { + name: "held", + author: "DragonOS Community", +}; diff --git a/src/modules/perferences/default.yaml b/src/modules/perferences/default.yaml new file mode 100644 index 0000000..7703edf --- /dev/null +++ b/src/modules/perferences/default.yaml @@ -0,0 +1,2 @@ +soft_tab: true +tab_width: 4 \ No newline at end of file diff --git a/src/modules/perferences/mod.rs b/src/modules/perferences/mod.rs new file mode 100644 index 0000000..251b4d3 --- /dev/null +++ b/src/modules/perferences/mod.rs @@ -0,0 +1,145 @@ +use crate::{errors::*, utils::ui::uicore::APP_INTERNAL_INFOMATION}; +use app_dirs2::{app_dir, AppDataType, AppInfo}; +use std::{ + cell::RefCell, + path::{Path, PathBuf}, + rc::Rc, +}; +use yaml::YamlPerferences; +use yaml_rust::{Yaml, YamlLoader}; + +use super::APP_INFO; + +pub mod yaml; + +const SYNTAX_PATH: &str = "syntaxes"; +const THEME_PATH: &str = "themes"; +const INPUT_CONFIG_PATH: &str = "input"; +const PLUGINS_PATH: &str = "plugins"; +const THEME_KET: &str = "theme"; +const LANGUAGE_KEY: &str = "language"; +const LANGUAGE_SYNTAX_KEY: &str = "syntax"; +const LINE_WRAPPING_KEY: &str = "line_wrapping"; +const SOFT_TAB_KEY: &str = "soft_tab"; +const TAB_WIDTH_KEY: &str = "tab_width"; + +pub trait Perferences { + /// 载入 + fn load(&mut self); + + /// 是否自动换行 + fn line_wrapping(&self) -> bool; + + // tab宽度 + fn tab_width(&self) -> usize; + + // 是否使用空格模拟tab + fn soft_tab(&self) -> bool; + + // 设置的主题文件路径 + fn theme_path(&self) -> Result { + #[cfg(not(feature = "dragonos"))] + { + app_dir(AppDataType::UserConfig, &APP_INFO, THEME_PATH) + .chain_err(|| "Couldn't create a themes directory or build a path tp it") + } + #[cfg(feature = "dragonos")] + Ok(PathBuf::new()) + } + + // 输入映射配置文件路径 + fn input_config_path(&self) -> Result { + #[cfg(not(feature = "dragonos"))] + { + app_dir(AppDataType::UserConfig, &APP_INFO, INPUT_CONFIG_PATH) + .chain_err(|| "Couldn't create a themes directory or build a path tp it") + } + #[cfg(feature = "dragonos")] + Ok(PathBuf::new()) + } + + // 插件路径 + fn plugins_path(&self) -> Result { + #[cfg(not(feature = "dragonos"))] + { + app_dir(AppDataType::UserConfig, &APP_INFO, PLUGINS_PATH) + .chain_err(|| "Couldn't create a themes directory or build a path tp it") + } + #[cfg(feature = "dragonos")] + Ok(PathBuf::new()) + } + + // 设置的主题名字 + fn theme_name(&self) -> Option; + + // 返回设置的语法定义:例:test.rs -> rs test.cpp -> cpp + fn syntax_definition_name(&self, path: &Path) -> Option; +} + +pub struct PerferencesManager; + +impl PerferencesManager { + pub fn load() -> Result>> { + match Self::load_extend()? { + Some(_) => todo!(), + None => return Ok(Rc::new(RefCell::new(Self::load_default_perferences()?))), + } + } + + pub fn user_syntax_path() -> Result { + app_dir(AppDataType::UserConfig, &APP_INFO, SYNTAX_PATH) + .chain_err(|| "Couldn't create syntax directory or build a path to it.") + } + + fn load_default_perferences() -> Result { + let yaml = YamlLoader::load_from_str(include_str!("default.yaml")) + .chain_err(|| "Couldn't parse default config file")? + .into_iter() + .next() + .chain_err(|| "No default preferences document found")?; + + Ok(YamlPerferences::new(yaml)) + } + + fn load_extend() -> Result>>> { + // 可能涉及加载其他格式的配置文件 + Ok(None) + } +} + +#[cfg(test)] +pub struct DummyPerferences; +#[cfg(test)] +impl Perferences for DummyPerferences { + fn line_wrapping(&self) -> bool { + true + } + + fn tab_width(&self) -> usize { + 2 + } + + fn soft_tab(&self) -> bool { + true + } + + fn theme_path(&self) -> Result { + todo!() + } + + fn theme_name(&self) -> Option { + todo!() + } + + fn load(&mut self) { + todo!() + } + + fn syntax_definition_name(&self, path: &Path) -> Option { + todo!() + } + + fn input_config_path(&self) -> Result { + todo!() + } +} diff --git a/src/modules/perferences/yaml.rs b/src/modules/perferences/yaml.rs new file mode 100644 index 0000000..6305790 --- /dev/null +++ b/src/modules/perferences/yaml.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; + +use super::{ + Perferences, APP_INFO, LINE_WRAPPING_KEY, SOFT_TAB_KEY, TAB_WIDTH_KEY, THEME_KET, THEME_PATH, +}; +use crate::{ + errors::*, + modules::perferences::{LANGUAGE_KEY, LANGUAGE_SYNTAX_KEY, SYNTAX_PATH}, +}; +use app_dirs2::{app_dir, AppDataType}; +use yaml_rust::Yaml; + +pub struct YamlPerferences { + data: Yaml, +} + +impl YamlPerferences { + pub fn new(yaml: Yaml) -> YamlPerferences { + YamlPerferences { data: yaml } + } +} + +impl Perferences for YamlPerferences { + fn load(&mut self) { + todo!() + } + + fn line_wrapping(&self) -> bool { + self.data[LINE_WRAPPING_KEY].as_bool().unwrap_or(true) + } + + fn tab_width(&self) -> usize { + self.data[TAB_WIDTH_KEY].as_i64().unwrap_or(4) as usize + } + + fn soft_tab(&self) -> bool { + self.data[SOFT_TAB_KEY].as_bool().unwrap_or(true) + } + + fn theme_name(&self) -> Option { + self.data[THEME_KET].as_str().map(|x| x.to_owned()) + } + + fn syntax_definition_name(&self, path: &std::path::Path) -> Option { + if let Some(extension) = path.extension().and_then(|f| f.to_str()) { + if let Some(syntax_definition) = + self.data[LANGUAGE_KEY][extension][LANGUAGE_SYNTAX_KEY].as_str() + { + return Some(syntax_definition.to_string()); + } + } + + None + } +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 index 0000000..9ecf7d2 --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1,43 @@ +use dlopen2::wrapper::{Container, WrapperApi}; +use held_core::{ + interface::ApplicationInterface, plugin::Plugin, view::render::ContentRenderBuffer, +}; + +pub mod system; + +#[derive(WrapperApi)] +pub struct PluginApi { + plugin_create: unsafe fn() -> *mut dyn Plugin, + init_plugin_application: unsafe fn(app: &'static mut dyn ApplicationInterface), +} + +#[allow(dead_code)] +pub struct PluginInstance { + // 顺序不能反,需要确保plugin在container之前销毁 + plugin: Box, + container: Container, +} + +impl PluginInstance { + pub fn new(plugin: Box, container: Container) -> PluginInstance { + PluginInstance { plugin, container } + } +} + +impl Plugin for PluginInstance { + fn name(&self) -> &'static str { + self.plugin.name() + } + + fn init(&self) { + self.plugin.init() + } + + fn deinit(&self) { + self.plugin.deinit() + } + + fn on_render_content(&self) -> Vec { + self.plugin.on_render_content() + } +} diff --git a/src/plugin/system.rs b/src/plugin/system.rs new file mode 100644 index 0000000..610bc6e --- /dev/null +++ b/src/plugin/system.rs @@ -0,0 +1,82 @@ +use crate::{errors::*, get_application}; +use dlopen2::wrapper::Container; +use held_core::{plugin::Plugin, view::render::ContentRenderBuffer}; +use std::{collections::HashMap, path::PathBuf, rc::Rc}; +use walkdir::WalkDir; + +use crate::plugin::PluginApi; + +use super::PluginInstance; + +pub struct PluginSystem { + plugins: HashMap<&'static str, Rc>, +} + +unsafe impl Send for PluginSystem {} +unsafe impl Sync for PluginSystem {} + +impl PluginSystem { + fn new() -> Self { + Self { + plugins: HashMap::new(), + } + } + + pub fn init_system(pulgin_dir: PathBuf) -> PluginSystem { + let mut system = PluginSystem::new(); + system.load_pulgins(pulgin_dir); + system + } + + pub fn load_pulgins(&mut self, pulgin_dir: PathBuf) { + for entry in WalkDir::new(pulgin_dir) { + if let Ok(entry) = entry { + if entry.file_type().is_file() { + let path = entry.into_path(); + if let Err(e) = unsafe { self.load_pulgin(&path) } { + error!("load pulgin: {:?}, load error: {e:?}", path) + } + } + } + } + } + + pub unsafe fn load_pulgin(&mut self, pulgin_path: &PathBuf) -> Result<()> { + let container: Container = Container::load(pulgin_path)?; + let plugin_raw = container.plugin_create(); + let plugin = Box::from_raw(plugin_raw); + self.plugins.insert( + plugin.name(), + Rc::new(PluginInstance::new(plugin, container)), + ); + Ok(()) + } +} + +impl Plugin for PluginSystem { + fn name(&self) -> &'static str { + "" + } + + fn init(&self) { + for (_, plugin) in self.plugins.iter() { + unsafe { plugin.container.init_plugin_application(get_application()) }; + plugin.init(); + } + } + + fn deinit(&self) { + for (_, plugin) in self.plugins.iter() { + plugin.deinit(); + } + } + + fn on_render_content(&self) -> Vec { + let mut ret = vec![]; + for (_, plugin) in self.plugins.iter() { + ret.append(&mut plugin.on_render_content()); + } + + ret + } +} diff --git a/src/themes/solarized_dark.tmTheme b/src/themes/solarized_dark.tmTheme new file mode 100644 index 0000000..47bd603 --- /dev/null +++ b/src/themes/solarized_dark.tmTheme @@ -0,0 +1,2061 @@ + + + + + name + Solarized Dark + settings + + + settings + + background + #002B36 + caret + #839496 + foreground + #b2b2b2 + invisibles + #073642 + lineHighlight + #303030 + selection + #EEE8D5 + + + + name + Comment + scope + comment + settings + + fontStyle + + foreground + #586E75 + + + + name + String + scope + string + settings + + foreground + #2AA198 + + + + name + StringNumber + scope + string + settings + + foreground + #586E75 + + + + name + Regexp + scope + string.regexp + settings + + foreground + #DC322F + + + + name + Number + scope + constant.numeric + settings + + foreground + #D33682 + + + + name + Variable + scope + variable.language, variable.other + settings + + foreground + #268BD2 + + + + name + Keyword + scope + keyword + settings + + foreground + #859900 + + + + name + Storage + scope + storage + settings + + fontStyle + + foreground + #859900 + + + + name + Class name + scope + entity.name.class, entity.name.type.class + settings + + foreground + #268BD2 + + + + name + Function name + scope + entity.name.function + settings + + foreground + #268BD2 + + + + name + Variable start + scope + punctuation.definition.variable + settings + + foreground + #859900 + + + + name + Embedded code markers + scope + punctuation.section.embedded.begin, punctuation.section.embedded.end + settings + + foreground + #DC322F + + + + name + Built-in constant + scope + constant.language, meta.preprocessor + settings + + foreground + #B58900 + + + + name + Support.construct + scope + support.function.construct, keyword.other.new + settings + + foreground + #DC322F + + + + name + User-defined constant + scope + constant.character, constant.other + settings + + foreground + #CB4B16 + + + + name + Inherited class + scope + entity.other.inherited-class + settings + + + + name + Function argument + scope + variable.parameter + settings + + + + name + Tag name + scope + entity.name.tag + settings + + foreground + #268BD2 + + + + name + Tag start/end + scope + punctuation.definition.tag.html, punctuation.definition.tag.begin, punctuation.definition.tag.end + settings + + foreground + #586E75 + + + + name + Tag attribute + scope + entity.other.attribute-name + settings + + foreground + #93A1A1 + + + + name + Library function + scope + support.function + settings + + foreground + #268BD2 + + + + name + Continuation + scope + punctuation.separator.continuation + settings + + foreground + #DC322F + + + + name + Library constant + scope + support.constant + settings + + + + name + Library class/type + scope + support.type, support.class + settings + + foreground + #859900 + + + + name + Library Exception + scope + support.type.exception + settings + + foreground + #CB4B16 + + + + name + Special + scope + keyword.other.special-method + settings + + foreground + #CB4B16 + + + + name + Library variable + scope + support.other.variable + settings + + + + name + Invalid + scope + invalid + settings + + + + name + Quoted String + scope + string.quoted.double, string.quoted.single + settings + + foreground + #2AA198 + + + + name + Quotes + scope + punctuation.definition.string.begin, punctuation.definition.string.end + settings + + foreground + #DC322F + + + + name + CSS: Property name (body) + scope + entity.name.tag.css, support.type.property-name.css, meta.property-name.css, support.type.property-name.scss + settings + + fontStyle + + foreground + #839496 + + + + name + CSS: @ rules (purple) + scope + punctuation.definition.keyword.scss, punctuation.definition.keyword.css, keyword.control.at-rule.charset.css, keyword.control.at-rule.charset.scss, keyword.control.each.css, keyword.control.each.scss, keyword.control.else.css, keyword.control.else.scss, keyword.control.at-rule.import.css, keyword.control.at-rule.import.scss, keyword.control.at-rule.fontface.css, keyword.control.at-rule.fontface.scss, keyword.control.for.css, keyword.control.for.scss, keyword.control.at-rule.function.css, keyword.control.at-rule.function.scss, keyword.control.if.css, keyword.control.if.scss, keyword.control.at-rule.include.scss, keyword.control.at-rule.media.css, keyword.control.at-rule.media.scss, keyword.control.at-rule.font-face.css, keyword.control.at-rule.font-face.scss, meta.at-rule.import.css, variable.other.less, variable.declaration.less, variable.interpolation.less, meta.at-rule.media.scss + settings + + foreground + #6C71c4 + + + + name + CSS: Numeric Value (blue) + scope + constant.numeric.css, keyword.other.unit.css, keyword.unit.css, constant.other.color.rgb-value.css, constant.numeric.scss, constant.other.color.rgb-value.scss, keyword.other.unit.scss, punctuation.definition.constant.scss, punctuation.definition.constant.css, constant.other.rgb-value.css + settings + + fontStyle + + foreground + #268BD2 + + + + name + CSS: String, value and constants (azure) + scope + variable.parameter.url.scss, meta.property-value.css, meta.property-value.scss, support.constant.property-value.scss, support.constant.font-name.scss, string.quoted.single.css, string.quoted.double.css, constant.character.escaped.css, string.quoted.variable.parameter.url, punctuation.definition.string.begin.scss, punctuation.definition.string.begin.css, punctuation.definition.string.end.scss, punctuation.definition.string.end.css, support.constant.property-value.css + settings + + fontStyle + + foreground + #2AA198 + + + + name + CSS: !Important (red) + scope + keyword.other.important.css, keyword.other.important.scss + settings + + foreground + #DC322F + + + + name + CSS: Standard color value (orange) + scope + support.constant.color, invalid.deprecated.color.w3c-non-standard-color-name.scss + settings + + foreground + #CB4b16 + + + + name + CSS: : , () (body) + scope + punctuation.terminator.rule.css, punctuation.section.function.css, punctuation.section.function.scss, punctuation.separator.key-value.csspunctuation.scss, punctuation.css, keyword.operator.less, entity.name.tag.wildcard.scss, entity.name.tag.wildcard.css, entity.name.tag.reference.scss + settings + + fontStyle + + foreground + #657B83 + + + + name + CSS: Selector > [] and non-spec tags (body) + scope + meta.selector.css + settings + + fontStyle + + foreground + #657B83 + + + + name + CSS: Tag (green) + scope + entity.name.tag.css, entity.name.tag.scss + settings + + fontStyle + + foreground + #859900 + + + + name + CSS .class (yellow) + scope + entity.other.attribute-name.class.css, entity.other.less.mixin + settings + + fontStyle + + foreground + #B58900 + + + + name + CSS: #id (yellow) + scope + source.css entity.other.attribute-name.id, source.scss entity.other.attribute-name.id + settings + + foreground + #B58900 + + + + name + CSS :pseudo (orange) + scope + entity.other.attribute-name.pseudo-element.css, entity.other.attribute-name.pseudo-class.css + settings + + fontStyle + + foreground + #CB4B16 + + + + name + SCSS: Variables (pink) + scope + variable, variable.scss + settings + + foreground + #D33682 + + + + name + JS: Function Name + scope + meta.function.js, entity.name.function.js, support.function.dom.js + settings + + foreground + #B58900 + + + + name + JS: Source + scope + text.html.basic source.js.embedded.html + settings + + fontStyle + + foreground + #B58900 + + + + name + JS: Function + scope + storage.type.function.js + settings + + foreground + #268BD2 + + + + name + JS: Numeric Constant + scope + constant.numeric.js + settings + + foreground + #2AA198 + + + + name + JS: [] + scope + meta.brace.square.js + settings + + foreground + #268BD2 + + + + name + JS: Storage Type + scope + storage.type.js + settings + + foreground + #268BD2 + + + + name + () + scope + meta.brace.round, punctuation.definition.parameters.begin.js, punctuation.definition.parameters.end.js + settings + + foreground + #93A1A1 + + + + name + {} + scope + meta.brace.curly.js + settings + + foreground + #839496 + + + + name + HTML: Doctype + scope + entity.name.tag.doctype.html, meta.tag.sgml.html, string.quoted.double.doctype.identifiers-and-DTDs.html + settings + + fontStyle + italic + foreground + #839496 + + + + name + HTML: Comment Block + scope + comment.block.html + settings + + fontStyle + italic + foreground + #839496 + + + + name + HTML: Script + scope + entity.name.tag.script.html + settings + + fontStyle + italic + + + + name + HTML: Style + scope + source.css.embedded.html string.quoted.double.html + settings + + fontStyle + + foreground + #2AA198 + + + + name + HTML: Text + scope + text.html.ruby + settings + + foreground + #839496 + + + + name + HTML: = + scope + text.html.basic meta.tag.other.html, text.html.basic meta.tag.any.html, text.html.basic meta.tag.block.any, text.html.basic meta.tag.inline.any, text.html.basic meta.tag.structure.any.html, text.html.basic source.js.embedded.html, punctuation.separator.key-value.html + settings + + fontStyle + + foreground + #657B83 + + + + name + HTML: something= + scope + text.html.basic entity.other.attribute-name.html + settings + + foreground + #657B83 + + + + name + HTML: " + scope + text.html.basic meta.tag.structure.any.html punctuation.definition.string.begin.html, punctuation.definition.string.begin.html, punctuation.definition.string.end.html + settings + + fontStyle + + foreground + #2AA198 + + + + name + HTML: <tag> + scope + entity.name.tag.block.any.html + settings + + foreground + #268BD2 + + + + name + HTML: style + scope + source.css.embedded.html entity.name.tag.style.html + settings + + fontStyle + italic + + + + name + HTML: <style> + scope + entity.name.tag.style.html + settings + + fontStyle + + + + + name + HTML: {} + scope + text.html.basic, punctuation.section.property-list.css + settings + + fontStyle + + + + + name + HTML: Embeddable + scope + source.css.embedded.html, comment.block.html + settings + + fontStyle + italic + foreground + #839496 + + + + name + Ruby: Variable definition + scope + punctuation.definition.variable.ruby + settings + + fontStyle + + foreground + #268BD2 + + + + name + Ruby: Function Name + scope + meta.function.method.with-arguments.ruby + settings + + foreground + #657B83 + + + + name + Ruby: Variable + scope + variable.language.ruby + settings + + foreground + #2AA198 + + + + name + Ruby: Function + scope + entity.name.function.ruby + settings + + foreground + #268BD2 + + + + name + Ruby: Keyword Control + scope + keyword.control.ruby, keyword.control.def.ruby + settings + + foreground + #859900 + + + + name + Ruby: Class + scope + keyword.control.class.ruby, meta.class.ruby + settings + + foreground + #859900 + + + + name + Ruby: Class Name + scope + entity.name.type.class.ruby + settings + + fontStyle + + foreground + #B58900 + + + + name + Ruby: Keyword + scope + keyword.control.ruby + settings + + fontStyle + + foreground + #859900 + + + + name + Ruby: Support Class + scope + support.class.ruby + settings + + fontStyle + + foreground + #B58900 + + + + name + Ruby: Special Method + scope + keyword.other.special-method.ruby + settings + + foreground + #859900 + + + + name + Ruby: Constant + scope + constant.language.ruby, constant.numeric.ruby + settings + + foreground + #2AA198 + + + + name + Ruby: Constant Other + scope + variable.other.constant.ruby + settings + + fontStyle + + foreground + #B58900 + + + + name + Ruby: :symbol + scope + constant.other.symbol.ruby + settings + + fontStyle + + foreground + #2AA198 + + + + name + Ruby: Punctuation Section '' + scope + punctuation.section.embedded.ruby, punctuation.definition.string.begin.ruby, punctuation.definition.string.end.ruby + settings + + foreground + #DC322F + + + + name + Ruby: Special Method + scope + keyword.other.special-method.ruby + settings + + foreground + #CB4B16 + + + + name + PHP: Include + scope + keyword.control.import.include.php + settings + + foreground + #CB4B16 + + + + name + Ruby: erb = + scope + text.html.ruby meta.tag.inline.any.html + settings + + fontStyle + + foreground + #839496 + + + + name + Ruby: erb "" + scope + text.html.ruby punctuation.definition.string.begin, text.html.ruby punctuation.definition.string.end + settings + + fontStyle + + foreground + #2AA198 + + + + name + PHP: Quoted Single + scope + punctuation.definition.string.begin, punctuation.definition.string.end + settings + + foreground + #839496 + + + + name + PHP: Class Names + scope + support.class.php + settings + + foreground + #93A1A1 + + + + name + PHP: [] + scope + keyword.operator.index-start.php, keyword.operator.index-end.php + settings + + foreground + #DC322F + + + + name + PHP: Array + scope + meta.array.php + settings + + foreground + #586E75 + + + + name + PHP: Array() + scope + meta.array.php support.function.construct.php, meta.array.empty.php support.function.construct.php + settings + + fontStyle + + foreground + #B58900 + + + + name + PHP: Array Construct + scope + support.function.construct.php + settings + + foreground + #B58900 + + + + name + PHP: Array Begin + scope + punctuation.definition.array.begin, punctuation.definition.array.end + settings + + foreground + #DC322F + + + + name + PHP: Numeric Constant + scope + constant.numeric.php + settings + + foreground + #2AA198 + + + + name + PHP: New + scope + keyword.other.new.php + settings + + foreground + #CB4B16 + + + + name + PHP: :: + scope + keyword.operator.class + settings + + fontStyle + + foreground + #93A1A1 + + + + name + PHP: Other Property + scope + variable.other.property.php + settings + + foreground + #839496 + + + + name + PHP: Class + scope + storage.modifier.extends.php, storage.type.class.php, keyword.operator.class.php + settings + + foreground + #B58900 + + + + name + PHP: Semicolon + scope + punctuation.terminator.expression.php + settings + + foreground + #839496 + + + + name + PHP: Inherited Class + scope + meta.other.inherited-class.php + settings + + fontStyle + + foreground + #586E75 + + + + name + PHP: Storage Type + scope + storage.type.php + settings + + foreground + #859900 + + + + name + PHP: Function + scope + entity.name.function.php + settings + + foreground + #839496 + + + + name + PHP: Function Construct + scope + support.function.construct.php + settings + + foreground + #859900 + + + + name + PHP: Function Call + scope + entity.name.type.class.php, meta.function-call.php, meta.function-call.static.php, meta.function-call.object.php + settings + + foreground + #839496 + + + + name + PHP: Comment + scope + keyword.other.phpdoc + settings + + fontStyle + + foreground + #839496 + + + + name + PHP: Source Emebedded + scope + source.php.embedded.block.html + settings + + foreground + #CB4B16 + + + + name + PHP: Storage Type Function + scope + storage.type.function.php + settings + + foreground + #CB4B16 + + + + name + C: constant + scope + constant.numeric.c + settings + + fontStyle + + foreground + #2AA198 + + + + name + C: Meta Preprocessor + scope + meta.preprocessor.c.include, meta.preprocessor.macro.c + settings + + fontStyle + + foreground + #CB4B16 + + + + name + C: Keyword + scope + keyword.control.import.define.c, keyword.control.import.include.c + settings + + fontStyle + + foreground + #CB4B16 + + + + name + C: Function Preprocessor + scope + entity.name.function.preprocessor.c + settings + + fontStyle + + foreground + #CB4B16 + + + + name + C: include <something.c> + scope + meta.preprocessor.c.include string.quoted.other.lt-gt.include.c, meta.preprocessor.c.include punctuation.definition.string.begin.c, meta.preprocessor.c.include punctuation.definition.string.end.c + settings + + fontStyle + + foreground + #2AA198 + + + + name + C: Function + scope + support.function.C99.c, support.function.any-method.c, entity.name.function.c + settings + + fontStyle + + foreground + #93A1A1 + + + + name + C: " + scope + punctuation.definition.string.begin.c, punctuation.definition.string.end.c + settings + + fontStyle + + foreground + #2AA198 + + + + name + C: Storage Type + scope + storage.type.c + settings + + fontStyle + + foreground + #B58900 + + + + name + diff: header + scope + meta.diff, meta.diff.header + settings + + background + #B58900 + fontStyle + italic + foreground + #EEE8D5 + + + + name + diff: deleted + scope + markup.deleted + settings + + background + #EEE8D5 + fontStyle + + foreground + #DC322F + + + + name + diff: changed + scope + markup.changed + settings + + background + #EEE8D5 + fontStyle + + foreground + #CB4B16 + + + + name + diff: inserted + scope + markup.inserted + settings + + background + #EEE8D5 + foreground + #2AA198 + + + + name + reST raw + scope + text.restructuredtext markup.raw + settings + + foreground + #2AA198 + + + + name + Other: Removal + scope + other.package.exclude, other.remove + settings + + fontStyle + + foreground + #DC322F + + + + name + Other: Add + scope + other.add + settings + + foreground + #2AA198 + + + + name + Tex: {} + scope + punctuation.section.group.tex , punctuation.definition.arguments.begin.latex, punctuation.definition.arguments.end.latex, punctuation.definition.arguments.latex + settings + + fontStyle + + foreground + #DC322F + + + + name + Tex: {text} + scope + meta.group.braces.tex + settings + + fontStyle + + foreground + #B58900 + + + + name + Tex: Other Math + scope + string.other.math.tex + settings + + fontStyle + + foreground + #B58900 + + + + name + Tex: {var} + scope + variable.parameter.function.latex + settings + + fontStyle + + foreground + #CB4B16 + + + + name + Tex: Math \\ + scope + punctuation.definition.constant.math.tex + settings + + fontStyle + + foreground + #DC322F + + + + name + Tex: Constant Math + scope + text.tex.latex constant.other.math.tex, constant.other.general.math.tex, constant.other.general.math.tex, constant.character.math.tex + settings + + fontStyle + + foreground + #2AA198 + + + + name + Tex: Other Math String + scope + string.other.math.tex + settings + + fontStyle + + foreground + #B58900 + + + + name + Tex: $ + scope + punctuation.definition.string.begin.tex, punctuation.definition.string.end.tex + settings + + fontStyle + + foreground + #DC322F + + + + name + Tex: \label + scope + keyword.control.label.latex, text.tex.latex constant.other.general.math.tex + settings + + fontStyle + + foreground + #2AA198 + + + + name + Tex: \label { } + scope + variable.parameter.definition.label.latex + settings + + fontStyle + + foreground + #DC322F + + + + name + Tex: Function + scope + support.function.be.latex + settings + + fontStyle + + foreground + #859900 + + + + name + Tex: Support Function Section + scope + support.function.section.latex + settings + + fontStyle + + foreground + #CB4B16 + + + + name + Tex: Support Function + scope + support.function.general.tex + settings + + fontStyle + + foreground + #2AA198 + + + + name + Tex: Comment + scope + punctuation.definition.comment.tex, comment.line.percentage.tex + settings + + fontStyle + italic + + + + name + Tex: Reference Label + scope + keyword.control.ref.latex + settings + + fontStyle + + foreground + #2AA198 + + + + name + Python: storage + scope + storage.type.class.python, storage.type.function.python, storage.modifier.global.python + settings + + fontStyle + + foreground + #859900 + + + + name + Python: import + scope + keyword.control.import.python, keyword.control.import.from.python + settings + + foreground + #CB4B16 + + + + name + Python: Support.exception + scope + support.type.exception.python + settings + + foreground + #B58900 + + + + name + Shell: builtin + scope + support.function.builtin.shell + settings + + foreground + #859900 + + + + name + Shell: variable + scope + variable.other.normal.shell + settings + + foreground + #CB4B16 + + + + name + Shell: DOT_FILES + scope + source.shell + settings + + fontStyle + + foreground + #268BD2 + + + + name + Shell: meta scope in loop + scope + meta.scope.for-in-loop.shell, variable.other.loop.shell + settings + + fontStyle + + foreground + #586E75 + + + + name + Shell: "" + scope + punctuation.definition.string.end.shell, punctuation.definition.string.begin.shell + settings + + fontStyle + + foreground + #859900 + + + + name + Shell: Meta Block + scope + meta.scope.case-block.shell, meta.scope.case-body.shell + settings + + fontStyle + + foreground + #586E75 + + + + name + Shell: [] + scope + punctuation.definition.logical-expression.shell + settings + + fontStyle + + foreground + #DC322F + + + + name + Shell: Comment + scope + comment.line.number-sign.shell + settings + + fontStyle + italic + + + + name + Java: import + scope + keyword.other.import.java + settings + + fontStyle + + foreground + #CB4B16 + + + + name + Java: meta-import + scope + storage.modifier.import.java + settings + + fontStyle + + foreground + #586E75 + + + + name + Java: Class + scope + meta.class.java storage.modifier.java + settings + + fontStyle + + foreground + #B58900 + + + + name + Java: /* comment */ + scope + source.java comment.block + settings + + fontStyle + + foreground + #586E75 + + + + name + Java: /* @param */ + scope + comment.block meta.documentation.tag.param.javadoc keyword.other.documentation.param.javadoc + settings + + fontStyle + + foreground + #586E75 + + + + name + Perl: variables + scope + punctuation.definition.variable.perl, variable.other.readwrite.global.perl, variable.other.predefined.perl, keyword.operator.comparison.perl + settings + + foreground + #B58900 + + + + name + Perl: functions + scope + support.function.perl + settings + + foreground + #859900 + + + + name + Perl: comments + scope + comment.line.number-sign.perl + settings + + fontStyle + italic + foreground + #586E75 + + + + name + Perl: quotes + scope + punctuation.definition.string.begin.perl, punctuation.definition.string.end.perl + settings + + foreground + #2AA198 + + + + name + Perl: \char + scope + constant.character.escape.perl + settings + + foreground + #DC322F + + + + Name + Markdown punctuation + scope + markup.list, text.html.markdown punctuation.definition, meta.separator.markdown + settings + + foreground + #CB4b16 + + + + Name + Markdown heading + scope + markup.heading + settings + + foreground + #268BD2 + + + + Name + Markdown text inside some block element + scope + markup.quote, meta.paragraph.list + settings + + foreground + #2AA198 + + + + Name + Markdown em + scope + markup.italic + settings + + fontStyle + italic + + + + Name + Markdown strong + scope + markup.bold + settings + + fontStyle + bold + + + + Name + Markdown reference + scope + markup.underline.link.markdown, meta.link.inline punctuation.definition.metadata, meta.link.reference.markdown punctuation.definition.constant, meta.link.reference.markdown constant.other.reference + settings + + foreground + #B58900 + + + + Name + Markdown linebreak + scope + meta.paragraph.markdown meta.dummy.line-break + settings + + background + #6C71c4 + + + + + name + GitGutter deleted + scope + markup.deleted.git_gutter + settings + + foreground + #F92672 + + + + name + GitGutter inserted + scope + markup.inserted.git_gutter + settings + + foreground + #A6E22E + + + + name + GitGutter changed + scope + markup.changed.git_gutter + settings + + foreground + #967EFB + + + + + name + SublimeLinter Annotations + scope + sublimelinter.notes + settings + + background + #eee8d5 + foreground + #eee8d5 + + + + name + SublimeLinter Error Outline + scope + sublimelinter.outline.illegal + settings + + background + #93a1a1 + foreground + #93a1a1 + + + + name + SublimeLinter Error Underline + scope + sublimelinter.underline.illegal + settings + + background + #dc322f + + + + name + SublimeLinter Warning Outline + scope + sublimelinter.outline.warning + settings + + background + #839496 + foreground + #839496 + + + + name + SublimeLinter Warning Underline + scope + sublimelinter.underline.warning + settings + + background + #b58900 + + + + name + SublimeLinter Violation Outline + scope + sublimelinter.outline.violation + settings + + background + #657b83 + foreground + #657b83 + + + + name + SublimeLinter Violation Underline + scope + sublimelinter.underline.violation + settings + + background + #cb4b16 + + + + name + SublimeBracketHighlighter + scope + brackethighlighter.all + settings + + background + #002b36 + foreground + #cb4b16 + + + + uuid + A4299D9B-1DE5-4BC4-87F6-A757E71B1597 + + diff --git a/src/util/line_iterator.rs b/src/util/line_iterator.rs new file mode 100644 index 0000000..f62594e --- /dev/null +++ b/src/util/line_iterator.rs @@ -0,0 +1,63 @@ +pub struct LineIterator<'a> { + data: &'a str, + line_number: usize, + line_start: usize, + line_end: usize, + done: bool, +} + +impl<'a> LineIterator<'a> { + pub fn new(data: &str) -> LineIterator { + LineIterator { + data, + line_number: 0, + line_start: 0, + line_end: 0, + done: false, + } + } + + fn out_of_data(&self) -> bool { + self.line_end == self.data.len() + } +} + +impl<'a> Iterator for LineIterator<'a> { + type Item = (usize, &'a str); + + fn next(&mut self) -> Option { + if self.done { + return None; + } + + // Move the range beyond its previous position. + self.line_start = self.line_end; + + // We track trailing newlines because, if the buffer ends immediately + // after one, we want to return one last line on the next iteration. + let mut trailing_newline = false; + + // Find the next line range. + for c in self.data[self.line_start..].chars() { + // Extend the current line range to include this char. + self.line_end += c.len_utf8(); + + if c == '\n' { + trailing_newline = true; + break; + } + } + + let line = Some((self.line_number, &self.data[self.line_start..self.line_end])); + + // Flag the iterator as done as soon as we've exhausted its data, + // and have given one last line for data with a trailing newline. + if self.out_of_data() && !trailing_newline { + self.done = true; + } else { + self.line_number += 1; + } + + line + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..1132c78 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod line_iterator; diff --git a/src/utils/buffer.rs b/src/utils/buffer.rs index b4b220c..628d2f0 100644 --- a/src/utils/buffer.rs +++ b/src/utils/buffer.rs @@ -13,7 +13,7 @@ use crossterm::style::Color; use super::{ style::StyleManager, - ui::uicore::{APP_INFO, CONTENT_WINSIZE}, + ui::uicore::{APP_INTERNAL_INFOMATION, CONTENT_WINSIZE}, }; #[derive(Debug, Default, Clone)] @@ -223,7 +223,7 @@ impl EditBuffer { if previous_line.flags.contains(LineState::LOCKED) || cur_line.flags.contains(LineState::LOCKED) { - APP_INFO.lock().unwrap().info = "Row is locked".to_string(); + APP_INTERNAL_INFOMATION.lock().unwrap().info = "Row is locked".to_string(); return (false, 0); } @@ -444,7 +444,7 @@ impl EditBuffer { while left <= right && right < linesize { let lchar = line[left] as char; let rchar = line[right] as char; - if rchar.is_ascii_punctuation() && right != x.into() { + if rchar.is_ascii_punctuation() && right != x as usize { break; } if !(lchar == ' ' || lchar == '\t') { @@ -476,7 +476,7 @@ impl EditBuffer { while left <= right && right < linesize { let lchar = line[left] as char; let rchar = line[right] as char; - if rchar.is_ascii_punctuation() && right != x.into() { + if rchar.is_ascii_punctuation() && right != x as usize { break; } if lchar == ' ' || lchar == '\t' { @@ -511,7 +511,7 @@ impl EditBuffer { let lchar = line[left as usize] as char; let rchar = line[right as usize] as char; - if lchar.is_ascii_punctuation() && left != x.into() { + if lchar.is_ascii_punctuation() && left != x as i32 { return Some(left as usize); } if rchar == ' ' || rchar == '\t' { @@ -521,7 +521,7 @@ impl EditBuffer { } if lchar == ' ' || lchar == '\t' { - if left + 1 == x.into() { + if left + 1 == x as i32 { right = left; continue; } @@ -541,7 +541,7 @@ impl EditBuffer { let lchar = line[left as usize] as char; let rchar = line[right as usize] as char; - if lchar.is_ascii_punctuation() && left != x.into() { + if lchar.is_ascii_punctuation() && left != x as i32 { return left as usize; } if rchar == ' ' || rchar == '\t' { @@ -551,7 +551,7 @@ impl EditBuffer { } if lchar == ' ' || lchar == '\t' { - if left + 1 == x.into() { + if left + 1 == x as i32 { right = left; continue; } diff --git a/src/utils/ui/event.rs b/src/utils/ui/event.rs index b223065..6fad963 100644 --- a/src/utils/ui/event.rs +++ b/src/utils/ui/event.rs @@ -4,7 +4,7 @@ use crate::utils::{buffer::LineState, cursor::CursorCrtl, style::StyleManager}; use super::{ mode::mode::ModeType, - uicore::{UiCore, APP_INFO, CONTENT_WINSIZE, DEF_STYLE, UI_CMD_HEIGHT}, + uicore::{UiCore, APP_INTERNAL_INFOMATION, CONTENT_WINSIZE, DEF_STYLE, UI_CMD_HEIGHT}, }; pub const TAB_STR: &'static str = " "; @@ -62,7 +62,7 @@ pub trait KeyEventCallback { let line = ui.buffer.get_line(y); if line.flags.contains(LineState::LOCKED) { - APP_INFO.lock().unwrap().info = "Row is locked".to_string(); + APP_INTERNAL_INFOMATION.lock().unwrap().info = "Row is locked".to_string(); return Ok(WarpUiCallBackType::None); } self.left(ui)?; diff --git a/src/utils/ui/mod.rs b/src/utils/ui/mod.rs index 1b7646f..ebd1132 100644 --- a/src/utils/ui/mod.rs +++ b/src/utils/ui/mod.rs @@ -9,12 +9,12 @@ pub mod mode; pub mod uicore; #[derive(Debug)] -pub struct AppInfo { +pub struct AppInternalInfomation { pub level: InfoLevel, pub info: String, } -impl AppInfo { +impl AppInternalInfomation { pub fn reset(&mut self) { self.level = InfoLevel::Info; self.info = String::new(); diff --git a/src/utils/ui/mode/mode.rs b/src/utils/ui/mode/mode.rs index 08fd05b..a83bd7f 100644 --- a/src/utils/ui/mode/mode.rs +++ b/src/utils/ui/mode/mode.rs @@ -10,7 +10,7 @@ use crate::utils::input::KeyEventType; use crate::utils::terminal::TermManager; -use crate::utils::ui::uicore::{UiCore, APP_INFO, TAB_SIZE}; +use crate::utils::ui::uicore::{UiCore, APP_INTERNAL_INFOMATION, TAB_SIZE}; use crate::utils::ui::{ event::KeyEventCallback, uicore::{CONTENT_WINSIZE, DEF_STYLE}, @@ -596,7 +596,7 @@ impl KeyEventCallback for Insert { let line = ui.buffer.get_line(line_idx); if line.flags.contains(LineState::LOCKED) { - APP_INFO.lock().unwrap().info = "Row is locked".to_string(); + APP_INTERNAL_INFOMATION.lock().unwrap().info = "Row is locked".to_string(); return Ok(WarpUiCallBackType::None); } ui.buffer.input_enter(col, line_idx); @@ -664,7 +664,7 @@ impl KeyEventCallback for Insert { let line = ui.buffer.get_line(y); if line.flags.contains(LineState::LOCKED) { - APP_INFO.lock().unwrap().info = "Row is locked".to_string(); + APP_INTERNAL_INFOMATION.lock().unwrap().info = "Row is locked".to_string(); return Ok(WarpUiCallBackType::None); } diff --git a/src/utils/ui/uicore.rs b/src/utils/ui/uicore.rs index ccf86ad..bc59032 100644 --- a/src/utils/ui/uicore.rs +++ b/src/utils/ui/uicore.rs @@ -23,7 +23,7 @@ use crate::utils::input::Input; use super::{ mode::mode::{Command, InputMode, Insert, LastLine, ModeType}, mode::normal::Normal, - AppInfo, + AppInternalInfomation, }; lazy_static! { @@ -31,10 +31,11 @@ lazy_static! { static ref INSERT: Arc = Arc::new(Insert); static ref LASTLINE: Arc = Arc::new(LastLine::new()); static ref NORMAL: Arc = Arc::new(Normal::new()); - pub static ref APP_INFO: Mutex = Mutex::new(AppInfo { - level: InfoLevel::Info, - info: String::new() - }); + pub static ref APP_INTERNAL_INFOMATION: Mutex = + Mutex::new(AppInternalInfomation { + level: InfoLevel::Info, + info: String::new() + }); } pub static TAB_SIZE: AtomicU16 = AtomicU16::new(4); @@ -108,7 +109,7 @@ impl UiCore { self.cursor.set_prefix_mode(true); self.cursor.move_to(store_x, store_y)?; - let mut info = APP_INFO.lock().unwrap(); + let mut info = APP_INTERNAL_INFOMATION.lock().unwrap(); info.level.set_style()?; self.cursor .write_with_pos(&info.info, size.cols / 3, cmd_y, false)?; diff --git a/src/view/colors/map.rs b/src/view/colors/map.rs new file mode 100644 index 0000000..b61bbdd --- /dev/null +++ b/src/view/colors/map.rs @@ -0,0 +1,100 @@ +use crossterm::style::Color; +use held_core::view::colors::Colors; +use syntect::highlighting::Theme; + +use super::to_rgb; + +pub trait ColorMap { + fn map_colors(&self, colors: Colors) -> Colors; +} + +impl ColorMap for Theme { + fn map_colors(&self, colors: Colors) -> Colors { + let fg = self.settings.foreground.map(to_rgb).unwrap_or(Color::Rgb { + r: 255, + g: 255, + b: 255, + }); + + let bg = self + .settings + .background + .map(to_rgb) + .unwrap_or(Color::Rgb { r: 0, g: 0, b: 0 }); + + let alt_bg = self + .settings + .line_highlight + .map(to_rgb) + .unwrap_or(Color::Rgb { + r: 55, + g: 55, + b: 55, + }); + + match colors { + Colors::Default => Colors::CustomForeground(fg), + Colors::Focused => Colors::Custom(fg, alt_bg), + Colors::Inverted => Colors::Custom(bg, fg), + Colors::Insert => Colors::Custom( + Color::Rgb { + r: 255, + g: 255, + b: 255, + }, + Color::Rgb { r: 0, g: 180, b: 0 }, + ), + Colors::Warning => Colors::Custom( + Color::Rgb { + r: 255, + g: 255, + b: 255, + }, + Color::Rgb { + r: 240, + g: 140, + b: 20, + }, + ), + Colors::PathMode => Colors::Custom( + Color::Rgb { + r: 255, + g: 255, + b: 255, + }, + Color::Rgb { + r: 255, + g: 20, + b: 137, + }, + ), + Colors::SearchMode => Colors::Custom( + Color::Rgb { + r: 255, + g: 255, + b: 255, + }, + Color::Rgb { + r: 120, + g: 0, + b: 120, + }, + ), + Colors::SelectMode => Colors::Custom( + Color::Rgb { + r: 255, + g: 255, + b: 255, + }, + Color::Rgb { + r: 0, + g: 120, + b: 160, + }, + ), + Colors::CustomForeground(custom_fg) => Colors::CustomForeground(custom_fg), + Colors::CustomFocusedForeground(custom_fg) => Colors::Custom(custom_fg, alt_bg), + Colors::Custom(custom_fg, custom_bg) => Colors::Custom(custom_fg, custom_bg), + } + } +} diff --git a/src/view/colors/mod.rs b/src/view/colors/mod.rs new file mode 100644 index 0000000..e50d190 --- /dev/null +++ b/src/view/colors/mod.rs @@ -0,0 +1,12 @@ +pub mod map; + +use crossterm::style::Color; +use syntect::highlighting::Color as RGBAColor; + +pub fn to_rgb(highlight_color: RGBAColor) -> Color { + Color::Rgb { + r: highlight_color.r, + g: highlight_color.g, + b: highlight_color.b, + } +} diff --git a/src/view/mod.rs b/src/view/mod.rs new file mode 100644 index 0000000..b770684 --- /dev/null +++ b/src/view/mod.rs @@ -0,0 +1,7 @@ +pub mod colors; +pub mod monitor; +pub mod presenter; +pub mod render; +pub mod status_data; +pub mod terminal; +pub mod theme_loadler; diff --git a/src/view/monitor/mod.rs b/src/view/monitor/mod.rs new file mode 100644 index 0000000..d4320a8 --- /dev/null +++ b/src/view/monitor/mod.rs @@ -0,0 +1,132 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; + +use super::{ + presenter::Presenter, + render::{render_buffer::CachedRenderBuffer, render_state::RenderState}, + terminal::{cross_terminal::CrossTerminal, Terminal}, + theme_loadler::ThemeLoader, +}; +use crate::errors::*; +use crate::modules::perferences::Perferences; +use crate::{buffer::Buffer, plugin::system::PluginSystem}; +use crossterm::event::{Event, KeyEvent}; +use scroll_controller::ScrollController; +use syntect::highlighting::{Theme, ThemeSet}; + +pub mod scroll_controller; + +/// 管理所有的显示 +pub struct Monitor { + pub terminal: Arc>, + theme_set: ThemeSet, + pub perference: Rc>, + scroll_controllers: HashMap, + render_caches: HashMap>>>, + pub last_key: Option, + pub cached_render_buffer: Rc>, + pub plugin_system: Rc>, +} + +impl Monitor { + pub fn new( + perference: Rc>, + plugin_system: Rc>, + ) -> Result { + let terminal = CrossTerminal::new()?; + let cached_render_buffer = CachedRenderBuffer::new(terminal.width()?, terminal.height()?); + let theme_set = ThemeLoader::new(perference.borrow().theme_path()?).load()?; + Ok(Monitor { + terminal: Arc::new(Box::new(terminal)), + theme_set, + perference, + scroll_controllers: HashMap::new(), + render_caches: HashMap::new(), + last_key: None, + cached_render_buffer: Rc::new(RefCell::new(cached_render_buffer)), + plugin_system, + }) + } + + pub fn init_buffer(&mut self, buffer: &mut Buffer) -> Result<()> { + let id = buffer.id()?; + self.scroll_controllers.insert( + id, + ScrollController::new(self.terminal.clone(), buffer.cursor.line), + ); + let render_cache = Rc::new(RefCell::new(HashMap::new())); + self.render_caches.insert(id, render_cache.clone()); + + // 回调清除render_cache + buffer.change_callback = Some(Box::new(move |change_position| { + render_cache + .borrow_mut() + .retain(|&k, _| k < change_position.line); + })); + Ok(()) + } + + pub fn deinit_buffer(&mut self, buffer: &Buffer) -> Result<()> { + let id = buffer.id()?; + self.scroll_controllers.remove(&id); + self.render_caches.remove(&id); + Ok(()) + } + + pub fn listen(&mut self) -> Result { + let ev = self.terminal.listen()?; + if let Event::Key(key) = ev { + self.last_key.replace(key); + } + Ok(ev) + } + + pub fn width(&self) -> Result { + self.terminal.width() + } + + pub fn height(&self) -> Result { + self.terminal.height() + } + + pub fn get_theme(&self, name: &String) -> Option { + self.theme_set.themes.get(name).cloned() + } + + pub fn first_theme(&self) -> Option { + self.theme_set + .themes + .first_key_value() + .map(|(_, v)| v.clone()) + } + + pub fn build_presenter(&mut self) -> Result { + Presenter::new(self) + } + + pub fn get_render_cache(&self, buffer: &Buffer) -> &Rc>> { + self.render_caches.get(&buffer.id.unwrap()).unwrap() + } + + pub fn get_scroll_controller(&mut self, buffer: &Buffer) -> &mut ScrollController { + self.scroll_controllers + .entry(buffer.id.unwrap()) + .or_insert(ScrollController::new(self.terminal.clone(), 0)) + } + + pub fn scroll_to_cursor(&mut self, buffer: &Buffer) -> Result<()> { + self.get_scroll_controller(buffer) + .scroll_into_monitor(buffer) + } + + pub fn scroll_to_center(&mut self, buffer: &Buffer) -> Result<()> { + self.get_scroll_controller(buffer).scroll_to_center(buffer) + } + + pub fn scroll_up(&mut self, buffer: &Buffer, count: usize) { + self.get_scroll_controller(buffer).scroll_up(count); + } + + pub fn scroll_down(&mut self, buffer: &Buffer, count: usize) { + self.get_scroll_controller(buffer).scroll_down(count); + } +} diff --git a/src/view/monitor/scroll_controller.rs b/src/view/monitor/scroll_controller.rs new file mode 100644 index 0000000..bcb7a60 --- /dev/null +++ b/src/view/monitor/scroll_controller.rs @@ -0,0 +1,55 @@ +use crate::errors::*; +use crate::{buffer::Buffer, view::terminal::Terminal}; +use std::sync::Arc; + +/// 对于滚动操作的抽象对象 +/// +/// 外部通过line_offset方法获取滚动后buffer的offset +pub struct ScrollController { + terminal: Arc>, + line_offset: usize, +} + +impl ScrollController { + pub fn new(terminal: Arc>, init_line_index: usize) -> ScrollController { + ScrollController { + terminal, + line_offset: init_line_index, + } + } + + // 若将buffer指针指向的行滚动到显示区域顶部 + pub fn scroll_into_monitor(&mut self, buffer: &Buffer) -> Result<()> { + let terminal_height = self.terminal.height()? - 1; + if self.line_offset > buffer.cursor.line { + self.line_offset = buffer.cursor.line; + } else if self.line_offset + terminal_height - 1 < buffer.cursor.line { + self.line_offset = buffer.cursor.line - terminal_height + 1; + } + Ok(()) + } + + // 将buffer指针指向的行滚动到显示区域中间区域 + pub fn scroll_to_center(&mut self, buffer: &Buffer) -> Result<()> { + self.line_offset = buffer + .cursor + .line + .saturating_sub(self.terminal.height()?.saturating_div(2)); + Ok(()) + } + + // 向上滚动n行 + pub fn scroll_up(&mut self, line_count: usize) { + self.line_offset = self.line_offset.saturating_sub(line_count); + } + + // 向下滚动n行 + pub fn scroll_down(&mut self, line_count: usize) { + self.line_offset = self.line_offset.saturating_add(line_count); + } + + // 返回当前的offset + pub fn line_offset(&mut self) -> usize { + self.line_offset + } +} diff --git a/src/view/presenter.rs b/src/view/presenter.rs new file mode 100644 index 0000000..f9888f3 --- /dev/null +++ b/src/view/presenter.rs @@ -0,0 +1,155 @@ +use std::{borrow::Cow, cell::RefCell, fmt::Debug, rc::Rc}; + +use super::{ + colors::map::ColorMap, + monitor::Monitor, + render::{ + lexeme_mapper::LexemeMapper, + render_buffer::{Cell, RenderBuffer}, + }, + status_data::StatusLineData, +}; +use crate::{ + buffer::Buffer, errors::*, util::line_iterator::LineIterator, view::render::renderer::Renderer, +}; +use held_core::{ + utils::{position::Position, range::Range}, + view::{colors::Colors, style::CharStyle}, +}; +use syntect::{highlighting::Theme, parsing::SyntaxSet}; + +pub struct Presenter<'a> { + view: &'a mut Monitor, + theme: Theme, + present_buffer: RenderBuffer<'a>, + cursor_position: Option, +} + +impl<'a> Presenter<'a> { + pub fn new(monitor: &mut Monitor) -> Result { + let theme_name = monitor.perference.borrow().theme_name(); + let mut theme = monitor + .first_theme() + .ok_or_else(|| format!("Couldn't find anyone theme"))?; + if let Some(theme_name) = theme_name { + theme = monitor + .get_theme(&theme_name) + .ok_or_else(|| format!("Couldn't find \"{}\" theme", theme_name))?; + } + let present_buffer = RenderBuffer::new( + monitor.width()?, + monitor.height()?, + monitor.cached_render_buffer.clone(), + ); + Ok(Presenter { + view: monitor, + theme, + present_buffer, + cursor_position: None, + }) + } + + pub fn set_cursor(&mut self, position: Position) { + self.cursor_position = Some(position); + } + + pub fn present(&self) -> Result<()> { + for (position, cell) in self.present_buffer.iter() { + self.view + .terminal + .print( + &position, + cell.style, + self.theme.map_colors(cell.colors), + &cell.content, + ) + .unwrap(); + } + + self.view.terminal.set_cursor(self.cursor_position)?; + self.view.terminal.present()?; + Ok(()) + } + + pub fn print_status_line(&mut self, datas: &[StatusLineData]) -> Result<()> { + let line_width = self.view.terminal.width()?; + let line = self.view.terminal.height()? - 1; + + let count = datas.len(); + let mut offset = 0; + // 从左往右输出,最后一个参数在最后 + for (index, data) in datas.iter().enumerate() { + let content = match count { + 1 => { + format!("{:width$}", data.content, width = line_width) + } + _ => { + if index == count - 1 { + format!( + "{:width$}", + data.content, + width = line_width.saturating_sub(offset) + ) + } else { + data.content.to_owned() + } + } + }; + + let len = content.len(); + warn!("line {line}, offset {offset}, content {content}"); + self.print(&Position { line, offset }, data.style, data.color, content); + offset += len; + } + + Ok(()) + } + + // 按照预设渲染buffer + pub fn print_buffer( + &mut self, + buffer: &Buffer, + buffer_data: &'a str, + syntax_set: &'a SyntaxSet, + highlights: Option<&'a [(Range, CharStyle, Colors)]>, + lexeme_mapper: Option<&'a mut dyn LexemeMapper>, + ) -> Result<()> { + let scroll_offset = self.view.get_scroll_controller(buffer).line_offset(); + let lines = LineIterator::new(&buffer_data); + + let cursor_position = Renderer::new( + buffer, + &mut self.present_buffer, + &**self.view.terminal, + &*self.view.perference.borrow(), + highlights, + self.view.get_render_cache(buffer), + &self.theme, + syntax_set, + scroll_offset, + &mut self.view.plugin_system.borrow_mut(), + ) + .render(lines, lexeme_mapper)?; + + match cursor_position { + Some(position) => self.set_cursor(position), + None => self.cursor_position = None, + } + + Ok(()) + } + + pub fn print(&mut self, position: &Position, style: CharStyle, colors: Colors, content: C) + where + C: Into> + Debug, + { + self.present_buffer.set_cell( + *position, + Cell { + content: content.into(), + style, + colors, + }, + ); + } +} diff --git a/src/view/render/lexeme_mapper.rs b/src/view/render/lexeme_mapper.rs new file mode 100644 index 0000000..d009ce8 --- /dev/null +++ b/src/view/render/lexeme_mapper.rs @@ -0,0 +1,15 @@ +use held_core::utils::position::Position; + +#[derive(Debug, PartialEq)] +pub enum MappedLexeme<'a> { + Focused(&'a str), + Blurred(&'a str), +} + +/// 词素映射器 +/// 在渲染时会优先按照词素映射器映射的风格进行映射 +/// +/// 例:按照正则表达式搜索文本时,聚焦正则匹配的部分 +pub trait LexemeMapper { + fn map<'x>(&'x mut self, lexeme: &str, position: Position) -> Vec>; +} diff --git a/src/view/render/line_number_string_iter.rs b/src/view/render/line_number_string_iter.rs new file mode 100644 index 0000000..09aeb4a --- /dev/null +++ b/src/view/render/line_number_string_iter.rs @@ -0,0 +1,34 @@ +use crate::buffer::Buffer; + +const PADDING_SIZE: usize = 2; +pub struct LineNumberStringIter { + current: usize, + max_width: usize, +} + +impl LineNumberStringIter { + pub fn new(buffer: &Buffer, offset: usize) -> LineNumberStringIter { + let line_count = buffer.line_count(); + LineNumberStringIter { + current: offset, + max_width: line_count.to_string().len(), + } + } + + pub fn width(&self) -> usize { + self.max_width + PADDING_SIZE + } +} + +impl Iterator for LineNumberStringIter { + type Item = String; + + fn next(&mut self) -> Option { + self.current += 1; + Some(format!( + " {:>width$} ", + self.current, + width = self.max_width + )) + } +} diff --git a/src/view/render/mod.rs b/src/view/render/mod.rs new file mode 100644 index 0000000..fa664d4 --- /dev/null +++ b/src/view/render/mod.rs @@ -0,0 +1,5 @@ +pub mod lexeme_mapper; +pub mod line_number_string_iter; +pub mod render_buffer; +pub mod render_state; +pub mod renderer; diff --git a/src/view/render/render_buffer.rs b/src/view/render/render_buffer.rs new file mode 100644 index 0000000..1d00906 --- /dev/null +++ b/src/view/render/render_buffer.rs @@ -0,0 +1,159 @@ +use std::{borrow::Cow, cell::RefCell, rc::Rc}; + +use held_core::view::{colors::Colors, style::CharStyle}; +use unicode_segmentation::UnicodeSegmentation; + +use held_core::utils::position::Position; + +#[derive(Debug, Clone, PartialEq)] +pub struct Cell<'a> { + pub content: Cow<'a, str>, + pub colors: Colors, + pub style: CharStyle, +} + +impl<'c> Default for Cell<'c> { + fn default() -> Self { + Self { + content: " ".into(), + colors: Default::default(), + style: Default::default(), + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct CachedCell { + pub content: String, + pub colors: Colors, + pub style: CharStyle, +} + +#[derive(Debug)] +pub struct CachedRenderBuffer { + pub cells: Vec>, +} + +impl CachedRenderBuffer { + pub fn new(width: usize, height: usize) -> CachedRenderBuffer { + CachedRenderBuffer { + cells: vec![None; width * height], + } + } + + // 返回对应index是否与cell相等 + pub fn compare_and_update(&mut self, cell: &Cell, index: usize) -> bool { + if index < self.cells.len() { + let cache = &mut self.cells[index]; + let cell_content = String::from_iter(cell.content.chars()); + + let equal = if let Some(cache) = cache { + let equal = cache.colors == cell.colors + && cache.style == cell.style + && cache.content == cell_content; + + equal + } else { + false + }; + + if !equal { + let mut cache_cell = CachedCell::default(); + let content_len = cell_content.len(); + cache_cell.colors = cell.colors; + cache_cell.style = cell.style; + cache_cell.content = cell_content; + for i in (index + 1)..(index + content_len) { + self.cells[i] = None + } + + self.cells[index] = Some(cache_cell); + } + + return equal; + } else { + return false; + } + } +} + +#[derive(Debug)] +pub struct RenderBuffer<'a> { + width: usize, + height: usize, + cells: Vec>, + cached: Rc>, +} + +impl<'a> RenderBuffer<'a> { + pub fn new( + width: usize, + height: usize, + cached: Rc>, + ) -> RenderBuffer<'a> { + RenderBuffer { + width, + height, + cells: vec![Cell::default(); width * height], + cached, + } + } + + pub fn set_cell(&mut self, position: Position, cell: Cell<'a>) { + if position.line >= self.height || position.offset >= self.width { + return; + } + let index = position.line * self.width + position.offset; + if index < self.cells.len() { + self.cells[index] = cell; + } + } + + pub fn clear(&mut self) { + self.cells = vec![Cell::default(); self.width * self.height]; + } + + pub fn iter(&self) -> RenderBufferIter { + RenderBufferIter::new(self) + } +} + +pub struct RenderBufferIter<'a> { + index: usize, + width: usize, + cells: &'a Vec>, + cached: Rc>, +} + +impl<'a> RenderBufferIter<'a> { + pub fn new(render_buffer: &'a RenderBuffer) -> RenderBufferIter<'a> { + RenderBufferIter { + index: 0, + width: render_buffer.width, + cells: &render_buffer.cells, + cached: render_buffer.cached.clone(), + } + } +} + +impl<'a> Iterator for RenderBufferIter<'a> { + type Item = (Position, &'a Cell<'a>); + + fn next(&mut self) -> Option { + while self.index < self.cells.len() { + let position = Position { + line: self.index / self.width, + offset: self.index % self.width, + }; + + let index = self.index; + let cell = &self.cells[self.index]; + self.index += cell.content.graphemes(true).count().max(1); + + if !self.cached.borrow_mut().compare_and_update(cell, index) { + return Some((position, cell)); + } + } + None + } +} diff --git a/src/view/render/render_state.rs b/src/view/render/render_state.rs new file mode 100644 index 0000000..832c58c --- /dev/null +++ b/src/view/render/render_state.rs @@ -0,0 +1,20 @@ +use syntect::{ + highlighting::{HighlightState, Highlighter}, + parsing::{ParseState, ScopeStack, SyntaxReference}, +}; + +/// 记录某一个渲染状态 +#[derive(Debug, Clone, PartialEq)] +pub struct RenderState { + pub highlight: HighlightState, + pub parse: ParseState, +} + +impl RenderState { + pub fn new(highlighter: &Highlighter, syntax: &SyntaxReference) -> RenderState { + Self { + highlight: HighlightState::new(highlighter, ScopeStack::new()), + parse: ParseState::new(syntax), + } + } +} diff --git a/src/view/render/renderer.rs b/src/view/render/renderer.rs new file mode 100644 index 0000000..317bd0d --- /dev/null +++ b/src/view/render/renderer.rs @@ -0,0 +1,521 @@ +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::str::FromStr; + +use crate::modules::perferences::Perferences; +use crate::plugin::system::PluginSystem; +use crate::view::colors::to_rgb; +use crate::{buffer::Buffer, util::line_iterator::LineIterator, view::terminal::Terminal}; +use crate::{errors::*, get_application}; +use crossterm::style::Color; +use held_core::plugin::Plugin; +use held_core::utils::position::Position; +use held_core::utils::range::Range; +use held_core::view::colors::Colors; +use held_core::view::render::ContentRenderBuffer; +use held_core::view::style::CharStyle; +use syntect::highlighting::{HighlightIterator, Highlighter, Style, Theme}; +use syntect::parsing::{ScopeStack, SyntaxSet}; + +use unicode_segmentation::UnicodeSegmentation; + +use super::line_number_string_iter::LineNumberStringIter; +use super::render_buffer::Cell; +use super::render_state::RenderState; +use super::{lexeme_mapper::LexemeMapper, render_buffer::RenderBuffer}; + +const RENDER_CACHE_FREQUENCY: usize = 100; + +pub struct Renderer<'a, 'p> { + buffer: &'a Buffer, + render_buffer: &'a mut RenderBuffer<'p>, + terminal: &'a dyn Terminal, + theme: &'a Theme, + highlight_ranges: Option<&'a [(Range, CharStyle, Colors)]>, + scroll_offset: usize, + line_number_iter: LineNumberStringIter, + content_start_of_line: usize, + cached_render_state: &'a Rc>>, + syntax_set: &'a SyntaxSet, + screen_position: Position, + buffer_position: Position, + cursor_position: Option, + current_style: Style, + perferences: &'a dyn Perferences, + plugin_system: &'a mut PluginSystem, +} + +impl<'a, 'p> Renderer<'a, 'p> { + pub fn new( + buffer: &'a Buffer, + render_buffer: &'a mut RenderBuffer<'p>, + terminal: &'a dyn Terminal, + perferences: &'a dyn Perferences, + highlight_ranges: Option<&'a [(Range, CharStyle, Colors)]>, + cached_render_state: &'a Rc>>, + theme: &'a Theme, + syntax_set: &'a SyntaxSet, + scroll_offset: usize, + plugin_system: &'a mut PluginSystem, + ) -> Renderer<'a, 'p> { + let line_number_iter = LineNumberStringIter::new(buffer, scroll_offset); + let content_start_of_line = line_number_iter.width() + 1; + Self { + buffer, + render_buffer, + terminal, + theme, + scroll_offset, + syntax_set, + cached_render_state, + screen_position: Position::default(), + buffer_position: Position::default(), + cursor_position: None, + current_style: Style::default(), + perferences, + highlight_ranges, + line_number_iter, + content_start_of_line, + plugin_system, + } + } + + pub fn render( + &mut self, + lines: LineIterator<'a>, + mut lexeme_mapper: Option<&mut dyn LexemeMapper>, + ) -> Result> { + self.terminal.set_cursor(None)?; + self.render_line_number(); + + let highlighter = Highlighter::new(&self.theme); + let syntax_definition = self + .buffer + .syntax_definition + .as_ref() + .ok_or("Buffer has no syntax definition")?; + + let focused_style = Renderer::mapper_keyword_style(&highlighter); + let blurred_style = Renderer::mapper_comment_style(&highlighter); + + let (cached_line_num, mut state) = self + .current_cached_render_state() + .unwrap_or((0, RenderState::new(&highlighter, syntax_definition))); + + for (line_num, line_data) in lines { + if line_num >= cached_line_num { + if line_num % RENDER_CACHE_FREQUENCY == 0 { + self.cached_render_state + .borrow_mut() + .insert(line_num, state.clone()); + } + + if self.before_visible() { + self.try_to_advance_to_next_line(&line_data); + continue; + } + + if self.after_visible() { + break; + } + + let events = state + .parse + .parse_line(&line_data, self.syntax_set) + .chain_err(|| "Failed to parse buffer")?; + + let styled_lexemes = + HighlightIterator::new(&mut state.highlight, &events, &line_data, &highlighter); + + for (style, lexeme) in styled_lexemes { + if let Some(ref mut mapper) = lexeme_mapper { + let mapped_lexemes = mapper.map(lexeme, self.buffer_position); + for mapped_lexeme in mapped_lexemes { + match mapped_lexeme { + super::lexeme_mapper::MappedLexeme::Focused(val) => { + self.current_style = focused_style; + self.render_lexeme(val.to_string()); + } + super::lexeme_mapper::MappedLexeme::Blurred(val) => { + self.current_style = blurred_style; + self.render_lexeme(val.to_string()); + } + } + } + } else { + self.current_style = style; + self.render_lexeme(lexeme); + } + } + } + + self.try_to_advance_to_next_line(&line_data); + } + + self.render_plugins()?; + + Ok(self.cursor_position) + } + + fn mapper_keyword_style(highlighter: &Highlighter) -> Style { + highlighter.style_for_stack( + ScopeStack::from_str("keyword") + .unwrap_or_default() + .as_slice(), + ) + } + + fn render_plugins(&mut self) -> Result<()> { + let plugin_buffers = self.plugin_system.on_render_content(); + for plugin_buffer in plugin_buffers { + self.render_plugin(plugin_buffer)?; + } + Ok(()) + } + + fn render_plugin(&mut self, buffer: ContentRenderBuffer) -> Result<()> { + let mut line = 0; + let mut offset = 0; + let init_pos = buffer.rectangle.position; + + warn!("plugin cells {:?}", buffer.cells); + for cell in buffer.cells { + if let Some(cell) = cell { + self.render_cell( + Position { + line: init_pos.line + line, + offset: init_pos.offset + offset, + }, + cell.style, + cell.colors, + cell.content.to_string(), + ); + } + + offset += 1; + if offset == buffer.rectangle.width { + offset = 0; + line += 1; + } + } + + Ok(()) + } + + fn mapper_comment_style(highlighter: &Highlighter) -> Style { + highlighter.style_for_stack( + ScopeStack::from_str("keyword") + .unwrap_or_default() + .as_slice(), + ) + } + + fn current_cached_render_state(&self) -> Option<(usize, RenderState)> { + self.cached_render_state + .borrow() + .iter() + .filter(|(k, _)| **k < self.scroll_offset) + .max_by(|a, b| a.0.cmp(b.0)) + .map(|x| (*x.0, x.1.clone())) + } + + fn after_visible(&self) -> bool { + self.screen_position.line >= (self.terminal.height().unwrap() - 1) + } + + fn before_visible(&self) -> bool { + self.buffer_position.line < self.scroll_offset + } + + fn inside_visible(&self) -> bool { + !self.before_visible() && !self.after_visible() + } + + fn set_cursor(&mut self) { + if self.inside_visible() && *self.buffer.cursor == self.buffer_position { + self.cursor_position = Some(self.screen_position); + get_application().state_data.cursor_state.screen_position = self.screen_position; + } + } + + fn on_cursor_line(&self) -> bool { + self.buffer.cursor.line == self.buffer_position.line + } + + fn try_to_advance_to_next_line(&mut self, line: &str) { + if line.chars().last().map(|x| x == '\n').unwrap_or(false) { + self.advance_to_next_line(); + } + } + + fn advance_to_next_line(&mut self) { + if self.inside_visible() { + self.set_cursor(); + self.render_rest_of_line(); + self.screen_position.line += 1; + } + + self.buffer_position.line += 1; + self.buffer_position.offset = 0; + self.render_line_number(); + } + + fn render_rest_of_line(&mut self) { + let on_cursor_line = self.on_cursor_line(); + for offset in self.screen_position.offset..self.terminal.width().unwrap() { + let colors = if on_cursor_line { + Colors::Focused + } else { + Colors::Default + }; + + self.render_cell( + Position { + line: self.screen_position.line, + offset, + }, + CharStyle::Default, + colors, + " ", + ); + } + } + + fn render_line_number(&mut self) { + if !self.inside_visible() { + return; + } + let line_number = self.line_number_iter.next().unwrap(); + let is_on_cursor_line = self.on_cursor_line(); + + let style = if is_on_cursor_line { + CharStyle::Bold + } else { + CharStyle::Default + }; + // 渲染行号 + self.render_cell( + Position { + line: self.screen_position.line, + offset: 0, + }, + style, + Colors::Focused, + line_number, + ); + + // 行号后的gap + let gap_color = if is_on_cursor_line { + Colors::Focused + } else { + Colors::Default + }; + self.render_cell( + Position { + line: self.screen_position.line, + offset: self.line_number_iter.width(), + }, + style, + gap_color, + " ", + ); + + self.screen_position.offset = self.line_number_iter.width() + 1; + } + + fn render_lexeme>>(&mut self, lexeme: T) { + for character in lexeme.into().graphemes(true) { + if character == "\n" { + continue; + } + + self.set_cursor(); + + let token_color = to_rgb(self.current_style.foreground); + let (style, color) = self.current_char_style(token_color); + + if self.perferences.line_wrapping() + && self.screen_position.offset == self.terminal.width().unwrap() - 1 + { + self.render_cell(self.screen_position, style, color, character.to_string()); + self.buffer_position.offset += 1; + + // 屏幕上换行但是渲染原来的line + let prefix_len = self.content_start_of_line; + let prefix = " ".repeat(prefix_len); + self.screen_position.offset = 0; + self.screen_position.line += 1; + self.render_cell( + Position { + line: self.screen_position.line, + offset: self.screen_position.offset, + }, + style, + Colors::Default, + prefix, + ); + self.screen_position.offset += prefix_len; + } else if character == "\t" { + let tab_len = self.perferences.tab_width(); + let width = tab_len - (self.screen_position.offset + 1) % tab_len; + let tab_str = " ".repeat(width); + self.render_lexeme(tab_str); + } else { + self.render_cell(self.screen_position, style, color, character.to_string()); + self.screen_position.offset += 1; + self.buffer_position.offset += 1; + } + + // 退出循环前更新 + self.set_cursor(); + } + } + + fn render_cell>>( + &mut self, + position: Position, + style: CharStyle, + colors: Colors, + content: C, + ) { + self.render_buffer.set_cell( + position, + Cell { + content: content.into(), + colors, + style, + }, + ); + } + + fn current_char_style(&self, token_color: Color) -> (CharStyle, Colors) { + let (style, colors) = match self.highlight_ranges { + Some(highlight_ranges) => { + for (range, style, colors) in highlight_ranges { + if range.includes(&self.buffer_position) { + // 修正背景色 + let fix_colors = if let Colors::CustomForeground(color) = colors { + if self.on_cursor_line() { + Colors::CustomFocusedForeground(*color) + } else { + *colors + } + } else { + *colors + }; + return (*style, fix_colors); + } + } + + // We aren't inside one of the highlighted areas. + // Fall back to other styling considerations. + if self.on_cursor_line() { + ( + CharStyle::Default, + Colors::CustomFocusedForeground(token_color), + ) + } else { + (CharStyle::Default, Colors::CustomForeground(token_color)) + } + } + None => { + if self.on_cursor_line() { + ( + CharStyle::Default, + Colors::CustomFocusedForeground(token_color), + ) + } else { + (CharStyle::Default, Colors::CustomForeground(token_color)) + } + } + }; + + (style, colors) + } +} + +#[cfg(test)] +mod tests { + use std::{ + cell::RefCell, + collections::HashMap, + io::{BufReader, Cursor}, + path::Path, + rc::Rc, + }; + + use syntect::{highlighting::ThemeSet, parsing::SyntaxSet}; + + use crate::{ + buffer::Buffer, + modules::perferences::DummyPerferences, + util::line_iterator::LineIterator, + view::{ + colors::map::ColorMap, + render::render_buffer::{CachedRenderBuffer, RenderBuffer}, + terminal::{cross_terminal::CrossTerminal, Terminal}, + }, + }; + + use super::Renderer; + + #[test] + fn test_display() { + let terminal = CrossTerminal::new().unwrap(); + let mut buffer = Buffer::from_file(Path::new("src/main.rs")).unwrap(); + let mut render_buffer = RenderBuffer::new( + terminal.width().unwrap(), + terminal.height().unwrap(), + Rc::new(RefCell::new(CachedRenderBuffer::new( + terminal.width().unwrap(), + terminal.height().unwrap(), + ))), + ); + let perferences = DummyPerferences; + let cached_render_state = Rc::new(RefCell::new(HashMap::new())); + + let mut reader = BufReader::new(Cursor::new(include_str!( + "../../themes/solarized_dark.tmTheme" + ))); + let theme = ThemeSet::load_from_reader(&mut reader).unwrap(); + let syntax_set = SyntaxSet::load_defaults_newlines(); + let definition = buffer + .file_extension() + .and_then(|ex| syntax_set.find_syntax_by_extension(&ex)) + .or_else(|| Some(syntax_set.find_syntax_plain_text())) + .cloned(); + + buffer.syntax_definition = definition; + let binding = buffer.data(); + { + let mut renderer = Renderer::new( + &buffer, + &mut render_buffer, + &terminal, + &perferences, + None, + &cached_render_state, + &theme, + &syntax_set, + 0, + todo!(), + ); + renderer.render(LineIterator::new(&binding), None).unwrap(); + } + + for (position, cell) in render_buffer.iter() { + terminal + .print( + &position, + cell.style, + theme.map_colors(cell.colors), + &cell.content, + ) + .unwrap(); + } + + terminal.present().unwrap(); + } +} diff --git a/src/view/status_data.rs b/src/view/status_data.rs new file mode 100644 index 0000000..7887068 --- /dev/null +++ b/src/view/status_data.rs @@ -0,0 +1,37 @@ +use held_core::view::{colors::Colors, style::CharStyle}; + +use crate::buffer::Buffer; + +pub struct StatusLineData { + pub content: String, + pub color: Colors, + pub style: CharStyle, +} + +pub fn buffer_status_data(buffer: &Option) -> StatusLineData { + if let Some(buffer) = buffer { + let modified = buffer.modified(); + let (title, style) = buffer + .path + .as_ref() + .map(|path| { + if modified { + (format!(" {}*", path.to_string_lossy()), CharStyle::Bold) + } else { + (format!(" {}", path.to_string_lossy()), CharStyle::Default) + } + }) + .unwrap_or_default(); + StatusLineData { + content: title, + color: Colors::Focused, + style, + } + } else { + StatusLineData { + content: String::new(), + color: Colors::Focused, + style: CharStyle::Default, + } + } +} diff --git a/src/view/terminal/cross_terminal.rs b/src/view/terminal/cross_terminal.rs new file mode 100644 index 0000000..dcc3ff9 --- /dev/null +++ b/src/view/terminal/cross_terminal.rs @@ -0,0 +1,186 @@ +use std::{ + cell::{RefCell, RefMut}, + io::{stdout, Write}, +}; + +use crossterm::{ + event::Event, + terminal::{self, disable_raw_mode}, + QueueableCommand, +}; +use held_core::{ + utils::position::Position, + view::{colors::Colors, style::CharStyle}, +}; + +use super::{Terminal, MIN_HEIGHT, MIN_WIDTH, TERMINAL_EXECUTE_ERROR}; +use crate::errors::*; + +#[derive(Debug)] +pub struct CrossTerminal { + ansi_buffer: RefCell>, +} + +unsafe impl Send for CrossTerminal {} +unsafe impl Sync for CrossTerminal {} + +impl CrossTerminal { + pub fn new() -> Result { + crossterm::terminal::enable_raw_mode()?; + let terminal = CrossTerminal { + ansi_buffer: RefCell::default(), + }; + terminal.clear()?; + terminal.present()?; + Ok(terminal) + } + + fn buffer(&self) -> RefMut> { + return self.ansi_buffer.borrow_mut(); + } + + fn update_style(&self, char_style: CharStyle, colors: Colors) -> Result<()> { + self.buffer().queue(crossterm::style::SetAttribute( + crossterm::style::Attribute::Reset, + ))?; + + match char_style { + CharStyle::Default => { + self.buffer().queue(crossterm::style::SetAttribute( + crossterm::style::Attribute::Reset, + ))?; + } + CharStyle::Bold => { + self.buffer().queue(crossterm::style::SetAttribute( + crossterm::style::Attribute::Bold, + ))?; + } + CharStyle::Reverse => { + self.buffer().queue(crossterm::style::SetAttribute( + crossterm::style::Attribute::Reverse, + ))?; + } + CharStyle::Italic => { + self.buffer().queue(crossterm::style::SetAttribute( + crossterm::style::Attribute::Italic, + ))?; + } + } + + match colors { + Colors::Default => { + self.buffer().queue(crossterm::style::ResetColor)?; + } + Colors::CustomForeground(color) => { + self.buffer() + .queue(crossterm::style::SetForegroundColor(color))?; + } + Colors::Custom(fg, bg) => { + self.buffer() + .queue(crossterm::style::SetForegroundColor(fg))?; + self.buffer() + .queue(crossterm::style::SetBackgroundColor(bg))?; + } + _ => { + unreachable!(); + } + } + + Ok(()) + } +} + +impl Terminal for CrossTerminal { + fn listen(&self) -> Result { + crossterm::event::read().chain_err(|| "Handle event io error") + } + + fn clear(&self) -> Result<()> { + self.buffer() + .queue(crossterm::style::SetAttribute( + crossterm::style::Attribute::Reset, + )) + .chain_err(|| TERMINAL_EXECUTE_ERROR) + .map(|_| ())?; + self.buffer() + .queue(terminal::Clear(terminal::ClearType::All)) + .chain_err(|| TERMINAL_EXECUTE_ERROR) + .map(|_| ())?; + Ok(()) + } + + fn present(&self) -> Result<()> { + stdout().write_all(&self.buffer())?; + stdout().flush()?; + self.buffer().clear(); + Ok(()) + } + + fn width(&self) -> Result { + let (width, _) = crossterm::terminal::size()?; + return Ok(width.max(MIN_WIDTH).into()); + } + + fn height(&self) -> Result { + let (_, height) = crossterm::terminal::size()?; + return Ok(height.max(MIN_HEIGHT).into()); + } + + fn set_cursor(&self, position: Option) -> Result<()> { + match position { + Some(position) => { + self.buffer() + .queue(crossterm::cursor::MoveTo( + position.offset as u16, + position.line as u16, + )) + .unwrap() + .queue(crossterm::cursor::Show) + .chain_err(|| TERMINAL_EXECUTE_ERROR)?; + } + None => { + self.buffer() + .queue(crossterm::cursor::Hide) + .chain_err(|| TERMINAL_EXECUTE_ERROR)?; + } + } + + Ok(()) + } + + fn set_cursor_type(&self, cursor_type: crossterm::cursor::SetCursorStyle) -> Result<()> { + self.buffer() + .queue(cursor_type) + .chain_err(|| TERMINAL_EXECUTE_ERROR) + .map(|_| ()) + } + + fn print( + &self, + position: &Position, + char_style: CharStyle, + colors: Colors, + content: &str, + ) -> Result<()> { + self.update_style(char_style, colors)?; + self.set_cursor(Some(*position))?; + self.buffer().queue(crossterm::style::Print(content))?; + Ok(()) + } + + fn suspend(&self) { + let _ = self.clear(); + let _ = self.set_cursor(Some(Position::from((0, 0)))); + let _ = stdout().write_all(&self.buffer()); + let _ = stdout().flush(); + + self.buffer().clear(); + } +} + +impl Drop for CrossTerminal { + fn drop(&mut self) { + self.suspend(); + let _ = disable_raw_mode(); + } +} diff --git a/src/view/terminal/mod.rs b/src/view/terminal/mod.rs new file mode 100644 index 0000000..f37a71a --- /dev/null +++ b/src/view/terminal/mod.rs @@ -0,0 +1,27 @@ +use std::fmt::Debug; + +use crate::errors::*; +use crossterm::event::Event; +use held_core::utils::position::Position; +use held_core::view::colors::Colors; +use held_core::view::style::CharStyle; + +pub mod cross_terminal; + +pub(super) const MIN_WIDTH: u16 = 10; +pub(super) const MIN_HEIGHT: u16 = 10; + +pub const TERMINAL_EXECUTE_ERROR: &'static str = "Terminal IO Error"; + +#[allow(dead_code)] +pub trait Terminal: Send + Sync + Debug { + fn listen(&self) -> Result; + fn clear(&self) -> Result<()>; + fn present(&self) -> Result<()>; + fn width(&self) -> Result; + fn height(&self) -> Result; + fn set_cursor(&self, _: Option) -> Result<()>; + fn set_cursor_type(&self, _: crossterm::cursor::SetCursorStyle) -> Result<()>; + fn print(&self, _: &Position, _: CharStyle, _: Colors, _: &str) -> Result<()>; + fn suspend(&self); +} diff --git a/src/view/theme_loadler.rs b/src/view/theme_loadler.rs new file mode 100644 index 0000000..074ceb6 --- /dev/null +++ b/src/view/theme_loadler.rs @@ -0,0 +1,81 @@ +use crate::errors::*; +use app_dirs2::{app_dir, AppDataType}; +use error_chain::bail; +use std::{ + collections::BTreeMap, + ffi::OsStr, + fs::File, + io::{BufReader, Cursor, Read, Seek}, + path::PathBuf, +}; +use syntect::highlighting::{Theme, ThemeSet}; + +pub struct ThemeLoader { + path: PathBuf, + themes: BTreeMap, +} + +impl ThemeLoader { + pub fn new(path: PathBuf) -> ThemeLoader { + ThemeLoader { + path, + themes: BTreeMap::new(), + } + } + + pub fn load(mut self) -> Result { + self.load_default(); + #[cfg(not(feature = "dragonos"))] + { + self.load_user()?; + } + + Ok(ThemeSet { + themes: self.themes, + }) + } + + fn load_user(&mut self) -> Result<()> { + let dir = self.path.read_dir()?; + + let entries = dir + .filter_map(|f| f.ok()) + .map(|f| f.path()) + .filter(|f| f.is_file()) + .filter(|f| f.extension() == Some(OsStr::new("tmTheme"))); + + for entry in entries { + if let Ok(file) = File::open(&entry) { + if let Some(name) = entry.file_stem() { + if let Some(name) = name.to_str() { + self.add_theme(name.into(), file); + } + } + } + } + + Ok(()) + } + + fn load_default(&mut self) { + self.add_theme( + "solarized_dark".into(), + Cursor::new(include_str!("../themes/solarized_dark.tmTheme")), + ); + } + + fn add_theme(&mut self, name: String, data: D) { + let mut reader = BufReader::new(data); + + match ThemeSet::load_from_reader(&mut reader) { + Ok(theme) => { + self.themes + .insert(theme.name.clone().unwrap_or(name).to_owned(), theme); + } + Err(e) => { + // 主题加载出错不必直接退出 + error!("Failed load theme: {name:?},error: {e:?}"); + } + }; + } +} diff --git a/src/workspace.rs b/src/workspace.rs new file mode 100644 index 0000000..7371eca --- /dev/null +++ b/src/workspace.rs @@ -0,0 +1,186 @@ +use std::{ + cell::Ref, + collections::HashMap, + env, + os::unix::fs::MetadataExt, + path::{Path, PathBuf}, +}; + +use crate::{ + errors::*, + modules::perferences::{Perferences, PerferencesManager}, + view::monitor::Monitor, +}; +use syntect::parsing::SyntaxSet; + +use crate::buffer::Buffer; + +pub struct Workspace { + pub path: PathBuf, + buffers: HashMap, + // ino -> id + buffers_ino_map: HashMap, + pub current_buffer: Option, + pub syntax_set: SyntaxSet, + buffer_ida: usize, +} + +impl Workspace { + pub fn create_workspace( + monitor: &mut Monitor, + perferences: Ref, + args: &[String], + ) -> Result { + let mut path_args = args.iter().skip(1).peekable(); + + let initial_dir = env::current_dir()?; + // 若第一个参数为dir,则修改工作区 + if let Some(dir) = path_args.peek() { + let path = Path::new(dir); + if path.is_dir() { + env::set_current_dir(path.canonicalize()?)?; + } + } + let workspace_dir = env::current_dir()?; + #[cfg(feature = "dragonos")] + let syntax_path: Option = None; + #[cfg(not(feature = "dragonos"))] + let syntax_path = PerferencesManager::user_syntax_path().map(Some)?; + let mut workspace = Workspace::new(&workspace_dir, syntax_path.as_deref())?; + + if workspace_dir != initial_dir { + path_args.next(); + } + + for path_str in path_args { + let path = Path::new(path_str); + if path.is_dir() { + continue; + } + + let syntax_ref = perferences + .syntax_definition_name(&path) + .and_then(|name| workspace.syntax_set.find_syntax_by_name(&name).cloned()); + + let buffer = if path.exists() { + let mut buffer = Buffer::from_file(&path)?; + buffer.syntax_definition = syntax_ref; + buffer + } else { + let mut buffer = Buffer::new(); + buffer.syntax_definition = syntax_ref; + + if path.is_absolute() { + buffer.path = Some(path.to_path_buf()); + } else { + buffer.path = Some(workspace_dir.join(path)) + } + buffer + }; + + workspace.add_buffer(buffer); + monitor.init_buffer(workspace.current_buffer.as_mut().unwrap())?; + } + + Ok(workspace) + } + + fn new(path: &Path, syntax_definitions: Option<&Path>) -> Result { + let mut syntax_set = SyntaxSet::load_defaults_newlines(); + if let Some(syntax_definitions) = syntax_definitions { + if syntax_definitions.is_dir() { + if syntax_definitions.read_dir()?.count() > 0 { + let mut builder = syntax_set.into_builder(); + builder.add_from_folder(syntax_definitions, true)?; + syntax_set = builder.build(); + } + } + } + + Ok(Workspace { + path: path.canonicalize()?, + buffers: HashMap::new(), + buffers_ino_map: HashMap::new(), + current_buffer: None, + syntax_set, + buffer_ida: 0, + }) + } + + pub fn add_buffer(&mut self, mut buffer: Buffer) -> usize { + let id = self.alloc_buffer_id(); + buffer.id = Some(id); + + if let Some(ref path) = buffer.path { + if let Ok(metadata) = path.metadata() { + self.buffers_ino_map.insert(metadata.ino(), id); + } + } + + self.buffers.insert(id, buffer); + self.select_buffer(id); + + if let Some(buffer) = self.current_buffer.as_ref() { + if buffer.syntax_definition.is_none() { + let _ = self.update_current_syntax(); + } + } + + return id; + } + + fn alloc_buffer_id(&mut self) -> usize { + self.buffer_ida += 1; + self.buffer_ida + } + + pub fn get_buffer(&self, id: usize) -> Option<&Buffer> { + if let Some(ref buffer) = self.current_buffer { + if buffer.id.unwrap() == id { + return Some(buffer); + } + } + return self.buffers.get(&id); + } + + pub fn get_buffer_with_ino(&self, ino: u64) -> Option<&Buffer> { + if let Some(id) = self.buffers_ino_map.get(&ino) { + return self.get_buffer(*id); + } + None + } + + pub fn select_buffer(&mut self, id: usize) -> bool { + // 将当前buffer放回 + if let Some(buffer) = self.current_buffer.take() { + if buffer.id.unwrap() == id { + self.current_buffer = Some(buffer); + return true; + } + self.buffers.insert(buffer.id.unwrap(), buffer); + } + + // 选择新buffer + if let Some(buffer) = self.buffers.remove(&id) { + self.current_buffer = Some(buffer); + return true; + } + + false + } + + pub fn update_current_syntax(&mut self) -> Result<()> { + let buffer = self + .current_buffer + .as_mut() + .ok_or(ErrorKind::EmptyWorkspace)?; + let definition = buffer + .file_extension() + .and_then(|ex| self.syntax_set.find_syntax_by_extension(&ex)) + .or_else(|| Some(self.syntax_set.find_syntax_plain_text())) + .cloned(); + buffer.syntax_definition = definition; + + Ok(()) + } +} diff --git a/test/test_render_plugin/Cargo.toml b/test/test_render_plugin/Cargo.toml new file mode 100644 index 0000000..18320c2 --- /dev/null +++ b/test/test_render_plugin/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test_render_plugin" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib", "dylib", "staticlib"] + +[dependencies] +held_core = { path = "../../held_core" } \ No newline at end of file diff --git a/test/test_render_plugin/src/lib.rs b/test/test_render_plugin/src/lib.rs new file mode 100644 index 0000000..c12346e --- /dev/null +++ b/test/test_render_plugin/src/lib.rs @@ -0,0 +1,53 @@ +use held_core::{ + declare_plugin, interface, + plugin::Plugin, + utils::{position::Position, rectangle::Rectangle}, + view::{colors::Colors, render::ContentRenderBuffer, style::CharStyle}, +}; + +declare_plugin!(RenderTestPlugin, RenderTestPlugin::new); + +struct RenderTestPlugin; + +impl RenderTestPlugin { + fn new() -> RenderTestPlugin { + RenderTestPlugin + } +} + +impl Plugin for RenderTestPlugin { + fn name(&self) -> &'static str { + "render test plugin" + } + + fn init(&self) {} + + fn deinit(&self) {} + + fn on_render_content(&self) -> Vec { + let cursor_position = interface::cursor::screen_cursor_position(); + + let width = 10; + let mut buffer = ContentRenderBuffer::new(Rectangle { + position: Position { + line: cursor_position.line.saturating_sub(1), + offset: cursor_position.offset - width / 2, + }, + width, + height: 1, + }); + + let buffer_str = format!("{}:{}", cursor_position.offset, cursor_position.line); + buffer.put_buffer( + Position { + line: 0, + offset: (width - buffer_str.len()) / 2, + }, + buffer_str, + CharStyle::Bold, + Colors::Warning, + ); + + vec![buffer] + } +}