use std::iter::repeat; use std::path::Path; use std::process::Stdio; use std::sync::Arc; use Default::default; use Into::into; use dsb::Cell; use dsb::cell::Style; use lsp_types::*; use rootcause::{bail, report}; use rust_analyzer::lsp::ext::*; use crate::FG; use crate::edi::{Editor, lsp}; use crate::gotolist::{At, GoToList}; use crate::lsp::{PathURI, Rq, tdpp}; use crate::menu::charc; use crate::menu::generic::{CorA, GenericMenu, MenuData}; use crate::sym::GoTo; use crate::text::{Bookmark, RopeExt, SortTedits, TextArea, col, color_}; macro_rules! repl { ($x:ty, $($with:tt)+) => { $($with)+ }; } macro_rules! commands { ($(#[doc = $d: literal] $t:tt $identifier: ident$(($($thing:ty),+))?: $c:literal),+ $(,)?) => { #[derive(Clone, PartialEq, Eq, Debug)] pub enum Cmd { $(#[doc = $d] $identifier $( ( $(Option<$thing>,)+ ) )? ),+ } impl Cmd { pub const ALL: [Cmd; { [$($c),+].len() }] = [$(Self::$identifier $( ( $(None::<$thing>,)+) )? ,)+]; pub fn name(&self) -> &'static str { match self { $(Self::$identifier $( ( $(repl!($thing, _),)+ ) )? => $c,)+ } } // pub fn wants(self) -> &'static [wants] { // match self { // $(Self::$identifier => &[$($(wants::$thing as u8,)+)?],)+ // } // } pub fn desc(&self) -> &'static str { match self { $(Self::$identifier $( ( $(repl!($thing, _),)+ ) )? => $d,)+ } } pub fn needs_lsp(&self) -> bool { match self { $(Self::$identifier $( ( $(repl!($thing, _),)+ ) )? => stringify!($t) == "@",)+ } } fn map(self, t: &TextArea) -> Result { match self { $($( Self::$identifier($( repl!($thing, None),)+) => { if !t.rope.to_string().starts_with(Self::$identifier(None).name()) { return Ok(self) } if let Some((_, x)) = t.to_string().split_once(" ") // && x.chars().all(|x| x.is_numeric())s && let Ok(n) = x.parse() { Ok(Self::$identifier(Some(n))) } else { Err(concat! ("supply ", $(stringify!($thing))+)) } })?)+ _ => Ok(self), } } fn cora(&self) -> CorA { match self { $($( Self::$identifier($( repl!($thing, None),)+) => { CorA::Complete })?)+ _ => CorA::Accept, } } } }; } commands!( /// move item at cursor down @ RAMoveID: "move-item-down", /// move item at cursor up @ RAMoveIU: "move-item-up", /// restart rust analyzer @ RARestart: "ra-restart", /// go to parent module @ RAParent: "parent", /// join lines under cursors. @ RAJoinLines: "join-lines", /// gets list of runnables @ RARunnables: "runnables", /// 󱛉 Open docs for type at cursor @ RADocs: "open-docs", /// Rebuilds rust-analyzer proc macros @ RARebuildProcMacros: "rebuild-proc-macros", /// Cancels current running rust-analyzer check process @ RACancelFlycheck: "cancel-flycheck", ///  Opens Cargo.toml file for this workspace @ RAOpenCargoToml: "open-cargo-toml", /// Runs the test at the cursor @ RARunTest: "run-test", ///  Go to the references to this symbol @ References: "references", /// Incoming callers of this function @ Incoming: "callers-of", /// Functions this function calls @ Outgoing: "calling", // /// View child modules // @ ViewChildModules: "child-modules", /// GoTo line, | GoTo(u32): "g", /// 󰃅 Define bookmark | DefineBookmark(String): "bookmark", /// 󰃆 Remove bookmark | RemoveBookmark: "remove-bookmark", /// Global bookmark menu | Bookmarks: "goto-bookmarks", ); pub enum Cmds {} impl MenuData for Cmds { const HEIGHT: usize = 30; type Data = (); type Element<'a> = Cmd; type E = &'static str; fn gn((): &()) -> impl Iterator { Cmd::ALL.into_iter() } fn should_complete<'a>(m: &GenericMenu) -> bool { !Cmd::ALL.iter().any(|x| m.tedit.to_string().starts_with(x.name())) } fn map<'a>( m: &GenericMenu, x: Self::Element<'a>, ) -> Result, Self::E> { x.map(&m.tedit) } fn complete_or_accept<'a>(x: &Self::Element<'a>) -> CorA { x.cora() } fn f(m: &GenericMenu) -> String { m.tedit .to_string() .split_once(" ") .map(|x| x.0) .unwrap_or(&m.tedit.to_string()) .into() } fn r( _: &Self::Data, x: Cmd, _: &Path, c: usize, selected: bool, indices: &[u32], to: &mut Vec, ) { let bg = if selected { col!("#262d3b") } else { col!("#1c212b") }; let ds: Style = Style::new(FG, bg); let d: Cell = Cell { letter: None, style: ds }; let mut b = vec![d; c]; let (bgt, col, ty) = (col!("#FFFFFF"), col!("#ACACAC"), ""); b.iter_mut().zip(ty.chars()).for_each(|(x, c)| { *x = (Style::new(col, bgt) | Style::BOLD).basic(c) }); let i = &mut b[..]; let qualifier = x.desc().chars(); let _left = i.len() as i32 - (charc(&x.name()) as i32 + qualifier.clone().count() as i32) - 3; i.iter_mut() .zip( x.name() .chars() .chain([' ']) .map(|x| ds.basic(x)) .zip(0..) .chain( qualifier .map(|x| { Style { bg, fg: color_("#858685"), ..default() } .basic(x) }) .zip(repeat(u32::MAX)), ), ) .for_each(|(a, (b, i))| { *a = b; if indices.contains(&i) { a.style |= (Style::BOLD, color_("#ffcc66")); } }); to.extend(b); } } pub type Commands = GenericMenu; impl<'a> crate::menu::Key<'a> for Cmd { fn key(&self) -> impl Into> { self.name() } } impl Editor { pub fn handle_command( &mut self, z: Cmd, w: Arc, ) -> rootcause::Result<()> { match z { Cmd::GoTo(Some(x)) => if let Ok(x) = self.text.try_line_to_char(x as _) { self.text.cursor.just(x, &self.text.rope); self.text.scroll_to_cursor_centering(); }, Cmd::DefineBookmark(Some(x)) => self.text.bookmarks.push(Bookmark { position: *self.text.cursor.first(), text: x.clone(), }), Cmd::RemoveBookmark => { let y = self.text.y(*self.text.cursor.first()).unwrap(); let r = self.text.line_to_char(y) ..self.text.line_to_char(y + 1); self.text .bookmarks .clone() .iter() .enumerate() .filter(|(_, x)| r.contains(&x.position)) .map(ttools::fst) .rev() .for_each(|x| drop(self.text.bookmarks.remove(x))); } Cmd::Bookmarks => { let src = self .files .iter() .chain(self.origin.as_ref().zip(Some(&*self))) .flat_map(|(f, x)| { x.text.bookmarks.iter().zip(repeat(f)) }) .map(|(b, path)| { ( GoTo { path: path.clone().into(), at: At::P(b.position), }, Some(b.text.clone()), ) }) .collect::>(); self.state = crate::edi::st::State::GoToL(GoToList { data: (src, Some(crate::gotolist::O::Bmk)), ..default() }); } z if z.needs_lsp() => return self.handle_lsp_command(z, w), x => unimplemented!("{x:?}"), } Ok(()) } pub fn handle_lsp_command( &mut self, z: Cmd, w: Arc, ) -> rootcause::Result<()> { let Some((l, o)) = lsp!(self + p) else { bail!("no lsp"); }; match z { Cmd::RAMoveIU | Cmd::RAMoveID => { let r = self .text .to_l_position(*self.text.cursor.first()) .unwrap(); let mut x = l.request_immediate::(&MoveItemParams { direction: if let Cmd::RAMoveIU = z { MoveItemDirection::Up } else { MoveItemDirection::Down }, text_document: o.tid(), range: Range { start : r, end : r}, })?; x.sort_tedits(); for t in x { self.text.apply_snippet_tedit(&t)?; } } Cmd::RARestart => { _ = l.request::(&())?.0; } Cmd::RAParent => { let Some(GotoDefinitionResponse::Link([ref x])) = l.request_immediate::( &TextDocumentPositionParams { text_document: o.tid(), position: self .text .to_l_position(*self.text.cursor.first()) .unwrap(), }, )? else { self.bar.last_action = "no such parent".into(); return Ok(()); }; self.go(x, w)?; } Cmd::RAJoinLines => { let teds = l.request_immediate::(&JoinLinesParams { ranges: self .text .cursor .iter() .filter_map(|x| { self.text.to_l_range( x.sel.map(into).unwrap_or(*x..*x), ) }) .collect(), text_document: o.tid(), })?; self.text .apply_tedits(&mut { teds }) .map_err(|_| report!("couldnt apply edits"))?; } Cmd::RADocs => { let u = l.request_immediate::( &TextDocumentPositionParams { position: self .text .to_l_position(*self.text.cursor.first()) .unwrap(), text_document: o.tid(), }, )?; use rust_analyzer::lsp::ext::ExternalDocsResponse::*; std::process::Command::new("firefox-nightly") .arg( match &u { WithLocal(ExternalDocsPair { web: Some(x), .. }) if let Some("doc.rust-lang.org") = x.domain() && let Some(x) = x.path().strip_prefix("/nightly/") && option_env!("USER") == Some("os") => format!("http://127.0.0.1:3242/{x}"), // i have a lighttpd server running WithLocal(ExternalDocsPair { local: Some(url), .. }) if let Ok(p) = url.to_file_path() && p.exists() => url.to_string(), WithLocal(ExternalDocsPair { web: Some(url), .. }) | Simple(Some(url)) => url.to_string(), _ => return Ok(()), } .to_string(), ) .stdout(Stdio::null()) .spawn() .unwrap(); } Cmd::RARebuildProcMacros => { _ = l.request::(&())?; } Cmd::RACancelFlycheck => l.notify::(&())?, Cmd::RAOpenCargoToml => { let Some(GotoDefinitionResponse::Scalar(x)) = &l.request_immediate::( &OpenCargoTomlParams { text_document: o.tid() }, )? else { bail!("wtf?"); }; self.go(x, w)?; } Cmd::RARunnables => { let p = self.text.to_l_position(*self.text.cursor.first()); let o = o.to_owned(); let x = l.runtime.spawn(l.runnables(&o, p)?); self.state = crate::edi::st::State::Runnables(Rq::new(x)); } Cmd::References => self.state = crate::edi::st::State::GoToL(GoToList { data: ( vec![], Some(crate::gotolist::O::References(Rq::new( l.runtime .spawn(l.go_to_references(tdpp!(self))?), ))), ), ..default() }), Cmd::Incoming => { let x = l.runtime.spawn(l.callers(tdpp!(self))); self.state = crate::edi::st::State::GoToL(GoToList { data: ( vec![], Some(crate::gotolist::O::Incoming(Rq::new(x))), ), ..default() }); } Cmd::Outgoing => { let x = l.runtime.spawn(l.calling(tdpp!(self))); self.state = crate::edi::st::State::GoToL(GoToList { data: ( vec![], Some(crate::gotolist::O::Outgoing(Rq::new(x))), ), ..default() }); } _ => unimplemented!(), } Ok(()) } }