use frostmark::MarkState;
use iced::keyboard::Modifiers;
use iced::widget::{
button, column, container, markdown, mouse_area, row, scrollable, stack, text, text_input,
};
use iced::window;
use iced::{Background, Color, Element, Length, Subscription};
use iced_code_editor::CodeEditor;
use iced_term::Terminal as IcedTerminal;
use std::collections::HashMap;
#[cfg(feature = "unstable-comet")]
use std::collections::VecDeque;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use crate::autocomplete::engine::Autocomplete;
use crate::config::preferences::{self as prefs, EditorPreferences};
use crate::features::command_input::CommandInput;
use crate::features::command_palette::CommandPalette;
use crate::features::file_tree::FileTree;
use crate::features::find_replace::FindReplace;
use crate::features::fuzzy_finder::FuzzyFinder;
use crate::features::terminal::Terminal;
use crate::features::updater::UpdateInfo;
use crate::message::Message;
use crate::scripting::{self, EditorCommand};
use crate::theme::*;
use crate::ui::{
activity_panel_separator_style, editor_container_style, empty_editor, file_finder_item_style,
file_finder_panel_style, search_input_style, search_panel_style, sidebar_editor_separator_style,
status_bar_style, tab_button_style, tab_close_button_style, tree_button_style, view_sidebar,
};
use crate::wakatime::{self, WakaTimeConfig};
mod commands;
mod lifecycle;
mod subscription;
mod update;
mod view_editor;
mod view_finders;
mod view_integrations;
mod view_overlays;
mod view_root;
mod view_settings;
#[allow(clippy::large_enum_variant)]
pub enum TabKind {
Editor {
code_editor: CodeEditor,
buffer: crate::features::editor_buffer::EditorBuffer,
},
#[allow(dead_code)]
Preview {
md_items: Vec<markdown::Item>,
},
Image {
handle: iced::widget::image::Handle,
},
#[cfg(feature = "video")]
Video {
player: iced_video_player::Video,
},
Audio {
file_path: std::path::PathBuf,
sink: std::sync::Arc<std::sync::Mutex<Option<rodio::Sink>>>,
stream: std::sync::Arc<(rodio::OutputStream, rodio::OutputStreamHandle)>,
playing: bool,
duration_secs: f32,
position_secs: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusPane {
Editor,
Terminal,
}
impl std::fmt::Debug for TabKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TabKind::Editor { .. } => f.debug_struct("Editor").finish_non_exhaustive(),
TabKind::Preview { .. } => f.debug_struct("Preview").finish_non_exhaustive(),
TabKind::Image { .. } => f.debug_struct("Image").finish_non_exhaustive(),
#[cfg(feature = "video")]
TabKind::Video { .. } => f.debug_struct("Video").finish_non_exhaustive(),
TabKind::Audio { .. } => f.debug_struct("Audio").finish_non_exhaustive(),
}
}
}
#[derive(Debug)]
pub struct Tab {
pub path: PathBuf,
pub name: String,
pub kind: TabKind,
pub autosave_requested_at: Option<Instant>,
pub autosave_in_flight: bool,
}
pub struct MarkdownPreviewPane {
pub source_path: PathBuf,
pub state: MarkState,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Notification {
pub message: String,
pub shown_at: Instant,
}
#[derive(Debug, Clone)]
struct PendingHoverRequest {
path: PathBuf,
position: iced_code_editor::LspPosition,
anchor_point: iced::Point,
started_at: Instant,
requested: bool,
}
const HOVER_TRIGGER_DELAY: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActivePanel {
Files,
Git,
}
pub struct App {
tabs: Vec<Tab>,
active_tab: Option<usize>,
cursor_line: usize,
cursor_col: usize,
file_tree: Option<FileTree>,
sidebar_visible: bool,
sidebar_width: f32,
resizing_sidebar: bool,
resize_start_x: Option<f32>,
resize_start_width: f32,
search_visible: bool,
search_query: String,
search_results: Vec<crate::features::search::SearchResult>,
search_input_id: iced::widget::Id,
file_finder_visible: bool,
file_finder_query: String,
file_finder_results: Vec<(i64, String, PathBuf)>,
file_finder_selected: usize,
all_workspace_files: Vec<(String, PathBuf)>,
recent_files: Vec<PathBuf>,
file_finder_input_id: iced::widget::Id,
fuzzy_finder: FuzzyFinder,
command_palette: CommandPalette,
command_palette_selected: usize,
command_palette_input_id: iced::widget::Id,
markdown_preview: Option<MarkdownPreviewPane>,
terminal: Terminal,
terminal_pane: Option<IcedTerminal>,
terminal_open: bool,
terminal_panel_height: f32,
focused_pane: FocusPane,
find_replace: FindReplace,
find_input_id: iced::widget::Id,
replace_input_id: iced::widget::Id,
command_input: CommandInput,
command_input_id: iced::widget::Id,
active_panel: ActivePanel,
git_changes: Vec<(String, String)>,
startup_page_open: bool,
startup_vim_mode: bool,
startup_helix_mode: bool,
startup_run_on_startup: bool,
startup_selected_theme: String,
settings_open: bool,
settings_section: String,
editor_preferences: EditorPreferences,
active_theme_name: String,
theme_dropdown_open: bool,
wakatime: WakaTimeConfig,
wakatime_api_key_hovered: bool,
last_wakatime_entity: Option<String>,
last_wakatime_sent_at: Option<Instant>,
notification: Option<Notification>,
update_banner: Option<UpdateInfo>,
lsp: crate::features::lsp::LspManager,
lsp_diagnostics: HashMap<PathBuf, Vec<crate::features::lsp::InlineDiagnostic>>,
lsp_overlay: iced_code_editor::LspOverlayState,
lsp_enabled: bool,
lsp_server_keys: HashMap<PathBuf, &'static str>,
pending_hover_request: Option<PendingHoverRequest>,
pending_sensitive_open: Option<PathBuf>,
tab_drag_index: Option<usize>,
tab_drag_start_x: f32,
tab_drag_curent_x: f32,
tab_drag_target: Option<usize>,
tab_drag_cursor_pos: Option<(f32, f32)>,
tab_hover_index: Option<usize>,
tab_hold_started_at: Option<Instant>,
tab_hold_index: Option<usize>,
autocomplete: Autocomplete,
modifier_state: Modifiers,
#[cfg(feature = "unstable-comet")]
developer_logs: VecDeque<(Instant, String)>,
#[cfg(feature = "unstable-comet")]
developer_panel_visible: bool,
}
impl Default for App {
fn default() -> Self {
let editor_preferences = prefs::load_preferences();
let active_theme_name = {
let name = &editor_preferences.theme_name;
if name == "Custom (theme.lua)" {
use crate::config::theme_manager;
let lua_theme = theme_manager::load_theme();
let t = crate::theme::ThemeColors::from_lua_theme(&lua_theme);
crate::theme::set_theme(t);
"Custom (theme.lua)".to_string()
} else if name == "default" || name.is_empty() {
let t = crate::theme::pinel_blueberry_dark_theme();
crate::theme::set_theme(t);
"Pinel Blueberry Dark".to_string()
} else {
let found = crate::theme::BUILTIN_THEMES
.iter()
.find(|&&t| t == name.as_str());
if let Some(&theme_name) = found {
let t = crate::theme::builtin_theme(theme_name);
crate::theme::set_theme(t);
theme_name.to_string()
} else {
let t = crate::theme::builtin_theme("Pinel Blueberry Dark");
crate::theme::set_theme(t);
"Pinel Blueberry Dark".to_string()
}
}
};
let mut app = Self {
tabs: Vec::new(),
active_tab: None,
cursor_line: 1,
cursor_col: 1,
file_tree: None,
sidebar_visible: true,
sidebar_width: SIDEBAR_DEFAULT_WIDTH,
resizing_sidebar: false,
resize_start_x: None,
resize_start_width: SIDEBAR_DEFAULT_WIDTH,
search_visible: false,
search_query: String::new(),
search_results: Vec::new(),
search_input_id: iced::widget::Id::unique(),
file_finder_visible: false,
file_finder_query: String::new(),
file_finder_results: Vec::new(),
file_finder_selected: 0,
all_workspace_files: Vec::new(),
recent_files: Vec::new(),
file_finder_input_id: iced::widget::Id::unique(),
fuzzy_finder: FuzzyFinder::default(),
command_palette: CommandPalette::default(),
command_palette_selected: 0,
command_palette_input_id: iced::widget::Id::unique(),
markdown_preview: None,
terminal: Terminal::default(),
terminal_pane: {
let shell = if cfg!(target_os = "windows") {
std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string())
} else {
std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string())
};
let settings = iced_term::settings::Settings {
backend: iced_term::settings::BackendSettings {
program: shell,
..Default::default()
},
..Default::default()
};
match IcedTerminal::new(0, settings) {
Ok(term) => Some(term),
Err(err) => {
eprintln!("Failed to initialize embedded terminal: {err}");
None
}
}
},
terminal_open: false,
terminal_panel_height: 240.0,
focused_pane: FocusPane::Editor,
find_replace: FindReplace::default(),
find_input_id: iced::widget::Id::unique(),
replace_input_id: iced::widget::Id::unique(),
command_input: CommandInput::default(),
command_input_id: iced::widget::Id::unique(),
active_panel: ActivePanel::Files,
git_changes: Vec::new(),
startup_page_open: editor_preferences.first_launch,
startup_vim_mode: false,
startup_helix_mode: false,
startup_run_on_startup: true,
startup_selected_theme: editor_preferences.theme_name.clone(),
settings_open: false,
settings_section: "general".to_string(),
editor_preferences,
active_theme_name,
theme_dropdown_open: false,
wakatime: wakatime::load(),
wakatime_api_key_hovered: false,
last_wakatime_entity: None,
last_wakatime_sent_at: None,
notification: None,
update_banner: None,
lsp: crate::features::lsp::LspManager::new(),
lsp_diagnostics: HashMap::new(),
lsp_overlay: iced_code_editor::LspOverlayState::new(),
lsp_enabled: true,
lsp_server_keys: HashMap::new(),
pending_hover_request: None,
pending_sensitive_open: None,
tab_drag_index: None,
tab_drag_start_x: 0.0,
tab_drag_curent_x: 0.0,
tab_drag_target: None,
tab_drag_cursor_pos: None,
tab_hover_index: None,
tab_hold_started_at: None,
tab_hold_index: None,
autocomplete: Autocomplete::new(),
modifier_state: Modifiers::default(),
#[cfg(feature = "unstable-comet")]
developer_logs: VecDeque::new(),
#[cfg(feature = "unstable-comet")]
developer_panel_visible: false,
};
let startup_script = scripting::load_startup_script();
#[cfg(not(feature = "unstable-comet"))]
let _ = (
&startup_script.path,
&startup_script.source,
&startup_script.error,
);
#[cfg(feature = "unstable-comet")]
{
app.push_developer_log(format!(
"startup lua path: {}",
startup_script.path.display()
));
match &startup_script.source {
Some(source) => {
app.push_developer_log("startup lua source begin".to_string());
for line in source.lines() {
app.push_developer_log(format!("lua> {line}"));
}
app.push_developer_log("startup lua source end".to_string());
}
None => {
app.push_developer_log("startup lua source not found".to_string());
}
}
if let Some(error) = &startup_script.error {
app.push_developer_log(format!("startup lua error: {error}"));
}
}
for command in startup_script.commands {
#[cfg(feature = "unstable-comet")]
app.push_developer_log(format!("startup lua command: {:?}", command));
app.apply_editor_command(command);
}
app
}
}
impl App {
#[cfg(feature = "unstable-comet")]
fn push_developer_log(&mut self, message: String) {
let now = Instant::now();
self.developer_logs.push_back((now, message));
if self.developer_logs.len() > 1000 {
self.developer_logs.pop_front();
}
}
#[cfg(feature = "unstable-comet")]
pub fn dev_log(&mut self, message: String) {
if self.editor_preferences.developer_mode {
self.push_developer_log(message);
}
}
#[cfg(not(feature = "unstable-comet"))]
pub fn dev_log(&mut self, _message: String) {}
pub(super) fn configured_code_editor(&self, content: &str, syntax: &str) -> CodeEditor {
let mut editor = iced_code_editor::CodeEditor::new(content, syntax);
editor.set_theme(theme().editor_style);
editor.set_font(iced::Font {
family: iced::font::Family::Name("Fira Code"),
weight: iced::font::Weight::Normal,
..iced::Font::DEFAULT
});
editor.set_search_replace_enabled(false);
editor.set_line_numbers_enabled(true);
editor.set_wrap_enabled(false);
editor.set_font_size(13.0, true);
let indent_style = if self.editor_preferences.use_spaces {
iced_code_editor::IndentStyle::Spaces(
self.editor_preferences.tab_size.clamp(1, 8) as u8
)
} else {
iced_code_editor::IndentStyle::Tab
};
editor.set_indent_style(indent_style);
editor
}
pub(super) fn apply_editor_theme_to_tabs(&mut self) {
let editor_style = theme().editor_style;
for tab in &mut self.tabs {
if let TabKind::Editor { code_editor, .. } = &mut tab.kind {
code_editor.set_theme(editor_style);
}
}
}
pub(super) fn apply_indent_style_to_tabs(&mut self) {
let indent_style = if self.editor_preferences.use_spaces {
iced_code_editor::IndentStyle::Spaces(
self.editor_preferences.tab_size.clamp(1, 8) as u8
)
} else {
iced_code_editor::IndentStyle::Tab
};
for tab in &mut self.tabs {
if let TabKind::Editor { code_editor, .. } = &mut tab.kind {
code_editor.set_indent_style(indent_style);
}
}
}
pub fn apply_editor_command(&mut self, command: EditorCommand) {
match command {
EditorCommand::UseBuiltinTheme(name) => {
let new_theme = crate::theme::builtin_theme(&name);
crate::theme::set_theme(new_theme);
self.apply_editor_theme_to_tabs();
self.active_theme_name = name.clone();
self.editor_preferences.theme_name = name;
}
EditorCommand::SetThemeColor { name, value } => {
let color = match crate::theme::parse_hex_color(&value) {
Ok(color) => color,
Err(err) => {
eprintln!("Lua theme error: {err}");
return;
}
};
let mut current = crate::theme::theme().clone();
if let Err(err) = current.set_named_color(&name, color) {
eprintln!("Lua theme error: {err}");
return;
}
crate::theme::set_theme(current);
self.apply_editor_theme_to_tabs();
self.active_theme_name = "Custom (init.lua)".to_string();
self.editor_preferences.theme_name = "Custom (init.lua)".to_string();
}
EditorCommand::SetSidebarVisible(visible) => {
self.sidebar_visible = visible;
}
EditorCommand::SetSidebarWidth(width) => {
self.sidebar_width = width.clamp(SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH);
}
}
}
}