From 44ff4d3c1f5da05e57ce99ba9d67b80a334def83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 25 May 2020 13:02:21 +0900 Subject: [PATCH] Implement a new core based on CodeMirror. --- Cargo.lock | 227 ++++++++------------------- README.md | 1 + helix-core/Cargo.toml | 2 + helix-core/src/buffer.rs | 18 +++ helix-core/src/lib.rs | 16 +- helix-core/src/position.rs | 27 ---- helix-core/src/range.rs | 9 -- helix-core/src/selection.rs | 222 +++++++++++++++++++++++++++ helix-core/src/state.rs | 38 +++++ helix-core/src/transaction.rs | 25 +++ helix-term/Cargo.toml | 5 +- helix-term/src/editor.rs | 281 ++++++++++++++++++++++++++++++++++ helix-term/src/main.rs | 233 ++-------------------------- 13 files changed, 679 insertions(+), 425 deletions(-) create mode 100644 helix-core/src/buffer.rs delete mode 100644 helix-core/src/position.rs delete mode 100644 helix-core/src/range.rs create mode 100644 helix-core/src/selection.rs create mode 100644 helix-core/src/state.rs create mode 100644 helix-core/src/transaction.rs create mode 100644 helix-term/src/editor.rs diff --git a/Cargo.lock b/Cargo.lock index 3d182e52..defeb699 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,21 +21,41 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b585a98a234c46fc563103e9278c9391fde1f4e6850334da895d27edb9580f62" +[[package]] +name = "argh" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca1877e24cecacd700d469066e0160c4f8497cc5635367163f50c8beec820154" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e742194e0f43fc932bcb801708c2b279d3ec8f527e3acda05a6a9f342c5ef764" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2 1.0.13", + "quote 1.0.6", + "syn 1.0.22", +] + +[[package]] +name = "argh_shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ba68f4276a778591e36a0c348a269888f3a177c8d2054969389e3b59611ff5" + [[package]] name = "arrayref" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" -[[package]] -name = "arrayvec" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" -dependencies = [ - "nodrop", -] - [[package]] name = "arrayvec" version = "0.5.1" @@ -76,7 +96,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" dependencies = [ "arrayref", - "arrayvec 0.5.1", + "arrayvec", "constant_time_eq", ] @@ -140,11 +160,9 @@ dependencies = [ [[package]] name = "filedescriptor" version = "0.7.1" -source = "git+https://github.com/wez/wezterm#58686f925f0f4a0942452e8feb0ababd48ec936c" dependencies = [ "anyhow", "libc", - "thiserror", "winapi", ] @@ -165,15 +183,31 @@ dependencies = [ "wasi", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "helix-core" version = "0.1.0" +dependencies = [ + "anyhow", + "ropey", + "smallvec 1.4.0", +] [[package]] name = "helix-term" version = "0.1.0" dependencies = [ "anyhow", + "argh", + "helix-core", "termwiz", ] @@ -183,20 +217,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lexical-core" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86d66d380c9c5a685aaac7a11818bdfa1f733198dfd9ec09c70b762cd12ad6f" -dependencies = [ - "arrayvec 0.4.12", - "bitflags", - "cfg-if", - "rustc_version", - "ryu", - "static_assertions", -] - [[package]] name = "libc" version = "0.2.70" @@ -230,58 +250,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "nom" version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" dependencies = [ - "lexical-core", "memchr", "version_check", ] -[[package]] -name = "num" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" -dependencies = [ - "autocfg", - "num-traits", -] - [[package]] name = "num-derive" version = "0.2.5" @@ -293,39 +271,6 @@ dependencies = [ "syn 0.15.44", ] -[[package]] -name = "num-integer" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" -dependencies = [ - "autocfg", - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.11" @@ -510,6 +455,15 @@ version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" +[[package]] +name = "ropey" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba326a8508a4add47e7b260333aa2d896213a5f3572fde11ed6e9130241b7f71" +dependencies = [ + "smallvec 0.6.13", +] + [[package]] name = "rust-argon2" version = "0.7.0" @@ -522,21 +476,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver", -] - -[[package]] -name = "ryu" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1" - [[package]] name = "semver" version = "0.9.0" @@ -552,26 +491,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -[[package]] -name = "serde" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" -dependencies = [ - "proc-macro2 1.0.13", - "quote 1.0.6", - "syn 1.0.22", -] - [[package]] name = "signal-hook" version = "0.1.15" @@ -608,10 +527,10 @@ dependencies = [ ] [[package]] -name = "static_assertions" -version = "0.3.4" +name = "smallvec" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" +checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" [[package]] name = "syn" @@ -637,9 +556,9 @@ dependencies = [ [[package]] name = "terminfo" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12064715207074ac562f450722884f981268a916ce1eb21c5bda2c806c8fecfc" +checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e" dependencies = [ "dirs", "fnv", @@ -660,7 +579,6 @@ dependencies = [ [[package]] name = "termwiz" version = "0.9.0" -source = "git+https://github.com/wez/wezterm#58686f925f0f4a0942452e8feb0ababd48ec936c" dependencies = [ "anyhow", "base64 0.10.1", @@ -672,15 +590,13 @@ dependencies = [ "libc", "log", "memmem", - "num", "num-derive", "num-traits", "ordered-float", "regex", "semver", - "serde", "signal-hook", - "smallvec", + "smallvec 0.6.13", "terminfo", "termios", "unicode-segmentation", @@ -690,26 +606,6 @@ dependencies = [ "xi-unicode", ] -[[package]] -name = "thiserror" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5976891d6950b4f68477850b5b9e5aa64d955961466f9e174363f573e54e8ca7" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab81dbd1cd69cd2ce22ecfbdd3bdb73334ba25350649408cc6c085f46d89573d" -dependencies = [ - "proc-macro2 1.0.13", - "quote 1.0.6", - "syn 1.0.22", -] - [[package]] name = "thread_local" version = "1.0.1" @@ -758,7 +654,6 @@ checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" [[package]] name = "vtparse" version = "0.2.2" -source = "git+https://github.com/wez/wezterm#58686f925f0f4a0942452e8feb0ababd48ec936c" dependencies = [ "utf8parse", ] diff --git a/README.md b/README.md index 70eca234..325ff522 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,4 @@ conceal for markdown markers, etc codemirror uses offsets exclusively with Line being computed when necessary (with start/end extents) +lines are temporarily cached in a lineCache diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 446e8e42..f1f6264f 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -8,4 +8,6 @@ edition = "2018" [dependencies] ropey = "1.1.0" +anyhow = "1.0.31" +smallvec = "1.4.0" # slab = "0.4.2" diff --git a/helix-core/src/buffer.rs b/helix-core/src/buffer.rs new file mode 100644 index 00000000..9dd22773 --- /dev/null +++ b/helix-core/src/buffer.rs @@ -0,0 +1,18 @@ +use anyhow::Error; +use ropey::Rope; +use std::{env, fs::File, io::BufReader, path::PathBuf}; + +pub struct Buffer { + pub contents: Rope, +} + +impl Buffer { + pub fn load(path: PathBuf) -> Result { + let current_dir = env::current_dir()?; + + let contents = Rope::from_reader(BufReader::new(File::open(path)?))?; + + // TODO: create if not found + Ok(Buffer { contents }) + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 71a66030..ceed961f 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,5 +1,13 @@ -mod position; -mod range; +mod buffer; +mod selection; +mod state; +mod transaction; -use position::Position; -use range::Range; +pub use buffer::Buffer; + +pub use selection::Range as SelectionRange; +pub use selection::Selection; + +pub use state::State; + +pub use transaction::{Change, Transaction}; diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs deleted file mode 100644 index 8c82b83b..00000000 --- a/helix-core/src/position.rs +++ /dev/null @@ -1,27 +0,0 @@ -/// Represents a single point in a text buffer. Zero indexed. -#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Position { - pub row: usize, - pub col: usize, -} - -impl Position { - pub fn new(row: usize, col: usize) -> Self { - Self { row, col } - } - - pub fn is_zero(self) -> bool { - self.row == 0 && self.col == 0 - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_ordering() { - // (0, 5) is less than (1, 0 w v f) - assert!(Position::new(0, 5) < Position::new(1, 0)); - } -} diff --git a/helix-core/src/range.rs b/helix-core/src/range.rs deleted file mode 100644 index 46411664..00000000 --- a/helix-core/src/range.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::Position; - -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct Range { - pub start: Position, - pub end: Position, -} - -// range traversal iters diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs new file mode 100644 index 00000000..24a8be46 --- /dev/null +++ b/helix-core/src/selection.rs @@ -0,0 +1,222 @@ +//! Selections are the primary editing construct. Even a single cursor is defined as an empty +//! single selection range. +//! +//! All positioning is done via `char` offsets into the buffer. +use smallvec::{smallvec, SmallVec}; + +#[inline] +fn abs_difference(x: usize, y: usize) -> usize { + if x < y { + y - x + } else { + x - y + } +} + +/// A single selection range. Anchor-inclusive, head-exclusive. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Range { + // TODO: optimize into u32 + /// The anchor of the range: the side that doesn't move when extending. + pub anchor: usize, + /// The head of the range, moved when extending. + pub head: usize, +} + +impl Range { + pub fn new(anchor: usize, head: usize) -> Self { + Self { anchor, head } + } + + /// Start of the range. + #[inline] + pub fn from(&self) -> usize { + std::cmp::min(self.anchor, self.head) + } + + /// End of the range. + #[inline] + pub fn to(&self) -> usize { + std::cmp::max(self.anchor, self.head) + } + + /// `true` when head and anchor are at the same position. + #[inline] + pub fn is_empty(&self) -> bool { + self.anchor == self.head + } + + /// Check two ranges for overlap. + pub fn overlaps(&self, other: &Self) -> bool { + // cursor overlap is checked differently + if self.is_empty() { + self.from() <= other.to() + } else { + self.from() < other.to() + } + } + + // TODO: map + + /// Extend the range to cover at least `from` `to`. + pub fn extend(&self, from: usize, to: usize) -> Self { + if from <= self.anchor && to >= self.anchor { + return Range { + anchor: from, + head: to, + }; + } + + Range { + anchor: self.anchor, + head: if abs_difference(from, self.anchor) > abs_difference(to, self.anchor) { + from + } else { + to + }, + } + } + + // groupAt +} + +/// A selection consists of one or more selection ranges. +pub struct Selection { + // TODO: decide how many ranges to inline SmallVec<[Range; 1]> + ranges: Vec, + primary_index: usize, +} + +impl Selection { + // map + // eq + pub fn primary(&self) -> Range { + self.ranges[self.primary_index] + } + + /// Ensure selection containing only the primary selection. + pub fn as_single(self) -> Self { + if self.ranges.len() == 1 { + self + } else { + Self { + ranges: vec![self.ranges[self.primary_index]], + primary_index: 0, + } + } + } + + // add_range // push + // replace_range + + /// Constructs a selection holding a single range. + pub fn single(anchor: usize, head: usize) -> Self { + Self { + ranges: vec![Range { anchor, head }], + primary_index: 0, + } + } + + pub fn new(ranges: Vec, primary_index: usize) -> Self { + fn normalize(mut ranges: Vec, primary_index: usize) -> Selection { + let primary = ranges[primary_index]; + ranges.sort_unstable_by_key(|range| range.from()); + let mut primary_index = ranges.iter().position(|&range| range == primary).unwrap(); + + let mut result: Vec = Vec::new(); + + // TODO: we could do with one vec by removing elements as we mutate + + for (i, range) in ranges.into_iter().enumerate() { + // if previous value exists + if let Some(prev) = result.last_mut() { + // and we overlap it + if range.overlaps(prev) { + let from = prev.from(); + let to = std::cmp::max(range.to(), prev.to()); + + if i <= primary_index { + primary_index -= 1 + } + + // merge into previous + if range.anchor > range.head { + prev.anchor = to; + prev.head = from; + } else { + prev.anchor = from; + prev.head = to; + } + continue; + } + } + + result.push(range) + } + + Selection { + ranges: result, + primary_index, + } + } + + // TODO: only normalize if needed (any ranges out of order) + normalize(ranges, primary_index) + } +} + +// TODO: checkSelection -> check if valid for doc length + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_create_normalizes_and_merges() { + let sel = Selection::new( + vec![ + Range::new(10, 12), + Range::new(6, 7), + Range::new(4, 5), + Range::new(3, 4), + Range::new(0, 6), + Range::new(7, 8), + Range::new(9, 13), + Range::new(13, 14), + ], + 0, + ); + + let res = sel + .ranges + .into_iter() + .map(|range| format!("{}/{}", range.anchor, range.head)) + .collect::>() + .join(","); + + assert_eq!(res, "0/6,6/7,7/8,9/13,13/14"); + } + + #[test] + fn test_create_merges_adjacent_points() { + let sel = Selection::new( + vec![ + Range::new(10, 12), + Range::new(12, 12), + Range::new(12, 12), + Range::new(10, 10), + Range::new(8, 10), + ], + 0, + ); + + let res = sel + .ranges + .into_iter() + .map(|range| format!("{}/{}", range.anchor, range.head)) + .collect::>() + .join(","); + + assert_eq!(res, "8/10,10/12"); + } +} diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs new file mode 100644 index 00000000..81b8e108 --- /dev/null +++ b/helix-core/src/state.rs @@ -0,0 +1,38 @@ +use crate::{Buffer, Selection}; + +/// A state represents the current editor state of a single buffer. +pub struct State { + // TODO: maybe doc: ? + buffer: Buffer, + selection: Selection, +} + +impl State { + pub fn new(buffer: Buffer) -> Self { + Self { + buffer, + selection: Selection::single(0, 0), + } + } + + // TODO: buf/selection accessors + + // update/transact + // replaceSelection (transaction that replaces selection) + // changeByRange + // changes + // slice + // + // getters: + // tabSize + // indentUnit + // languageDataAt() + // + // config: + // indentation + // tabSize + // lineUnit + // syntax + // foldable + // changeFilter/transactionFilter +} diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs new file mode 100644 index 00000000..ecbe0c50 --- /dev/null +++ b/helix-core/src/transaction.rs @@ -0,0 +1,25 @@ +pub struct Change { + from: usize, + to: usize, + insert: Option, +} + +impl Change { + pub fn new(from: usize, to: usize, insert: Option) { + // old_extent, new_extent, insert + } +} + +pub struct Transaction {} + +// ChangeSpec = Change | ChangeSet | Vec +// ChangeDesc as a ChangeSet without text: can't be applied, cheaper to store. +// ChangeSet = ChangeDesc with Text +pub struct ChangeSet { + // basically Vec where ChangeDesc = (current len, replacement len?) + // (0, n>0) for insertion, (n>0, 0) for deletion, (>0, >0) for replacement + sections: Vec<(usize, isize)>, +} +// +// trait Transaction +// trait StrictTransaction diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index f6528034..ea4d5b55 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -15,5 +15,8 @@ path = "src/main.rs" # path = "src/line.rs" [dependencies] -termwiz = { git = "https://github.com/wez/wezterm", features = ["widgets"] } +# termwiz = { git = "https://github.com/wez/wezterm", features = ["widgets"] } +termwiz = { path = "../../wezterm/termwiz", default-features = false, features = ["widgets"] } anyhow = "1.0.31" +argh = "0.1.3" +helix-core = { path = "../helix-core" } diff --git a/helix-term/src/editor.rs b/helix-term/src/editor.rs new file mode 100644 index 00000000..54c70e1b --- /dev/null +++ b/helix-term/src/editor.rs @@ -0,0 +1,281 @@ +#![allow(unused)] +use anyhow::Error; +use termwiz::caps::Capabilities; +use termwiz::cell::AttributeChange; +use termwiz::color::{AnsiColor, ColorAttribute, RgbColor}; +use termwiz::input::*; +use termwiz::surface::Change; +use termwiz::terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal}; +use termwiz::widgets::*; + +use crate::Args; + +use std::{env, path::PathBuf}; + +use helix_core::Buffer; + +/// This is a widget for our application +pub struct MainScreen {} + +impl MainScreen { + pub fn new() -> Self { + Self {} + } +} + +impl Widget for MainScreen { + fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { + true // handled it all + } + + /// Draw ourselves into the surface provided by RenderArgs + fn render(&mut self, args: &mut RenderArgs) { + // args.surface.add_change(Change::ClearScreen( + // ColorAttribute::TrueColorWithPaletteFallback( + // RgbColor::new(0x31, 0x1B, 0x92), + // AnsiColor::Black.into(), + // ), + // )); + // args.surface + // .add_change(Change::Attribute(AttributeChange::Foreground( + // ColorAttribute::TrueColorWithPaletteFallback( + // RgbColor::new(0xB3, 0x88, 0xFF), + // AnsiColor::Purple.into(), + // ), + // ))); + } + + fn get_size_constraints(&self) -> layout::Constraints { + let mut constraints = layout::Constraints::default(); + constraints.child_orientation = layout::ChildOrientation::Vertical; + constraints + } +} + +pub struct BufferComponent<'a> { + text: String, + buffer: &'a mut Buffer, + + first_line: usize, +} + +impl<'a> BufferComponent<'a> { + /// Initialize the widget with the input text + pub fn new(buffer: &'a mut Buffer) -> Self { + Self { + buffer, + text: String::new(), + + first_line: 0, + } + } +} + +impl<'a> Widget for BufferComponent<'a> { + fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { + match event { + WidgetEvent::Input(InputEvent::Key(KeyEvent { + key: KeyCode::Char('k'), + .. + })) => { + self.first_line = self.first_line.saturating_sub(1); + } + WidgetEvent::Input(InputEvent::Key(KeyEvent { + key: KeyCode::Char('j'), + .. + })) => { + self.first_line = self.first_line.saturating_add(1); + } + WidgetEvent::Input(InputEvent::Key(KeyEvent { + key: KeyCode::Enter, + .. + })) => { + self.text.push_str("\r\n"); + } + WidgetEvent::Input(InputEvent::Paste(s)) => { + self.text.push_str(&s); + } + _ => {} + } + + true // handled it all + } + + /// Draw ourselves into the surface provided by RenderArgs + fn render(&mut self, args: &mut RenderArgs) { + args.surface + .add_change(Change::ClearScreen(ColorAttribute::Default)); + + // args.surface + // .add_change(Change::Attribute(AttributeChange::Foreground( + // ColorAttribute::TrueColorWithPaletteFallback( + // RgbColor::new(0x11, 0x00, 0xFF), + // AnsiColor::Purple.into(), + // ), + // ))); + let (_width, height) = args.surface.dimensions(); + + for line in self.buffer.contents.lines_at(self.first_line).take(height) { + args.surface + .add_change(unsafe { String::from_utf8_unchecked(line.bytes().collect()) }); + args.surface.add_change("\r"); + } + // args.surface + // .add_change(format!("🤷 surface size is {:?}\r\n", dims)); + // args.surface.add_change(self.text.clone()); + + // Place the cursor at the end of the text. + // A more advanced text editing widget would manage the + // cursor position differently. + *args.cursor = CursorShapeAndPosition { + coords: args.surface.cursor_position().into(), + shape: termwiz::surface::CursorShape::SteadyBar, + ..Default::default() + }; + } + + fn get_size_constraints(&self) -> layout::Constraints { + let mut c = layout::Constraints::default(); + c.set_valign(layout::VerticalAlignment::Top); + c + } +} + +pub struct StatusLine {} + +impl StatusLine { + pub fn new() -> Self { + StatusLine {} + } +} +impl Widget for StatusLine { + fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { + true + } + + fn render(&mut self, args: &mut RenderArgs) { + args.surface.add_change(Change::ClearScreen( + ColorAttribute::TrueColorWithPaletteFallback( + RgbColor::new(0xFF, 0xFF, 0xFF), + AnsiColor::Black.into(), + ), + )); + args.surface + .add_change(Change::Attribute(AttributeChange::Foreground( + ColorAttribute::TrueColorWithPaletteFallback( + RgbColor::new(0x00, 0x00, 0x00), + AnsiColor::Black.into(), + ), + ))); + + args.surface.add_change(" helix"); + } + + fn get_size_constraints(&self) -> layout::Constraints { + *layout::Constraints::default() + .set_fixed_height(1) + .set_valign(layout::VerticalAlignment::Bottom) + } +} + +pub struct Editor { + terminal: BufferedTerminal, + + buffer: Option, +} + +impl Editor { + pub fn new(mut args: Args) -> Result { + // Create a terminal + let caps = Capabilities::new_from_env()?; + let mut terminal = BufferedTerminal::new(SystemTerminal::new(caps)?)?; + + let mut editor = Editor { + terminal, + buffer: None, + }; + + if let Some(file) = args.files.pop() { + editor.open(file)?; + } + + Ok(editor) + } + + pub fn open(&mut self, path: PathBuf) -> Result<(), Error> { + let buffer = Buffer::load(path)?; + self.buffer = Some(buffer); + Ok(()) + } + + pub fn run(&mut self) -> Result<(), Error> { + // Start with an empty string; typing into the app will + // update this string. + let mut typed_text = String::new(); + + { + let buf = &mut self.terminal; + // Put the terminal in raw mode + alternate screen + buf.terminal().enter_alternate_screen()?; + buf.terminal().set_raw_mode()?; + + // Set up the UI + let mut ui = Ui::new(); + + let root_id = ui.set_root(MainScreen::new()); + let buffer_id = + ui.add_child(root_id, BufferComponent::new(self.buffer.as_mut().unwrap())); + // let root_id = ui.set_root(Buffer::new(&mut typed_text)); + ui.add_child(root_id, StatusLine::new()); + ui.set_focus(buffer_id); + + loop { + ui.process_event_queue()?; + + // After updating and processing all of the widgets, compose them + // and render them to the screen. + if ui.render_to_screen(buf)? { + // We have more events to process immediately; don't block waiting + // for input below, but jump to the top of the loop to re-run the + // updates. + continue; + } + // Compute an optimized delta to apply to the terminal and display it + buf.flush()?; + + // Wait for user input + match buf.terminal().poll_input(None) { + Ok(Some(InputEvent::Resized { rows, cols })) => { + // FIXME: this is working around a bug where we don't realize + // that we should redraw everything on resize in BufferedTerminal. + buf.add_change(Change::ClearScreen(Default::default())); + buf.resize(cols, rows); + } + Ok(Some(input)) => match input { + InputEvent::Key(KeyEvent { + key: KeyCode::Escape, + .. + }) => { + // Quit the app when escape is pressed + break; + } + input @ _ => { + // Feed input into the Ui + ui.queue_event(WidgetEvent::Input(input)); + } + }, + Ok(None) => {} + Err(e) => { + print!("{:?}\r\n", e); + break; + } + } + } + } + + // After we've stopped the full screen raw terminal, + // print out the final edited value of the input text. + println!("The text you entered: {}", typed_text); + Ok(()) + } +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index e13309eb..9dfa3767 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,227 +1,24 @@ -//! This example shows how to make a basic widget that accumulates -//! text input and renders it to the screen -#![allow(unused)] +mod editor; + +use editor::Editor; + +use argh::FromArgs; +use std::{env, path::PathBuf}; + use anyhow::Error; -use termwiz::caps::Capabilities; -use termwiz::cell::AttributeChange; -use termwiz::color::{AnsiColor, ColorAttribute, RgbColor}; -use termwiz::input::*; -use termwiz::surface::Change; -use termwiz::terminal::buffered::BufferedTerminal; -use termwiz::terminal::{new_terminal, Terminal}; -use termwiz::widgets::*; -/// This is a widget for our application -struct MainScreen {} - -impl MainScreen { - pub fn new() -> Self { - Self {} - } -} - -impl Widget for MainScreen { - fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { - true // handled it all - } - - /// Draw ourselves into the surface provided by RenderArgs - fn render(&mut self, args: &mut RenderArgs) { - // args.surface.add_change(Change::ClearScreen( - // ColorAttribute::TrueColorWithPaletteFallback( - // RgbColor::new(0x31, 0x1B, 0x92), - // AnsiColor::Black.into(), - // ), - // )); - // args.surface - // .add_change(Change::Attribute(AttributeChange::Foreground( - // ColorAttribute::TrueColorWithPaletteFallback( - // RgbColor::new(0xB3, 0x88, 0xFF), - // AnsiColor::Purple.into(), - // ), - // ))); - } - - fn get_size_constraints(&self) -> layout::Constraints { - let mut constraints = layout::Constraints::default(); - constraints.child_orientation = layout::ChildOrientation::Vertical; - constraints - } -} - -struct Buffer<'a> { - /// Holds the input text that we wish the widget to display - text: &'a mut String, -} - -impl<'a> Buffer<'a> { - /// Initialize the widget with the input text - pub fn new(text: &'a mut String) -> Self { - Self { text } - } -} - -impl<'a> Widget for Buffer<'a> { - fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { - match event { - WidgetEvent::Input(InputEvent::Key(KeyEvent { - key: KeyCode::Char(c), - .. - })) => self.text.push(*c), - WidgetEvent::Input(InputEvent::Key(KeyEvent { - key: KeyCode::Enter, - .. - })) => { - self.text.push_str("\r\n"); - } - WidgetEvent::Input(InputEvent::Paste(s)) => { - self.text.push_str(&s); - } - _ => {} - } - - true // handled it all - } - - /// Draw ourselves into the surface provided by RenderArgs - fn render(&mut self, args: &mut RenderArgs) { - args.surface - .add_change(Change::ClearScreen(ColorAttribute::Default)); - - // args.surface - // .add_change(Change::Attribute(AttributeChange::Foreground( - // ColorAttribute::TrueColorWithPaletteFallback( - // RgbColor::new(0x11, 0x00, 0xFF), - // AnsiColor::Purple.into(), - // ), - // ))); - let dims = args.surface.dimensions(); - args.surface - .add_change(format!("🤷 surface size is {:?}\r\n", dims)); - args.surface.add_change(self.text.clone()); - - // Place the cursor at the end of the text. - // A more advanced text editing widget would manage the - // cursor position differently. - *args.cursor = CursorShapeAndPosition { - coords: args.surface.cursor_position().into(), - shape: termwiz::surface::CursorShape::SteadyBar, - ..Default::default() - }; - } - - fn get_size_constraints(&self) -> layout::Constraints { - let mut c = layout::Constraints::default(); - c.set_valign(layout::VerticalAlignment::Top); - c - } -} - -struct StatusLine {} - -impl StatusLine { - pub fn new() -> Self { - StatusLine {} - } -} -impl Widget for StatusLine { - fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { - true - } - - fn render(&mut self, args: &mut RenderArgs) { - args.surface.add_change(Change::ClearScreen( - ColorAttribute::TrueColorWithPaletteFallback( - RgbColor::new(0xFF, 0xFF, 0xFF), - AnsiColor::Black.into(), - ), - )); - args.surface - .add_change(Change::Attribute(AttributeChange::Foreground( - ColorAttribute::TrueColorWithPaletteFallback( - RgbColor::new(0x00, 0x00, 0x00), - AnsiColor::Black.into(), - ), - ))); - - args.surface.add_change(" helix"); - } - - fn get_size_constraints(&self) -> layout::Constraints { - *layout::Constraints::default() - .set_fixed_height(1) - .set_valign(layout::VerticalAlignment::Bottom) - } +#[derive(FromArgs)] +/// A post-modern text editor. +pub struct Args { + #[argh(positional)] + files: Vec, } fn main() -> Result<(), Error> { - // Start with an empty string; typing into the app will - // update this string. - let mut typed_text = String::new(); + let args: Args = argh::from_env(); + let mut editor = Editor::new(args)?; - { - // Create a terminal and put it into full screen raw mode - let caps = Capabilities::new_from_env()?; - let mut buf = BufferedTerminal::new(new_terminal(caps)?)?; - buf.terminal().enter_alternate_screen()?; - buf.terminal().set_raw_mode()?; - - // Set up the UI - let mut ui = Ui::new(); - - let root_id = ui.set_root(MainScreen::new()); - let buffer_id = ui.add_child(root_id, Buffer::new(&mut typed_text)); - // let root_id = ui.set_root(Buffer::new(&mut typed_text)); - ui.add_child(root_id, StatusLine::new()); - ui.set_focus(buffer_id); - - loop { - ui.process_event_queue()?; - - // After updating and processing all of the widgets, compose them - // and render them to the screen. - if ui.render_to_screen(&mut buf)? { - // We have more events to process immediately; don't block waiting - // for input below, but jump to the top of the loop to re-run the - // updates. - continue; - } - // Compute an optimized delta to apply to the terminal and display it - buf.flush()?; - - // Wait for user input - match buf.terminal().poll_input(None) { - Ok(Some(InputEvent::Resized { rows, cols })) => { - // FIXME: this is working around a bug where we don't realize - // that we should redraw everything on resize in BufferedTerminal. - buf.add_change(Change::ClearScreen(Default::default())); - buf.resize(cols, rows); - } - Ok(Some(input)) => match input { - InputEvent::Key(KeyEvent { - key: KeyCode::Escape, - .. - }) => { - // Quit the app when escape is pressed - break; - } - input @ _ => { - // Feed input into the Ui - ui.queue_event(WidgetEvent::Input(input)); - } - }, - Ok(None) => {} - Err(e) => { - print!("{:?}\r\n", e); - break; - } - } - } - } - - // After we've stopped the full screen raw terminal, - // print out the final edited value of the input text. - println!("The text you entered: {}", typed_text); + editor.run()?; Ok(()) }