A simple CPU rendered GUI IDE experience.
hmm
| -rw-r--r-- | src/com.rs | 88 | ||||
| -rw-r--r-- | src/lsp.rs | 29 | ||||
| -rw-r--r-- | src/main.rs | 157 | ||||
| -rw-r--r-- | src/text.rs | 17 |
4 files changed, 160 insertions, 131 deletions
@@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::mem::MaybeUninit; use std::sync::LazyLock; @@ -8,48 +9,61 @@ use fimg::Image; use itertools::Itertools; use lsp_types::*; +use crate::text::color; use crate::{Complete, FG}; -const BG: [u8; 3] = crate::text::color(*b"1c212b"); -const T_BG: [u8; 3] = crate::text::color(*b"11141a"); -pub fn s(x: &Complete, c: usize, filter: &str) -> Vec<Cell> { +pub fn s(completion: &Complete, c: usize, filter: &str) -> Vec<Cell> { let mut out = vec![]; - let x = &x.r; + let x = &completion.r; let y = match x { CompletionResponse::Array(x) => x, CompletionResponse::List(x) => &x.items, }; + #[thread_local] static mut MATCHER: LazyLock<nucleo::Matcher> = LazyLock::new(|| nucleo::Matcher::new(nucleo::Config::DEFAULT)); + let p = nucleo::pattern::Pattern::parse( filter, - nucleo::pattern::CaseMatching::Ignore, + nucleo::pattern::CaseMatching::Smart, nucleo::pattern::Normalization::Smart, ); - + dbg!(filter); let mut i = y .iter() .filter(|y| { - y.filter_text - .as_deref() - .unwrap_or(&y.label) - .starts_with(filter) + filter.is_empty() + || y.filter_text + .as_deref() + .unwrap_or(&y.label) + .chars() + .collect::<HashSet<_>>() + .intersection(&filter.chars().collect()) + .count() + > 0 }) .map(|y| { - let mut to = vec![]; + let mut utf32 = vec![]; + + let hay = y.filter_text.as_deref().unwrap_or(&y.label); + let mut indices = vec![]; let score = p - .score( - nucleo::Utf32Str::new( - y.filter_text.as_deref().unwrap_or(&y.label), - &mut to, - ), + .indices( + nucleo::Utf32Str::new(hay, &mut utf32), unsafe { &mut *MATCHER }, + &mut indices, ) .unwrap_or(0); - (score, y) + indices.sort_unstable(); + indices.dedup(); + + (score, y, indices) }) .sorted_by_key(|x| x.0) - .take(13); + .rev() + .zip(0..) + .skip(completion.vo) + .take(N); // let Some((s, x)) = i.next() else { // return vec![]; @@ -93,15 +107,29 @@ pub fn s(x: &Complete, c: usize, filter: &str) -> Vec<Cell> { // // .starts_with(filter) // // }) // .take(13) - i.for_each(|(_, x)| r(x, c, &mut out)); + i.for_each(|((_, x, indices), i)| { + r(x, c, i == completion.selection, &indices, &mut out) + }); + out } fn charc(c: &str) -> usize { c.chars().count() } #[implicit_fn::implicit_fn] -fn r(x: &CompletionItem, c: usize, to: &mut Vec<Cell>) { - let mut b = vec![D; c]; +fn r( + x: &CompletionItem, + c: usize, + selected: bool, + indices: &[u32], + to: &mut Vec<Cell>, +) { + let bg = if selected { color(*b"262d3b") } else { color(*b"1c212b") }; + const T_BG: [u8; 3] = color(*b"11141a"); + + let ds: Style = Style { bg: bg, color: FG, flags: 0 }; + let d: Cell = Cell { letter: None, style: ds }; + let mut b = vec![d; c]; let ty = match x.kind { Some(CompletionItemKind::TEXT) => " ", Some( @@ -149,18 +177,22 @@ fn r(x: &CompletionItem, c: usize, to: &mut Vec<Cell>) { i.iter_mut() .rev() .zip(details.map(|x| { - Style { bg: BG, color: [154, 155, 154], ..default() } - .basic(x) + Style { bg, color: [154, 155, 154], ..default() }.basic(x) })) .for_each(|(a, b)| *a = b); } i.iter_mut() - .zip(x.label.chars().map(|x| DS.basic(x))) - .for_each(|(a, b)| *a = b); + .zip(x.label.chars().map(|x| ds.basic(x))) + .zip(0..) + .for_each(|((a, b), i)| { + *a = b; + if indices.contains(&i) { + a.style |= (Style::BOLD, color(*b"ffcc66")); + } + }); to.extend(b); } -const DS: Style = Style { bg: BG, color: FG, flags: 0 }; -const D: Cell = Cell { letter: None, style: DS }; +pub const N: usize = 13; #[test] fn t() { let ppem = 20.0; @@ -170,7 +202,7 @@ fn t() { dbg!(dsb::size(&crate::FONT, ppem, lh, (c, r))); let y = serde_json::from_str(include_str!("../complete_")).unwrap(); let cells = - s(&Complete { r: y, start: 0, selection: 0, scroll: 0 }, c, ""); + s(&Complete { r: y, start: 0, selection: 0, vo: 0 }, c, ""); dbg!(c, r); dbg!(w, h); @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::path::Path; +use std::sync::Arc; use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering::Relaxed; use std::task::Poll; @@ -8,7 +9,7 @@ use std::time::Instant; use Default::default; use anyhow::Error; -use arc_swap::ArcSwap; +use arc_swap::{ArcSwap, ArcSwapOption}; use crossbeam::channel::{ Receiver, RecvError, SendError, Sender, unbounded, }; @@ -27,6 +28,7 @@ use lsp_types::*; use parking_lot::Mutex; use serde_json::json; use tokio::sync::oneshot; +use winit::window::Window; pub struct Client { pub runtime: tokio::runtime::Runtime, @@ -35,7 +37,6 @@ pub struct Client { pub initialized: Option<InitializeResult>, // pub pending: HashMap<i32, oneshot::Sender<Re>>, pub send_to: Sender<(i32, oneshot::Sender<Re>)>, - pub ch_tx: Sender<()>, pub progress: &'static papaya::HashMap<ProgressToken, Option<WorkDoneProgress>>, pub not_rx: Receiver<N>, @@ -162,7 +163,11 @@ impl Client { rx } - pub fn rq_semantic_tokens(&self, f: &Path) -> anyhow::Result<()> { + pub fn rq_semantic_tokens( + &self, + f: &Path, + w: Option<Arc<Window>>, + ) -> anyhow::Result<()> { debug!("requested semantic tokens"); let Some(b"rs") = f.extension().map(|x| x.as_encoded_bytes()) else { @@ -186,7 +191,6 @@ impl Client { }, )?; let d = self.semantic_tokens.0; - let ch = self.ch_tx.clone(); let x = self.runtime.spawn(async move { let y = rx.await?.unwrap(); debug!("received semantic tokens"); @@ -197,7 +201,7 @@ impl Client { SemanticTokensResult::Tokens(x) => d.store(x.data.into_boxed_slice().into()), }; - ch.send(())?; + w.map(|x| x.request_redraw()); anyhow::Ok(()) }); *p = Some((x, id)); @@ -212,15 +216,19 @@ pub fn run( lsp_server::IoThreads, ), workspace: WorkspaceFolder, -) -> (Client, lsp_server::IoThreads, JoinHandle<()>, Receiver<()>) { +) -> ( + Client, + lsp_server::IoThreads, + JoinHandle<()>, + oneshot::Sender<Arc<Window>>, +) { let now = Instant::now(); let (req_tx, req_rx) = unbounded(); let (not_tx, not_rx) = unbounded(); let (_req_tx, _req_rx) = unbounded(); - let (ch_tx, ch_rx) = unbounded(); + let (window_tx, window_rx) = oneshot::channel::<Arc<Window>>(); let mut c: Client = Client { tx, - ch_tx: ch_tx.clone(), progress: Box::leak(Box::new(papaya::HashMap::new())), runtime: tokio::runtime::Builder::new_multi_thread() .worker_threads(2) @@ -431,6 +439,7 @@ CompletionItemKind::TYPE_PARAMETER] log::info!("lsp took {:?} to initialize", now.elapsed()); let h = spawn(move || { let mut map = HashMap::new(); + let w = window_rx.blocking_recv().unwrap(); loop { crossbeam::select! { recv(req_rx) -> x => match x { @@ -477,7 +486,7 @@ CompletionItemKind::TYPE_PARAMETER] Ok(Message::Notification(x @ N { method: "$/progress", .. })) => { let ProgressParams {token,value:ProgressParamsValue::WorkDone(x) } = x.load::<Progress>().unwrap(); progress.update(token, move |_| Some(x.clone()), &progress.guard()); - _ = ch_tx.send(()); + w.request_redraw(); } Ok(Message::Notification(notification)) => { debug!("rx {notification:?}"); @@ -490,7 +499,7 @@ CompletionItemKind::TYPE_PARAMETER] } } }); - (c, iot, h, ch_rx) + (c, iot, h, window_tx) } // trait RecvEepy<T>: Sized { diff --git a/src/main.rs b/src/main.rs index 89e1d90..b84dda5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ // this looks pretty good though #![feature(tuple_trait, unboxed_closures, fn_traits)] #![feature( + thread_local, result_option_map_or_default, iter_intersperse, stmt_expr_attributes, @@ -35,6 +36,7 @@ use std::time::Instant; use Default::default; use NamedKey::*; +use array_chunks::ExtensionTrait; use atools::prelude::AASAdd; use crossbeam::channel::RecvError; use diff_match_patch_rs::PatchInput; @@ -65,7 +67,7 @@ use winit::window::{Icon, Window}; use crate::bar::Bar; use crate::hov::Hovr; -use crate::text::{Diff, TextArea}; +use crate::text::{Diff, TextArea, is_word}; mod bar; pub mod com; pub mod hov; @@ -221,10 +223,10 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { ); c.open(&origin, std::fs::read_to_string(&origin).unwrap()) .unwrap(); - (c, (t, t2), changed) + (&*Box::leak(Box::new(c)), (t, t2), changed) }, ); - let (lsp, t, ch) = match c { + let (lsp, t, mut w) = match c { Some((a, b, c)) => (Some(a), Some(b), Some(c)), None => (None, None, None), }; @@ -261,15 +263,8 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { .map(|x| x.metadata().unwrap().modified().unwrap()) }; } - macro_rules! change { - () => { - lsp!().map(|(x, origin)| { - x.edit(&origin, text.rope.to_string()).unwrap(); - x.rq_semantic_tokens(origin).unwrap(); - }); - }; - } - lsp!().map(|(x, origin)| x.rq_semantic_tokens(origin).unwrap()); + + lsp!().map(|(x, origin)| x.rq_semantic_tokens(origin, None).unwrap()); let mut mtime = modify!(); macro_rules! save { () => {{ @@ -282,7 +277,6 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { mtime = modify!(); }}; } - static PUT: OnceLock<Arc<Window>> = OnceLock::new(); let app = winit_app::WinitAppBuilder::with_init( move |elwt| { let window = winit_app::make_window(elwt, |x| { @@ -290,9 +284,12 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { .with_name("com.bendn.gracilaria", "") .with_window_icon(Some(Icon::from_rgba(include_bytes!("../dist/icon-32").to_vec(), 32, 32).unwrap())) }); + if let Some(x) = w.take() { + x.send(window.clone()).unwrap(); + } + window.set_ime_allowed(true); window.set_ime_purpose(winit::window::ImePurpose::Terminal); - PUT.set(window.clone()).unwrap(); let context = softbuffer::Context::new(window.clone()).unwrap(); @@ -304,6 +301,14 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { ) .with_event_handler( move |(window, _context), surface, event, elwt| { + macro_rules! change { + () => { + lsp!().map(|(x, origin)| { + x.edit(&origin, text.rope.to_string()).unwrap(); + x.rq_semantic_tokens(origin, Some(window.clone())).unwrap(); + }); + }; + } elwt.set_control_flow(ControlFlow::Wait); let (fw, fh) = dsb::dims(&FONT, ppem); let (c, r) = dsb::fit( @@ -321,16 +326,15 @@ pub(crate) fn entry(event_loop: EventLoop<()>) { window.request_redraw(); } if let CompletionState::Complete(o, x)= &mut complete && - x.as_ref().is_some_and(|(x, _)|x.is_finished()) && let Some((task, c)) = x.take() && let Some(ref l) = lsp{ + x.as_ref().is_some_and(|(x, _)|x.is_finished()) && + let Some((task, c)) = x.take() && let Some(ref l) = lsp{ // if text.cursor() ==* c_ { - println!("bl0ck on"); - *o = l.runtime.block_on(task).ok().and_then(Result::ok).flatten().map(|x| Complete { -r:x,start:0,selection:0,scroll:0, +r:x,start:c,selection:0,vo:0, } ); if let Some(x) = o { - std::fs::write("complete_", serde_json::to_string_pretty(&x.r).unwrap()).unwrap(); - println!("resolved") + // std::fs::write("complete_", serde_json::to_string_pretty(&x.r).unwrap()).unwrap(); + // println!("resolved") } // println!("{complete:#?}"); // } else { @@ -456,7 +460,7 @@ r:x,start:0,selection:0,scroll:0, .unwrap_or("new buffer"), &state, &text, - lsp.as_ref() + lsp ); unsafe { dsb::render( @@ -723,10 +727,7 @@ RUNNING.remove(&hover,&RUNNING.guard()); if button == MouseButton::Left { unsafe { CLICKING = true }; } - match complete.consume(CompletionAction::Click).unwrap() { - Some(CDo::Abort(x))=>{ x.abort() }, - _ => {}, - } + _ = complete.consume(CompletionAction::Click).unwrap(); match state.consume(Action::M(button)).unwrap() { Some(Do::MoveCursor) => { text.cursor = text.index_at(cursor_position); @@ -832,56 +833,40 @@ RUNNING.remove(&hover,&RUNNING.guard()); Some(Do::Edit) => { hist.test_push(&text); let cb4 = text.cursor; - let r = handle2(&event.logical_key, &mut text); + if let Key::Named(Enter | ArrowUp | ArrowDown | Tab) = event.logical_key && let CompletionState::Complete(..) = complete{ + } else { + handle2(&event.logical_key, &mut text); + } + text.scroll_to_cursor(); + if cb4 != text.cursor && let CompletionState::Complete(Some(c), t)= &mut complete + && ((text.cursor < c.start) || (!is_word(text.at_())&& (text.at_() != '.' || text.at_() != ':')) ) { + if let Some((x, _)) = t.take() { + x.abort(); + } + complete = CompletionState::None; + } if hist.record(&text) { change!(); } -if let Some(r) = r { lsp!().map(|(lsp, o)|{ let window = window.clone(); - let ctx = match complete.consume(CompletionAction::TypeCharacter).unwrap() { - Some(CDo::Request) => { - CompletionContext { - trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, trigger_character:Some(r.to_string()) } - - // println!("make rq"); - // let x = lsp.request_complete(o, text.cursor(), ); - // let h = lsp.runtime.spawn(async move { - // let r = x.await; - // window.request_redraw(); - // r - // }); - // complete = CompletionState::Complete(None, Some((h,cb4))); - // dbg!(&complete); - } - Some(CDo::Update) => { - // CompletionContext { - // trigger_kind: CompletionTriggerKind - CompletionContext { - trigger_kind: CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS, trigger_character:None } - // complete = CompletionState::Complete(None, Some(lsp.runtime.spawn(async move { - // let r = x.await; - // window.request_redraw(); - // r - // }))); + match complete.consume(CompletionAction::K(event.logical_key.as_ref())).unwrap(){ + Some(CDo::Request(ctx)) => { + let x = lsp.request_complete(o, text.cursor(), ctx); + let h = lsp.runtime.spawn(async move { + x.await.inspect(|_| window.request_redraw()) + }); + let CompletionState::Complete(c, x) = &mut complete else { panic!()}; + *x = Some((h,c.as_ref().map(|x|x.start).or(x.as_ref().map(|x|x.1)).unwrap_or(text.cursor))); } None => {return}, _ => panic!(), }; - println!("make rq"); - let x = lsp.request_complete(o, text.cursor(), ctx); - let h = lsp.runtime.spawn(async move { - let r = x.await; - window.request_redraw(); - r - }); - complete = CompletionState::Complete(None, Some((h,text.cursor))); - dbg!(&complete); - + }); -} + } Some(Do::Undo) => { hist.test_push(&text); @@ -1018,16 +1003,6 @@ if let Some(r) = r { }; }, ); - ch.map(|ch| { - thread::Builder::new().name("redrawer".into()).spawn(move || { - for () in ch { - PUT.get().map(|x| { - x.request_redraw(); - println!("rq redraw"); - }); - } - }) - }); winit_app::run_app(event_loop, app); } @@ -1241,26 +1216,33 @@ impl<T> M<T> for Option<T> { rust_fsm::state_machine! { #[derive(Debug)] - pub(crate) CompletionState => CompletionAction => CDo + pub(crate) CompletionState => CompletionAction<'i> => CDo None => Click => None, - None => TypeCharacter => Complete( + None => K(Key<&'i str> => Key::Character(k @ ("." | ":"))) => Complete( (Option<Complete>, Option<(JoinHandle< Result< Option<CompletionResponse>, tokio::sync::oneshot::error::RecvError, >, >, usize)>) => (None,None) - ) [Request], + ) [Request(CompletionContext => CompletionContext {trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, trigger_character:Some(k.to_string()) })], + None => K(Key::Named(NamedKey::Space) if ctrl()) => Complete((None, None)) [Request(CompletionContext { trigger_kind: CompletionTriggerKind::INVOKED, trigger_character:None })], + None => K(Key::Character(x) if x.chars().next().is_some_and(is_word)) => Complete((None,None)) [Request(CompletionContext { trigger_kind: CompletionTriggerKind::INVOKED, trigger_character:None })], + None => K(_) => _, + + // when + Complete((_x,_y)) => K(Key::Named(NamedKey::Tab) if shift()) => _ [SelectPrevious], + Complete((_x,_y)) => K(Key::Named(NamedKey::Tab)) => _ [SelectNext], + + // exit cases Complete((_x, None)) => Click => None, - Complete((_x, Some((y, _)))) => Click => None [Abort(JoinHandle< - Result< - Option<CompletionResponse>, - tokio::sync::oneshot::error::RecvError, - >, - > => y)], - Complete((_x, _y)) => TypeCharacter => _ [Update], - Complete((Some(x), task)) => Enter => None [Finish(Complete => { - if let Some((task, _)) = task { task.abort() }; x + Complete((_x, Some((y, _)))) => Click => None [Abort(((),) => y.abort())], + Complete((_x, Some((y, _)))) => K(Key::Character(x) if !x.chars().all(is_word)) => None [Abort(y.abort())], + Complete((_x, None)) => K(Key::Character(x) if !x.chars().all(is_word)) => None, + + Complete((_x, _y)) => K(_) => _ [Request(CompletionContext { trigger_kind: CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS, trigger_character:None })], + Complete((Some(x), task)) => K(Key::Named(NamedKey::Enter)) => None [Finish(Complete => { + task.map(|(task, _)| task.abort()); x })] } impl Default for CompletionState { @@ -1268,9 +1250,10 @@ impl Default for CompletionState { Self::None } } -#[derive(Debug)] struct Complete { +#[derive(Debug)] +struct Complete { r: CompletionResponse, start: usize, selection: usize, - scroll: usize, + vo: usize, } diff --git a/src/text.rs b/src/text.rs index 55eaf0d..13edb6e 100644 --- a/src/text.rs +++ b/src/text.rs @@ -393,7 +393,11 @@ impl TextArea { self.set_ho(); } - pub fn at(&self) -> char { + pub fn at_(&self) -> char { + self.rope.get_char(self.cursor - 1).unwrap_or('\n') + } + /// ?? + pub fn at_plus_one(&self) -> char { self.rope.get_char(self.cursor).unwrap_or('\n') } #[implicit_fn] @@ -405,8 +409,8 @@ impl TextArea { .take_while(_.is_whitespace()) .count(); - self.cursor += if is_word(self.at()).not() - && !self.at().is_whitespace() + self.cursor += if is_word(self.at_plus_one()).not() + && !self.at_plus_one().is_whitespace() && !is_word(self.rope.char(self.cursor + 1)) { self.rope @@ -532,8 +536,9 @@ impl TextArea { self.set_ho(); } pub fn backspace_word(&mut self) { - _ = self.rope.try_remove(self.word_left_p()..self.cursor); - self.cursor = self.word_left_p(); + let c = self.word_left_p(); + _ = self.rope.try_remove(c..self.cursor); + self.cursor = c; self.setc(); self.set_ho(); } @@ -931,7 +936,7 @@ impl TextArea { } } -fn is_word(r: char) -> bool { +pub fn is_word(r: char) -> bool { matches!(r, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_') } pub static LOADER: LazyLock<Loader> = LazyLock::new(|| { |