Conversation
📝 WalkthroughWalkthroughThis pull request introduces a welcome panel feature to the Ferrite application. It adds a new Changes
Sequence DiagramsequenceDiagram
participant Main as Main Entry
participant App as FerriteApp
participant State as AppState
participant UI as WelcomePanel
Main->>App: new() + initialization
App->>App: initialize welcome_panel: WelcomePanel::new()
Main->>App: open_welcome_on_startup()
App->>State: show_welcome_tab()
State->>State: create Welcome tab variant
App->>UI: show() or show_inline()
UI->>UI: render welcome modal/inline
Note over UI: Theme, CJK, Settings, Width Config
UI-->>App: WelcomePanelOutput { close_requested }
App->>App: handle welcome panel close/settings
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/state.rs (1)
2318-2359:⚠️ Potential issue | 🔴 CriticalFix UTF‑8 index handling in search/selection slices.
Selectionand cursor positions are documented as character indices, butfind_next_occurrenceandget_primary_selection_textslice strings using those indices and mix them withsearch_text.len()(bytes). This can panic on non‑ASCII input and return incorrect ranges. Convert char indices to byte indices (and back) or standardize on byte indices throughout.#!/bin/bash # Verify call sites and index semantics for find_next_occurrence. rg -n "find_next_occurrence\\(" -g '!**/target/**' rg -n "Selection::cursor\\(|Selection::new\\(|cursor_char_index" src/state.rs🐛 Suggested fix (char → byte conversion)
pub fn find_next_occurrence( &self, search_text: &str, after_pos: usize ) -> Option<(usize, usize)> { if search_text.is_empty() { return None; } + + let to_byte = |char_idx: usize| { + self.content + .char_indices() + .nth(char_idx) + .map(|(i, _)| i) + .unwrap_or(self.content.len()) + }; + let after_byte = to_byte(after_pos); // Search from after_pos to end - if let Some(rel_pos) = self.content[after_pos..].find(search_text) { - let start = after_pos + rel_pos; - let end = start + search_text.len(); - return Some((start, end)); + if let Some(rel_byte) = self.content[after_byte..].find(search_text) { + let start_byte = after_byte + rel_byte; + let end_byte = start_byte + search_text.len(); + let start = self.content[..start_byte].chars().count(); + let end = self.content[..end_byte].chars().count(); + return Some((start, end)); } // Wrap around: search from beginning to after_pos - if let Some(rel_pos) = self.content[..after_pos].find(search_text) { - let end = rel_pos + search_text.len(); - return Some((rel_pos, end)); + if let Some(rel_byte) = self.content[..after_byte].find(search_text) { + let end_byte = rel_byte + search_text.len(); + let start = self.content[..rel_byte].chars().count(); + let end = self.content[..end_byte].chars().count(); + return Some((start, end)); } None } @@ pub fn get_primary_selection_text(&self) -> Option<String> { let primary = self.cursors.primary(); if primary.is_selection() { // Return selected text let (start, end) = primary.range(); - if end <= self.content.len() { - return Some(self.content[start..end].to_string()); - } + let to_byte = |char_idx: usize| { + self.content + .char_indices() + .nth(char_idx) + .map(|(i, _)| i) + .unwrap_or(self.content.len()) + }; + let start_byte = to_byte(start); + let end_byte = to_byte(end); + if let Some(slice) = self.content.get(start_byte..end_byte) { + return Some(slice.to_string()); + } } else { // No selection: find word at cursor return self.word_at_position(primary.head); } None }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/state.rs` around lines 2318 - 2359, find_next_occurrence and get_primary_selection_text treat cursor/selection values as char indices but use them to slice by bytes, which breaks on UTF‑8; convert char indices to byte offsets before slicing or searching. Specifically: in find_next_occurrence convert after_pos (char index) to a byte_offset (e.g. sum of .chars().take(after_pos).map(|c| c.len_utf8())) and use that byte_offset when slicing self.content[..] and when computing start/end bytes; compute end bytes as start_byte + search_text.len() (bytes). In get_primary_selection_text convert primary.range() (start_char,end_char) and primary.head (char index) to byte offsets before slicing or passing into word_at_position, and update word_at_position/Selection handling if it currently assumes byte indices so all callers are consistent; ensure all bounds checks compare against self.content.len() (bytes).
🧹 Nitpick comments (1)
src/ui/welcome.rs (1)
137-139: Use a Welcome‑specific overlay id.
about_overlaylooks reused from the About panel and could collide if both overlays coexist. A unique id keeps egui state isolated.♻️ Suggested tweak
- egui::Area - ::new(egui::Id::new("about_overlay")) + egui::Area + ::new(egui::Id::new("welcome_overlay"))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/welcome.rs` around lines 137 - 139, The overlay in welcome.rs is using the generic id "about_overlay" which may collide with the About panel; update the egui::Area id creation (egui::Area::new(egui::Id::new("about_overlay"))) to a Welcome-specific id such as "welcome_overlay" (or a tuple including a unique marker) to isolate egui state for the Welcome overlay (adjust any related code that references the old id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main.rs`:
- Around line 287-296: Only call app.open_welcome_on_startup() when there were
no initial paths and the session has no existing tabs: after
app.open_initial_paths(initial_paths) check that initial_paths.is_empty() and
that the app/session has zero restored/open tabs (e.g. via a session/tab-count
accessor on app) before invoking app.open_welcome_on_startup(); replace the
unconditional call with this gated conditional.
In `@src/state.rs`:
- Around line 3073-3087: The closure in show_welcome_tab is attempting to move
t.kind out of a shared reference when using matches!(t.kind,
TabKind::Special(SpecialTabKind::Welcome)); change the pattern to match on a
reference so it borrows rather than moves (e.g., match on &t.kind or use
matches!(t.kind, ref x) style), so update the iterator position predicate to
compare &t.kind to TabKind::Special(SpecialTabKind::Welcome) (or use
matches!(t.kind, TabKind::Special(ref k) if needed); this preserves ownership of
TabKind in tabs and allows setting active_tab_index and calling open_special_tab
as before.
---
Outside diff comments:
In `@src/state.rs`:
- Around line 2318-2359: find_next_occurrence and get_primary_selection_text
treat cursor/selection values as char indices but use them to slice by bytes,
which breaks on UTF‑8; convert char indices to byte offsets before slicing or
searching. Specifically: in find_next_occurrence convert after_pos (char index)
to a byte_offset (e.g. sum of .chars().take(after_pos).map(|c| c.len_utf8()))
and use that byte_offset when slicing self.content[..] and when computing
start/end bytes; compute end bytes as start_byte + search_text.len() (bytes). In
get_primary_selection_text convert primary.range() (start_char,end_char) and
primary.head (char index) to byte offsets before slicing or passing into
word_at_position, and update word_at_position/Selection handling if it currently
assumes byte indices so all callers are consistent; ensure all bounds checks
compare against self.content.len() (bytes).
---
Nitpick comments:
In `@src/ui/welcome.rs`:
- Around line 137-139: The overlay in welcome.rs is using the generic id
"about_overlay" which may collide with the About panel; update the egui::Area id
creation (egui::Area::new(egui::Id::new("about_overlay"))) to a Welcome-specific
id such as "welcome_overlay" (or a tuple including a unique marker) to isolate
egui state for the Welcome overlay (adjust any related code that references the
old id).
| // Open files/directories from CLI arguments and Apple Events | ||
| app.open_initial_paths(initial_paths); | ||
|
|
||
|
|
||
| // app.open_welcome_on_startup(); | ||
| app.open_welcome_on_startup(); | ||
|
|
||
| log_memory("After app creation and initial paths"); | ||
|
|
||
| Ok(Box::new(app)) | ||
| }), | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n -C3 'show_welcome_tab' src/state.rsRepository: OlaProeis/Ferrite
Length of output: 313
🏁 Script executed:
rg -n -C3 'open_welcome_on_startup' srcRepository: OlaProeis/Ferrite
Length of output: 828
🏁 Script executed:
sed -n '3074,3120p' src/state.rsRepository: OlaProeis/Ferrite
Length of output: 1576
🏁 Script executed:
rg -n 'active_tab_index|set_active_tab' src/app/mod.rs | head -20Repository: OlaProeis/Ferrite
Length of output: 543
🏁 Script executed:
sed -n '170,210p' src/app/mod.rsRepository: OlaProeis/Ferrite
Length of output: 2220
Gate the welcome tab to respect user intent.
The welcome screen currently opens unconditionally after initial paths are processed, even if the user has provided CLI/Apple Event arguments or a prior session exists. This can add an unnecessary tab and steal focus.
Suggested approach: Only show the welcome screen when there are no initial paths provided and the session has no existing tabs.
🔧 Suggested change
// Open files/directories from CLI arguments and Apple Events
+ let has_initial_paths = !initial_paths.is_empty();
app.open_initial_paths(initial_paths);
- // app.open_welcome_on_startup();
- app.open_welcome_on_startup();
+ if !has_initial_paths {
+ app.open_welcome_on_startup();
+ }Note: This gates on initial paths specifically. Consider also checking whether the session restored any tabs, since a user with a prior session should not see the welcome tab on subsequent launches.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Open files/directories from CLI arguments and Apple Events | |
| app.open_initial_paths(initial_paths); | |
| // app.open_welcome_on_startup(); | |
| app.open_welcome_on_startup(); | |
| log_memory("After app creation and initial paths"); | |
| Ok(Box::new(app)) | |
| }), | |
| }) | |
| // Open files/directories from CLI arguments and Apple Events | |
| let has_initial_paths = !initial_paths.is_empty(); | |
| app.open_initial_paths(initial_paths); | |
| if !has_initial_paths { | |
| app.open_welcome_on_startup(); | |
| } | |
| log_memory("After app creation and initial paths"); | |
| Ok(Box::new(app)) | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main.rs` around lines 287 - 296, Only call app.open_welcome_on_startup()
when there were no initial paths and the session has no existing tabs: after
app.open_initial_paths(initial_paths) check that initial_paths.is_empty() and
that the app/session has zero restored/open tabs (e.g. via a session/tab-count
accessor on app) before invoking app.open_welcome_on_startup(); replace the
unconditional call with this gated conditional.
| /// This is code of view tab welcome at first launch time | ||
| pub fn show_welcome_tab(&mut self) { | ||
| // If Settings tab already exists, just activate it. | ||
| if | ||
| let Some(i) = self.tabs | ||
| .iter() | ||
| .position(|t| { matches!(t.kind, TabKind::Special(SpecialTabKind::Welcome)) }) | ||
| { | ||
| self.active_tab_index = i; | ||
| return; | ||
| } | ||
|
|
||
| // Otherwise create it (this should also set active, but we’ll be safe) | ||
| self.open_special_tab(SpecialTabKind::Welcome); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check TabKind definition and derive macros
rg -A 5 "enum TabKind" src/state.rs | head -20
# Locate show_welcome_tab function
rg -n "fn show_welcome_tab" src/state.rs
# Check lines around 3073-3087
sed -n '3070,3090p' src/state.rsRepository: OlaProeis/Ferrite
Length of output: 916
🏁 Script executed:
# Get more context before the TabKind enum to see derive macros
rg -B 3 "pub enum TabKind" src/state.rs
# Also check SpecialTabKind definition
rg -A 10 "enum SpecialTabKind" src/state.rsRepository: OlaProeis/Ferrite
Length of output: 404
🏁 Script executed:
# Get the full SpecialTabKind enum with derive macros
rg -B 3 "pub enum SpecialTabKind" src/state.rsRepository: OlaProeis/Ferrite
Length of output: 282
Avoid moving t.kind out of a shared reference.
TabKind doesn't derive Copy (only Debug, Clone, PartialEq), so matches!(t.kind, ...) attempts to move out of &Tab and won't compile. Match on a reference instead:
Suggested fix
- .position(|t| { matches!(t.kind, TabKind::Special(SpecialTabKind::Welcome)) })
+ .position(|t| matches!(&t.kind, TabKind::Special(SpecialTabKind::Welcome)))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/state.rs` around lines 3073 - 3087, The closure in show_welcome_tab is
attempting to move t.kind out of a shared reference when using matches!(t.kind,
TabKind::Special(SpecialTabKind::Welcome)); change the pattern to match on a
reference so it borrows rather than moves (e.g., match on &t.kind or use
matches!(t.kind, ref x) style), so update the iterator position predicate to
compare &t.kind to TabKind::Special(SpecialTabKind::Welcome) (or use
matches!(t.kind, TabKind::Special(ref k) if needed); this preserves ownership of
TabKind in tabs and allows setting active_tab_index and calling open_special_tab
as before.
- Mark welcome view as completed in ROADMAP.md (PR #80 by @blizzard007dev) - Add @blizzard007dev to contributors in README.md - Update release highlight to v0.2.7 features Co-authored-by: Cursor <[email protected]>
Description
Brief description of the changes in this PR.
Fixes #(issue number)
Type of Change
Changes Made
Screenshots
If this PR includes UI changes, please add before/after screenshots:
Checklist
cargo fmtand it produces no changescargo clippyand it produces no warningscargo testand all tests passcargo build --releasesuccessfullyBreaking Changes
If this PR introduces breaking changes, describe what changes users need to make:
Testing
Describe how you tested these changes:
Additional Notes
Any additional information that reviewers should know.
Summary by CodeRabbit
Release Notes