From 0ed1d53946e421cd8037e0bda92606fa5ea00399 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Wed, 1 Mar 2023 18:46:33 -0800 Subject: [PATCH] Support completing commands, usernames, and room names (#44) --- Cargo.lock | 33 ++++- Cargo.toml | 2 +- src/base.rs | 228 +++++++++++++++++++++++++++++++-- src/commands.rs | 88 ++++++++++--- src/keybindings.rs | 17 +-- src/main.rs | 9 +- src/tests.rs | 8 ++ src/windows/mod.rs | 51 +++++--- src/windows/room/chat.rs | 22 +++- src/windows/room/mod.rs | 30 ++++- src/windows/room/scrollback.rs | 49 +++++-- src/windows/welcome.rs | 19 ++- src/worker.rs | 26 +++- 13 files changed, 491 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 018ae8c..1b08ce2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -860,6 +860,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "errno" version = "0.2.8" @@ -1302,7 +1308,7 @@ dependencies = [ [[package]] name = "iamb" -version = "0.0.4" +version = "0.0.5" dependencies = [ "bitflags", "chrono", @@ -1876,9 +1882,9 @@ dependencies = [ [[package]] name = "modalkit" -version = "0.0.11" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7bd7d02d65842dab4cea53016cf29c16cde197131dd6d9eea95662deb77778" +checksum = "9a4e5226a5d33a7bdf5b4fc067baa211c94f21aa6667af5eec8b357a8af5e4ba" dependencies = [ "anymap2", "arboard", @@ -1888,10 +1894,12 @@ dependencies = [ "intervaltree", "libc", "nom", + "radix_trie", "regex", "ropey", "thiserror", "tui", + "unicode-segmentation", ] [[package]] @@ -1900,6 +1908,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nix" version = "0.24.3" @@ -2344,6 +2361,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.7.3" diff --git a/Cargo.toml b/Cargo.toml index e757a9f..567f9e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ unicode-width = "0.1.10" url = {version = "^2.2.2", features = ["serde"]} [dependencies.modalkit] -version = "0.0.11" +version = "0.0.12" [dependencies.matrix-sdk] version = "0.6" diff --git a/src/base.rs b/src/base.rs index 8c085d2..0661a4d 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,8 +1,11 @@ +use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::hash::Hash; +use std::str::FromStr; use std::sync::Arc; use std::time::{Duration, Instant}; +use emojis::Emoji; use tokio::sync::Mutex as AsyncMutex; use tracing::warn; @@ -23,6 +26,7 @@ use matrix_sdk::{ AnyMessageLikeEvent, MessageLikeEvent, }, + presence::PresenceState, EventId, OwnedEventId, OwnedRoomId, @@ -42,11 +46,15 @@ use modalkit::{ ApplicationStore, ApplicationWindowId, }, + base::{CommandType, WordStyle}, + completion::{complete_path, CompletionMap}, context::EditContext, + cursor::Cursor, + rope::EditRope, store::Store, }, env::vim::{ - command::{CommandContext, VimCommand, VimCommandMachine}, + command::{CommandContext, CommandDescription, VimCommand, VimCommandMachine}, keybindings::VimMachine, VimContext, }, @@ -66,6 +74,20 @@ use crate::{ ApplicationSettings, }; +pub const MATRIX_ID_WORD: WordStyle = WordStyle::CharSet(is_mxid_char); + +/// Find the boundaries for a Matrix username, room alias, or room ID. +/// +/// Technically "[" and "]" should be here since IPv6 addresses are allowed +/// in the server name, but in practice that should be uncommon, and people +/// can just use `gf` and friends in Visual mode instead. +fn is_mxid_char(c: char) -> bool { + return c >= 'a' && c <= 'z' || + c >= 'A' && c <= 'Z' || + c >= '0' && c <= '9' || + ":-./@_#!".contains(c); +} + const ROOM_FETCH_DEBOUNCE: Duration = Duration::from_secs(2); #[derive(Clone, Debug, Eq, PartialEq)] @@ -528,13 +550,28 @@ impl RoomInfo { } } +fn emoji_map() -> CompletionMap { + let mut emojis = CompletionMap::default(); + + for emoji in emojis::iter() { + for shortcode in emoji.shortcodes() { + emojis.insert(shortcode.to_string(), emoji); + } + } + + return emojis; +} + pub struct ChatStore { + pub cmds: ProgramCommands, pub worker: Requester, - pub rooms: HashMap, - pub names: HashMap, + pub rooms: CompletionMap, + pub names: CompletionMap, + pub presences: CompletionMap, pub verifications: HashMap, pub settings: ApplicationSettings, pub need_load: HashSet, + pub emojis: CompletionMap, } impl ChatStore { @@ -543,10 +580,13 @@ impl ChatStore { worker, settings, + cmds: crate::commands::setup_commands(), names: Default::default(), rooms: Default::default(), + presences: Default::default(), verifications: Default::default(), need_load: Default::default(), + emojis: emoji_map(), } } @@ -587,10 +627,10 @@ impl ChatStore { } pub fn load_older(&mut self, limit: u32) { - let ChatStore { need_load, rooms, worker, .. } = self; + let ChatStore { need_load, presences, rooms, worker, .. } = self; for room_id in std::mem::take(need_load).into_iter() { - let info = rooms.entry(room_id.clone()).or_default(); + let info = rooms.get_or_default(room_id.clone()); if info.recently_fetched() { need_load.insert(room_id); @@ -610,6 +650,9 @@ impl ChatStore { match res { Ok((fetch_id, msgs)) => { for msg in msgs.into_iter() { + let sender = msg.sender().to_owned(); + let _ = presences.get_or_default(sender); + match msg { AnyMessageLikeEvent::RoomMessage(msg) => { info.insert(msg); @@ -639,11 +682,11 @@ impl ChatStore { } pub fn get_room_info(&mut self, room_id: OwnedRoomId) -> &mut RoomInfo { - self.rooms.entry(room_id).or_default() + self.rooms.get_or_default(room_id) } pub fn set_room_name(&mut self, room_id: &RoomId, name: &str) { - self.rooms.entry(room_id.to_owned()).or_default().name = name.to_string().into(); + self.rooms.get_or_default(room_id.to_owned()).name = name.to_string().into(); } pub fn insert_sas(&mut self, sas: SasVerification) { @@ -686,7 +729,7 @@ impl RoomFocus { #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum IambBufferId { - Command, + Command(CommandType), Room(OwnedRoomId, RoomFocus), DirectList, MemberList(OwnedRoomId), @@ -699,7 +742,7 @@ pub enum IambBufferId { impl IambBufferId { pub fn to_window(&self) -> Option { match self { - IambBufferId::Command => None, + IambBufferId::Command(_) => None, IambBufferId::Room(room, _) => Some(IambId::Room(room.clone())), IambBufferId::DirectList => Some(IambId::DirectList), IambBufferId::MemberList(room) => Some(IambId::MemberList(room.clone())), @@ -719,6 +762,133 @@ impl ApplicationInfo for IambInfo { type Action = IambAction; type WindowId = IambId; type ContentId = IambBufferId; + + fn complete( + text: &EditRope, + cursor: &mut Cursor, + content: &IambBufferId, + store: &mut ProgramStore, + ) -> Vec { + match content { + IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store), + IambBufferId::Command(CommandType::Search) => vec![], + + IambBufferId::Room(_, RoomFocus::MessageBar) => { + complete_matrix_names(text, cursor, store) + }, + IambBufferId::Room(_, RoomFocus::Scrollback) => vec![], + + IambBufferId::DirectList => vec![], + IambBufferId::MemberList(_) => vec![], + IambBufferId::RoomList => vec![], + IambBufferId::SpaceList => vec![], + IambBufferId::VerifyList => vec![], + IambBufferId::Welcome => vec![], + } + } + + fn content_of_command(ct: CommandType) -> IambBufferId { + IambBufferId::Command(ct) + } +} + +fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec { + let id = text + .get_prefix_word_mut(cursor, &MATRIX_ID_WORD) + .unwrap_or_else(EditRope::empty); + let id = Cow::from(&id); + + store + .application + .presences + .complete(id.as_ref()) + .into_iter() + .map(|i| i.to_string()) + .collect() +} + +fn complete_matrix_names( + text: &EditRope, + cursor: &mut Cursor, + store: &ProgramStore, +) -> Vec { + let id = text + .get_prefix_word_mut(cursor, &MATRIX_ID_WORD) + .unwrap_or_else(EditRope::empty); + let id = Cow::from(&id); + + let list = store.application.names.complete(id.as_ref()); + if !list.is_empty() { + return list; + } + + let list = store.application.presences.complete(id.as_ref()); + if !list.is_empty() { + return list.into_iter().map(|i| i.to_string()).collect(); + } + + store + .application + .rooms + .complete(id.as_ref()) + .into_iter() + .map(|i| i.to_string()) + .collect() +} + +fn complete_emoji(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec { + let sc = text.get_prefix_word_mut(cursor, &WordStyle::Little); + let sc = sc.unwrap_or_else(EditRope::empty); + let sc = Cow::from(&sc); + + store.application.emojis.complete(sc.as_ref()) +} + +fn complete_cmdarg( + desc: CommandDescription, + text: &EditRope, + cursor: &mut Cursor, + store: &ProgramStore, +) -> Vec { + let cmd = match store.application.cmds.get(desc.command.as_str()) { + Ok(cmd) => cmd, + Err(_) => return vec![], + }; + + match cmd.name.as_str() { + "cancel" | "dms" | "edit" | "redact" | "reply" => vec![], + "members" | "rooms" | "spaces" | "welcome" => vec![], + "download" | "open" | "upload" => complete_path(text, cursor), + "react" | "unreact" => complete_emoji(text, cursor, store), + + "invite" => complete_users(text, cursor, store), + "join" => complete_matrix_names(text, cursor, store), + "room" => vec![], + "verify" => vec![], + _ => panic!("unknown command {}", cmd.name.as_str()), + } +} + +fn complete_cmdbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec { + let eo = text.cursor_to_offset(cursor); + let slice = text.slice(0.into(), eo, false); + let cow = Cow::from(&slice); + + match CommandDescription::from_str(cow.as_ref()) { + Ok(desc) => { + if desc.arg.untrimmed.is_empty() { + // Complete command name and set cursor position. + let _ = text.get_prefix_word_mut(cursor, &WordStyle::Little); + store.application.cmds.complete_name(desc.command.as_str()) + } else { + // Complete command argument. + complete_cmdarg(desc, text, cursor, store) + } + }, + + // Can't parse command text, so return zero completions. + Err(_) => vec![], + } } #[cfg(test)] @@ -804,4 +974,44 @@ pub mod tests { ]) ); } + + #[tokio::test] + async fn test_complete_cmdbar() { + let store = mock_store().await; + + let text = EditRope::from("invite "); + let mut cursor = Cursor::new(0, 7); + let id = text + .get_prefix_word_mut(&mut cursor, &MATRIX_ID_WORD) + .unwrap_or_else(EditRope::empty); + assert_eq!(id.to_string(), ""); + assert_eq!(cursor, Cursor::new(0, 7)); + + let text = EditRope::from("invite "); + let mut cursor = Cursor::new(0, 7); + let res = complete_cmdbar(&text, &mut cursor, &store); + assert_eq!(res, vec![ + "@user1:example.com", + "@user2:example.com", + "@user3:example.com", + "@user4:example.com", + "@user5:example.com" + ]); + + let text = EditRope::from("invite ignored"); + let mut cursor = Cursor::new(0, 7); + let res = complete_cmdbar(&text, &mut cursor, &store); + assert_eq!(res, vec![ + "@user1:example.com", + "@user2:example.com", + "@user3:example.com", + "@user4:example.com", + "@user5:example.com" + ]); + + let text = EditRope::from("invite @user1ignored"); + let mut cursor = Cursor::new(0, 13); + let res = complete_cmdbar(&text, &mut cursor, &store); + assert_eq!(res, vec!["@user1:example.com"]); + } } diff --git a/src/commands.rs b/src/commands.rs index b3c061a..8f6dfa8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -395,24 +395,76 @@ fn iamb_open(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { } fn add_iamb_commands(cmds: &mut ProgramCommands) { - cmds.add_command(ProgramCommand { names: vec!["cancel".into()], f: iamb_cancel }); - cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms }); - cmds.add_command(ProgramCommand { names: vec!["download".into()], f: iamb_download }); - cmds.add_command(ProgramCommand { names: vec!["open".into()], f: iamb_open }); - cmds.add_command(ProgramCommand { names: vec!["edit".into()], f: iamb_edit }); - cmds.add_command(ProgramCommand { names: vec!["invite".into()], f: iamb_invite }); - cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join }); - cmds.add_command(ProgramCommand { names: vec!["members".into()], f: iamb_members }); - cmds.add_command(ProgramCommand { names: vec!["react".into()], f: iamb_react }); - cmds.add_command(ProgramCommand { names: vec!["redact".into()], f: iamb_redact }); - cmds.add_command(ProgramCommand { names: vec!["reply".into()], f: iamb_reply }); - cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms }); - cmds.add_command(ProgramCommand { names: vec!["room".into()], f: iamb_room }); - cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces }); - cmds.add_command(ProgramCommand { names: vec!["unreact".into()], f: iamb_unreact }); - cmds.add_command(ProgramCommand { names: vec!["upload".into()], f: iamb_upload }); - cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify }); - cmds.add_command(ProgramCommand { names: vec!["welcome".into()], f: iamb_welcome }); + cmds.add_command(ProgramCommand { + name: "cancel".into(), + aliases: vec![], + f: iamb_cancel, + }); + cmds.add_command(ProgramCommand { name: "dms".into(), aliases: vec![], f: iamb_dms }); + cmds.add_command(ProgramCommand { + name: "download".into(), + aliases: vec![], + f: iamb_download, + }); + cmds.add_command(ProgramCommand { name: "open".into(), aliases: vec![], f: iamb_open }); + cmds.add_command(ProgramCommand { name: "edit".into(), aliases: vec![], f: iamb_edit }); + cmds.add_command(ProgramCommand { + name: "invite".into(), + aliases: vec![], + f: iamb_invite, + }); + cmds.add_command(ProgramCommand { name: "join".into(), aliases: vec![], f: iamb_join }); + cmds.add_command(ProgramCommand { + name: "members".into(), + aliases: vec![], + f: iamb_members, + }); + cmds.add_command(ProgramCommand { + name: "react".into(), + aliases: vec![], + f: iamb_react, + }); + cmds.add_command(ProgramCommand { + name: "redact".into(), + aliases: vec![], + f: iamb_redact, + }); + cmds.add_command(ProgramCommand { + name: "reply".into(), + aliases: vec![], + f: iamb_reply, + }); + cmds.add_command(ProgramCommand { + name: "rooms".into(), + aliases: vec![], + f: iamb_rooms, + }); + cmds.add_command(ProgramCommand { name: "room".into(), aliases: vec![], f: iamb_room }); + cmds.add_command(ProgramCommand { + name: "spaces".into(), + aliases: vec![], + f: iamb_spaces, + }); + cmds.add_command(ProgramCommand { + name: "unreact".into(), + aliases: vec![], + f: iamb_unreact, + }); + cmds.add_command(ProgramCommand { + name: "upload".into(), + aliases: vec![], + f: iamb_upload, + }); + cmds.add_command(ProgramCommand { + name: "verify".into(), + aliases: vec![], + f: iamb_verify, + }); + cmds.add_command(ProgramCommand { + name: "welcome".into(), + aliases: vec![], + f: iamb_welcome, + }); } pub fn setup_commands() -> ProgramCommands { diff --git a/src/keybindings.rs b/src/keybindings.rs index 42f3a98..4b93efe 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -1,34 +1,21 @@ use modalkit::{ editing::action::WindowAction, - editing::base::WordStyle, env::vim::keybindings::{InputStep, VimBindings}, env::vim::VimMode, input::bindings::{EdgeEvent, EdgeRepeat, InputBindings}, input::key::TerminalKey, }; -use crate::base::{IambAction, IambInfo, Keybindings}; +use crate::base::{IambAction, IambInfo, Keybindings, MATRIX_ID_WORD}; type IambStep = InputStep; -/// Find the boundaries for a Matrix username, room alias, or room ID. -/// -/// Technically "[" and "]" should be here since IPv6 addresses are allowed -/// in the server name, but in practice that should be uncommon, and people -/// can just use `gf` and friends in Visual mode instead. -fn is_mxid_char(c: char) -> bool { - return c >= 'a' && c <= 'z' || - c >= 'A' && c <= 'Z' || - c >= '0' && c <= '9' || - ":-./@_#!".contains(c); -} - pub fn setup_keybindings() -> Keybindings { let mut ism = Keybindings::empty(); let vim = VimBindings::default() .submit_on_enter() - .cursor_open(WordStyle::CharSet(is_mxid_char)); + .cursor_open(MATRIX_ID_WORD.clone()); vim.setup(&mut ism); diff --git a/src/main.rs b/src/main.rs index e1bf3f4..68626f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,13 +54,11 @@ use crate::{ AsyncProgramStore, ChatStore, IambAction, - IambBufferId, IambError, IambId, IambInfo, IambResult, ProgramAction, - ProgramCommands, ProgramContext, ProgramStore, }, @@ -116,7 +114,6 @@ struct Application { terminal: Terminal>, bindings: KeyManager, actstack: VecDeque<(ProgramAction, ProgramContext)>, - cmds: ProgramCommands, screen: ScreenState, } @@ -138,7 +135,6 @@ impl Application { let bindings = crate::keybindings::setup_keybindings(); let bindings = KeyManager::new(bindings); - let cmds = crate::commands::setup_commands(); let mut locked = store.lock().await; @@ -149,7 +145,7 @@ impl Application { .or_else(|| IambWindow::open(IambId::Welcome, locked.deref_mut()).ok()) .unwrap(); - let cmd = CommandBarState::new(IambBufferId::Command, locked.deref_mut()); + let cmd = CommandBarState::new(locked.deref_mut()); let screen = ScreenState::new(win, cmd); let worker = locked.application.worker.clone(); @@ -163,7 +159,6 @@ impl Application { terminal, bindings, actstack, - cmds, screen, }) } @@ -321,7 +316,7 @@ impl Application { None }, Action::Command(act) => { - let acts = self.cmds.command(&act, &ctx)?; + let acts = store.application.cmds.command(&act, &ctx)?; self.action_prepend(acts); None diff --git a/src/tests.rs b/src/tests.rs index 8fd44c8..8d7fc1c 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -208,6 +208,14 @@ pub async fn mock_store() -> ProgramStore { let worker = Requester { tx, client }; let mut store = ChatStore::new(worker, mock_settings()); + + // Add presence information. + store.presences.get_or_default(TEST_USER1.clone()); + store.presences.get_or_default(TEST_USER2.clone()); + store.presences.get_or_default(TEST_USER3.clone()); + store.presences.get_or_default(TEST_USER4.clone()); + store.presences.get_or_default(TEST_USER5.clone()); + let room_id = TEST_ROOM1_ID.clone(); let info = mock_room(); diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 6f8b5a8..e5e5df2 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -1,5 +1,4 @@ use std::cmp::{Ord, Ordering, PartialOrd}; -use std::collections::hash_map::Entry; use matrix_sdk::{ encryption::verification::{format_emojis, SasVerification}, @@ -45,7 +44,9 @@ use modalkit::{ ScrollStyle, ViewportContext, WordStyle, + WriteFlags, }, + completion::CompletionList, }, widgets::{ list::{List, ListCursor, ListItem, ListState}, @@ -469,6 +470,19 @@ impl WindowOps for IambWindow { delegate!(self, w => w.close(flags, store)) } + fn write( + &mut self, + path: Option<&str>, + flags: WriteFlags, + store: &mut ProgramStore, + ) -> IambResult { + delegate!(self, w => w.write(path, flags, store)) + } + + fn get_completions(&self) -> Option { + delegate!(self, w => w.get_completions()) + } + fn get_cursor_word(&self, style: &WordStyle) -> Option { delegate!(self, w => w.get_cursor_word(style)) } @@ -575,21 +589,18 @@ impl Window for IambWindow { fn find(name: String, store: &mut ProgramStore) -> IambResult { let ChatStore { names, worker, .. } = &mut store.application; - match names.entry(name) { - Entry::Vacant(v) => { - let room_id = worker.join_room(v.key().to_string())?; - v.insert(room_id.clone()); + if let Some(room) = names.get_mut(&name) { + let id = IambId::Room(room.clone()); - let (room, name, tags) = store.application.worker.get_room(room_id)?; - let room = RoomState::new(room, name, tags, store); + IambWindow::open(id, store) + } else { + let room_id = worker.join_room(name.clone())?; + names.insert(name, room_id.clone()); - Ok(room.into()) - }, - Entry::Occupied(o) => { - let id = IambId::Room(o.get().clone()); + let (room, name, tags) = store.application.worker.get_room(room_id)?; + let room = RoomState::new(room, name, tags, store); - IambWindow::open(id, store) - }, + Ok(room.into()) } } @@ -620,11 +631,16 @@ impl RoomItem { store: &mut ProgramStore, ) -> Self { let name = name.to_string(); + let room_id = room.room_id(); - let info = store.application.get_room_info(room.room_id().to_owned()); + let info = store.application.get_room_info(room_id.to_owned()); info.name = name.clone().into(); info.tags = tags.clone(); + if let Some(alias) = room.canonical_alias() { + store.application.names.insert(alias.to_string(), room_id.to_owned()); + } + RoomItem { room, tags, name } } } @@ -772,8 +788,13 @@ pub struct SpaceItem { impl SpaceItem { fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self { let name = name.to_string(); + let room_id = room.room_id(); - store.application.set_room_name(room.room_id(), name.as_str()); + store.application.set_room_name(room_id, name.as_str()); + + if let Some(alias) = room.canonical_alias() { + store.application.names.insert(alias.to_string(), room_id.to_owned()); + } SpaceItem { room, name } } diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index c1cc56d..9117110 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -52,7 +52,8 @@ use modalkit::editing::{ Scrollable, UIError, }, - base::{CloseFlags, Count, MoveDir1D, PositionList, ScrollStyle, WordStyle}, + base::{CloseFlags, Count, MoveDir1D, PositionList, ScrollStyle, WordStyle, WriteFlags}, + completion::CompletionList, context::Resolve, history::{self, HistoryList}, rope::EditRope, @@ -154,7 +155,7 @@ impl ChatState { let client = &store.application.worker.client; let settings = &store.application.settings; - let info = store.application.rooms.entry(self.room_id.clone()).or_default(); + let info = store.application.rooms.get_or_default(self.room_id.clone()); let msg = self .scrollback @@ -389,7 +390,7 @@ impl ChatState { .client .get_joined_room(self.id()) .ok_or(IambError::NotJoined)?; - let info = store.application.rooms.entry(self.id().to_owned()).or_default(); + let info = store.application.rooms.get_or_default(self.id().to_owned()); let mut show_echo = true; let (event_id, msg) = match act { @@ -550,6 +551,21 @@ impl WindowOps for ChatState { true } + fn write( + &mut self, + _: Option<&str>, + _: WriteFlags, + _: &mut ProgramStore, + ) -> IambResult { + // XXX: what's the right writing behaviour for a room? + // Should write send a message? + Ok(None) + } + + fn get_completions(&self) -> Option { + delegate!(self, w => w.get_completions()) + } + fn get_cursor_word(&self, style: &WordStyle) -> Option { delegate!(self, w => w.get_cursor_word(style)) } diff --git a/src/windows/room/mod.rs b/src/windows/room/mod.rs index 5a59568..9e59b54 100644 --- a/src/windows/room/mod.rs +++ b/src/windows/room/mod.rs @@ -40,7 +40,9 @@ use modalkit::{ PositionList, ScrollStyle, WordStyle, + WriteFlags, }, + editing::completion::CompletionList, input::InputContext, widgets::{TermOffset, TerminalCursor, WindowOps}, }; @@ -383,10 +385,30 @@ impl WindowOps for RoomState { } } - fn close(&mut self, _: CloseFlags, _: &mut ProgramStore) -> bool { - // XXX: what's the right closing behaviour for a room? - // Should write send a message? - true + fn close(&mut self, flags: CloseFlags, store: &mut ProgramStore) -> bool { + match self { + RoomState::Chat(chat) => chat.close(flags, store), + RoomState::Space(space) => space.close(flags, store), + } + } + + fn write( + &mut self, + path: Option<&str>, + flags: WriteFlags, + store: &mut ProgramStore, + ) -> IambResult { + match self { + RoomState::Chat(chat) => chat.write(path, flags, store), + RoomState::Space(space) => space.write(path, flags, store), + } + } + + fn get_completions(&self) -> Option { + match self { + RoomState::Chat(chat) => chat.get_completions(), + RoomState::Space(space) => space.get_completions(), + } } fn get_cursor_word(&self, style: &WordStyle) -> Option { diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs index 86fc124..f3d614b 100644 --- a/src/windows/room/scrollback.rs +++ b/src/windows/room/scrollback.rs @@ -32,6 +32,9 @@ use modalkit::editing::{ base::{ Axis, CloseFlags, + CompletionDisplay, + CompletionSelection, + CompletionType, Count, EditRange, EditTarget, @@ -51,7 +54,9 @@ use modalkit::editing::{ TargetShape, ViewportContext, WordStyle, + WriteFlags, }, + completion::CompletionList, context::{EditContext, Resolve}, cursor::{CursorGroup, CursorState}, history::HistoryList, @@ -60,7 +65,7 @@ use modalkit::editing::{ }; use crate::{ - base::{IambBufferId, IambInfo, ProgramContext, ProgramStore, RoomFocus, RoomInfo}, + base::{IambBufferId, IambInfo, IambResult, ProgramContext, ProgramStore, RoomFocus, RoomInfo}, config::ApplicationSettings, message::{Message, MessageCursor, MessageKey, Messages}, }; @@ -515,6 +520,23 @@ impl WindowOps for ScrollbackState { true } + fn write( + &mut self, + _: Option<&str>, + flags: WriteFlags, + _: &mut ProgramStore, + ) -> IambResult { + if flags.contains(WriteFlags::FORCE) { + Ok(None) + } else { + Err(EditError::ReadOnly.into()) + } + } + + fn get_completions(&self) -> Option { + None + } + fn get_cursor_word(&self, _: &WordStyle) -> Option { None } @@ -532,7 +554,7 @@ impl EditorActions for ScrollbackState { ctx: &ProgramContext, store: &mut ProgramStore, ) -> EditResult { - let info = store.application.rooms.entry(self.room_id.clone()).or_default(); + let info = store.application.rooms.get_or_default(self.room_id.clone()); let key = if let Some(k) = self.cursor.to_key(info) { k.clone() } else { @@ -762,6 +784,17 @@ impl EditorActions for ScrollbackState { } } + fn complete( + &mut self, + _: &CompletionType, + _: &CompletionSelection, + _: &CompletionDisplay, + _: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult { + Err(EditError::ReadOnly) + } + fn insert_text( &mut self, _: &InsertTextAction, @@ -867,9 +900,9 @@ impl Editable for ScrollbackState { EditorAction::Mark(name) => self.mark(ctx.resolve(name), ctx, store), EditorAction::Selection(act) => self.selection_command(act, ctx, store), - EditorAction::Complete(_, _) => { - let msg = ""; - let err = EditError::Unimplemented(msg.into()); + EditorAction::Complete(_, _, _) => { + let msg = "Nothing to complete in message scrollback"; + let err = EditError::Failure(msg.into()); Err(err) }, @@ -991,7 +1024,7 @@ impl ScrollActions for ScrollbackState { ctx: &ProgramContext, store: &mut ProgramStore, ) -> EditResult { - let info = store.application.rooms.entry(self.room_id.clone()).or_default(); + let info = store.application.rooms.get_or_default(self.room_id.clone()); let settings = &store.application.settings; let mut corner = self.viewctx.corner.clone(); @@ -1105,7 +1138,7 @@ impl ScrollActions for ScrollbackState { Err(err) }, Axis::Vertical => { - let info = store.application.rooms.entry(self.room_id.clone()).or_default(); + let info = store.application.rooms.get_or_default(self.room_id.clone()); let settings = &store.application.settings; if let Some(key) = self.cursor.to_key(info).cloned() { @@ -1193,7 +1226,7 @@ impl<'a> StatefulWidget for Scrollback<'a> { type State = ScrollbackState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - let info = self.store.application.rooms.entry(state.room_id.clone()).or_default(); + let info = self.store.application.rooms.get_or_default(state.room_id.clone()); let settings = &self.store.application.settings; let area = info.render_typing(area, buf, &self.store.application.settings); diff --git a/src/windows/welcome.rs b/src/windows/welcome.rs index 5c8c6da..543a582 100644 --- a/src/windows/welcome.rs +++ b/src/windows/welcome.rs @@ -8,9 +8,11 @@ use modalkit::{ widgets::{TermOffset, TerminalCursor}, }; -use modalkit::editing::base::{CloseFlags, WordStyle}; +use modalkit::editing::action::EditInfo; +use modalkit::editing::base::{CloseFlags, WordStyle, WriteFlags}; +use modalkit::editing::completion::CompletionList; -use crate::base::{IambBufferId, IambInfo, ProgramStore}; +use crate::base::{IambBufferId, IambInfo, IambResult, ProgramStore}; const WELCOME_TEXT: &str = include_str!("welcome.md"); @@ -63,6 +65,19 @@ impl WindowOps for WelcomeState { self.tbox.close(flags, store) } + fn write( + &mut self, + path: Option<&str>, + flags: WriteFlags, + store: &mut ProgramStore, + ) -> IambResult { + self.tbox.write(path, flags, store) + } + + fn get_completions(&self) -> Option { + self.tbox.get_completions() + } + fn get_cursor_word(&self, style: &WordStyle) -> Option { self.tbox.get_cursor_word(style) } diff --git a/src/worker.rs b/src/worker.rs index 27988be..dc55977 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -32,6 +32,7 @@ use matrix_sdk::{ start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent}, VerificationMethod, }, + presence::PresenceEvent, reaction::ReactionEventContent, room::{ message::{MessageType, RoomMessageEventContent}, @@ -503,6 +504,15 @@ impl ClientWorker { }, ); + let _ = + self.client + .add_event_handler(|ev: PresenceEvent, store: Ctx| { + async move { + let mut locked = store.lock().await; + locked.application.presences.insert(ev.sender, ev.content.presence); + } + }); + let _ = self.client.add_event_handler( |ev: SyncStateEvent, room: MatrixRoom, @@ -513,8 +523,7 @@ impl ClientWorker { let room_id = room.room_id().to_owned(); let room_name = Some(room_name.to_string()); let mut locked = store.lock().await; - let mut info = - locked.application.rooms.entry(room_id.to_owned()).or_default(); + let mut info = locked.application.rooms.get_or_default(room_id.clone()); info.name = room_name; } } @@ -529,8 +538,6 @@ impl ClientWorker { store: Ctx| { async move { let room_id = room.room_id(); - let room_name = room.display_name().await.ok(); - let room_name = room_name.as_ref().map(ToString::to_string); if let Some(msg) = ev.as_original() { if let MessageType::VerificationRequest(_) = msg.content.msgtype { @@ -545,8 +552,11 @@ impl ClientWorker { } let mut locked = store.lock().await; - let mut info = locked.application.get_room_info(room_id.to_owned()); - info.name = room_name; + + let sender = ev.sender().to_owned(); + let _ = locked.application.presences.get_or_default(sender); + + let info = locked.application.get_room_info(room_id.to_owned()); info.insert(ev.into_full_event(room_id.to_owned())); } }, @@ -560,6 +570,10 @@ impl ClientWorker { let room_id = room.room_id(); let mut locked = store.lock().await; + + let sender = ev.sender().to_owned(); + let _ = locked.application.presences.get_or_default(sender); + let info = locked.application.get_room_info(room_id.to_owned()); info.insert_reaction(ev.into_full_event(room_id.to_owned())); }