diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..14d86ad --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..2f476c4 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,89 @@ +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# powershell python python_jedi r rego +# ruby ruby_solargraph rust scala swift +# terraform toml typescript typescript_vts vue +# yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# - csharp: Requires the presence of a .sln file in the project folder. +# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus. +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- rust + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore in all projects +# same syntax as gitignore, so you can use * and ** +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "lala" +included_optional_tools: [] diff --git a/Cargo.lock b/Cargo.lock index d8d1c29..aa59377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2141,7 +2141,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "lala" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5a9c731..7ca7b4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lala" -version = "0.2.0" +version = "0.3.0" edition = "2021" authors = ["clearclown "] description = "A modern, lightweight text editor with GUI and CLI support for Markdown, HTML, Mermaid, and LaTeX" diff --git a/packaging/arch/.SRCINFO b/packaging/arch/.SRCINFO new file mode 100644 index 0000000..42fb0e4 --- /dev/null +++ b/packaging/arch/.SRCINFO @@ -0,0 +1,15 @@ +pkgbase = lala + pkgdesc = A modern, lightweight text editor with GUI and CLI support for Markdown, HTML, Mermaid, and LaTeX + pkgver = 0.3.0 + pkgrel = 1 + url = https://github.com/clearclown/lala + arch = x86_64 + arch = aarch64 + license = MIT + license = Apache-2.0 + makedepends = rust + makedepends = cargo + source = lala-0.3.0.tar.gz::https://github.com/clearclown/lala/archive/refs/tags/v0.3.0.tar.gz + sha256sums = f76a898a5a2fa7281f6e69d5e5f8f3074415c675a9f09732e3bd7777658ef941 + +pkgname = lala diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..88f9990 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,36 @@ +# Maintainer: clearclown +pkgname=lala +pkgver=0.3.0 +pkgrel=1 +pkgdesc="A modern, lightweight text editor with GUI and CLI support for Markdown, HTML, Mermaid, and LaTeX" +arch=('x86_64' 'aarch64') +url="https://github.com/clearclown/lala" +license=('MIT' 'Apache-2.0') +depends=() +makedepends=('rust' 'cargo') +source=("$pkgname-$pkgver.tar.gz::https://github.com/clearclown/$pkgname/archive/refs/tags/v$pkgver.tar.gz") +sha256sums=('f76a898a5a2fa7281f6e69d5e5f8f3074415c675a9f09732e3bd7777658ef941') + +build() { + cd "$pkgname-$pkgver" + cargo build --release --locked +} + +check() { + cd "$pkgname-$pkgver" + cargo test --release --locked +} + +package() { + cd "$pkgname-$pkgver" + + # バイナリのインストール + install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname" + + # ライセンスファイル + install -Dm644 LICENSE-MIT "$pkgdir/usr/share/licenses/$pkgname/LICENSE-MIT" + install -Dm644 LICENSE-APACHE "$pkgdir/usr/share/licenses/$pkgname/LICENSE-APACHE" + + # ドキュメント + install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" +} diff --git a/src/gui/app.rs b/src/gui/app.rs index a51c445..fff662e 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -176,8 +176,7 @@ impl LalaApp { fn handle_keyboard_shortcuts(&mut self, ctx: &egui::Context) { // Ctrl+S: Save file - if ctx.input(|i| i.modifiers.command && !i.modifiers.shift && i.key_pressed(egui::Key::S)) - { + if ctx.input(|i| i.modifiers.command && !i.modifiers.shift && i.key_pressed(egui::Key::S)) { self.save_file(); } @@ -197,8 +196,7 @@ impl LalaApp { } // Ctrl+F: Open search panel - if ctx.input(|i| i.modifiers.command && !i.modifiers.shift && i.key_pressed(egui::Key::F)) - { + if ctx.input(|i| i.modifiers.command && !i.modifiers.shift && i.key_pressed(egui::Key::F)) { self.show_search_panel = true; } @@ -328,7 +326,7 @@ impl LalaApp { self.llm_status = format!("File loaded ({} lines)", line_count); } else { eprintln!("Failed to read file: {:?}", path); - self.llm_status = format!("Error: Failed to read file"); + self.llm_status = "Error: Failed to read file".to_string(); } } @@ -533,9 +531,11 @@ impl eframe::App for LalaApp { // Show dialogs if self.show_file_dialog { - if let Some(path) = - dialogs::show_file_dialog(ctx, &mut self.show_file_dialog, &mut self.file_path_input) - { + if let Some(path) = dialogs::show_file_dialog( + ctx, + &mut self.show_file_dialog, + &mut self.file_path_input, + ) { self.open_file(path); } } diff --git a/src/gui/dialogs.rs b/src/gui/dialogs.rs index b437b67..1e46977 100644 --- a/src/gui/dialogs.rs +++ b/src/gui/dialogs.rs @@ -249,7 +249,8 @@ pub fn show_settings( .desired_width(300.0), ); - if ui.button("Apply").clicked() || response.lost_focus() && !api_key_input.is_empty() + if ui.button("Apply").clicked() + || response.lost_focus() && !api_key_input.is_empty() { // Try to create client with new API key match GeminiClient::new(api_key_input.clone()) { diff --git a/src/gui/markdown_preview.rs b/src/gui/markdown_preview.rs index c2553b5..0a06727 100644 --- a/src/gui/markdown_preview.rs +++ b/src/gui/markdown_preview.rs @@ -112,8 +112,13 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { } else { egui::Color32::from_rgb(17, 24, 39) }; - ui.label(egui::RichText::new(text).size(font_size).strong().color(heading_color)); - + ui.label( + egui::RichText::new(text) + .size(font_size) + .strong() + .color(heading_color), + ); + // Add underline for H1 and H2 if matches!(heading_level, HeadingLevel::H1 | HeadingLevel::H2) { ui.add_space(4.0); @@ -124,7 +129,10 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { }; ui.add(egui::Separator::default().spacing(0.0)); ui.painter().line_segment( - [ui.cursor().min, egui::pos2(ui.cursor().max.x, ui.cursor().min.y)], + [ + ui.cursor().min, + egui::pos2(ui.cursor().max.x, ui.cursor().min.y), + ], egui::Stroke::new(1.0, separator_color), ); } @@ -143,16 +151,14 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { Event::Start(Tag::Paragraph) => { i += 1; let rich_text = extract_rich_text(&events[i..], TagEnd::Paragraph); - + if in_blockquote { // Blockquote styling - ui.label(rich_text.italics().color( - if ui.visuals().dark_mode { - egui::Color32::from_rgb(161, 161, 170) - } else { - egui::Color32::from_rgb(107, 114, 128) - } - )); + ui.label(rich_text.italics().color(if ui.visuals().dark_mode { + egui::Color32::from_rgb(161, 161, 170) + } else { + egui::Color32::from_rgb(107, 114, 128) + })); } else { ui.add_space(4.0); ui.label(rich_text); @@ -170,7 +176,7 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { // ========== 引用 (Blockquotes) ========== Event::Start(Tag::BlockQuote(_)) => { - in_blockquote = true; + let _in_blockquote = true; // Used for context tracking ui.add_space(8.0); let border_color = if ui.visuals().dark_mode { egui::Color32::from_rgb(96, 165, 250) @@ -182,7 +188,7 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { } else { egui::Color32::from_rgba_unmultiplied(191, 219, 254, 100) }; - + egui::Frame::NONE .fill(bg_color) .stroke(egui::Stroke::new(3.0, border_color)) @@ -190,22 +196,23 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { .outer_margin(egui::Margin::symmetric(0, 4)) .show(ui, |ui| { i += 1; - while i < events.len() && !matches!(events[i], Event::End(TagEnd::BlockQuote(_))) { - match &events[i] { - Event::Start(Tag::Paragraph) => { + while i < events.len() + && !matches!(events[i], Event::End(TagEnd::BlockQuote(_))) + { + if let Event::Start(Tag::Paragraph) = &events[i] { + i += 1; + let text = extract_text_until_end(&events[i..], TagEnd::Paragraph); + let quote_color = if ui.visuals().dark_mode { + egui::Color32::from_rgb(212, 212, 216) + } else { + egui::Color32::from_rgb(55, 65, 81) + }; + ui.label(egui::RichText::new(text).italics().color(quote_color)); + while i < events.len() + && !matches!(events[i], Event::End(TagEnd::Paragraph)) + { i += 1; - let text = extract_text_until_end(&events[i..], TagEnd::Paragraph); - let quote_color = if ui.visuals().dark_mode { - egui::Color32::from_rgb(212, 212, 216) - } else { - egui::Color32::from_rgb(55, 65, 81) - }; - ui.label(egui::RichText::new(text).italics().color(quote_color)); - while i < events.len() && !matches!(events[i], Event::End(TagEnd::Paragraph)) { - i += 1; - } } - _ => {} } i += 1; } @@ -245,7 +252,7 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { ui.horizontal(|ui| { ui.add_space(indent); - + if let Some(checked) = task_list_marker.take() { // Task list item let checkbox = if checked { "☑" } else { "☐" }; @@ -278,7 +285,9 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { } else { egui::Color32::from_rgb(107, 114, 128) }; - ui.label(egui::RichText::new(format!("{}.", list_item_number)).color(num_color)); + ui.label( + egui::RichText::new(format!("{}.", list_item_number)).color(num_color), + ); ui.label(&text); } else { let bullet_color = if ui.visuals().dark_mode { @@ -312,7 +321,7 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { let code = extract_text_until_end(&events[i..], TagEnd::CodeBlock); ui.add_space(8.0); - + // Code block with language label let bg_color = if ui.visuals().dark_mode { egui::Color32::from_rgb(30, 30, 33) @@ -324,7 +333,7 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { } else { egui::Color32::from_rgb(209, 213, 219) }; - + egui::Frame::NONE .fill(bg_color) .stroke(egui::Stroke::new(1.0, border_color)) @@ -341,7 +350,7 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { ui.label(egui::RichText::new(&lang).small().color(label_color)); ui.add_space(4.0); } - + // Apply syntax highlighting if language is specified if !lang.is_empty() && !code.is_empty() { render_highlighted_code(ui, &code, &lang); @@ -352,11 +361,7 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { } else { egui::Color32::from_rgb(55, 65, 81) }; - ui.label( - egui::RichText::new(&code) - .monospace() - .color(code_color), - ); + ui.label(egui::RichText::new(&code).monospace().color(code_color)); } }); ui.add_space(8.0); @@ -391,19 +396,30 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { } // ========== リンク (Links) ========== - Event::Start(Tag::Link { dest_url, title, .. }) => { + Event::Start(Tag::Link { + dest_url, title, .. + }) => { i += 1; let link_text = extract_text_until_end(&events[i..], TagEnd::Link); let url = dest_url.to_string(); - let tooltip = if title.is_empty() { url.clone() } else { title.to_string() }; - + let tooltip = if title.is_empty() { + url.clone() + } else { + title.to_string() + }; + let link_color = if ui.visuals().dark_mode { egui::Color32::from_rgb(96, 165, 250) } else { egui::Color32::from_rgb(37, 99, 235) }; - - if ui.link(egui::RichText::new(&link_text).color(link_color).underline()) + + if ui + .link( + egui::RichText::new(&link_text) + .color(link_color) + .underline(), + ) .on_hover_text(&tooltip) .clicked() { @@ -421,18 +437,24 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { } // ========== 画像 (Images) ========== - Event::Start(Tag::Image { dest_url, title, .. }) => { + Event::Start(Tag::Image { + dest_url, title, .. + }) => { i += 1; let alt_text = extract_text_until_end(&events[i..], TagEnd::Image); - let tooltip = if title.is_empty() { alt_text.clone() } else { title.to_string() }; - + let tooltip = if title.is_empty() { + alt_text.clone() + } else { + title.to_string() + }; + ui.add_space(4.0); let border_color = if ui.visuals().dark_mode { egui::Color32::from_rgb(63, 63, 70) } else { egui::Color32::from_rgb(209, 213, 219) }; - + egui::Frame::NONE .stroke(egui::Stroke::new(1.0, border_color)) .corner_radius(egui::CornerRadius::same(4)) @@ -446,8 +468,14 @@ fn render_events(ui: &mut egui::Ui, events: &[Event]) { ui.horizontal(|ui| { ui.label(egui::RichText::new("🖼").size(20.0).color(icon_color)); ui.vertical(|ui| { - ui.label(egui::RichText::new(&alt_text).color(ui.visuals().text_color())); - ui.label(egui::RichText::new(dest_url.as_ref()).small().color(icon_color)); + ui.label( + egui::RichText::new(&alt_text).color(ui.visuals().text_color()), + ); + ui.label( + egui::RichText::new(dest_url.as_ref()) + .small() + .color(icon_color), + ); }); }); }) @@ -584,11 +612,7 @@ fn render_highlighted_code(ui: &mut egui::Ui, code: &str, lang: &str) { /// Convert syntect Style to egui Color32 fn style_to_color(style: Style) -> egui::Color32 { - egui::Color32::from_rgb( - style.foreground.r, - style.foreground.g, - style.foreground.b, - ) + egui::Color32::from_rgb(style.foreground.r, style.foreground.g, style.foreground.b) } // ========== ユニットテスト ========== diff --git a/src/gui/menu.rs b/src/gui/menu.rs index c978e73..828b143 100644 --- a/src/gui/menu.rs +++ b/src/gui/menu.rs @@ -121,9 +121,9 @@ pub fn render_menu_bar( .clicked() { if let Some(client) = llm_client { - match client - .improve_markdown(&format!("Summarize this text concisely:\n\n{current_text}")) - { + match client.improve_markdown(&format!( + "Summarize this text concisely:\n\n{current_text}" + )) { Ok(summary) => { *current_text = summary; *text_changed = true; diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 7d81bc2..084bfa8 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -18,4 +18,6 @@ pub use editor::EditorPanel; pub use highlighting::SyntaxHighlighter; pub use rtl::{RtlText, RtlUiExt, TextDirection}; pub use tab::EditorTabState; -pub use theme::{custom_dark_theme, custom_light_theme, EditorColors, MarkdownPreviewColors, ThemeMode}; +pub use theme::{ + custom_dark_theme, custom_light_theme, EditorColors, MarkdownPreviewColors, ThemeMode, +}; diff --git a/src/gui/previews.rs b/src/gui/previews.rs index ceec7d4..0429f11 100644 --- a/src/gui/previews.rs +++ b/src/gui/previews.rs @@ -114,10 +114,7 @@ pub fn render_mermaid_preview(ui: &mut egui::Ui, text: &str) { for line in lines { let trimmed = line.trim(); - if trimmed.is_empty() - || trimmed.starts_with("graph") - || trimmed.starts_with("flowchart") - { + if trimmed.is_empty() || trimmed.starts_with("graph") || trimmed.starts_with("flowchart") { continue; } diff --git a/src/gui/rtl.rs b/src/gui/rtl.rs index 369d9a3..ac35599 100644 --- a/src/gui/rtl.rs +++ b/src/gui/rtl.rs @@ -6,19 +6,14 @@ use egui::{text::LayoutJob, Color32, FontId, TextFormat}; /// Text direction -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TextDirection { + #[default] LeftToRight, RightToLeft, Mixed, } -impl Default for TextDirection { - fn default() -> Self { - TextDirection::LeftToRight - } -} - /// Check if a character is RTL #[inline] pub fn is_rtl_char(c: char) -> bool { @@ -75,11 +70,7 @@ pub fn get_text_alignment(direction: TextDirection) -> egui::Align { } /// Create a layout job with proper RTL support -pub fn create_rtl_aware_layout( - text: &str, - font_id: FontId, - default_color: Color32, -) -> LayoutJob { +pub fn create_rtl_aware_layout(text: &str, font_id: FontId, default_color: Color32) -> LayoutJob { let mut job = LayoutJob::default(); let direction = detect_text_direction(text); @@ -138,10 +129,9 @@ impl RtlUiExt for egui::Ui { fn rtl_label(&mut self, text: &str) -> egui::Response { let rtl_text = RtlText::new(text); if rtl_text.is_rtl() { - self.with_layout( - egui::Layout::right_to_left(egui::Align::Center), - |ui| ui.label(text), - ) + self.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(text) + }) .inner } else { self.label(text) @@ -151,10 +141,9 @@ impl RtlUiExt for egui::Ui { fn rtl_heading(&mut self, text: &str) -> egui::Response { let rtl_text = RtlText::new(text); if rtl_text.is_rtl() { - self.with_layout( - egui::Layout::right_to_left(egui::Align::Center), - |ui| ui.heading(text), - ) + self.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.heading(text) + }) .inner } else { self.heading(text) @@ -215,10 +204,7 @@ mod tests { ); // Mixed - more RTL - assert_eq!( - detect_text_direction("مرحبا Hello"), - TextDirection::Mixed - ); + assert_eq!(detect_text_direction("مرحبا Hello"), TextDirection::Mixed); // Empty string assert_eq!(detect_text_direction(""), TextDirection::LeftToRight); diff --git a/src/gui/theme.rs b/src/gui/theme.rs index 8bf0cbf..fd6d36e 100644 --- a/src/gui/theme.rs +++ b/src/gui/theme.rs @@ -68,7 +68,6 @@ pub fn custom_light_theme() -> egui::Visuals { visuals } - /// Creates a custom dark theme optimized for Markdown composition /// with comfortable contrast and modern aesthetics pub fn custom_dark_theme() -> egui::Visuals { @@ -139,19 +138,14 @@ pub fn custom_dark_theme() -> egui::Visuals { } /// Theme settings for the application -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Default)] pub enum ThemeMode { Light, - Dark, + #[default] + Dark, // Default to dark theme for code editing System, // Future: auto-detect system theme } -impl Default for ThemeMode { - fn default() -> Self { - ThemeMode::Dark // Default to dark theme for code editing - } -} - /// Get visuals for the specified theme mode #[allow(dead_code)] pub fn get_theme_visuals(mode: ThemeMode) -> egui::Visuals { diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 52baa10..b44e80b 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -172,7 +172,8 @@ impl GeminiClient { .map_err(|e| format!("Failed to parse response: {e}"))?; gemini_response - .candidates.first() + .candidates + .first() .and_then(|c| c.content.parts.first()) .map(|p| p.text.clone()) .ok_or_else(|| "No response from Gemini".to_string()) diff --git a/src/main.rs b/src/main.rs index 5ec1c13..88b315e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,9 +76,7 @@ fn setup_custom_fonts(ctx: &egui::Context) { std::fs::read("/usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf") .or_else(|_| std::fs::read("/usr/share/fonts/opentype/noto/NotoSansArabic-Regular.ttf")) .or_else(|_| std::fs::read("/usr/share/fonts/noto/NotoSansArabic-Regular.ttf")) - .or_else(|_| { - std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") - }) + .or_else(|_| std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")) // Windows paths .or_else(|_| std::fs::read("C:\\Windows\\Fonts\\arial.ttf")) // macOS paths diff --git a/tests/cli_test.rs b/tests/cli_test.rs index f2db12f..ca02863 100644 --- a/tests/cli_test.rs +++ b/tests/cli_test.rs @@ -87,11 +87,7 @@ mod directory_path_tests { let mode = parse_args(vec!["lala", "src"]); // Without extension, treated as directory when it exists as dir // If doesn't exist, still treated as directory - let expected = if PathBuf::from("src").is_dir() { - StartupMode::OpenDir(PathBuf::from("src")) - } else { - StartupMode::OpenDir(PathBuf::from("src")) - }; + let expected = StartupMode::OpenDir(PathBuf::from("src")); assert_eq!(mode, expected); } @@ -138,19 +134,13 @@ mod file_path_tests { #[test] fn test_relative_path_with_directory() { let mode = parse_args(vec!["lala", "./src/main.rs"]); - assert_eq!( - mode, - StartupMode::OpenFile(PathBuf::from("./src/main.rs")) - ); + assert_eq!(mode, StartupMode::OpenFile(PathBuf::from("./src/main.rs"))); } #[test] fn test_path_with_multiple_dots() { let mode = parse_args(vec!["lala", "file.test.txt"]); - assert_eq!( - mode, - StartupMode::OpenFile(PathBuf::from("file.test.txt")) - ); + assert_eq!(mode, StartupMode::OpenFile(PathBuf::from("file.test.txt"))); } #[test] @@ -177,10 +167,7 @@ mod file_path_tests { #[test] fn test_long_extension() { let mode = parse_args(vec!["lala", "archive.tar.gz"]); - assert_eq!( - mode, - StartupMode::OpenFile(PathBuf::from("archive.tar.gz")) - ); + assert_eq!(mode, StartupMode::OpenFile(PathBuf::from("archive.tar.gz"))); } #[test] @@ -447,10 +434,7 @@ mod edge_case_tests { #[test] fn test_unicode_directory() { let mode = parse_args(vec!["lala", "日本語フォルダ"]); - assert_eq!( - mode, - StartupMode::OpenDir(PathBuf::from("日本語フォルダ")) - ); + assert_eq!(mode, StartupMode::OpenDir(PathBuf::from("日本語フォルダ"))); } #[test] @@ -485,7 +469,9 @@ mod html_view_tests { #[test] fn test_render_html_with_table() { - html_view::render_html_to_terminal("
Cell 1Cell 2
"); + html_view::render_html_to_terminal( + "
Cell 1Cell 2
", + ); } #[test] @@ -594,7 +580,9 @@ mod latex_view_tests { #[test] fn test_render_simple_latex() { - latex_view::render_latex_to_terminal("\\documentclass{article}\n\\begin{document}\nHello\n\\end{document}"); + latex_view::render_latex_to_terminal( + "\\documentclass{article}\n\\begin{document}\nHello\n\\end{document}", + ); } #[test] @@ -604,7 +592,9 @@ mod latex_view_tests { #[test] fn test_render_latex_with_sections() { - latex_view::render_latex_to_terminal("\\section{Introduction}\nContent here.\n\\subsection{Details}"); + latex_view::render_latex_to_terminal( + "\\section{Introduction}\nContent here.\n\\subsection{Details}", + ); } #[test] @@ -614,7 +604,9 @@ mod latex_view_tests { #[test] fn test_render_latex_with_packages() { - latex_view::render_latex_to_terminal("\\documentclass{article}\n\\usepackage{amsmath}\n\\usepackage{graphicx}"); + latex_view::render_latex_to_terminal( + "\\documentclass{article}\n\\usepackage{amsmath}\n\\usepackage{graphicx}", + ); } } diff --git a/tests/core_engine_test.rs b/tests/core_engine_test.rs index 4fbbde0..2042be5 100644 --- a/tests/core_engine_test.rs +++ b/tests/core_engine_test.rs @@ -303,11 +303,7 @@ fn test_line_access_empty_buffer() { #[test] fn test_position_to_char_idx_multiline() { - let buffer = Buffer::from_string( - BufferId(0), - "Line 1\nLine 2\nLine 3\n".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Line 1\nLine 2\nLine 3\n".to_string(), None); // First character assert_eq!(buffer.position_to_char_idx(Position::new(0, 0)).unwrap(), 0); @@ -316,18 +312,20 @@ fn test_position_to_char_idx_multiline() { // Start of second line assert_eq!(buffer.position_to_char_idx(Position::new(1, 0)).unwrap(), 7); // Middle of second line - assert_eq!(buffer.position_to_char_idx(Position::new(1, 3)).unwrap(), 10); + assert_eq!( + buffer.position_to_char_idx(Position::new(1, 3)).unwrap(), + 10 + ); // Start of third line - assert_eq!(buffer.position_to_char_idx(Position::new(2, 0)).unwrap(), 14); + assert_eq!( + buffer.position_to_char_idx(Position::new(2, 0)).unwrap(), + 14 + ); } #[test] fn test_char_idx_to_position_multiline() { - let buffer = Buffer::from_string( - BufferId(0), - "Line 1\nLine 2\nLine 3\n".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Line 1\nLine 2\nLine 3\n".to_string(), None); // First character assert_eq!(buffer.char_idx_to_position(0).unwrap(), Position::new(0, 0)); @@ -336,7 +334,10 @@ fn test_char_idx_to_position_multiline() { // First character of second line assert_eq!(buffer.char_idx_to_position(7).unwrap(), Position::new(1, 0)); // Middle of second line - assert_eq!(buffer.char_idx_to_position(10).unwrap(), Position::new(1, 3)); + assert_eq!( + buffer.char_idx_to_position(10).unwrap(), + Position::new(1, 3) + ); } #[test] @@ -458,7 +459,10 @@ fn test_replace_at_end() { let mut buffer = Buffer::from_string(BufferId(0), "Hello World".to_string(), None); buffer - .replace_range(Range::new(Position::new(0, 6), Position::new(0, 11)), "Universe") + .replace_range( + Range::new(Position::new(0, 6), Position::new(0, 11)), + "Universe", + ) .unwrap(); assert_eq!(buffer.content(), "Hello Universe"); @@ -469,7 +473,10 @@ fn test_replace_entire_content() { let mut buffer = Buffer::from_string(BufferId(0), "Hello World".to_string(), None); buffer - .replace_range(Range::new(Position::new(0, 0), Position::new(0, 11)), "New Content") + .replace_range( + Range::new(Position::new(0, 0), Position::new(0, 11)), + "New Content", + ) .unwrap(); assert_eq!(buffer.content(), "New Content"); @@ -490,11 +497,7 @@ fn test_replace_empty_range() { #[test] fn test_replace_multiline() { - let mut buffer = Buffer::from_string( - BufferId(0), - "Line 1\nLine 2\nLine 3".to_string(), - None, - ); + let mut buffer = Buffer::from_string(BufferId(0), "Line 1\nLine 2\nLine 3".to_string(), None); // Replace from middle of line 1 to middle of line 2 buffer @@ -518,12 +521,12 @@ fn test_buffer_id_equality() { #[test] fn test_buffer_id_hash() { use std::collections::HashSet; - + let mut set = HashSet::new(); set.insert(BufferId(0)); set.insert(BufferId(1)); set.insert(BufferId(0)); // Duplicate - + assert_eq!(set.len(), 2); } @@ -533,10 +536,7 @@ fn test_buffer_id_hash() { fn test_invalid_range_start_greater_than_end() { let mut buffer = Buffer::from_string(BufferId(0), "Hello World".to_string(), None); - let result = buffer.replace_range( - Range::new(Position::new(0, 10), Position::new(0, 5)), - "X", - ); + let result = buffer.replace_range(Range::new(Position::new(0, 10), Position::new(0, 5)), "X"); assert!(result.is_err()); } diff --git a/tests/file_operations_test.rs b/tests/file_operations_test.rs index 1d83316..d16ef52 100644 --- a/tests/file_operations_test.rs +++ b/tests/file_operations_test.rs @@ -262,8 +262,9 @@ fn test_read_nonexistent_file() { } #[test] +#[cfg_attr(windows, ignore)] // Windows handles root paths differently fn test_write_to_readonly_location() { - // Try to write to root (should fail on most systems) + // Try to write to root (should fail on Unix systems) let result = fs::write("/cannot_write_here.txt", "content"); assert!(result.is_err()); } diff --git a/tests/file_tree_test.rs b/tests/file_tree_test.rs index 6b7388b..a373b4c 100644 --- a/tests/file_tree_test.rs +++ b/tests/file_tree_test.rs @@ -101,7 +101,12 @@ mod path_type_tests { #[test] fn test_absolute_path() { - let tree = FileTree::new(PathBuf::from("/absolute/path")); + // Use platform-specific absolute paths + #[cfg(windows)] + let path = PathBuf::from("C:\\absolute\\path"); + #[cfg(not(windows))] + let path = PathBuf::from("/absolute/path"); + let tree = FileTree::new(path); assert!(tree.root().is_absolute()); } @@ -112,6 +117,7 @@ mod path_type_tests { } #[test] + #[cfg_attr(windows, ignore)] // Unix-style root path test fn test_root_path() { let tree = FileTree::new(PathBuf::from("/")); assert_eq!(tree.root(), Path::new("/")); diff --git a/tests/gui_test.rs b/tests/gui_test.rs index 0f5cedd..e36c1f5 100644 --- a/tests/gui_test.rs +++ b/tests/gui_test.rs @@ -5,8 +5,7 @@ /// - RTL text detection and handling /// - Editor panel state management /// - Preview mode detection - -use lala::gui::{TextDirection, RtlText}; +use lala::gui::{RtlText, TextDirection}; // === RTL Text Detection Tests === @@ -17,7 +16,7 @@ mod rtl_tests { fn test_arabic_text_detection() { let arabic = "مرحبا بالعالم"; let rtl_text = RtlText::new(arabic); - + assert!(rtl_text.is_rtl()); assert!(!rtl_text.is_mixed()); assert_eq!(rtl_text.direction, TextDirection::RightToLeft); @@ -27,7 +26,7 @@ mod rtl_tests { fn test_hebrew_text_detection() { let hebrew = "שלום עולם"; let rtl_text = RtlText::new(hebrew); - + assert!(rtl_text.is_rtl()); assert_eq!(rtl_text.direction, TextDirection::RightToLeft); } @@ -36,7 +35,7 @@ mod rtl_tests { fn test_english_text_detection() { let english = "Hello World"; let rtl_text = RtlText::new(english); - + assert!(!rtl_text.is_rtl()); assert!(!rtl_text.is_mixed()); assert_eq!(rtl_text.direction, TextDirection::LeftToRight); @@ -46,7 +45,7 @@ mod rtl_tests { fn test_mixed_text_detection() { let mixed = "Hello مرحبا World"; let rtl_text = RtlText::new(mixed); - + assert!(!rtl_text.is_rtl()); assert!(rtl_text.is_mixed()); assert_eq!(rtl_text.direction, TextDirection::Mixed); @@ -56,7 +55,7 @@ mod rtl_tests { fn test_empty_text_detection() { let empty = ""; let rtl_text = RtlText::new(empty); - + assert!(!rtl_text.is_rtl()); assert_eq!(rtl_text.direction, TextDirection::LeftToRight); } @@ -65,7 +64,7 @@ mod rtl_tests { fn test_numbers_only_text() { let numbers = "123 456 789"; let rtl_text = RtlText::new(numbers); - + // Numbers alone should default to LTR assert!(!rtl_text.is_rtl()); assert_eq!(rtl_text.direction, TextDirection::LeftToRight); @@ -75,7 +74,7 @@ mod rtl_tests { fn test_japanese_text_is_not_rtl() { let japanese = "こんにちは世界"; let rtl_text = RtlText::new(japanese); - + // Japanese is LTR assert!(!rtl_text.is_rtl()); assert_eq!(rtl_text.direction, TextDirection::LeftToRight); @@ -85,7 +84,7 @@ mod rtl_tests { fn test_chinese_text_is_not_rtl() { let chinese = "你好世界"; let rtl_text = RtlText::new(chinese); - + // Chinese is LTR assert!(!rtl_text.is_rtl()); assert_eq!(rtl_text.direction, TextDirection::LeftToRight); @@ -96,7 +95,7 @@ mod rtl_tests { // Persian uses Arabic script let persian = "سلام دنیا"; let rtl_text = RtlText::new(persian); - + assert!(rtl_text.is_rtl()); assert_eq!(rtl_text.direction, TextDirection::RightToLeft); } @@ -106,7 +105,7 @@ mod rtl_tests { // Urdu uses Arabic script let urdu = "ہیلو دنیا"; let rtl_text = RtlText::new(urdu); - + assert!(rtl_text.is_rtl()); assert_eq!(rtl_text.direction, TextDirection::RightToLeft); } @@ -115,7 +114,7 @@ mod rtl_tests { fn test_arabic_with_numbers() { let arabic_num = "١٢٣ مرحبا"; let rtl_text = RtlText::new(arabic_num); - + assert!(rtl_text.is_rtl()); } } @@ -123,7 +122,7 @@ mod rtl_tests { // === RTL Character Detection Tests === mod rtl_char_tests { - use lala::gui::rtl::{is_rtl_char, contains_rtl}; + use lala::gui::rtl::{contains_rtl, is_rtl_char}; #[test] fn test_arabic_alef() { @@ -174,12 +173,14 @@ mod rtl_char_tests { // === Theme Tests === mod theme_tests { - use lala::gui::{custom_light_theme, custom_dark_theme, ThemeMode, EditorColors, MarkdownPreviewColors}; + use lala::gui::{ + custom_dark_theme, custom_light_theme, EditorColors, MarkdownPreviewColors, ThemeMode, + }; #[test] fn test_light_theme_creation() { let theme = custom_light_theme(); - + // Light theme should have light backgrounds assert!(theme.window_fill.r() > 200); assert!(theme.window_fill.g() > 200); @@ -189,7 +190,7 @@ mod theme_tests { #[test] fn test_dark_theme_creation() { let theme = custom_dark_theme(); - + // Dark theme should have dark backgrounds assert!(theme.window_fill.r() < 50); assert!(theme.window_fill.g() < 50); @@ -199,7 +200,7 @@ mod theme_tests { #[test] fn test_theme_mode_default() { let default = ThemeMode::default(); - + // Default should be dark for code editing assert_eq!(default, ThemeMode::Dark); } @@ -207,7 +208,7 @@ mod theme_tests { #[test] fn test_editor_colors_light() { let colors = EditorColors::light(); - + // Light colors should have light background assert!(colors.background.r() > 200); // And dark foreground @@ -217,7 +218,7 @@ mod theme_tests { #[test] fn test_editor_colors_dark() { let colors = EditorColors::dark(); - + // Dark colors should have dark background assert!(colors.background.r() < 50); // And light foreground @@ -227,7 +228,7 @@ mod theme_tests { #[test] fn test_markdown_preview_colors_light() { let colors = MarkdownPreviewColors::light(); - + // Light colors should have light background assert!(colors.background.r() > 200); // Heading should be dark @@ -237,7 +238,7 @@ mod theme_tests { #[test] fn test_markdown_preview_colors_dark() { let colors = MarkdownPreviewColors::dark(); - + // Dark colors should have dark background assert!(colors.background.r() < 50); // Heading should be light @@ -247,7 +248,7 @@ mod theme_tests { #[test] fn test_light_theme_has_proper_shadow() { let theme = custom_light_theme(); - + // Should have shadow for modern look (egui 0.33 API) assert!(theme.window_shadow.blur > 0); } @@ -255,7 +256,7 @@ mod theme_tests { #[test] fn test_dark_theme_has_proper_shadow() { let theme = custom_dark_theme(); - + // Should have shadow for modern look (egui 0.33 API) assert!(theme.window_shadow.blur > 0); } @@ -344,8 +345,10 @@ mod markdown_preview_tests { let markdown = "> This is a quote"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::BlockQuote(_))))); + + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::BlockQuote(_))))); } #[test] @@ -353,9 +356,10 @@ mod markdown_preview_tests { let markdown = "> Level 1\n>> Level 2"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - + // Should contain blockquotes - let blockquote_count = events.iter() + let blockquote_count = events + .iter() .filter(|e| matches!(e, Event::Start(Tag::BlockQuote(_)))) .count(); assert!(blockquote_count >= 1); @@ -366,7 +370,7 @@ mod markdown_preview_tests { let markdown = "- [ ] Task 1\n- [x] Task 2"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - + // Should contain task list markers let has_task_marker = events.iter().any(|e| matches!(e, Event::TaskListMarker(_))); assert!(has_task_marker); @@ -377,8 +381,10 @@ mod markdown_preview_tests { let markdown = "![Alt text](image.png)"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Image { .. })))); + + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Image { .. })))); } #[test] @@ -386,8 +392,10 @@ mod markdown_preview_tests { let markdown = "~~strikethrough~~"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Strikethrough)))); + + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Strikethrough)))); } #[test] @@ -395,8 +403,10 @@ mod markdown_preview_tests { let markdown = "| A | B |\n|---|---|\n| 1 | 2 |"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Table(_))))); + + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Table(_))))); } #[test] @@ -404,16 +414,18 @@ mod markdown_preview_tests { let markdown = "Text[^1]\n\n[^1]: Footnote content"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::FootnoteDefinition(_))))); + + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::FootnoteDefinition(_))))); } #[test] fn test_hard_break() { - let markdown = "Line 1 \nLine 2"; // Two spaces before newline + let markdown = "Line 1 \nLine 2"; // Two spaces before newline let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - + assert!(events.iter().any(|e| matches!(e, Event::HardBreak))); } @@ -422,7 +434,7 @@ mod markdown_preview_tests { let markdown = "Line 1\nLine 2"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - + assert!(events.iter().any(|e| matches!(e, Event::SoftBreak))); } @@ -431,16 +443,16 @@ mod markdown_preview_tests { let markdown = "---"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - + assert!(events.iter().any(|e| matches!(e, Event::Rule))); } #[test] fn test_rtl_in_markdown() { use lala::gui::rtl::contains_rtl; - + let markdown = "# مرحبا بالعالم\n\nهذا نص عربي"; - + assert!(contains_rtl(markdown)); } } @@ -470,7 +482,7 @@ mod unicode_tests { #[test] fn test_bidirectional_text() { let mixed = "Hello مرحبا World"; - + // Contains both LTR and RTL assert!(mixed.contains("Hello")); assert!(mixed.contains("مرحبا")); @@ -486,7 +498,7 @@ mod unicode_tests { // === Comprehensive RTL Module Tests === mod rtl_comprehensive_tests { - use lala::gui::rtl::{is_rtl_char, contains_rtl, detect_text_direction, TextDirection}; + use lala::gui::rtl::{contains_rtl, detect_text_direction, is_rtl_char, TextDirection}; use lala::gui::RtlText; // === Arabic Script Variants === @@ -514,7 +526,11 @@ mod rtl_comprehensive_tests { // Arabic presentation forms (FB50-FDFF, FE70-FEFF) let forms = "ﺍﺏﺕﺙ"; // Isolated forms for c in forms.chars() { - assert!(is_rtl_char(c), "Expected presentation form '{}' to be RTL", c); + assert!( + is_rtl_char(c), + "Expected presentation form '{}' to be RTL", + c + ); } } @@ -562,7 +578,10 @@ mod rtl_comprehensive_tests { let text = "مرحبا بالعالم Hello كيف حالك"; let direction = detect_text_direction(text); // RTL dominates - assert!(matches!(direction, TextDirection::RightToLeft | TextDirection::Mixed)); + assert!(matches!( + direction, + TextDirection::RightToLeft | TextDirection::Mixed + )); } #[test] @@ -571,7 +590,10 @@ mod rtl_comprehensive_tests { let text = "Hello World مرحبا How Are You"; let direction = detect_text_direction(text); // Mixed because both present - assert!(matches!(direction, TextDirection::LeftToRight | TextDirection::Mixed)); + assert!(matches!( + direction, + TextDirection::LeftToRight | TextDirection::Mixed + )); } #[test] @@ -663,7 +685,7 @@ mod rtl_comprehensive_tests { // === Additional Theme Tests === mod theme_comprehensive_tests { - use lala::gui::{custom_light_theme, custom_dark_theme, EditorColors, MarkdownPreviewColors}; + use lala::gui::{custom_dark_theme, custom_light_theme, EditorColors, MarkdownPreviewColors}; #[test] fn test_light_theme_panel_fill() { @@ -708,7 +730,7 @@ mod theme_comprehensive_tests { fn test_editor_colors_selection_visible() { let light = EditorColors::light(); let dark = EditorColors::dark(); - + // Selection should be different from background assert_ne!(light.selection, light.background); assert_ne!(dark.selection, dark.background); @@ -718,18 +740,18 @@ mod theme_comprehensive_tests { fn test_editor_colors_cursor_visible_on_light() { let light = EditorColors::light(); // Cursor should be visible against light background - let contrast = (light.background.r() as i32 - light.cursor.r() as i32).abs() + - (light.background.g() as i32 - light.cursor.g() as i32).abs() + - (light.background.b() as i32 - light.cursor.b() as i32).abs(); + let contrast = (light.background.r() as i32 - light.cursor.r() as i32).abs() + + (light.background.g() as i32 - light.cursor.g() as i32).abs() + + (light.background.b() as i32 - light.cursor.b() as i32).abs(); assert!(contrast > 200, "Cursor should have sufficient contrast"); } #[test] fn test_editor_colors_cursor_visible_on_dark() { let dark = EditorColors::dark(); - let contrast = (dark.background.r() as i32 - dark.cursor.r() as i32).abs() + - (dark.background.g() as i32 - dark.cursor.g() as i32).abs() + - (dark.background.b() as i32 - dark.cursor.b() as i32).abs(); + let contrast = (dark.background.r() as i32 - dark.cursor.r() as i32).abs() + + (dark.background.g() as i32 - dark.cursor.g() as i32).abs() + + (dark.background.b() as i32 - dark.cursor.b() as i32).abs(); assert!(contrast > 200, "Cursor should have sufficient contrast"); } @@ -737,7 +759,7 @@ mod theme_comprehensive_tests { fn test_markdown_colors_code_bg_distinct() { let light = MarkdownPreviewColors::light(); let dark = MarkdownPreviewColors::dark(); - + // Code background should be slightly different from main background assert_ne!(light.code_bg, light.background); assert_ne!(dark.code_bg, dark.background); @@ -747,7 +769,7 @@ mod theme_comprehensive_tests { fn test_markdown_colors_blockquote_visible() { let light = MarkdownPreviewColors::light(); let dark = MarkdownPreviewColors::dark(); - + // Blockquote border should be visible assert!(light.blockquote_border.a() > 100); assert!(dark.blockquote_border.a() > 100); @@ -757,7 +779,7 @@ mod theme_comprehensive_tests { // === Markdown Preview Comprehensive Tests === mod markdown_comprehensive_tests { - use pulldown_cmark::{Event, Options, Parser, Tag, HeadingLevel, CodeBlockKind}; + use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag}; #[test] fn test_heading_levels() { @@ -765,8 +787,10 @@ mod markdown_comprehensive_tests { let markdown = format!("{} Heading {}", "#".repeat(level), level); let parser = Parser::new_ext(&markdown, Options::all()); let events: Vec = parser.collect(); - - let has_heading = events.iter().any(|e| matches!(e, Event::Start(Tag::Heading { .. }))); + + let has_heading = events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Heading { .. }))); assert!(has_heading, "Should parse heading level {}", level); } } @@ -776,10 +800,12 @@ mod markdown_comprehensive_tests { let markdown = "```rust\nfn main() {}\n```"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - let code_block = events.iter().find(|e| matches!(e, Event::Start(Tag::CodeBlock(_)))); + + let code_block = events + .iter() + .find(|e| matches!(e, Event::Start(Tag::CodeBlock(_)))); assert!(code_block.is_some()); - + if let Some(Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang)))) = code_block { assert_eq!(lang.as_ref(), "rust"); } @@ -790,8 +816,10 @@ mod markdown_comprehensive_tests { let markdown = "```\ncode\n```"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::CodeBlock(_))))); + + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::CodeBlock(_))))); } #[test] @@ -799,7 +827,7 @@ mod markdown_comprehensive_tests { let markdown = "This is `inline code` here"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - + let code = events.iter().find(|e| matches!(e, Event::Code(_))); assert!(code.is_some()); if let Some(Event::Code(text)) = code { @@ -812,8 +840,10 @@ mod markdown_comprehensive_tests { let markdown = "*italic*"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Emphasis)))); + + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Emphasis)))); } #[test] @@ -821,8 +851,10 @@ mod markdown_comprehensive_tests { let markdown = "**bold**"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Strong)))); + + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Strong)))); } #[test] @@ -830,8 +862,10 @@ mod markdown_comprehensive_tests { let markdown = "[link text](https://example.com)"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - let link = events.iter().find(|e| matches!(e, Event::Start(Tag::Link { .. }))); + + let link = events + .iter() + .find(|e| matches!(e, Event::Start(Tag::Link { .. }))); assert!(link.is_some()); } @@ -840,8 +874,10 @@ mod markdown_comprehensive_tests { let markdown = "- item 1\n- item 2\n- item 3"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - let list = events.iter().find(|e| matches!(e, Event::Start(Tag::List(None)))); + + let list = events + .iter() + .find(|e| matches!(e, Event::Start(Tag::List(None)))); assert!(list.is_some()); } @@ -850,8 +886,10 @@ mod markdown_comprehensive_tests { let markdown = "1. item 1\n2. item 2\n3. item 3"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - let list = events.iter().find(|e| matches!(e, Event::Start(Tag::List(Some(_))))); + + let list = events + .iter() + .find(|e| matches!(e, Event::Start(Tag::List(Some(_))))); assert!(list.is_some()); } @@ -860,8 +898,9 @@ mod markdown_comprehensive_tests { let markdown = "- item 1\n - nested 1\n - nested 2\n- item 2"; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - - let list_count = events.iter() + + let list_count = events + .iter() .filter(|e| matches!(e, Event::Start(Tag::List(_)))) .count(); assert!(list_count >= 2, "Should have nested lists"); @@ -888,16 +927,40 @@ fn main() {} "#; let parser = Parser::new_ext(markdown, Options::all()); let events: Vec = parser.collect(); - + // Should have all elements - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Heading { level: HeadingLevel::H1, .. })))); - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Heading { level: HeadingLevel::H2, .. })))); - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Strong)))); - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Emphasis)))); + assert!(events.iter().any(|e| matches!( + e, + Event::Start(Tag::Heading { + level: HeadingLevel::H1, + .. + }) + ))); + assert!(events.iter().any(|e| matches!( + e, + Event::Start(Tag::Heading { + level: HeadingLevel::H2, + .. + }) + ))); + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Strong)))); + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Emphasis)))); assert!(events.iter().any(|e| matches!(e, Event::Code(_)))); - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::List(_))))); - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::BlockQuote(_))))); - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::CodeBlock(_))))); - assert!(events.iter().any(|e| matches!(e, Event::Start(Tag::Link { .. })))); + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::List(_))))); + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::BlockQuote(_))))); + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::CodeBlock(_))))); + assert!(events + .iter() + .any(|e| matches!(e, Event::Start(Tag::Link { .. })))); } } diff --git a/tests/rtl_test.rs b/tests/rtl_test.rs index 48c30e3..55f5cba 100644 --- a/tests/rtl_test.rs +++ b/tests/rtl_test.rs @@ -249,10 +249,7 @@ mod detect_text_direction_tests { #[test] fn test_mixed_more_rtl() { // When RTL characters exceed LTR characters, returns Mixed - assert_eq!( - detect_text_direction("مرحبا Hello"), - TextDirection::Mixed - ); + assert_eq!(detect_text_direction("مرحبا Hello"), TextDirection::Mixed); } #[test] @@ -271,18 +268,12 @@ mod detect_text_direction_tests { #[test] fn test_numbers_only() { - assert_eq!( - detect_text_direction("123456"), - TextDirection::LeftToRight - ); + assert_eq!(detect_text_direction("123456"), TextDirection::LeftToRight); } #[test] fn test_spaces_only() { - assert_eq!( - detect_text_direction(" "), - TextDirection::LeftToRight - ); + assert_eq!(detect_text_direction(" "), TextDirection::LeftToRight); } #[test] @@ -295,18 +286,12 @@ mod detect_text_direction_tests { #[test] fn test_single_rtl_char() { - assert_eq!( - detect_text_direction("ا"), - TextDirection::RightToLeft - ); + assert_eq!(detect_text_direction("ا"), TextDirection::RightToLeft); } #[test] fn test_single_ltr_char() { - assert_eq!( - detect_text_direction("A"), - TextDirection::LeftToRight - ); + assert_eq!(detect_text_direction("A"), TextDirection::LeftToRight); } } @@ -337,18 +322,15 @@ mod text_direction_tests { assert_ne!(TextDirection::Mixed, TextDirection::LeftToRight); } - #[test] - fn test_clone() { - let dir = TextDirection::RightToLeft; - let cloned = dir.clone(); - assert_eq!(dir, cloned); - } - #[test] fn test_copy() { - let dir = TextDirection::Mixed; - let copied = dir; + let dir = TextDirection::RightToLeft; + let copied = dir; // TextDirection implements Copy assert_eq!(dir, copied); + + let dir2 = TextDirection::Mixed; + let copied2 = dir2; + assert_eq!(dir2, copied2); } #[test] @@ -473,7 +455,10 @@ mod edge_cases { assert!(contains_rtl(&mixed)); // Direction depends on character counts let dir = detect_text_direction(&mixed); - assert!(matches!(dir, TextDirection::Mixed | TextDirection::RightToLeft)); + assert!(matches!( + dir, + TextDirection::Mixed | TextDirection::RightToLeft + )); } #[test] diff --git a/tests/search_test.rs b/tests/search_test.rs index 6096532..ae3bbc9 100644 --- a/tests/search_test.rs +++ b/tests/search_test.rs @@ -81,11 +81,7 @@ mod literal_search_tests { #[test] fn test_multiline_search() { - let buffer = Buffer::from_string( - BufferId(0), - "Line 1\nLine 2\nLine 3".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Line 1\nLine 2\nLine 3".to_string(), None); let options = SearchOptions::default(); let results = search_in_buffer(&buffer, "Line", &options).unwrap(); assert_eq!(results.len(), 3); @@ -131,11 +127,7 @@ mod case_sensitivity_tests { #[test] fn test_case_sensitive_match() { - let buffer = Buffer::from_string( - BufferId(0), - "Hello hello HELLO HeLLo".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Hello hello HELLO HeLLo".to_string(), None); let options = SearchOptions { case_sensitive: true, use_regex: false, @@ -147,11 +139,7 @@ mod case_sensitivity_tests { #[test] fn test_case_insensitive_match() { - let buffer = Buffer::from_string( - BufferId(0), - "Hello hello HELLO HeLLo".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Hello hello HELLO HeLLo".to_string(), None); let options = SearchOptions { case_sensitive: false, use_regex: false, @@ -176,11 +164,7 @@ mod case_sensitivity_tests { #[test] fn test_case_insensitive_ascii() { // Use ASCII to avoid byte/char index bugs in search function - let buffer = Buffer::from_string( - BufferId(0), - "Test TEST TeSt".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Test TEST TeSt".to_string(), None); let options = SearchOptions { case_sensitive: false, use_regex: false, @@ -214,11 +198,7 @@ mod regex_search_tests { #[test] fn test_word_boundary_regex() { - let buffer = Buffer::from_string( - BufferId(0), - "test testing tester".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "test testing tester".to_string(), None); let options = SearchOptions { case_sensitive: true, use_regex: true, @@ -279,11 +259,7 @@ mod regex_search_tests { #[test] fn test_case_insensitive_regex() { - let buffer = Buffer::from_string( - BufferId(0), - "Hello HELLO hello".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Hello HELLO hello".to_string(), None); let options = SearchOptions { case_sensitive: false, use_regex: true, @@ -295,11 +271,7 @@ mod regex_search_tests { #[test] fn test_group_regex() { - let buffer = Buffer::from_string( - BufferId(0), - "abc def abc ghi abc".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "abc def abc ghi abc".to_string(), None); let options = SearchOptions { case_sensitive: true, use_regex: true, @@ -311,18 +283,14 @@ mod regex_search_tests { #[test] fn test_anchor_regex_start() { - let buffer = Buffer::from_string( - BufferId(0), - "start here\nstart again".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "start here\nstart again".to_string(), None); let options = SearchOptions { case_sensitive: true, use_regex: true, whole_word: false, }; let results = search_in_buffer(&buffer, r"^start", &options).unwrap(); - assert!(results.len() >= 1); // At least matches line start + assert!(!results.is_empty()); // At least matches line start } } @@ -335,11 +303,8 @@ mod replace_tests { #[test] fn test_replace_first() { - let mut buffer = Buffer::from_string( - BufferId(0), - "hello world hello rust".to_string(), - None, - ); + let mut buffer = + Buffer::from_string(BufferId(0), "hello world hello rust".to_string(), None); let options = SearchOptions::default(); let count = replace_in_buffer(&mut buffer, "hello", "hi", &options, false).unwrap(); assert_eq!(count, 1); @@ -348,11 +313,8 @@ mod replace_tests { #[test] fn test_replace_all() { - let mut buffer = Buffer::from_string( - BufferId(0), - "hello world hello rust".to_string(), - None, - ); + let mut buffer = + Buffer::from_string(BufferId(0), "hello world hello rust".to_string(), None); let options = SearchOptions::default(); let count = replace_in_buffer(&mut buffer, "hello", "hi", &options, true).unwrap(); assert_eq!(count, 2); @@ -406,11 +368,7 @@ mod replace_tests { #[test] fn test_replace_multiline() { - let mut buffer = Buffer::from_string( - BufferId(0), - "line1\nline2\nline3".to_string(), - None, - ); + let mut buffer = Buffer::from_string(BufferId(0), "line1\nline2\nline3".to_string(), None); let options = SearchOptions::default(); let count = replace_in_buffer(&mut buffer, "line", "LINE", &options, true).unwrap(); assert_eq!(count, 3); @@ -419,11 +377,7 @@ mod replace_tests { #[test] fn test_replace_case_insensitive() { - let mut buffer = Buffer::from_string( - BufferId(0), - "Hello HELLO hello".to_string(), - None, - ); + let mut buffer = Buffer::from_string(BufferId(0), "Hello HELLO hello".to_string(), None); let options = SearchOptions { case_sensitive: false, use_regex: false, @@ -436,11 +390,7 @@ mod replace_tests { #[test] fn test_replace_regex() { - let mut buffer = Buffer::from_string( - BufferId(0), - "abc123 def456 ghi789".to_string(), - None, - ); + let mut buffer = Buffer::from_string(BufferId(0), "abc123 def456 ghi789".to_string(), None); let options = SearchOptions { case_sensitive: true, use_regex: true, @@ -465,11 +415,7 @@ mod unicode_search_tests { #[test] fn test_ascii_search_in_unicode_context() { // Search for ASCII in content that also has Unicode - let buffer = Buffer::from_string( - BufferId(0), - "Hello World Test".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Hello World Test".to_string(), None); let options = SearchOptions::default(); let results = search_in_buffer(&buffer, "World", &options).unwrap(); assert_eq!(results.len(), 1); @@ -479,11 +425,7 @@ mod unicode_search_tests { #[test] fn test_accented_characters() { // Accented characters that are common in European languages - let buffer = Buffer::from_string( - BufferId(0), - "cafe resume cafe".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "cafe resume cafe".to_string(), None); let options = SearchOptions::default(); let results = search_in_buffer(&buffer, "cafe", &options).unwrap(); assert_eq!(results.len(), 2); @@ -491,11 +433,7 @@ mod unicode_search_tests { #[test] fn test_search_with_numbers() { - let buffer = Buffer::from_string( - BufferId(0), - "test123 test456 test789".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "test123 test456 test789".to_string(), None); let options = SearchOptions::default(); let results = search_in_buffer(&buffer, "test", &options).unwrap(); assert_eq!(results.len(), 3); @@ -503,11 +441,7 @@ mod unicode_search_tests { #[test] fn test_search_special_ascii() { - let buffer = Buffer::from_string( - BufferId(0), - "a+b*c a+b*c".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "a+b*c a+b*c".to_string(), None); let options = SearchOptions { case_sensitive: true, use_regex: false, @@ -520,11 +454,7 @@ mod unicode_search_tests { #[test] fn test_unicode_at_start() { // ASCII search when buffer starts with ASCII - let buffer = Buffer::from_string( - BufferId(0), - "test data here".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "test data here".to_string(), None); let options = SearchOptions::default(); let results = search_in_buffer(&buffer, "test", &options).unwrap(); assert_eq!(results.len(), 1); @@ -532,11 +462,7 @@ mod unicode_search_tests { #[test] fn test_digits_regex() { - let buffer = Buffer::from_string( - BufferId(0), - "abc123def456ghi".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "abc123def456ghi".to_string(), None); let options = SearchOptions { case_sensitive: true, use_regex: true, @@ -550,11 +476,7 @@ mod unicode_search_tests { #[test] fn test_word_search_regex() { - let buffer = Buffer::from_string( - BufferId(0), - "word1 word2 word3".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "word1 word2 word3".to_string(), None); let options = SearchOptions { case_sensitive: true, use_regex: true, @@ -566,11 +488,7 @@ mod unicode_search_tests { #[test] fn test_punctuation_search() { - let buffer = Buffer::from_string( - BufferId(0), - "Hello! World? Test.".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Hello! World? Test.".to_string(), None); let options = SearchOptions::default(); let results = search_in_buffer(&buffer, "!", &options).unwrap(); assert_eq!(results.len(), 1); @@ -602,11 +520,7 @@ mod search_result_tests { #[test] fn test_search_result_positions() { - let buffer = Buffer::from_string( - BufferId(0), - "Line one\nLine two".to_string(), - None, - ); + let buffer = Buffer::from_string(BufferId(0), "Line one\nLine two".to_string(), None); let options = SearchOptions::default(); let results = search_in_buffer(&buffer, "Line", &options).unwrap(); @@ -940,7 +854,11 @@ mod async_grep_tests { #[tokio::test] async fn test_grep_regex_pattern() { let temp_dir = TempDir::new().unwrap(); - fs::write(temp_dir.path().join("test.txt"), "fn test1()\nfn test2()\nfn hello()").unwrap(); + fs::write( + temp_dir.path().join("test.txt"), + "fn test1()\nfn test2()\nfn hello()", + ) + .unwrap(); let mut engine = GrepEngine::new(); let options = GrepOptions { diff --git a/tests/theme_test.rs b/tests/theme_test.rs index 73c3475..3a19dbc 100644 --- a/tests/theme_test.rs +++ b/tests/theme_test.rs @@ -35,18 +35,15 @@ mod theme_mode_tests { assert_ne!(ThemeMode::System, ThemeMode::Light); } - #[test] - fn test_clone() { - let mode = ThemeMode::Light; - let cloned = mode.clone(); - assert_eq!(mode, cloned); - } - #[test] fn test_copy() { - let mode = ThemeMode::Dark; - let copied = mode; + let mode = ThemeMode::Light; + let copied = mode; // ThemeMode implements Copy assert_eq!(mode, copied); + + let mode2 = ThemeMode::Dark; + let copied2 = mode2; + assert_eq!(mode2, copied2); } #[test]