/
githubmirror
/
lapce
Обзор
Документация
Войти
/
githubmirror
/
lapce
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
lapce-app/src/plugin.rs
1 023 строки
37 KB
Hanif Ariffin
Fix compilation with Rust 2024 Edition (#3637)
19 июн 2025, 10:52
Не верифицирован
19 июн 2025, 10:52
8d50fda
Код
Авторство
О чём код?
use std::{ collections::HashSet, rc::Rc, sync::{Arc, atomic::AtomicU64}, }; use anyhow::Result; use floem::{ IntoView, View, action::show_context_menu, ext_event::create_ext_action, keyboard::Modifiers, kurbo::Rect, menu::{Menu, MenuItem}, reactive::{ RwSignal, Scope, SignalGet, SignalUpdate, SignalWith, create_effect, create_memo, create_rw_signal, use_context, }, style::CursorStyle, views::{ Decorators, container, dyn_container, dyn_stack, empty, img, label, rich_text, scroll, stack, svg, text, }, }; use indexmap::IndexMap; use lapce_core::{command::EditCommand, directory::Directory, mode::Mode}; use lapce_proxy::plugin::{download_volt, volt_icon, wasi::find_all_volts}; use lapce_rpc::{ core::{CoreNotification, CoreRpcHandler}, plugin::{VoltID, VoltInfo, VoltMetadata}, }; use lsp_types::MessageType; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use crate::{ command::{CommandExecuted, CommandKind}, config::{LapceConfig, color::LapceColor}, db::LapceDb, editor::EditorData, keypress::{KeyPressFocus, condition::Condition}, main_split::Editors, markdown::{MarkdownContent, parse_markdown}, panel::plugin_view::VOLT_DEFAULT_PNG, web_link::web_link, window_tab::CommonData, }; type PluginInfo = Option<( Option<VoltMetadata>, VoltInfo, Option<VoltIcon>, Option<VoltInfo>, Option<RwSignal<bool>>, )>; #[derive(Clone, PartialEq, Eq)] pub enum VoltIcon { Svg(String), Img(Vec<u8>), } impl VoltIcon { pub fn from_bytes(buf: &[u8]) -> Result<Self> { if let Ok(s) = std::str::from_utf8(buf) { Ok(VoltIcon::Svg(s.to_string())) } else { Ok(VoltIcon::Img(buf.to_vec())) } } } #[derive(Deserialize, Serialize)] pub struct VoltsInfo { pub plugins: Vec<VoltInfo>, pub total: usize, } #[derive(Clone)] pub struct InstalledVoltData { pub meta: RwSignal<VoltMetadata>, pub icon: RwSignal<Option<VoltIcon>>, pub latest: RwSignal<VoltInfo>, } #[derive(Clone, PartialEq)] pub struct AvailableVoltData { pub info: RwSignal<VoltInfo>, pub icon: RwSignal<Option<VoltIcon>>, pub installing: RwSignal<bool>, } #[derive(Clone, Debug)] pub struct AvailableVoltList { pub loading: RwSignal<bool>, pub query_id: RwSignal<usize>, pub query_editor: EditorData, pub volts: RwSignal<IndexMap<VoltID, AvailableVoltData>>, pub total: RwSignal<usize>, } #[derive(Clone, Debug)] pub struct PluginData { pub installed: RwSignal<IndexMap<VoltID, InstalledVoltData>>, pub available: AvailableVoltList, pub all: RwSignal<im::HashMap<VoltID, AvailableVoltData>>, pub disabled: RwSignal<HashSet<VoltID>>, pub workspace_disabled: RwSignal<HashSet<VoltID>>, pub common: Rc<CommonData>, } impl KeyPressFocus for PluginData { fn get_mode(&self) -> Mode { Mode::Insert } fn check_condition(&self, condition: Condition) -> bool { matches!(condition, Condition::PanelFocus) } fn run_command( &self, command: &crate::command::LapceCommand, count: Option<usize>, mods: Modifiers, ) -> CommandExecuted { match &command.kind { CommandKind::Workbench(_) => {} CommandKind::Scroll(_) => {} CommandKind::Focus(_) => {} CommandKind::Edit(_) | CommandKind::Move(_) | CommandKind::MultiSelection(_) => { #[allow(clippy::single_match)] match command.kind { CommandKind::Edit(EditCommand::InsertNewLine) => { return CommandExecuted::Yes; } _ => {} } return self .available .query_editor .run_command(command, count, mods); } CommandKind::MotionMode(_) => {} } CommandExecuted::No } fn receive_char(&self, c: &str) { self.available.query_editor.receive_char(c); } } impl PluginData { pub fn new( cx: Scope, disabled: HashSet<VoltID>, workspace_disabled: HashSet<VoltID>, editors: Editors, common: Rc<CommonData>, core_rpc: CoreRpcHandler, ) -> Self { let installed = cx.create_rw_signal(IndexMap::new()); let available = AvailableVoltList { loading: cx.create_rw_signal(false), volts: cx.create_rw_signal(IndexMap::new()), total: cx.create_rw_signal(0), query_id: cx.create_rw_signal(0), query_editor: editors.make_local(cx, common.clone()), }; let disabled = cx.create_rw_signal(disabled); let workspace_disabled = cx.create_rw_signal(workspace_disabled); let plugin = Self { installed, available, all: cx.create_rw_signal(im::HashMap::new()), disabled, workspace_disabled, common, }; plugin.load_available_volts("", 0, core_rpc.clone()); { let plugin = plugin.clone(); let extra_plugin_paths = plugin.common.window_common.extra_plugin_paths.clone(); let send = create_ext_action( cx, move |volts: Vec<(Option<Vec<u8>>, VoltMetadata)>| { for (icon, meta) in volts { plugin.volt_installed(&meta, &icon); } }, ); std::thread::Builder::new() .name("FindAllVolts".to_owned()) .spawn(move || { let volts = find_all_volts(&extra_plugin_paths); let volts = volts .into_iter() .filter_map(|meta| { if meta.wasm.is_none() { Some((volt_icon(&meta), meta)) } else { None } }) .collect::<Vec<_>>(); send(volts); }) .unwrap(); } { let plugin = plugin.clone(); cx.create_effect(move |s| { let query = plugin .available .query_editor .doc_signal() .get() .buffer .with(|buffer| buffer.to_string()); if s.as_ref() == Some(&query) { return query; } plugin.available.query_id.update(|id| *id += 1); plugin.available.loading.set(false); plugin.available.volts.update(|v| v.clear()); plugin.load_available_volts(&query, 0, core_rpc.clone()); query }); } plugin } pub fn volt_installed(&self, volt: &VoltMetadata, icon: &Option<Vec<u8>>) { let volt_id = volt.id(); let (existing, is_latest, volt_data) = self .installed .try_update(|installed| { if let Some(v) = installed.get(&volt_id) { (true, true, v.to_owned()) } else { let (info, is_latest) = if let Some(volt) = self .available .volts .with_untracked(|all| all.get(&volt_id).cloned()) { (volt.info.get_untracked(), true) } else { (volt.info(), false) }; let latest = self.common.scope.create_rw_signal(info); let data = InstalledVoltData { meta: self.common.scope.create_rw_signal(volt.clone()), icon: self.common.scope.create_rw_signal( icon.as_ref() .and_then(|icon| VoltIcon::from_bytes(icon).ok()), ), latest, }; installed.insert(volt_id, data.clone()); (false, is_latest, data) } }) .unwrap(); if existing { volt_data.meta.set(volt.clone()); volt_data.icon.set( icon.as_ref() .and_then(|icon| VoltIcon::from_bytes(icon).ok()), ); } let latest = volt_data.latest; if !is_latest { let url = format!( "https://plugins.lapce.dev/api/v1/plugins/{}/{}/latest", volt.author, volt.name ); let send = create_ext_action(self.common.scope, move |info| { if let Some(info) = info { latest.set(info); } }); std::thread::spawn(move || { let info: Option<VoltInfo> = lapce_proxy::get_url(url, None) .ok() .and_then(|r| r.json().ok()); send(info); }); } } pub fn volt_removed(&self, volt: &VoltInfo) { let id = volt.id(); self.installed.update(|installed| { installed.swap_remove(&id); }); if self.disabled.with_untracked(|d| d.contains(&id)) { self.disabled.update(|d| { d.remove(&id); }); let db: Arc<LapceDb> = use_context().unwrap(); db.save_disabled_volts( self.disabled.get_untracked().into_iter().collect(), ); } if self.workspace_disabled.with_untracked(|d| d.contains(&id)) { self.workspace_disabled.update(|d| { d.remove(&id); }); let db: Arc<LapceDb> = use_context().unwrap(); db.save_workspace_disabled_volts( self.common.workspace.clone(), self.workspace_disabled .get_untracked() .into_iter() .collect(), ); } } fn load_available_volts( &self, query: &str, offset: usize, core_rpc: CoreRpcHandler, ) { if self.available.loading.get_untracked() { return; } self.available.loading.set(true); let volts = self.available.volts; let volts_total = self.available.total; let cx = self.common.scope; let loading = self.available.loading; let query_id = self.available.query_id; let current_query_id = self.available.query_id.get_untracked(); let all = self.all; let send = create_ext_action(self.common.scope, move |new: Result<VoltsInfo>| { loading.set(false); if query_id.get_untracked() != current_query_id { return; } match new { Ok(new) => { volts.update(|volts| { volts.extend(new.plugins.into_iter().map(|volt| { let icon = cx.create_rw_signal(None); let send = create_ext_action(cx, move |result| { if let Ok(i) = result { icon.set(Some(i)); } }); { let volt = volt.clone(); std::thread::spawn(move || { let result = Self::load_icon(&volt); send(result); }); } let data = AvailableVoltData { info: cx.create_rw_signal(volt.clone()), icon, installing: cx.create_rw_signal(false), }; all.update(|all| { all.insert(volt.id(), data.clone()); }); (volt.id(), data) })); }); volts_total.set(new.total); } Err(err) => { tracing::error!("{:?}", err); core_rpc.notification(CoreNotification::ShowMessage { title: "Request Available Plugins".to_string(), message: lsp_types::ShowMessageParams { typ: MessageType::ERROR, message: err.to_string(), }, }); } } }); let query = query.to_string(); std::thread::spawn(move || { let volts = Self::query_volts(&query, offset); send(volts); }); } fn load_icon(volt: &VoltInfo) -> Result<VoltIcon> { let url = format!( "https://plugins.lapce.dev/api/v1/plugins/{}/{}/{}/icon?id={}", volt.author, volt.name, volt.version, volt.updated_at_ts ); let cache_file_path = Directory::cache_directory().map(|cache_dir| { let mut hasher = Sha256::new(); hasher.update(url.as_bytes()); let filename = format!("{:x}", hasher.finalize()); cache_dir.join(filename) }); let cache_content = cache_file_path.as_ref().and_then(|p| std::fs::read(p).ok()); let content = match cache_content { Some(content) => content, None => { let resp = lapce_proxy::get_url(&url, None)?; if !resp.status().is_success() { return Err(anyhow::anyhow!("can't download icon")); } let buf = resp.bytes()?.to_vec(); if let Some(path) = cache_file_path.as_ref() { if let Err(err) = std::fs::write(path, &buf) { tracing::error!("{:?}", err); } } buf } }; VoltIcon::from_bytes(&content) } fn download_readme( volt: &VoltInfo, config: &LapceConfig, ) -> Result<Vec<MarkdownContent>> { let url = format!( "https://plugins.lapce.dev/api/v1/plugins/{}/{}/{}/readme", volt.author, volt.name, volt.version ); let resp = lapce_proxy::get_url(&url, None)?; if resp.status() != 200 { let text = parse_markdown("Plugin doesn't have a README", 2.0, config); return Ok(text); } let text = resp.text()?; let text = parse_markdown(&text, 2.0, config); Ok(text) } fn query_volts(query: &str, offset: usize) -> Result<VoltsInfo> { let url = format!( "https://plugins.lapce.dev/api/v1/plugins?q={query}&offset={offset}" ); let plugins: VoltsInfo = lapce_proxy::get_url(url, None)?.json()?; Ok(plugins) } fn all_loaded(&self) -> bool { self.available.volts.with_untracked(|v| v.len()) >= self.available.total.get_untracked() } pub fn load_more_available(&self, core_rpc: CoreRpcHandler) { if self.all_loaded() { return; } let query = self .available .query_editor .doc() .buffer .with_untracked(|buffer| buffer.to_string()); let offset = self.available.volts.with_untracked(|v| v.len()); self.load_available_volts(&query, offset, core_rpc); } pub fn install_volt(&self, info: VoltInfo) { self.available.volts.with_untracked(|volts| { if let Some(volt) = volts.get(&info.id()) { volt.installing.set(true); }; }); if info.wasm { self.common.proxy.install_volt(info); } else { let plugin = self.clone(); let send = create_ext_action(self.common.scope, move |result| { if let Ok((meta, icon)) = result { plugin.volt_installed(&meta, &icon); } }); std::thread::spawn(move || { let download = || -> Result<(VoltMetadata, Option<Vec<u8>>)> { let download_volt_result = download_volt(&info); let meta = download_volt_result?; let icon = volt_icon(&meta); Ok((meta, icon)) }; send(download()); }); } } pub fn plugin_disabled(&self, id: &VoltID) -> bool { self.disabled.with_untracked(|d| d.contains(id)) || self.workspace_disabled.with_untracked(|d| d.contains(id)) } pub fn enable_volt(&self, volt: VoltInfo) { let id = volt.id(); self.disabled.update(|d| { d.remove(&id); }); if !self.plugin_disabled(&id) { self.common.proxy.enable_volt(volt); } let db: Arc<LapceDb> = use_context().unwrap(); db.save_disabled_volts(self.disabled.get_untracked().into_iter().collect()); } pub fn disable_volt(&self, volt: VoltInfo) { let id = volt.id(); self.disabled.update(|d| { d.insert(id); }); self.common.proxy.disable_volt(volt); let db: Arc<LapceDb> = use_context().unwrap(); db.save_disabled_volts(self.disabled.get_untracked().into_iter().collect()); } pub fn enable_volt_for_ws(&self, volt: VoltInfo) { let id = volt.id(); self.workspace_disabled.update(|d| { d.remove(&id); }); if !self.plugin_disabled(&id) { self.common.proxy.enable_volt(volt); } let db: Arc<LapceDb> = use_context().unwrap(); db.save_workspace_disabled_volts( self.common.workspace.clone(), self.disabled.get_untracked().into_iter().collect(), ); } pub fn disable_volt_for_ws(&self, volt: VoltInfo) { let id = volt.id(); self.workspace_disabled.update(|d| { d.insert(id); }); self.common.proxy.disable_volt(volt); let db: Arc<LapceDb> = use_context().unwrap(); db.save_workspace_disabled_volts( self.common.workspace.clone(), self.disabled.get_untracked().into_iter().collect(), ); } pub fn uninstall_volt(&self, volt: VoltMetadata) { if volt.wasm.is_some() { self.common.proxy.remove_volt(volt); } else { let plugin = self.clone(); let info = volt.info(); let send = create_ext_action(self.common.scope, move |result: Result<()>| { if let Ok(()) = result { plugin.volt_removed(&info); } }); std::thread::spawn(move || { let uninstall = || -> Result<()> { let path = volt .dir .as_ref() .ok_or_else(|| anyhow::anyhow!("don't have dir"))?; std::fs::remove_dir_all(path)?; Ok(()) }; send(uninstall()); }); } } pub fn reload_volt(&self, volt: VoltMetadata) { self.common.proxy.reload_volt(volt); } pub fn plugin_controls(&self, meta: VoltMetadata, latest: VoltInfo) -> Menu { let volt_id = meta.id(); let mut menu = Menu::new(""); if meta.version != latest.version { menu = menu .entry(MenuItem::new("Upgrade Plugin").action({ let plugin = self.clone(); let info = latest.clone(); move || { plugin.install_volt(info.clone()); } })) .separator(); } menu = menu .entry(MenuItem::new("Reload Plugin").action({ let plugin = self.clone(); let meta = meta.clone(); move || { plugin.reload_volt(meta.clone()); } })) .separator() .entry( MenuItem::new("Enable") .enabled( self.disabled .with_untracked(|disabled| disabled.contains(&volt_id)), ) .action({ let plugin = self.clone(); let volt = meta.info(); move || { plugin.enable_volt(volt.clone()); } }), ) .entry( MenuItem::new("Disable") .enabled( self.disabled .with_untracked(|disabled| !disabled.contains(&volt_id)), ) .action({ let plugin = self.clone(); let volt = meta.info(); move || { plugin.disable_volt(volt.clone()); } }), ) .separator() .entry( MenuItem::new("Enable For Workspace") .enabled( self.workspace_disabled .with_untracked(|disabled| disabled.contains(&volt_id)), ) .action({ let plugin = self.clone(); let volt = meta.info(); move || { plugin.enable_volt_for_ws(volt.clone()); } }), ) .entry( MenuItem::new("Disable For Workspace") .enabled( self.workspace_disabled .with_untracked(|disabled| !disabled.contains(&volt_id)), ) .action({ let plugin = self.clone(); let volt = meta.info(); move || { plugin.disable_volt_for_ws(volt.clone()); } }), ) .separator() .entry(MenuItem::new("Uninstall").action({ let plugin = self.clone(); move || { plugin.uninstall_volt(meta.clone()); } })); menu } } pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View { let config = plugin.common.config; let header_rect = create_rw_signal(Rect::ZERO); let scroll_width: RwSignal<f64> = create_rw_signal(0.0); let internal_command = plugin.common.internal_command; let local_plugin = plugin.clone(); let plugin_info = create_memo(move |_| { plugin .installed .with(|volts| { volts.get(&volt).map(|v| { ( Some(v.meta.get()), v.meta.get().info(), v.icon.get(), Some(v.latest.get()), None, ) }) }) .or_else(|| { plugin.all.with(|volts| { volts.get(&volt).map(|v| { (None, v.info.get(), v.icon.get(), None, Some(v.installing)) }) }) }) }); let version_view = move |plugin: PluginData, plugin_info: PluginInfo| { let version_info = plugin_info.as_ref().map(|(_, volt, _, latest, _)| { ( volt.version.clone(), latest.as_ref().map(|i| i.version.clone()), ) }); let installing = plugin_info .as_ref() .and_then(|(_, _, _, _, installing)| *installing); let local_version_info = version_info.clone(); let control = { move |version_info: Option<(String, Option<String>)>| match version_info .as_ref() .map(|(v, l)| match l { Some(l) => (true, l == v), None => (false, false), }) { Some((true, true)) => "Installed ▼", Some((true, false)) => "Upgrade ▼", _ => { if installing.map(|i| i.get()).unwrap_or(false) { "Installing" } else { "Install" } } } }; let local_plugin_info = plugin_info.clone(); let local_plugin = plugin.clone(); stack(( text( version_info .as_ref() .map(|(v, _)| format!("v{v}")) .unwrap_or_default(), ), label(move || control(local_version_info.clone())) .style(move |s| { let config = config.get(); s.margin_left(10) .padding_horiz(10) .border_radius(6.0) .color( config .color(LapceColor::LAPCE_BUTTON_PRIMARY_FOREGROUND), ) .background( config .color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND), ) .hover(|s| { s.cursor(CursorStyle::Pointer).background( config .color( LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND, ) .multiply_alpha(0.8), ) }) .active(|s| { s.background( config .color( LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND, ) .multiply_alpha(0.6), ) }) .disabled(|s| { s.background(config.color(LapceColor::EDITOR_DIM)) }) .selectable(false) }) .disabled(move || installing.map(|i| i.get()).unwrap_or(false)) .on_click_stop(move |_| { if let Some((meta, info, _, latest, _)) = local_plugin_info.as_ref() { if let Some(meta) = meta { let menu = local_plugin.plugin_controls( meta.to_owned(), latest.clone().unwrap_or_else(|| info.to_owned()), ); show_context_menu(menu, None); } else { local_plugin.install_volt(info.to_owned()); } } }), )) }; scroll( dyn_container( move || plugin_info.get(), move |plugin_info| { stack(( stack(( match plugin_info .as_ref() .and_then(|(_, _, icon, _, _)| icon.clone()) { None => container( img(move || VOLT_DEFAULT_PNG.to_vec()) .style(|s| s.size_full()), ), Some(VoltIcon::Svg(svg_str)) => container( svg(move || svg_str.clone()) .style(|s| s.size_full()), ), Some(VoltIcon::Img(buf)) => container( img(move || buf.clone()).style(|s| s.size_full()), ), } .style(|s| { s.min_size(150.0, 150.0).size(150.0, 150.0).padding(20) }), stack(( text( plugin_info .as_ref() .map(|(_, volt, _, _, _)| { volt.display_name.as_str() }) .unwrap_or(""), ) .style(move |s| { s.font_bold().font_size( (config.get().ui.font_size() as f32 * 1.6) .round(), ) }), text( plugin_info .as_ref() .map(|(_, volt, _, _, _)| { volt.description.as_str() }) .unwrap_or(""), ) .style(move |s| { let scroll_width = scroll_width.get(); s.max_width( scroll_width .clamp(200.0 + 60.0 * 2.0 + 200.0, 800.0) - 60.0 * 2.0 - 200.0, ) }), { let repo = plugin_info .as_ref() .and_then(|(_, volt, _, _, _)| { volt.repository.as_deref() }) .unwrap_or("") .to_string(); let local_repo = repo.clone(); stack(( text("Repository: "), web_link( move || repo.clone(), move || local_repo.clone(), move || { config .get() .color(LapceColor::EDITOR_LINK) }, internal_command, ), )) }, text( plugin_info .as_ref() .map(|(_, volt, _, _, _)| volt.author.as_str()) .unwrap_or(""), ) .style(move |s| { s.color(config.get().color(LapceColor::EDITOR_DIM)) }), version_view(local_plugin.clone(), plugin_info.clone()), )) .style(|s| s.flex_col().line_height(1.6)), )) .style(|s| s.absolute()) .on_resize(move |rect| { if header_rect.get_untracked() != rect { header_rect.set(rect); } }), empty().style(move |s| { let rect = header_rect.get(); s.size(rect.width(), rect.height()).pointer_events_none() }), empty().style(move |s| { s.margin_vert(6) .height(1) .width_full() .background(config.get().color(LapceColor::LAPCE_BORDER)) }), { let readme = create_rw_signal(None); let info = plugin_info .as_ref() .map(|(_, info, _, _, _)| info.to_owned()); create_effect(move |_| { let config = config.get(); let info = info.clone(); if let Some(info) = info { let cx = Scope::current(); let send = create_ext_action(cx, move |result| { if let Ok(md) = result { readme.set(Some(md)); } }); std::thread::spawn(move || { let result = PluginData::download_readme(&info, &config); send(result); }); } }); { let id = AtomicU64::new(0); dyn_stack( move || { readme.get().unwrap_or_else(|| { parse_markdown( "Loading README", 2.0, &config.get(), ) }) }, move |_| { id.fetch_add( 1, std::sync::atomic::Ordering::Relaxed, ) }, move |content| match content { MarkdownContent::Text(text_layout) => container( rich_text(move || text_layout.clone()) .style(|s| s.width_full()), ) .style(|s| s.width_full()), MarkdownContent::Image { .. } => { container(empty()) } MarkdownContent::Separator => { container(empty().style(move |s| { s.width_full() .margin_vert(5.0) .height(1.0) .background( config.get().color( LapceColor::LAPCE_BORDER, ), ) })) } }, ) .style(|s| s.flex_col().width_full()) } }, )) .style(move |s| { let padding = 60.0; s.flex_col() .width( scroll_width .get() .min(800.0) .max(header_rect.get().width() + padding * 2.0), ) .padding(padding) }) .into_any() }, ) .style(|s| s.min_width_full().justify_center()), ) .on_resize(move |rect| { if scroll_width.get_untracked() != rect.width() { scroll_width.set(rect.width()); } }) .style(|s| s.absolute().size_full()) .debug_name("Plugin Info") }