diff --git a/Cargo.lock b/Cargo.lock index a3b3c11..32b7451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,12 +306,27 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1834,6 +1849,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linkme" version = "0.3.26" @@ -1998,6 +2025,7 @@ dependencies = [ "serde_json", "slotmap", "smallvec", + "smol", "taffy", "thiserror", "time", @@ -2105,7 +2133,9 @@ version = "0.1.0" dependencies = [ "anyhow", "funnylog", + "log", "ming", + "syntect", "widestring", ] @@ -2294,6 +2324,28 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "oo7" version = "0.3.2" @@ -2502,6 +2554,20 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64 0.21.7", + "indexmap", + "line-wrap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "png" version = "0.16.8" @@ -3164,6 +3230,23 @@ dependencies = [ "wayland-backend", ] +[[package]] +name = "smol" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e635339259e51ef85ac7aa29a1cd991b957047507288697a690e80ab97d07cad" +dependencies = [ + "async-channel 2.3.0", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite 2.3.0", +] + [[package]] name = "socket2" version = "0.5.7" @@ -3269,6 +3352,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "sys-locale" version = "0.3.1" @@ -3654,7 +3759,7 @@ version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c704361d822337cfc00387672c7b59eaa72a1f0744f62b2a68aa228a0c6927d" dependencies = [ - "base64", + "base64 0.22.1", "data-url", "flate2", "imagesize", @@ -4180,6 +4285,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yazi" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 058ebf6..ae058ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ funnylog.workspace = true ming.workspace = true anyhow.workspace = true widestring = "1.1.0" +syntect = "5.2.0" +log.workspace = true [workspace] members = [ diff --git a/crates/ming/Cargo.toml b/crates/ming/Cargo.toml index 7b5a3a1..44d5b87 100644 --- a/crates/ming/Cargo.toml +++ b/crates/ming/Cargo.toml @@ -61,6 +61,7 @@ time.workspace = true util.workspace = true uuid.workspace = true waker-fn = "1.1.0" +smol = "2.0.0" [dev-dependencies] backtrace = "0.3" diff --git a/crates/ming/src/elements/text.rs b/crates/ming/src/elements/text.rs index 2adb2db..515eda7 100644 --- a/crates/ming/src/elements/text.rs +++ b/crates/ming/src/elements/text.rs @@ -143,7 +143,13 @@ impl StyledText { } } - /// todo!() + /// Get a reference to the text inside this [`StyledText`] + pub fn text(&self) -> &str { + &self.text + } + + /// Access the text layout. + /// This method is not intended to be used outside ming. pub fn layout(&self) -> &TextLayout { &self.layout } diff --git a/crates/ming/src/shared_string.rs b/crates/ming/src/shared_string.rs index 3801276..52a036f 100644 --- a/crates/ming/src/shared_string.rs +++ b/crates/ming/src/shared_string.rs @@ -9,13 +9,9 @@ use util::arc_cow::ArcCow; pub struct SharedString(ArcCow<'static, str>); impl SharedString { - /// Create a new shared string from anything that can be turned into an [`ArcCow`], including: - /// - `&'static `[`str`] - /// - [`String`] - /// - [`Arc`]`<`[`str`]`>` - /// - a reference to any of the above - pub fn new(ac: impl Into>) -> Self { - Self(ac.into()) + /// Create a new empty [`SharedString`]. + pub fn new() -> Self { + Self(ArcCow::Borrowed("")) } /// Create a new shared string from a `&'static `[`str`]. diff --git a/crates/ming/src/test.rs b/crates/ming/src/test.rs index 3f26754..a5870f0 100644 --- a/crates/ming/src/test.rs +++ b/crates/ming/src/test.rs @@ -102,7 +102,7 @@ impl futures::Stream for Observation { /// observe returns a stream of the change events from the given `View` or `Model` pub fn observe(entity: &impl Entity, cx: &mut TestAppContext) -> Observation<()> { - let (tx, rx) = smol::channel::unbounded(); + let (tx, rx) = channel::unbounded(); let _subscription = cx.update(|cx| { cx.observe(entity, move |_, _| { let _ = smol::block_on(tx.send(())); diff --git a/crates/ming/src/util.rs b/crates/ming/src/util.rs index 4bff3da..089ce01 100644 --- a/crates/ming/src/util.rs +++ b/crates/ming/src/util.rs @@ -2,10 +2,7 @@ use std::time::Duration; #[cfg(any(test, feature = "test-support"))] -use futures::Future; - -#[cfg(any(test, feature = "test-support"))] -use smol::future::FutureExt; +use futures::{Future, FutureExt}; pub use util::*; diff --git a/flake.nix b/flake.nix index b982301..64814c0 100644 --- a/flake.nix +++ b/flake.nix @@ -68,7 +68,7 @@ scripts = { fmt.exec = "${config.treefmt.build.wrapper}/bin/treefmt ."; - nite.exec = "RUST_LOG=info,nite=trace,ming=trace cargo run"; + nite.exec = "RUST_LOG=info,nite=trace,ming=trace cargo run -- $@"; }; }; }; diff --git a/src/buffer.rs b/src/buffer.rs index 25a4b09..8fa27f0 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,4 +1,10 @@ use std::{ops::Range, path::PathBuf}; +use syntect::{ + highlighting::{ + Color, FontStyle as SFontStyle, HighlightState, Highlighter, RangedHighlightIterator, Theme, + }, + parsing::{ParseState, ScopeStack, SyntaxSet}, +}; use widestring::Utf16String; use super::*; @@ -6,18 +12,19 @@ use super::*; pub struct Buffer { pub text: Utf16String, pub path: Option, - pub marked: Option> + pub marked: Option>, + pub selected: Option>, } impl Buffer { - pub fn make_scratch(cx: &mut ViewContext) -> Model { - cx.new_model(|_cx| Self::scratch()) - } - pub fn read(path: PathBuf) -> anyhow::Result { + let text = std::fs::read_to_string(&path)?; + log::trace!("read file: {text}"); Ok(Self { - text: std::fs::read_to_string(&path)?, + text: text.into(), path: Some(path), + marked: None, + selected: None, }) } @@ -25,6 +32,81 @@ impl Buffer { Self { text: Utf16String::new(), path: None, + marked: None, + selected: None, + } + } + + pub fn styled( + &self, + default_style: &TextStyle, + theme: &Theme, + syntax_set: &SyntaxSet, + scrolled: usize, + ) -> Vec { + let text = self.text.to_string(); + + if let Some(syntax) = self + .path + .as_ref() + .and_then(|path| path.extension()) + .and_then(|extension| { + syntax_set.find_syntax_by_extension(extension.to_str().expect("non-utf8 file ext")) + }) + { + let mut parse = ParseState::new(syntax); + let hler = Highlighter::new(theme); + let mut hl = HighlightState::new(&hler, ScopeStack::new()); + + text.lines() + .skip(scrolled) + .flat_map(|line| { + parse.parse_line(line, &syntax_set).map(|parsed| { + StyledText::new(Arc::from(line)).with_highlights( + default_style, + RangedHighlightIterator::new(&mut hl, &parsed, line, &hler).map( + move |(style, _text, range)| { + ( + range, + HighlightStyle { + color: Some(hsla_from_syntect(style.foreground)), + background_color: None, + font_style: style + .font_style + .contains(SFontStyle::ITALIC) + .then(|| FontStyle::Italic), + underline: style + .font_style + .contains(SFontStyle::UNDERLINE) + .then(|| UnderlineStyle::default()), + font_weight: style + .font_style + .contains(SFontStyle::BOLD) + .then(|| FontWeight::BOLD), + ..Default::default() + }, + ) + }, + ), + ) + }) + }) + .collect() + } else { + text.lines() + .skip(scrolled) + .map(|line| StyledText::new(Arc::from(line))) + .collect() } } } + +pub fn hsla_from_syntect(color: Color) -> Hsla { + Rgba { + r: color.r as f32 / 255.0, + g: color.g as f32 / 255.0, + b: color.b as f32 / 255.0, + a: color.a as f32 / 255.0, + } + .into() +} diff --git a/src/editor.rs b/src/editor.rs index 94bc8c1..c69bbb9 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,25 +1,68 @@ +use std::path::PathBuf; + +use syntect::{highlighting::ThemeSet, parsing::SyntaxSet}; + use super::*; mod element; mod input; +pub struct EditorSettings { + theme_set: ThemeSet, + syntax_set: SyntaxSet, +} + +impl EditorSettings { + pub fn load_defaults() -> Self { + Self { + theme_set: ThemeSet::load_defaults(), + syntax_set: SyntaxSet::load_defaults_nonewlines(), + } + } +} + pub struct Editor { focus_handle: FocusHandle, - buf: Model + buf: Model, + settings: Model, } impl Editor { - pub fn make(cx: &mut ViewContext) -> View { + pub fn make( + cx: &mut ViewContext, + settings: Model, + ) -> View { cx.new_view(|cx| Self { - buf: crate::buffer::Buffer::make_scratch(cx), - focus_handle: cx.focus_handle() + buf: cx.new_model(|_cx| { + if let Some(path) = std::env::args().skip(1).next() { + crate::buffer::Buffer::read(PathBuf::from(path)).expect("file failed to read") + } else { + crate::buffer::Buffer::scratch() + } + }), + focus_handle: cx.focus_handle(), + settings, }) } } impl Render for Editor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - element::EditorElement::new(cx.view()) + element::EditorElement::new( + cx.view(), + element::EditorStyle { + font_size: AbsoluteLength::Pixels(px(14.0)), + font_family: SharedString::from_static("ComicShannsMono Nerd Font Mono"), + hl_theme: self + .settings + .read(cx) + .theme_set + .themes + .get("base16-mocha.dark") + .expect("mocha.dark wasn't available even if it's told it's there") + .clone(), + }, + ) } } diff --git a/src/editor/element.rs b/src/editor/element.rs index be14899..d69d4a0 100644 --- a/src/editor/element.rs +++ b/src/editor/element.rs @@ -1,29 +1,29 @@ +use std::{cell::Cell, rc::Rc}; + +use syntect::highlighting::Theme; + +use self::buffer::hsla_from_syntect; + use super::*; pub struct EditorStyle { pub font_family: SharedString, pub font_size: AbsoluteLength, -} - -impl Default for EditorStyle { - fn default() -> Self { - Self { - font_family: SharedString::from_static("Monospace"), - font_size: AbsoluteLength::Pixels(px(14)), - } - } + pub hl_theme: Theme, } pub struct EditorElement { editor: View, style: EditorStyle, + scrolled: Rc>, } impl EditorElement { - pub fn new(viewref: &View) -> Self { + pub fn new(viewref: &View, style: EditorStyle) -> Self { Self { editor: viewref.clone(), - style: EditorStyle::default(), + style, + scrolled: Rc::new(Cell::new(0)), } } } @@ -36,10 +36,13 @@ impl IntoElement for EditorElement { } } -struct EditorLayout {} +pub struct EditorLayout {} +pub struct EditorRequestLayout { + styled: Vec<(LayoutId, StyledText, Bounds)>, +} impl Element for EditorElement { - type RequestLayoutState = (); + type RequestLayoutState = EditorRequestLayout; type PrepaintState = EditorLayout; fn id(&self) -> Option { @@ -51,6 +54,41 @@ impl Element for EditorElement { id: Option<&GlobalElementId>, cx: &mut WindowContext, ) -> (LayoutId, Self::RequestLayoutState) { + let editor = self.editor.read(cx); + let mut styled = editor.buf.read(cx).styled( + &TextStyle { + font_size: self.style.font_size, + font_family: self.style.font_family.clone(), + ..Default::default() + }, + &self.style.hl_theme, + &editor.settings.read(cx).syntax_set, + self.scrolled.get(), + ); + + let children = styled + .iter_mut() + .map(|styled| styled.request_layout(None, cx).0) + .collect::>(); + + ( + cx.request_layout( + Style { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_content: Some(AlignContent::FlexStart), + ..Default::default() + }, + children.iter().copied(), + ), + EditorRequestLayout { + styled: children + .into_iter() + .zip(styled) + .map(|(id, styled)| (id, styled, Bounds::default())) + .collect(), + }, + ) } fn prepaint( @@ -60,6 +98,14 @@ impl Element for EditorElement { request_layout: &mut Self::RequestLayoutState, cx: &mut WindowContext, ) -> Self::PrepaintState { + request_layout + .styled + .iter_mut() + .for_each(|(id, styled, bounds)| { + *bounds = cx.layout_bounds(*id); + styled.prepaint(None, *bounds, &mut (), cx) + }); + EditorLayout {} } @@ -80,14 +126,44 @@ impl Element for EditorElement { ElementInputHandler::new(bounds, self.editor.clone()), ); + let scrolled = self.scrolled.clone(); + let font_size = self.style.font_size.to_pixels(px(4.)); + cx.on_mouse_event(move |scroll: &ScrollWheelEvent, _dispatch, cx| { + match scroll.delta { + ScrollDelta::Lines(lines) => scrolled.set({ + let lines_y = lines.y.ceil() as i32; + scrolled.get().saturating_add_signed(lines_y as isize) + }), + ScrollDelta::Pixels(pix) => scrolled.set({ + let px_y = (pix.y.ceil().0 / font_size.0) as i32; + scrolled.get().saturating_add_signed(px_y as isize) + }), + }; + }); + cx.with_text_style( Some(TextStyleRefinement { - font_size: Some(self.style.font_size), - font_family: Some(self.style.font_family), + color: self + .style + .hl_theme + .settings + .foreground + .map(hsla_from_syntect), + background_color: self + .style + .hl_theme + .settings + .background + .map(hsla_from_syntect), ..Default::default() }), |cx| { - todo!("draw text??") + request_layout + .styled + .iter_mut() + .for_each(|(id, styled, bounds)| { + styled.paint(None, *bounds, &mut (), &mut (), cx); + }); }, ); } diff --git a/src/editor/input.rs b/src/editor/input.rs index ff03707..bb58499 100644 --- a/src/editor/input.rs +++ b/src/editor/input.rs @@ -1,4 +1,4 @@ -use widestring::Utf16Str; +use widestring::{Utf16Str, Utf16String}; use super::*; @@ -41,6 +41,30 @@ impl ViewInputHandler for Editor { element_bounds: Bounds, cx: &mut ViewContext, ) -> Option> { + None + } + + fn selected_text_range( + &mut self, + cx: &mut ViewContext, + ) -> Option> { + self.buf.read(cx).selected.clone() + } + + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut ViewContext, + ) { + let text = Utf16String::from_str(text); + self.buf.update(cx, |buf, cx| { + if let Some(range) = range { + buf.text.replace_range(range, &text) + } else { + buf.text = text; + } + }) } fn replace_and_mark_text_in_range( @@ -50,7 +74,13 @@ impl ViewInputHandler for Editor { new_selected_range: Option>, cx: &mut ViewContext, ) { + let new_text = Utf16String::from_str(new_text); self.buf.update(cx, |buf, cx| { + if let Some(range) = range { + buf.text.replace_range(range, &new_text) + } else { + buf.text = new_text; + } buf.marked = new_selected_range; }) } diff --git a/src/main.rs b/src/main.rs index 58ffda6..134fe8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,16 @@ mod editor; struct Nite { editor: View, - logger: Logger + logger: Logger, } -type Logger = funnylog::Logger>>>>; +type Logger = funnylog::Logger< + Arc< + funnylog::IgnoreError< + funnylog::filter::FilterDrain>, + >, + >, +>; impl Render for Nite { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { @@ -19,8 +25,8 @@ impl Render for Nite { .flex() .size(Length::Definite(DefiniteLength::Fraction(1f32))) .bg(transparent_black()) - .justify_center() - .items_center() + .justify_start() + .items_start() .text_xl() .text_color(white()) .font_family("ComicShannsMono Nerd Font Mono") @@ -29,8 +35,13 @@ impl Render for Nite { } fn main() { - let drain = Arc::new(funnylog::terminal::TerminalConfig::default().to_stderr().env_filter("RUST_LOG").ignore_error()); - funnylog::stdlog::setup(drain.clone()); + let drain = Arc::new( + funnylog::terminal::TerminalConfig::default() + .to_stderr() + .env_filter("RUST_LOG") + .ignore_error(), + ); + funnylog::stdlog::setup(drain.clone()).unwrap(); let logger = Logger::new(drain); App::new().run(|cx| { @@ -41,9 +52,13 @@ fn main() { |cx| { funnylog::info!(logger, "Hello from nite"); + let settings = cx.new_model(|_cx| editor::EditorSettings::load_defaults()); cx.new_view(|cx| Nite { - editor: editor::Editor::make(cx), - logger + editor: editor::Editor::make( + cx, + settings, + ), + logger, }) }, );