Compare commits
No commits in common. "1dbfd4e233a88a469a1749f663fb4e603830bc95" and "a3158129d1e353408d54af39ffb7c2d9694951ff" have entirely different histories.
1dbfd4e233
...
a3158129d1
12
Cargo.toml
12
Cargo.toml
@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "ced"
|
||||
version = "0.3.3"
|
||||
version = "0.1.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.33.3"
|
||||
egui = "0.33.3"
|
||||
egui_extras = { version = "0.33.3", features = ["syntect"] }
|
||||
eframe = "0.32"
|
||||
egui = "0.32"
|
||||
egui_extras = { version = "0.32", features = ["syntect"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.141"
|
||||
rfd = "0.15.4"
|
||||
@ -17,7 +17,3 @@ syntect = "5.2.0"
|
||||
plist = "1.7.4"
|
||||
diffy = "0.4.2"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
egui_commonmark = { version = "0.22" }
|
||||
egui_nerdfonts = "0.1.3"
|
||||
vte = "0.13"
|
||||
nix = { version = "0.29", features = ["term", "process", "fs"] }
|
||||
|
||||
12
README.md
12
README.md
@ -10,8 +10,6 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel
|
||||
|
||||
* Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.).
|
||||
* Choose between a fresh start each time you open, or maintaining a consistent state.
|
||||
* Built-in Markdown viewer.
|
||||
* Toggleable file tree.
|
||||
* Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`).
|
||||
* Ricers rejoice, your `pywal` colors will be used!
|
||||
* Weirdly smooth typing experience.
|
||||
@ -24,7 +22,7 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel
|
||||
##### Ubuntu/Debian
|
||||
`sudo apt install git rust`
|
||||
|
||||
### Install
|
||||
#### Install
|
||||
```bash
|
||||
git clone https://code.lampnet.io/candle/ced
|
||||
cd ced && cargo build --release
|
||||
@ -34,7 +32,7 @@ sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop
|
||||
|
||||
`ced` should now appear as 'Text Editor' in your application launcher. You can remove the cloned directory at this point.
|
||||
|
||||
### Configuration
|
||||
## Configuration
|
||||
|
||||
`ced` will look for, and create if needed, a configuration file at: `$XDG_CONFIG_HOME/ced/config.toml`.
|
||||
|
||||
@ -47,8 +45,6 @@ show_line_numbers = false
|
||||
word_wrap = false
|
||||
theme = "System"
|
||||
line_side = false
|
||||
show_file_tree = true
|
||||
file_tree_side = false
|
||||
font_family = "Monospace"
|
||||
font_size = 16.0
|
||||
syntax_highlighting = true
|
||||
@ -60,12 +56,10 @@ syntax_highlighting = true
|
||||
|--------|---------|-------------|
|
||||
| `state_cache` | `false` | If `true`, opened files will remain opened with their unsaved changes when running the application again. |
|
||||
| `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. |
|
||||
| `show_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. |
|
||||
| `hide_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. |
|
||||
| `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. |
|
||||
| `show_file_tree` | `false` | If `true`, a file tree will be displayed on the side specified by `file_tree_side`. |
|
||||
| `syntax_highlighting` | `false` | If `true`, text will be highlighted based on detected language. |
|
||||
| `line_side` | `false` | If `false`, line numbers are on the left. If `true`, they are on the right. |
|
||||
| `file_tree_side` | `false` | If `false`, file tree will appear on the lift. If `true` it will appear on the right. |
|
||||
| `word_wrap` | `false` | If `true`, lines will wrap when they reach the edge of the window. |
|
||||
| `font_family` | `"Proportional"` | The font family used for the editor text. |
|
||||
| `font_size` | `14.0` | `8.0-32.0` The font size for text editing. |
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
pub mod actions;
|
||||
pub mod config;
|
||||
pub mod shortcuts;
|
||||
pub mod state;
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ShortcutAction {
|
||||
NewFile,
|
||||
OpenFile,
|
||||
SaveFile,
|
||||
SaveAsFile,
|
||||
NewTab,
|
||||
CloseTab,
|
||||
ToggleLineNumbers,
|
||||
ToggleLineSide,
|
||||
ToggleWordWrap,
|
||||
ToggleAutoHideToolbar,
|
||||
ToggleBottomBar,
|
||||
ToggleFileTree,
|
||||
ToggleFileTreeSide,
|
||||
ToggleFind,
|
||||
ToggleReplace,
|
||||
ToggleMarkdown,
|
||||
FocusFind,
|
||||
NextTab,
|
||||
PrevTab,
|
||||
PageUp,
|
||||
PageDown,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
GlobalZoomIn,
|
||||
GlobalZoomOut,
|
||||
ResetZoom,
|
||||
Escape,
|
||||
Preferences,
|
||||
ToggleVimMode,
|
||||
ToggleFocusMode,
|
||||
}
|
||||
@ -4,54 +4,72 @@ use std::path::PathBuf;
|
||||
use super::theme::Theme;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_state_cache")]
|
||||
pub state_cache: bool,
|
||||
#[serde(default = "default_auto_hide_toolbar")]
|
||||
pub auto_hide_toolbar: bool,
|
||||
pub show_tab_bar: bool,
|
||||
pub show_bottom_bar: bool,
|
||||
pub show_file_tree: bool,
|
||||
#[serde(default = "default_hide_tab_bar")]
|
||||
pub hide_tab_bar: bool,
|
||||
#[serde(default = "default_show_line_numbers")]
|
||||
pub show_line_numbers: bool,
|
||||
#[serde(default = "default_word_wrap")]
|
||||
pub word_wrap: bool,
|
||||
#[serde(default = "Theme::default")]
|
||||
pub theme: Theme,
|
||||
#[serde(default = "default_line_side")]
|
||||
pub line_side: bool,
|
||||
pub file_tree_side: bool,
|
||||
pub show_hidden_files: bool,
|
||||
pub show_terminal: bool,
|
||||
pub follow_git: bool,
|
||||
pub tab_char: bool,
|
||||
pub tab_width: usize,
|
||||
#[serde(default = "default_font_family")]
|
||||
pub font_family: String,
|
||||
#[serde(default = "default_font_size")]
|
||||
pub font_size: f32,
|
||||
#[serde(default = "default_syntax_highlighting")]
|
||||
pub syntax_highlighting: bool,
|
||||
pub auto_indent: bool,
|
||||
pub focus_mode: bool,
|
||||
// pub vim_mode: bool,
|
||||
}
|
||||
|
||||
fn default_state_cache() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_auto_hide_toolbar() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_hide_tab_bar() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_show_line_numbers() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_word_wrap() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_line_side() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_font_family() -> String {
|
||||
"Proportional".to_string()
|
||||
}
|
||||
fn default_font_size() -> f32 {
|
||||
14.0
|
||||
}
|
||||
fn default_syntax_highlighting() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state_cache: false,
|
||||
auto_hide_toolbar: false,
|
||||
show_tab_bar: false,
|
||||
show_bottom_bar: true,
|
||||
show_file_tree: false,
|
||||
show_line_numbers: false,
|
||||
word_wrap: true,
|
||||
state_cache: default_state_cache(),
|
||||
auto_hide_toolbar: default_auto_hide_toolbar(),
|
||||
hide_tab_bar: default_hide_tab_bar(),
|
||||
show_line_numbers: default_show_line_numbers(),
|
||||
word_wrap: default_word_wrap(),
|
||||
theme: Theme::default(),
|
||||
line_side: false,
|
||||
file_tree_side: false,
|
||||
show_hidden_files: false,
|
||||
show_terminal: false,
|
||||
follow_git: true,
|
||||
tab_char: false,
|
||||
tab_width: 4,
|
||||
font_family: "Proportional".to_string(),
|
||||
font_size: 14.0,
|
||||
syntax_highlighting: false,
|
||||
auto_indent: true,
|
||||
focus_mode: false,
|
||||
line_side: default_line_side(),
|
||||
font_family: default_font_family(),
|
||||
font_size: default_font_size(),
|
||||
syntax_highlighting: default_syntax_highlighting(),
|
||||
// vim_mode: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,36 @@
|
||||
use crate::app::actions::ShortcutAction;
|
||||
use crate::app::state::TextEditor;
|
||||
use crate::io;
|
||||
use eframe::egui;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ShortcutAction {
|
||||
NewFile,
|
||||
OpenFile,
|
||||
SaveFile,
|
||||
SaveAsFile,
|
||||
NewTab,
|
||||
CloseTab,
|
||||
ToggleLineNumbers,
|
||||
ToggleLineSide,
|
||||
ToggleWordWrap,
|
||||
ToggleAutoHideToolbar,
|
||||
ToggleFind,
|
||||
ToggleReplace,
|
||||
FocusFind,
|
||||
NextTab,
|
||||
PrevTab,
|
||||
PageUp,
|
||||
PageDown,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
GlobalZoomIn,
|
||||
GlobalZoomOut,
|
||||
ResetZoom,
|
||||
Escape,
|
||||
Preferences,
|
||||
ToggleVimMode,
|
||||
}
|
||||
|
||||
type ShortcutDefinition = (egui::Modifiers, egui::Key, ShortcutAction);
|
||||
|
||||
fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||
@ -28,11 +57,6 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||
egui::Key::W,
|
||||
ShortcutAction::CloseTab,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL | egui::Modifiers::ALT,
|
||||
egui::Key::F,
|
||||
ShortcutAction::ToggleFocusMode,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||
egui::Key::F,
|
||||
@ -68,21 +92,6 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||
egui::Key::H,
|
||||
ShortcutAction::ToggleAutoHideToolbar,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::B,
|
||||
ShortcutAction::ToggleBottomBar,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||
egui::Key::E,
|
||||
ShortcutAction::ToggleFileTreeSide
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::E,
|
||||
ShortcutAction::ToggleFileTree,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||
egui::Key::Tab,
|
||||
@ -143,23 +152,153 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||
egui::Key::Escape,
|
||||
ShortcutAction::Escape,
|
||||
),
|
||||
(
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::M,
|
||||
ShortcutAction::ToggleMarkdown,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
||||
editor.perform_action(action)
|
||||
match action {
|
||||
ShortcutAction::NewFile => {
|
||||
io::new_file(editor);
|
||||
false
|
||||
}
|
||||
ShortcutAction::OpenFile => {
|
||||
io::open_file(editor);
|
||||
false
|
||||
}
|
||||
ShortcutAction::SaveFile => {
|
||||
io::save_file(editor);
|
||||
false
|
||||
}
|
||||
ShortcutAction::SaveAsFile => {
|
||||
io::save_as_file(editor);
|
||||
false
|
||||
}
|
||||
ShortcutAction::NewTab => {
|
||||
editor.add_new_tab();
|
||||
false
|
||||
}
|
||||
ShortcutAction::CloseTab => {
|
||||
if editor.tabs.len() > 1 {
|
||||
if let Some(current_tab) = editor.get_active_tab() {
|
||||
if current_tab.is_modified {
|
||||
editor.pending_unsaved_action = Some(
|
||||
super::state::UnsavedAction::CloseTab(editor.active_tab_index),
|
||||
);
|
||||
} else {
|
||||
editor.close_tab(editor.active_tab_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleLineNumbers => {
|
||||
editor.show_line_numbers = !editor.show_line_numbers;
|
||||
editor.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleLineSide => {
|
||||
editor.line_side = !editor.line_side;
|
||||
editor.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleWordWrap => {
|
||||
editor.word_wrap = !editor.word_wrap;
|
||||
editor.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleAutoHideToolbar => {
|
||||
editor.auto_hide_toolbar = !editor.auto_hide_toolbar;
|
||||
editor.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::NextTab => {
|
||||
let next_tab_index = editor.active_tab_index + 1;
|
||||
if next_tab_index < editor.tabs.len() {
|
||||
editor.switch_to_tab(next_tab_index);
|
||||
} else {
|
||||
editor.switch_to_tab(0);
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::PrevTab => {
|
||||
if editor.active_tab_index == 0 {
|
||||
editor.switch_to_tab(editor.tabs.len() - 1);
|
||||
} else {
|
||||
editor.switch_to_tab(editor.active_tab_index - 1);
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::PageUp => false,
|
||||
ShortcutAction::PageDown => false,
|
||||
ShortcutAction::ZoomIn => {
|
||||
editor.font_size += 1.0;
|
||||
true
|
||||
}
|
||||
ShortcutAction::ZoomOut => {
|
||||
editor.font_size -= 1.0;
|
||||
true
|
||||
}
|
||||
ShortcutAction::GlobalZoomIn => {
|
||||
editor.zoom_factor += 0.1;
|
||||
false
|
||||
}
|
||||
ShortcutAction::GlobalZoomOut => {
|
||||
editor.zoom_factor -= 0.1;
|
||||
if editor.zoom_factor < 0.1 {
|
||||
editor.zoom_factor = 0.1;
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::ResetZoom => {
|
||||
editor.zoom_factor = 1.0;
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleVimMode => {
|
||||
// editor.vim_mode = !editor.vim_mode;
|
||||
false
|
||||
}
|
||||
ShortcutAction::Escape => {
|
||||
editor.show_about = false;
|
||||
editor.show_shortcuts = false;
|
||||
if editor.show_find {
|
||||
editor.should_select_current_match = true;
|
||||
}
|
||||
editor.show_find = false;
|
||||
editor.show_preferences = false;
|
||||
editor.pending_unsaved_action = None;
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleFind => {
|
||||
editor.show_find = !editor.show_find;
|
||||
if editor.show_find && !editor.find_query.is_empty() {
|
||||
editor.update_find_matches();
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleReplace => {
|
||||
editor.show_find = !editor.show_find;
|
||||
editor.show_replace_section = true;
|
||||
if editor.show_find && !editor.find_query.is_empty() {
|
||||
editor.update_find_matches();
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::FocusFind => {
|
||||
if editor.show_find {
|
||||
editor.focus_find = true;
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::Preferences => {
|
||||
editor.show_preferences = !editor.show_preferences;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||
let mut font_zoom_occurred = false;
|
||||
let mut global_zoom_occurred = false;
|
||||
let mut page_up_pressed = false;
|
||||
let mut page_down_pressed = false;
|
||||
|
||||
ctx.input_mut(|i| {
|
||||
for (modifiers, key, action) in get_shortcuts() {
|
||||
@ -174,12 +313,6 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||
execute_action(action, editor);
|
||||
global_zoom_occurred = true;
|
||||
}
|
||||
ShortcutAction::PageUp => {
|
||||
page_up_pressed = true;
|
||||
}
|
||||
ShortcutAction::PageDown => {
|
||||
page_down_pressed = true;
|
||||
}
|
||||
_ => {
|
||||
execute_action(action, editor);
|
||||
}
|
||||
@ -197,14 +330,6 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||
ctx.set_zoom_factor(editor.zoom_factor);
|
||||
}
|
||||
|
||||
if page_up_pressed {
|
||||
editor.handle_page_movement(ctx, false);
|
||||
}
|
||||
|
||||
if page_down_pressed {
|
||||
editor.handle_page_movement(ctx, true);
|
||||
}
|
||||
|
||||
if editor.should_select_current_match {
|
||||
editor.select_current_match(ctx);
|
||||
editor.should_select_current_match = false;
|
||||
|
||||
@ -1,21 +1,15 @@
|
||||
use super::editor::TextEditor;
|
||||
use crate::app::shortcuts;
|
||||
use crate::ui::about_window::about_window;
|
||||
use crate::ui::bottom_bar::bottom_bar;
|
||||
use crate::ui::central_panel::central_panel;
|
||||
use crate::ui::file_tree::file_tree;
|
||||
use crate::ui::find_window::find_window;
|
||||
use crate::ui::menu_bar::menu_bar;
|
||||
use crate::ui::preferences_window::preferences_window;
|
||||
use crate::ui::shortcuts_window::shortcuts_window;
|
||||
use crate::ui::tab_bar::tab_bar;
|
||||
use crate::ui::shell_bar::shell_bar;
|
||||
|
||||
impl eframe::App for TextEditor {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// Reset focus manager at the start of each frame
|
||||
self.focus_manager.reset();
|
||||
|
||||
if ctx.input(|i| i.viewport().close_requested())
|
||||
&& !self.force_quit_confirmed
|
||||
&& !self.clean_quit_requested
|
||||
@ -28,35 +22,21 @@ impl eframe::App for TextEditor {
|
||||
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.get_title()));
|
||||
|
||||
if !self.focus_mode {
|
||||
menu_bar(self, ctx);
|
||||
}
|
||||
|
||||
if self.show_tab_bar && !self.focus_mode {
|
||||
if !self.hide_tab_bar {
|
||||
tab_bar(self, ctx);
|
||||
}
|
||||
|
||||
if self.show_file_tree && !self.focus_mode {
|
||||
file_tree(self, ctx);
|
||||
}
|
||||
|
||||
if self.show_bottom_bar && !self.focus_mode {
|
||||
bottom_bar(self, ctx);
|
||||
}
|
||||
|
||||
if self.show_terminal && !self.focus_mode {
|
||||
shell_bar(self, ctx);
|
||||
}
|
||||
|
||||
central_panel(self, ctx);
|
||||
|
||||
if self.show_about && !self.focus_mode {
|
||||
if self.show_about {
|
||||
about_window(self, ctx);
|
||||
}
|
||||
if self.show_shortcuts && !self.focus_mode {
|
||||
if self.show_shortcuts {
|
||||
shortcuts_window(self, ctx);
|
||||
}
|
||||
if self.show_preferences && !self.focus_mode {
|
||||
if self.show_preferences {
|
||||
preferences_window(self, ctx);
|
||||
}
|
||||
if self.show_find {
|
||||
@ -66,9 +46,6 @@ impl eframe::App for TextEditor {
|
||||
self.show_unsaved_changes_dialog(ctx);
|
||||
}
|
||||
|
||||
// Apply focus requests at the end of the frame
|
||||
self.focus_manager.apply_focus(ctx);
|
||||
|
||||
self.prev_show_find = self.show_find;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,22 +11,12 @@ impl TextEditor {
|
||||
show_line_numbers: config.show_line_numbers,
|
||||
word_wrap: config.word_wrap,
|
||||
auto_hide_toolbar: config.auto_hide_toolbar,
|
||||
show_tab_bar: config.show_tab_bar,
|
||||
show_bottom_bar: config.show_bottom_bar,
|
||||
show_file_tree: config.show_file_tree,
|
||||
show_terminal: config.show_terminal,
|
||||
hide_tab_bar: config.hide_tab_bar,
|
||||
theme: config.theme,
|
||||
line_side: config.line_side,
|
||||
file_tree_side: config.file_tree_side,
|
||||
show_hidden_files: config.show_hidden_files,
|
||||
follow_git: config.follow_git,
|
||||
tab_char: config.tab_char,
|
||||
tab_width: config.tab_width,
|
||||
font_family: config.font_family,
|
||||
font_size: config.font_size,
|
||||
syntax_highlighting: config.syntax_highlighting,
|
||||
auto_indent: config.auto_indent,
|
||||
focus_mode: config.focus_mode,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@ -90,8 +80,6 @@ impl TextEditor {
|
||||
|
||||
editor.apply_font_settings(&cc.egui_ctx);
|
||||
|
||||
editor.previous_content = editor.get_active_tab().map(|tab| tab.content.to_owned()).unwrap_or_default();
|
||||
editor.previous_cursor_char_index = Some(0);
|
||||
editor
|
||||
}
|
||||
|
||||
@ -100,23 +88,13 @@ impl TextEditor {
|
||||
state_cache: self.state_cache,
|
||||
auto_hide_toolbar: self.auto_hide_toolbar,
|
||||
show_line_numbers: self.show_line_numbers,
|
||||
show_tab_bar: self.show_tab_bar,
|
||||
show_bottom_bar: self.show_bottom_bar,
|
||||
show_file_tree: self.show_file_tree,
|
||||
show_terminal: self.show_terminal,
|
||||
hide_tab_bar: self.hide_tab_bar,
|
||||
word_wrap: self.word_wrap,
|
||||
theme: self.theme,
|
||||
line_side: self.line_side,
|
||||
file_tree_side: self.file_tree_side,
|
||||
show_hidden_files: self.show_hidden_files,
|
||||
follow_git: self.follow_git,
|
||||
tab_char: self.tab_char,
|
||||
tab_width: self.tab_width,
|
||||
font_family: self.font_family.to_string(),
|
||||
font_size: self.font_size,
|
||||
syntax_highlighting: self.syntax_highlighting,
|
||||
auto_indent: self.auto_indent,
|
||||
focus_mode: self.focus_mode,
|
||||
// vim_mode: self.vim_mode,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
use super::editor::TextEditor;
|
||||
use super::editor::TextProcessingResult;
|
||||
use crate::app::{tab::Tab, theme::Theme};
|
||||
use egui_commonmark::CommonMarkCache;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
impl Default for TextEditor {
|
||||
@ -15,32 +14,19 @@ impl Default for TextEditor {
|
||||
show_shortcuts: false,
|
||||
show_find: false,
|
||||
show_preferences: false,
|
||||
show_markdown: false,
|
||||
pending_unsaved_action: None,
|
||||
force_quit_confirmed: false,
|
||||
clean_quit_requested: false,
|
||||
show_line_numbers: false,
|
||||
word_wrap: true,
|
||||
auto_hide_toolbar: false,
|
||||
show_tab_bar: false,
|
||||
show_bottom_bar: true,
|
||||
show_file_tree: false,
|
||||
show_terminal: false,
|
||||
file_tree_root: None,
|
||||
file_tree_state: crate::ui::file_tree::FileTreeState::default(),
|
||||
hide_tab_bar: true,
|
||||
syntax_highlighting: false,
|
||||
auto_indent: true,
|
||||
theme: Theme::default(),
|
||||
line_side: false,
|
||||
file_tree_side: false,
|
||||
show_hidden_files: false,
|
||||
follow_git: true,
|
||||
tab_char: false,
|
||||
tab_width: 4,
|
||||
font_family: "Proportional".to_string(),
|
||||
font_size: 14.0,
|
||||
font_size_input: None,
|
||||
tab_width_input: None,
|
||||
zoom_factor: 1.0,
|
||||
menu_interaction_active: false,
|
||||
tab_bar_rect: None,
|
||||
@ -59,15 +45,10 @@ impl Default for TextEditor {
|
||||
previous_content: String::new(),
|
||||
previous_cursor_char_index: None,
|
||||
current_cursor_line: 0,
|
||||
current_cursor_index: 0,
|
||||
previous_cursor_line: 0,
|
||||
font_settings_changed: false,
|
||||
text_needs_processing: false,
|
||||
should_select_current_match: false,
|
||||
markdown_cache: CommonMarkCache::default(),
|
||||
focus_mode: false,
|
||||
focus_manager: crate::ui::focus_manager::FocusManager::default(),
|
||||
shell_state: crate::ui::shell_bar::ShellState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,6 @@
|
||||
use crate::app::actions::ShortcutAction;
|
||||
use crate::app::tab::Tab;
|
||||
use crate::app::theme::Theme;
|
||||
use crate::io;
|
||||
use crate::ui::file_tree::FileTreeState;
|
||||
use crate::ui::focus_manager::{FocusManager, FocusTarget, priorities};
|
||||
use crate::ui::shell_bar::ShellState;
|
||||
use eframe::egui;
|
||||
use egui_commonmark::CommonMarkCache;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
@ -46,32 +39,19 @@ pub struct TextEditor {
|
||||
pub(crate) show_shortcuts: bool,
|
||||
pub(crate) show_find: bool,
|
||||
pub(crate) show_preferences: bool,
|
||||
pub(crate) show_markdown: bool,
|
||||
pub(crate) pending_unsaved_action: Option<UnsavedAction>,
|
||||
pub(crate) force_quit_confirmed: bool,
|
||||
pub(crate) clean_quit_requested: bool,
|
||||
pub(crate) show_line_numbers: bool,
|
||||
pub(crate) word_wrap: bool,
|
||||
pub(crate) auto_hide_toolbar: bool,
|
||||
pub(crate) show_tab_bar: bool,
|
||||
pub(crate) show_bottom_bar: bool,
|
||||
pub(crate) show_file_tree: bool,
|
||||
pub(crate) show_terminal: bool,
|
||||
pub(crate) file_tree_root: Option<PathBuf>,
|
||||
pub(crate) file_tree_state: FileTreeState,
|
||||
pub(crate) hide_tab_bar: bool,
|
||||
pub(crate) syntax_highlighting: bool,
|
||||
pub(crate) auto_indent: bool,
|
||||
pub(crate) theme: Theme,
|
||||
pub(crate) line_side: bool,
|
||||
pub(crate) file_tree_side: bool,
|
||||
pub(crate) show_hidden_files: bool,
|
||||
pub(crate) follow_git: bool,
|
||||
pub(crate) tab_char: bool,
|
||||
pub(crate) tab_width: usize,
|
||||
pub(crate) font_family: String,
|
||||
pub(crate) font_size: f32,
|
||||
pub(crate) font_size_input: Option<String>,
|
||||
pub(crate) tab_width_input: Option<String>,
|
||||
pub(crate) zoom_factor: f32,
|
||||
pub(crate) menu_interaction_active: bool,
|
||||
pub(crate) tab_bar_rect: Option<egui::Rect>,
|
||||
@ -89,179 +69,9 @@ pub struct TextEditor {
|
||||
pub(crate) previous_content: String,
|
||||
pub(crate) previous_cursor_char_index: Option<usize>,
|
||||
pub(crate) current_cursor_line: usize,
|
||||
pub(crate) current_cursor_index: usize,
|
||||
pub(crate) previous_cursor_line: usize,
|
||||
pub(crate) font_settings_changed: bool,
|
||||
pub(crate) text_needs_processing: bool,
|
||||
pub(crate) should_select_current_match: bool,
|
||||
pub(crate) previous_cursor_position: Option<usize>,
|
||||
pub(crate) markdown_cache: CommonMarkCache,
|
||||
pub(crate) focus_mode: bool,
|
||||
pub(crate) focus_manager: FocusManager,
|
||||
pub(crate) shell_state: ShellState,
|
||||
}
|
||||
|
||||
impl TextEditor {
|
||||
pub fn perform_action(&mut self, action: ShortcutAction) -> bool {
|
||||
match action {
|
||||
ShortcutAction::NewFile => {
|
||||
io::new_file(self);
|
||||
false
|
||||
}
|
||||
ShortcutAction::OpenFile => {
|
||||
io::open_file(self);
|
||||
false
|
||||
}
|
||||
ShortcutAction::SaveFile => {
|
||||
io::save_file(self);
|
||||
false
|
||||
}
|
||||
ShortcutAction::SaveAsFile => {
|
||||
io::save_as_file(self);
|
||||
false
|
||||
}
|
||||
ShortcutAction::NewTab => {
|
||||
self.add_new_tab();
|
||||
false
|
||||
}
|
||||
ShortcutAction::CloseTab => {
|
||||
if self.tabs.len() > 1 {
|
||||
if let Some(current_tab) = self.get_active_tab() {
|
||||
if current_tab.is_modified {
|
||||
self.pending_unsaved_action =
|
||||
Some(UnsavedAction::CloseTab(self.active_tab_index));
|
||||
} else {
|
||||
self.close_tab(self.active_tab_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleLineNumbers => {
|
||||
self.show_line_numbers = !self.show_line_numbers;
|
||||
self.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleLineSide => {
|
||||
self.line_side = !self.line_side;
|
||||
self.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleWordWrap => {
|
||||
self.word_wrap = !self.word_wrap;
|
||||
self.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleAutoHideToolbar => {
|
||||
self.auto_hide_toolbar = !self.auto_hide_toolbar;
|
||||
self.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleBottomBar => {
|
||||
self.show_bottom_bar = !self.show_bottom_bar;
|
||||
self.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleFileTree => {
|
||||
self.show_file_tree = !self.show_file_tree;
|
||||
self.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleFileTreeSide => {
|
||||
self.file_tree_side = !self.file_tree_side;
|
||||
self.save_config();
|
||||
false
|
||||
}
|
||||
ShortcutAction::NextTab => {
|
||||
let next_tab_index = self.active_tab_index + 1;
|
||||
if next_tab_index < self.tabs.len() {
|
||||
self.switch_to_tab(next_tab_index);
|
||||
} else {
|
||||
self.switch_to_tab(0);
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::PrevTab => {
|
||||
if self.active_tab_index == 0 {
|
||||
self.switch_to_tab(self.tabs.len() - 1);
|
||||
} else {
|
||||
self.switch_to_tab(self.active_tab_index - 1);
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::PageUp | ShortcutAction::PageDown => false,
|
||||
ShortcutAction::ZoomIn => {
|
||||
self.font_size += 1.0;
|
||||
true
|
||||
}
|
||||
ShortcutAction::ZoomOut => {
|
||||
self.font_size -= 1.0;
|
||||
true
|
||||
}
|
||||
ShortcutAction::GlobalZoomIn => {
|
||||
self.zoom_factor += 0.1;
|
||||
false
|
||||
}
|
||||
ShortcutAction::GlobalZoomOut => {
|
||||
self.zoom_factor -= 0.1;
|
||||
if self.zoom_factor < 0.1 {
|
||||
self.zoom_factor = 0.1;
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::ResetZoom => {
|
||||
self.zoom_factor = 1.0;
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleVimMode => {
|
||||
// self.vim_mode = !self.vim_mode;
|
||||
false
|
||||
}
|
||||
ShortcutAction::Escape => {
|
||||
self.show_about = false;
|
||||
self.show_shortcuts = false;
|
||||
if self.show_find {
|
||||
self.should_select_current_match = true;
|
||||
}
|
||||
self.show_find = false;
|
||||
self.show_preferences = false;
|
||||
self.pending_unsaved_action = None;
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleFind => {
|
||||
self.show_find = !self.show_find;
|
||||
if self.show_find && !self.find_query.is_empty() {
|
||||
self.update_find_matches();
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleReplace => {
|
||||
self.show_find = !self.show_find;
|
||||
self.show_replace_section = true;
|
||||
if self.show_find && !self.find_query.is_empty() {
|
||||
self.update_find_matches();
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::FocusFind => {
|
||||
if self.show_find {
|
||||
self.focus_find = true;
|
||||
}
|
||||
false
|
||||
}
|
||||
ShortcutAction::Preferences => {
|
||||
self.show_preferences = !self.show_preferences;
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleMarkdown => {
|
||||
self.show_markdown = !self.show_markdown;
|
||||
false
|
||||
}
|
||||
ShortcutAction::ToggleFocusMode => {
|
||||
self.focus_mode = !self.focus_mode;
|
||||
self.save_config();
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::editor::TextEditor;
|
||||
use crate::util::safe_slice_to_pos;
|
||||
use eframe::egui;
|
||||
|
||||
impl TextEditor {
|
||||
@ -115,15 +114,10 @@ impl TextEditor {
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
|
||||
let start_char = safe_slice_to_pos(content, start_byte).chars().count();
|
||||
let end_char = safe_slice_to_pos(content, end_byte).chars().count();
|
||||
let start_char = Self::safe_slice_to_pos(content, start_byte).chars().count();
|
||||
let end_char = Self::safe_slice_to_pos(content, end_byte).chars().count();
|
||||
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
let selection_range = egui::text::CCursorRange::two(
|
||||
egui::text::CCursor::new(start_char),
|
||||
@ -158,16 +152,12 @@ impl TextEditor {
|
||||
self.update_find_matches();
|
||||
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let replacement_end_char = safe_slice_to_pos(&active_tab.content, replacement_end)
|
||||
let replacement_end_char =
|
||||
Self::safe_slice_to_pos(&active_tab.content, replacement_end)
|
||||
.chars()
|
||||
.count();
|
||||
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
state
|
||||
.cursor
|
||||
@ -216,14 +206,7 @@ impl TextEditor {
|
||||
|
||||
self.current_match_index = None;
|
||||
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||
state
|
||||
.cursor
|
||||
@ -233,5 +216,4 @@ impl TextEditor {
|
||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
use super::editor::{TextEditor, TextProcessingResult};
|
||||
use crate::util::safe_slice_to_pos;
|
||||
use eframe::egui;
|
||||
|
||||
impl TextEditor {
|
||||
pub(crate) fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
||||
let pos = pos.min(content.len());
|
||||
let mut boundary_pos = pos;
|
||||
while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) {
|
||||
boundary_pos -= 1;
|
||||
}
|
||||
&content[..boundary_pos]
|
||||
}
|
||||
|
||||
pub fn process_text_for_rendering(&mut self, content: &str, ui: &egui::Ui) {
|
||||
let line_count = content.bytes().filter(|&b| b == b'\n').count() + 1;
|
||||
|
||||
@ -42,12 +50,13 @@ impl TextEditor {
|
||||
let font_id = self.get_font_id();
|
||||
let longest_line_pixel_width = if longest_line_length > 0 {
|
||||
let longest_line_text = lines[longest_line_index];
|
||||
ui.fonts_mut(|fonts| {
|
||||
ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout_no_wrap(
|
||||
.layout(
|
||||
longest_line_text.to_string(),
|
||||
font_id,
|
||||
egui::Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
@ -74,6 +83,15 @@ impl TextEditor {
|
||||
new_cursor_pos: usize,
|
||||
ui: &egui::Ui,
|
||||
) {
|
||||
let line_change = self.calculate_cursor_line_change(
|
||||
old_content,
|
||||
new_content,
|
||||
old_cursor_pos,
|
||||
new_cursor_pos,
|
||||
);
|
||||
|
||||
self.current_cursor_line = (self.current_cursor_line as isize + line_change) as usize;
|
||||
|
||||
if old_content.len() == new_content.len() {
|
||||
self.handle_character_replacement(
|
||||
old_content,
|
||||
@ -103,6 +121,25 @@ impl TextEditor {
|
||||
self.previous_cursor_line = self.current_cursor_line;
|
||||
}
|
||||
|
||||
fn calculate_cursor_line_change(
|
||||
&self,
|
||||
old_content: &str,
|
||||
new_content: &str,
|
||||
old_cursor_pos: usize,
|
||||
new_cursor_pos: usize,
|
||||
) -> isize {
|
||||
let old_newlines = Self::safe_slice_to_pos(old_content, old_cursor_pos)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
|
||||
let new_newlines = Self::safe_slice_to_pos(new_content, new_cursor_pos)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
|
||||
new_newlines as isize - old_newlines as isize
|
||||
}
|
||||
|
||||
fn handle_character_replacement(
|
||||
&mut self,
|
||||
@ -131,43 +168,41 @@ impl TextEditor {
|
||||
new_cursor_pos: usize,
|
||||
ui: &egui::Ui,
|
||||
) {
|
||||
let old_char_count = old_content.chars().count();
|
||||
let new_char_count = new_content.chars().count();
|
||||
|
||||
let safe_new_cursor = new_cursor_pos.min(new_char_count);
|
||||
|
||||
let new_byte_pos = new_content.char_indices()
|
||||
.nth(safe_new_cursor)
|
||||
.map(|(idx, _)| idx)
|
||||
.unwrap_or(new_content.len());
|
||||
|
||||
let added_chars = new_char_count.saturating_sub(old_char_count);
|
||||
let addition_start_byte = new_byte_pos.saturating_sub(
|
||||
new_content[..new_byte_pos]
|
||||
.chars()
|
||||
.rev()
|
||||
.take(added_chars)
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>()
|
||||
);
|
||||
let addition_end_byte = new_byte_pos;
|
||||
|
||||
let added_text = if addition_start_byte < addition_end_byte && addition_end_byte <= new_content.len() {
|
||||
&new_content[addition_start_byte..addition_end_byte]
|
||||
let min_len = old_content.len().min(new_content.len());
|
||||
let mut common_prefix = 0;
|
||||
let mut common_suffix = 0;
|
||||
for i in 0..min_len {
|
||||
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
|
||||
common_prefix += 1;
|
||||
} else {
|
||||
""
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..min_len - common_prefix {
|
||||
let old_idx = old_content.len() - 1 - i;
|
||||
let new_idx = new_content.len() - 1 - i;
|
||||
if old_content.as_bytes()[old_idx] == new_content.as_bytes()[new_idx] {
|
||||
common_suffix += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let added_start = common_prefix;
|
||||
let added_end = new_content.len() - common_suffix;
|
||||
let added_text = &new_content[added_start..added_end];
|
||||
let newlines_added = added_text.bytes().filter(|&b| b == b'\n').count();
|
||||
|
||||
if newlines_added > 0 {
|
||||
let mut current_result = self.get_text_processing_result();
|
||||
current_result.line_count += newlines_added;
|
||||
|
||||
let addition_start_line = new_content[..addition_start_byte]
|
||||
let addition_start_line = Self::safe_slice_to_pos(old_content, added_start)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
let addition_end_line = new_content[..addition_end_byte]
|
||||
let addition_end_line = Self::safe_slice_to_pos(old_content, added_end)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
@ -183,7 +218,7 @@ impl TextEditor {
|
||||
self.update_processing_result(current_result);
|
||||
}
|
||||
} else {
|
||||
let current_line = self.extract_current_line(new_content, safe_new_cursor);
|
||||
let current_line = self.extract_current_line(new_content, new_cursor_pos);
|
||||
let current_line_length = current_line.chars().count();
|
||||
self.update_line_if_longer(
|
||||
self.current_cursor_line,
|
||||
@ -233,11 +268,11 @@ impl TextEditor {
|
||||
let mut current_result = self.get_text_processing_result();
|
||||
current_result.line_count = current_result.line_count.saturating_sub(newlines_removed);
|
||||
|
||||
let removal_start_line = safe_slice_to_pos(old_content, removed_start)
|
||||
let removal_start_line = Self::safe_slice_to_pos(old_content, removed_start)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
let removal_end_line = safe_slice_to_pos(old_content, removed_end)
|
||||
let removal_end_line = Self::safe_slice_to_pos(old_content, removed_end)
|
||||
.bytes()
|
||||
.filter(|&b| b == b'\n')
|
||||
.count();
|
||||
@ -296,7 +331,7 @@ impl TextEditor {
|
||||
{
|
||||
content[line_start_boundary..line_end_boundary].to_string()
|
||||
} else {
|
||||
safe_slice_to_pos(content, line_end_boundary)[line_start_boundary..].to_string()
|
||||
Self::safe_slice_to_pos(content, line_end_boundary)[line_start_boundary..].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,12 +346,13 @@ impl TextEditor {
|
||||
|
||||
if line_length > current_result.longest_line_length {
|
||||
let font_id = self.get_font_id();
|
||||
let pixel_width = ui.fonts_mut(|fonts| {
|
||||
let pixel_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout_no_wrap(
|
||||
.layout(
|
||||
line_content.to_string(),
|
||||
font_id,
|
||||
egui::Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
|
||||
@ -36,7 +36,7 @@ impl TextEditor {
|
||||
self.update_find_matches();
|
||||
}
|
||||
self.text_needs_processing = true;
|
||||
self.file_tree_state.set_selected(Some(self.get_active_tab().unwrap().file_path.clone().unwrap_or(std::path::PathBuf::new())));
|
||||
|
||||
if let Err(e) = self.save_state_cache() {
|
||||
eprintln!("Failed to save state cache: {e}");
|
||||
}
|
||||
@ -49,10 +49,7 @@ impl TextEditor {
|
||||
if self.show_find && !self.find_query.is_empty() {
|
||||
self.update_find_matches();
|
||||
}
|
||||
self.previous_content = self.get_active_tab().unwrap().content.to_owned();
|
||||
self.previous_cursor_char_index = Some(0);
|
||||
self.text_needs_processing = true;
|
||||
self.file_tree_state.set_selected(Some(self.get_active_tab().unwrap().file_path.clone().unwrap_or(std::path::PathBuf::new())));
|
||||
|
||||
if let Err(e) = self.save_state_cache() {
|
||||
eprintln!("Failed to save state cache: {e}");
|
||||
|
||||
@ -46,7 +46,6 @@ impl TextEditor {
|
||||
egui::TextStyle::Monospace,
|
||||
egui::FontId::new(self.font_size, font_family),
|
||||
);
|
||||
self.font_size_input = Some(self.font_size.to_string());
|
||||
|
||||
ctx.set_style(style);
|
||||
self.font_settings_changed = true;
|
||||
@ -66,18 +65,18 @@ impl TextEditor {
|
||||
let processing_result = self.get_text_processing_result();
|
||||
let line_count = processing_result.line_count;
|
||||
|
||||
let monospace_font_id = egui::FontId::monospace(self.font_size);
|
||||
let font_id = self.get_font_id();
|
||||
let line_count_digits = line_count.to_string().len();
|
||||
let sample_text = "9".repeat(line_count_digits);
|
||||
let base_line_number_width = ui.fonts_mut(|fonts| {
|
||||
let base_line_number_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout_no_wrap(sample_text, monospace_font_id, egui::Color32::WHITE)
|
||||
.layout(sample_text, font_id, egui::Color32::WHITE, f32::INFINITY)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
|
||||
let line_number_width = if self.line_side {
|
||||
base_line_number_width + crate::ui::constants::SCROLLBAR_WIDTH
|
||||
base_line_number_width + 25.0 // Scrollbar width
|
||||
} else {
|
||||
base_line_number_width
|
||||
};
|
||||
@ -96,149 +95,10 @@ impl TextEditor {
|
||||
return self.calculate_editor_dimensions(ui).text_width;
|
||||
}
|
||||
|
||||
let longest_line_width = processing_result.longest_line_pixel_width;
|
||||
let font_id = self.get_font_id();
|
||||
let char_width = ui.fonts_mut(|fonts| {
|
||||
fonts
|
||||
.layout_no_wrap("M".to_string(), font_id, egui::Color32::WHITE)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
let extra_space = char_width * 2.0;
|
||||
let longest_line_width =
|
||||
processing_result.longest_line_pixel_width + (self.font_size * 3.0);
|
||||
|
||||
let dimensions = self.calculate_editor_dimensions(ui);
|
||||
(longest_line_width + extra_space).max(dimensions.text_width)
|
||||
}
|
||||
|
||||
pub fn get_cursor_position(&self) -> (usize, usize) {
|
||||
if let Some(active_tab) = self.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
let char_count = content.chars().count();
|
||||
let safe_char_pos = self.current_cursor_index.min(char_count);
|
||||
|
||||
// Convert character index to byte index
|
||||
let byte_pos = content
|
||||
.char_indices()
|
||||
.nth(safe_char_pos)
|
||||
.map(|(byte_idx, _)| byte_idx)
|
||||
.unwrap_or(content.len());
|
||||
|
||||
// Calculate column (chars since last newline)
|
||||
let mut column = 0;
|
||||
for c in content[..byte_pos].chars().rev() {
|
||||
if c == '\n' {
|
||||
break;
|
||||
}
|
||||
column += 1;
|
||||
}
|
||||
|
||||
(column + 1, self.current_cursor_line)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_page_movement(&mut self, ctx: &egui::Context, direction_down: bool) {
|
||||
let Some(active_tab) = self.get_active_tab() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
|
||||
let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(cursor_range) = state.cursor.char_range() else {
|
||||
return;
|
||||
};
|
||||
let current_pos = cursor_range.primary.index;
|
||||
|
||||
let content = &active_tab.content;
|
||||
|
||||
let available_height = ctx.available_rect().height();
|
||||
let row_height = self.font_size * 1.5;
|
||||
let visible_rows = (available_height / row_height).floor() as usize;
|
||||
let rows_to_move = visible_rows.max(1);
|
||||
|
||||
let new_pos = if direction_down {
|
||||
move_cursor_down_lines(content, current_pos, rows_to_move)
|
||||
} else {
|
||||
move_cursor_up_lines(content, current_pos, rows_to_move)
|
||||
};
|
||||
|
||||
let new_cursor = egui::text::CCursor::new(new_pos);
|
||||
state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(new_cursor)));
|
||||
egui::TextEdit::store_state(ctx, text_edit_id, state);
|
||||
longest_line_width.max(dimensions.text_width)
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_down_lines(content: &str, current_pos: usize, lines: usize) -> usize {
|
||||
let char_count = content.chars().count();
|
||||
let safe_char_pos = current_pos.min(char_count);
|
||||
|
||||
// Convert character index to byte index
|
||||
let byte_pos = content
|
||||
.char_indices()
|
||||
.nth(safe_char_pos)
|
||||
.map(|(byte_idx, _)| byte_idx)
|
||||
.unwrap_or(content.len());
|
||||
|
||||
let mut result_byte_pos = byte_pos;
|
||||
let mut lines_moved = 0;
|
||||
|
||||
// char_indices() returns (byte_index, char), so idx is a byte index
|
||||
for (idx, ch) in content[byte_pos..].char_indices() {
|
||||
if ch == '\n' {
|
||||
lines_moved += 1;
|
||||
if lines_moved >= lines {
|
||||
result_byte_pos = byte_pos + idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lines_moved < lines && result_byte_pos == byte_pos {
|
||||
result_byte_pos = content.len();
|
||||
}
|
||||
|
||||
// Convert byte index back to character index
|
||||
content[..result_byte_pos.min(content.len())].chars().count()
|
||||
}
|
||||
|
||||
fn move_cursor_up_lines(content: &str, current_pos: usize, lines: usize) -> usize {
|
||||
let char_count = content.chars().count();
|
||||
let safe_char_pos = current_pos.min(char_count);
|
||||
|
||||
// Convert character index to byte index
|
||||
let byte_pos = content
|
||||
.char_indices()
|
||||
.nth(safe_char_pos)
|
||||
.map(|(byte_idx, _)| byte_idx)
|
||||
.unwrap_or(content.len());
|
||||
|
||||
let mut result_byte_pos = byte_pos;
|
||||
let mut lines_moved = 0;
|
||||
|
||||
// Use char_indices() and iterate in reverse to get correct byte positions
|
||||
for (byte_idx, ch) in content[..byte_pos].char_indices().rev() {
|
||||
if ch == '\n' {
|
||||
lines_moved += 1;
|
||||
if lines_moved >= lines {
|
||||
result_byte_pos = byte_idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
result_byte_pos = byte_idx;
|
||||
}
|
||||
|
||||
// Convert byte index back to character index
|
||||
content[..result_byte_pos].chars().count()
|
||||
}
|
||||
|
||||
46
src/io.rs
46
src/io.rs
@ -250,49 +250,3 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rename_file(app: &mut TextEditor, old_path: &PathBuf, new_name: &str) -> Result<(), String> {
|
||||
let parent = old_path.parent().ok_or("Cannot rename root directory")?;
|
||||
let new_path = parent.join(new_name);
|
||||
|
||||
// If renaming to the same path, just return success (no-op)
|
||||
if new_path == *old_path {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if target already exists
|
||||
if new_path.exists() {
|
||||
return Err(format!("File {} already exists", new_path.display()));
|
||||
}
|
||||
|
||||
// Rename the file on disk
|
||||
fs::rename(old_path, &new_path)
|
||||
.map_err(|e| format!("Failed to rename file: {}", e))?;
|
||||
|
||||
// Update any tabs that reference this file
|
||||
for tab in &mut app.tabs {
|
||||
if let Some(tab_path) = &tab.file_path {
|
||||
// Check if this tab's path matches the old path
|
||||
if tab_path == old_path {
|
||||
tab.file_path = Some(new_path.clone());
|
||||
tab.title = new_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("Untitled")
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the active tab index if needed
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
if let Some(tab_path) = &active_tab.file_path {
|
||||
if tab_path == &new_path {
|
||||
// The active tab was updated, mark as needing processing
|
||||
app.text_needs_processing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -8,8 +8,7 @@ use std::path::PathBuf;
|
||||
mod app;
|
||||
mod io;
|
||||
mod ui;
|
||||
mod util;
|
||||
use app::{TextEditor, config::Config};
|
||||
use app::{config::Config, TextEditor};
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
pub(crate) mod about_window;
|
||||
pub(crate) mod bottom_bar;
|
||||
pub(crate) mod central_panel;
|
||||
pub(crate) mod constants;
|
||||
pub(crate) mod find_window;
|
||||
pub(crate) mod file_tree;
|
||||
pub(crate) mod focus_manager;
|
||||
pub(crate) mod menu_bar;
|
||||
pub(crate) mod preferences_window;
|
||||
pub(crate) mod shortcuts_window;
|
||||
pub(crate) mod tab_bar;
|
||||
pub(crate) mod shell_bar;
|
||||
@ -1,43 +0,0 @@
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::central_panel::languages::get_language_from_extension;
|
||||
use eframe::egui::{self, Frame};
|
||||
|
||||
pub(crate) fn bottom_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let line_count = app.get_text_processing_result().line_count;
|
||||
let char_count = app
|
||||
.get_active_tab()
|
||||
.map(|tab| tab.content.chars().count())
|
||||
.unwrap_or(0);
|
||||
let cursor_position = app.get_cursor_position();
|
||||
let cursor_column = cursor_position.0;
|
||||
let cursor_row = cursor_position.1;
|
||||
|
||||
let active_tab = app.get_active_tab();
|
||||
let file_path = active_tab.and_then(|tab| tab.file_path.as_deref());
|
||||
let language = get_language_from_extension(file_path);
|
||||
|
||||
if app.show_bottom_bar {
|
||||
let frame = Frame::NONE.fill(ctx.style().visuals.panel_fill);
|
||||
egui::TopBottomPanel::bottom("bottom_bar")
|
||||
.frame(frame)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("Ln {}, Col {}", cursor_row, cursor_column));
|
||||
ui.separator();
|
||||
ui.label(format!("{} chars, {} lines", char_count, line_count));
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("{}", language.to_uppercase()));
|
||||
ui.separator();
|
||||
ui.label("UTF-8");
|
||||
ui.separator();
|
||||
ui.label(format!("{}pt", app.font_size as u32));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,35 +1,22 @@
|
||||
mod editor;
|
||||
mod find_highlight;
|
||||
pub mod languages;
|
||||
mod languages;
|
||||
mod line_numbers;
|
||||
mod markdown;
|
||||
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::constants::*;
|
||||
use eframe::egui;
|
||||
use egui::UiKind;
|
||||
|
||||
use self::editor::editor_view_ui;
|
||||
use self::languages::get_language_from_extension;
|
||||
use self::line_numbers::{calculate_visual_line_mapping, render_line_numbers};
|
||||
use self::markdown::markdown_view_ui;
|
||||
|
||||
fn is_markdown_tab(app: &TextEditor) -> bool {
|
||||
app.get_active_tab()
|
||||
.and_then(|tab| tab.file_path.as_deref())
|
||||
.map(|path| get_language_from_extension(Some(path)) == "md")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let show_line_numbers = app.show_line_numbers;
|
||||
let word_wrap = app.word_wrap;
|
||||
let line_side = app.line_side;
|
||||
let font_size = app.font_size;
|
||||
let monospace = if app.font_family.as_str() == "Monospace" { true } else { false };
|
||||
let font_id = app.get_font_id();
|
||||
let show_markdown = app.show_markdown;
|
||||
let is_markdown_file = is_markdown_tab(app);
|
||||
let focus_mode = app.focus_mode;
|
||||
|
||||
let _output = egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE)
|
||||
@ -39,81 +26,55 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.painter().rect_filled(panel_rect, 0.0, bg_color);
|
||||
let editor_height = panel_rect.height();
|
||||
|
||||
// Handle markdown split view
|
||||
if show_markdown && is_markdown_file {
|
||||
let half_width = panel_rect.width() / 2.0;
|
||||
|
||||
ui.push_id("markdown_split_container", |ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(panel_rect.width(), editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
// Left side: Editor
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(half_width, editor_height),
|
||||
egui::Layout::top_down(egui::Align::LEFT),
|
||||
|ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("editor_scroll_area")
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
editor_view_ui(ui, app);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Separator
|
||||
let separator_x = ui.cursor().left();
|
||||
let mut y_range = ui.available_rect_before_wrap().y_range();
|
||||
y_range.max += 2.0 * font_size;
|
||||
ui.painter()
|
||||
.vline(separator_x, y_range, ui.visuals().window_stroke);
|
||||
ui.add_space(SMALL);
|
||||
|
||||
// Right side: Markdown view
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(half_width, editor_height),
|
||||
egui::Layout::top_down(egui::Align::LEFT),
|
||||
|ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("markdown_scroll_area")
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
egui::Frame::new()
|
||||
.inner_margin(egui::Margin {
|
||||
left: 0,
|
||||
right: SCROLLBAR_WIDTH as i8,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
})
|
||||
.show(ui, |ui| {
|
||||
markdown_view_ui(ui, app);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if !show_line_numbers || app.get_active_tab().is_none() || focus_mode {
|
||||
if !show_line_numbers || app.get_active_tab().is_none() {
|
||||
let _scroll_response =
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
let full_rect = ui.available_rect_before_wrap();
|
||||
let context_response =
|
||||
ui.allocate_response(full_rect.size(), egui::Sense::click());
|
||||
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
|
||||
editor_view_ui(ui, app);
|
||||
});
|
||||
|
||||
handle_empty(ui, app, &context_response);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let line_count = app.get_text_processing_result().line_count;
|
||||
let line_number_width = app.calculate_editor_dimensions(ui).line_number_width;
|
||||
let editor_dimensions = app.calculate_editor_dimensions(ui);
|
||||
let line_number_width = editor_dimensions.line_number_width;
|
||||
let editor_width = editor_dimensions.text_width - line_number_width;
|
||||
|
||||
let visual_line_mapping = if word_wrap {
|
||||
app.get_active_tab()
|
||||
.map(|active_tab| {
|
||||
let actual_editor_width = ui.available_width() - line_number_width;
|
||||
calculate_visual_line_mapping(
|
||||
ui,
|
||||
&active_tab.content,
|
||||
actual_editor_width,
|
||||
font_id,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(Vec::new)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let line_numbers_widget = |ui: &mut egui::Ui| {
|
||||
render_line_numbers(
|
||||
ui,
|
||||
line_count,
|
||||
&visual_line_mapping,
|
||||
line_number_width,
|
||||
word_wrap,
|
||||
line_side,
|
||||
font_size,
|
||||
);
|
||||
};
|
||||
|
||||
let separator_widget = |ui: &mut egui::Ui| {
|
||||
let separator_x = ui.cursor().left();
|
||||
@ -127,51 +88,29 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
let available_width = ui.available_width();
|
||||
let actual_editor_width = (available_width - line_number_width).max(0.0);
|
||||
|
||||
let visual_line_mapping = if word_wrap {
|
||||
app.get_active_tab()
|
||||
.map(|active_tab| {
|
||||
calculate_visual_line_mapping(
|
||||
ui,
|
||||
&active_tab.content,
|
||||
actual_editor_width - (if line_side { 8.0 } else { 20.0 }),
|
||||
font_id,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let line_numbers_widget = |ui: &mut egui::Ui| {
|
||||
render_line_numbers(
|
||||
ui,
|
||||
line_count,
|
||||
&visual_line_mapping,
|
||||
line_number_width,
|
||||
word_wrap,
|
||||
line_side,
|
||||
font_size,
|
||||
monospace,
|
||||
);
|
||||
};
|
||||
|
||||
if line_side {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(available_width, editor_height),
|
||||
egui::vec2(editor_dimensions.text_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(actual_editor_width, editor_height),
|
||||
egui::vec2(editor_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
let full_rect: egui::Rect = ui.available_rect_before_wrap();
|
||||
let context_response = ui.allocate_response(
|
||||
full_rect.size(),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(full_rect),
|
||||
|ui| editor_view_ui(ui, app),
|
||||
|ui| {
|
||||
editor_view_ui(ui, app);
|
||||
},
|
||||
);
|
||||
|
||||
handle_empty(ui, app, &context_response);
|
||||
},
|
||||
);
|
||||
separator_widget(ui);
|
||||
@ -180,17 +119,24 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
);
|
||||
} else {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(available_width, editor_height),
|
||||
egui::vec2(editor_dimensions.text_width, editor_height),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
line_numbers_widget(ui);
|
||||
separator_widget(ui);
|
||||
|
||||
let editor_area = ui.available_rect_before_wrap();
|
||||
let context_response =
|
||||
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
||||
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(editor_area),
|
||||
|ui| editor_view_ui(ui, app),
|
||||
|ui| {
|
||||
editor_view_ui(ui, app);
|
||||
},
|
||||
);
|
||||
|
||||
handle_empty(ui, app, &context_response);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -198,3 +144,74 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_empty(_ui: &mut egui::Ui, app: &mut TextEditor, context_response: &egui::Response) {
|
||||
if context_response.clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(_ui.ctx(), text_edit_id) {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let text_len = active_tab.content.len();
|
||||
let cursor_pos = egui::text::CCursor::new(text_len);
|
||||
state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(cursor_pos)));
|
||||
egui::TextEdit::store_state(_ui.ctx(), text_edit_id, state);
|
||||
|
||||
_ui.ctx().memory_mut(|mem| {
|
||||
mem.request_focus(text_edit_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context_response.context_menu(|ui| {
|
||||
let text_len = app.get_active_tab().unwrap().content.len();
|
||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||
|
||||
if ui.button("Cut").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Copy").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Paste").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Delete").clicked() {
|
||||
ui.ctx().input_mut(|i| {
|
||||
i.events.push(egui::Event::Key {
|
||||
key: egui::Key::Delete,
|
||||
physical_key: None,
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
})
|
||||
});
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Select All").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let select_all_range = egui::text::CCursorRange::two(
|
||||
egui::text::CCursor::new(0),
|
||||
egui::text::CCursor::new(text_len),
|
||||
);
|
||||
state.cursor.set_char_range(Some(select_all_range));
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
}
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Reset Zoom").clicked() {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(reset_zoom_key, true);
|
||||
});
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,36 +1,37 @@
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::focus_manager::{FocusTarget, priorities};
|
||||
use eframe::egui;
|
||||
use egui_extras::syntax_highlighting::{self};
|
||||
|
||||
use super::find_highlight;
|
||||
|
||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response {
|
||||
let _current_match_position = app.get_current_match_position();
|
||||
let show_find = app.show_find;
|
||||
let _prev_show_find = app.prev_show_find;
|
||||
let show_preferences = app.show_preferences;
|
||||
let show_about = app.show_about;
|
||||
let show_shortcuts = app.show_shortcuts;
|
||||
let show_terminal = app.show_terminal;
|
||||
let is_renaming = app
|
||||
.get_active_tab()
|
||||
.and_then(|tab| tab.file_path.as_ref())
|
||||
.map(|file_path| app.file_tree_state.is_renaming(file_path))
|
||||
.unwrap_or(false);
|
||||
|
||||
let word_wrap = app.word_wrap;
|
||||
let font_size = app.font_size;
|
||||
let font_id = app.get_font_id();
|
||||
let syntax_highlighting_enabled = app.syntax_highlighting;
|
||||
let previous_cursor_position = app.previous_cursor_position;
|
||||
let auto_indent = app.auto_indent;
|
||||
let tab_char = app.tab_char;
|
||||
let tab_width = app.tab_width;
|
||||
|
||||
let bg_color = ui.visuals().extreme_bg_color;
|
||||
let editor_rect = ui.available_rect_before_wrap();
|
||||
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
||||
|
||||
handle_zoom_reset(ui, app);
|
||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||
let should_reset_zoom = ui
|
||||
.ctx()
|
||||
.memory_mut(|mem| mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false));
|
||||
|
||||
if should_reset_zoom {
|
||||
app.zoom_factor = 1.0;
|
||||
ui.ctx().set_zoom_factor(1.0);
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(reset_zoom_key, false);
|
||||
});
|
||||
}
|
||||
|
||||
let (estimated_width, desired_width) = if !word_wrap {
|
||||
(app.calculate_content_based_width(ui), f32::INFINITY)
|
||||
@ -54,355 +55,18 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
|
||||
return ui.label("No file open, how did you get here?");
|
||||
};
|
||||
|
||||
let draw_highlights = |ui: &mut egui::Ui| {
|
||||
draw_editor_highlights(ui, &find_data, &font_id, font_size, desired_width);
|
||||
};
|
||||
|
||||
let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref());
|
||||
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
|
||||
let layout_job = editor_layouter(
|
||||
ui,
|
||||
string,
|
||||
wrap_width,
|
||||
syntax_highlighting_enabled,
|
||||
&language,
|
||||
&font_id,
|
||||
);
|
||||
ui.fonts_mut(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
|
||||
let mut indent_result = false;
|
||||
let mut tab_result = false;
|
||||
if should_have_focus(
|
||||
show_find,
|
||||
show_preferences,
|
||||
show_about,
|
||||
show_shortcuts,
|
||||
is_renaming,
|
||||
show_terminal,
|
||||
) {
|
||||
indent_result = handle_auto_indent(ui, active_tab, text_edit_id, auto_indent);
|
||||
tab_result = handle_tab_insertion(ui, active_tab, text_edit_id, tab_char, tab_width);
|
||||
}
|
||||
|
||||
let allow_interaction = ui.is_enabled()
|
||||
&& !ui.input(|i| {
|
||||
i.pointer.button_down(egui::PointerButton::Secondary)
|
||||
|| i.pointer.button_down(egui::PointerButton::Middle)
|
||||
});
|
||||
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
||||
.frame(false)
|
||||
.code_editor()
|
||||
.desired_width(desired_width)
|
||||
.desired_rows(0)
|
||||
.lock_focus(!show_find)
|
||||
.cursor_at_end(false)
|
||||
.layouter(&mut layouter)
|
||||
.interactive(allow_interaction)
|
||||
.id(text_edit_id);
|
||||
|
||||
let output = if word_wrap {
|
||||
draw_highlights(ui);
|
||||
text_edit.show(ui)
|
||||
} else {
|
||||
egui::ScrollArea::horizontal()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::Vec2::new(estimated_width, ui.available_height()),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| {
|
||||
draw_highlights(ui);
|
||||
let output = text_edit.show(ui);
|
||||
ensure_cursor_visible(ui, &output, &font_id, previous_cursor_position);
|
||||
output
|
||||
},
|
||||
)
|
||||
})
|
||||
.inner
|
||||
.inner
|
||||
};
|
||||
|
||||
ensure_cursor_visible(ui, &output, &font_id, previous_cursor_position);
|
||||
|
||||
handle_post_render_updates(ui, app, &output, indent_result, tab_result);
|
||||
|
||||
output.response.context_menu(|ui| {
|
||||
let text_len = app.get_active_tab().unwrap().content.chars().count();
|
||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||
|
||||
if ui.button("Cut").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestCut);
|
||||
ui.close_kind(egui::UiKind::Menu);
|
||||
}
|
||||
if ui.button("Copy").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestCopy);
|
||||
ui.close_kind(egui::UiKind::Menu);
|
||||
}
|
||||
if ui.button("Paste").clicked() {
|
||||
ui.ctx()
|
||||
.send_viewport_cmd(egui::ViewportCommand::RequestPaste);
|
||||
ui.close_kind(egui::UiKind::Menu);
|
||||
}
|
||||
if ui.button("Delete").clicked() {
|
||||
ui.ctx().input_mut(|i| {
|
||||
i.events.push(egui::Event::Key {
|
||||
key: egui::Key::Delete,
|
||||
physical_key: None,
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
})
|
||||
});
|
||||
ui.close_kind(egui::UiKind::Menu);
|
||||
}
|
||||
if ui.button("Select All").clicked() {
|
||||
let text_edit_id = output.response.id;
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let select_all_range = egui::text::CCursorRange::two(
|
||||
egui::text::CCursor::new(0),
|
||||
egui::text::CCursor::new(text_len),
|
||||
);
|
||||
state.cursor.set_char_range(Some(select_all_range));
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
}
|
||||
ui.close_kind(egui::UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Reset Zoom").clicked() {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(reset_zoom_key, true);
|
||||
});
|
||||
ui.close_kind(egui::UiKind::Menu);
|
||||
}
|
||||
});
|
||||
|
||||
if !output.response.has_focus()
|
||||
&& should_have_focus(
|
||||
show_find,
|
||||
show_preferences,
|
||||
show_about,
|
||||
show_shortcuts,
|
||||
is_renaming,
|
||||
show_terminal,
|
||||
)
|
||||
// Don't steal focus during file tree renaming or when terminal is showing
|
||||
{
|
||||
app.focus_manager
|
||||
.request_focus(FocusTarget::Editor, priorities::NORMAL);
|
||||
}
|
||||
|
||||
output.response
|
||||
}
|
||||
|
||||
fn should_have_focus(
|
||||
show_find: bool,
|
||||
show_preferences: bool,
|
||||
show_about: bool,
|
||||
show_shortcuts: bool,
|
||||
is_renaming: bool,
|
||||
show_terminal: bool,
|
||||
) -> bool {
|
||||
!show_find
|
||||
&& !show_preferences
|
||||
&& !show_about
|
||||
&& !show_shortcuts
|
||||
&& !is_renaming
|
||||
&& !show_terminal
|
||||
}
|
||||
|
||||
fn handle_zoom_reset(ui: &mut egui::Ui, app: &mut TextEditor) {
|
||||
let reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||
let should_reset_zoom = ui
|
||||
.ctx()
|
||||
.memory_mut(|mem| mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false));
|
||||
|
||||
if should_reset_zoom {
|
||||
app.zoom_factor = 1.0;
|
||||
ui.ctx().set_zoom_factor(1.0);
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(reset_zoom_key, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_cursor_visible(
|
||||
ui: &mut egui::Ui,
|
||||
output: &egui::text_edit::TextEditOutput,
|
||||
font_id: &egui::FontId,
|
||||
previous_cursor_position: Option<usize>,
|
||||
) {
|
||||
let current_cursor_pos = output
|
||||
.state
|
||||
.cursor
|
||||
.char_range()
|
||||
.map(|range| range.primary.index);
|
||||
|
||||
if let Some(cursor_pos) = current_cursor_pos {
|
||||
let cursor_moved = Some(cursor_pos) != previous_cursor_position;
|
||||
let text_changed = output.response.changed();
|
||||
|
||||
// Check if there's an active text selection
|
||||
let has_selection = output
|
||||
.state
|
||||
.cursor
|
||||
.char_range()
|
||||
.map(|range| range.primary.index != range.secondary.index)
|
||||
.unwrap_or(false);
|
||||
|
||||
if cursor_moved || text_changed {
|
||||
let visible_area = ui.clip_rect();
|
||||
|
||||
if has_selection && output.response.dragged() {
|
||||
if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
|
||||
if !visible_area.contains(mouse_pos) {
|
||||
let line_height = ui.fonts_mut(|fonts| fonts.row_height(font_id));
|
||||
let margin = egui::vec2(20.0, line_height); // Smaller margin for mouse-following
|
||||
let target_rect =
|
||||
egui::Rect::from_center_size(mouse_pos, egui::vec2(1.0, 1.0))
|
||||
.expand2(margin);
|
||||
ui.scroll_to_rect(target_rect, Some(egui::Align::Center));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let cursor_rect = output
|
||||
.galley
|
||||
.pos_from_cursor(egui::text::CCursor::new(cursor_pos));
|
||||
let global_cursor_rect = cursor_rect.translate(output.response.rect.min.to_vec2());
|
||||
let line_height = ui.fonts_mut(|fonts| fonts.row_height(font_id));
|
||||
let margin = egui::vec2(40.0, line_height * 2.0);
|
||||
let target_rect = global_cursor_rect.expand2(margin);
|
||||
if !visible_area.contains_rect(target_rect) {
|
||||
ui.scroll_to_rect(target_rect, Some(egui::Align::Center));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_post_render_updates(
|
||||
ui: &mut egui::Ui,
|
||||
app: &mut TextEditor,
|
||||
output: &egui::text_edit::TextEditOutput,
|
||||
indent_result: bool,
|
||||
tab_result: bool,
|
||||
) {
|
||||
let content_changed = output.response.changed() || indent_result || tab_result;
|
||||
let current_cursor_pos = output
|
||||
.state
|
||||
.cursor
|
||||
.char_range()
|
||||
.map(|range| range.primary.index);
|
||||
|
||||
if content_changed {
|
||||
handle_content_change(app, current_cursor_pos, ui);
|
||||
}
|
||||
|
||||
if app.font_settings_changed || app.text_needs_processing {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let content = active_tab.content.to_owned();
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
}
|
||||
app.font_settings_changed = false;
|
||||
app.text_needs_processing = false;
|
||||
}
|
||||
|
||||
if let Some(cursor_pos) = current_cursor_pos {
|
||||
app.previous_cursor_position = Some(cursor_pos);
|
||||
app.current_cursor_index = cursor_pos;
|
||||
update_cursor_line_info(app, cursor_pos);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_content_change(
|
||||
app: &mut TextEditor,
|
||||
current_cursor_pos: Option<usize>,
|
||||
ui: &mut egui::Ui,
|
||||
) {
|
||||
let Some(active_tab) = app.get_active_tab_mut() else {
|
||||
return;
|
||||
};
|
||||
active_tab.update_modified_state();
|
||||
let content = active_tab.content.to_owned();
|
||||
|
||||
if let Err(e) = app.save_state_cache() {
|
||||
eprintln!("Failed to save state cache: {e}");
|
||||
}
|
||||
|
||||
if app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
}
|
||||
|
||||
let previous_content = app.previous_content.to_owned();
|
||||
let previous_cursor_pos = app.previous_cursor_char_index;
|
||||
|
||||
if !previous_content.is_empty()
|
||||
&& let (Some(prev_cursor_pos), Some(curr_cursor_pos)) =
|
||||
(previous_cursor_pos, current_cursor_pos)
|
||||
{
|
||||
app.process_incremental_change(
|
||||
&previous_content,
|
||||
&content,
|
||||
prev_cursor_pos,
|
||||
curr_cursor_pos,
|
||||
ui,
|
||||
);
|
||||
} else {
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
}
|
||||
|
||||
app.previous_content = content;
|
||||
app.previous_cursor_char_index = current_cursor_pos;
|
||||
}
|
||||
|
||||
fn update_cursor_line_info(app: &mut TextEditor, cursor_pos: usize) {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
let char_count = content.chars().count();
|
||||
let safe_char_pos = cursor_pos.min(char_count);
|
||||
|
||||
// Convert character index to byte index
|
||||
let byte_pos = content
|
||||
.char_indices()
|
||||
.nth(safe_char_pos)
|
||||
.map(|(byte_idx, _)| byte_idx)
|
||||
.unwrap_or(content.len());
|
||||
|
||||
// Count newlines before cursor for line number
|
||||
let line_number = content[..byte_pos].chars().filter(|&c| c == '\n').count() + 1;
|
||||
app.current_cursor_line = line_number;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_editor_highlights(
|
||||
ui: &mut egui::Ui,
|
||||
find_data: &Option<(String, Vec<(usize, usize)>, Option<usize>)>,
|
||||
font_id: &egui::FontId,
|
||||
font_size: f32,
|
||||
wrap_width: f32,
|
||||
) {
|
||||
if let Some((content, matches, current_match_index)) = find_data {
|
||||
let temp_galley = ui.fonts_mut(|fonts| {
|
||||
if let Some((content, matches, current_match_index)) = &find_data {
|
||||
let temp_galley = ui.fonts(|fonts| {
|
||||
fonts.layout(
|
||||
content.to_owned(),
|
||||
font_id.to_owned(),
|
||||
ui.visuals().text_color(),
|
||||
wrap_width - 8.0,
|
||||
desired_width,
|
||||
)
|
||||
});
|
||||
|
||||
let cursor_pos = ui.cursor().min;
|
||||
let text_area_left = cursor_pos.x + 4.0; // Text Editor default margins
|
||||
let text_area_top = cursor_pos.y + 2.0;
|
||||
let text_area_left = editor_rect.left() + 4.0; // Text Editor default margins
|
||||
let text_area_top = editor_rect.top() + 2.0;
|
||||
|
||||
find_highlight::draw_find_highlights(
|
||||
ui,
|
||||
@ -415,157 +79,159 @@ fn draw_editor_highlights(
|
||||
font_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_layouter(
|
||||
ui: &egui::Ui,
|
||||
string: &dyn egui::TextBuffer,
|
||||
wrap_width: f32,
|
||||
syntax_highlighting_enabled: bool,
|
||||
language: &str,
|
||||
font_id: &egui::FontId,
|
||||
) -> egui::text::LayoutJob {
|
||||
let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref());
|
||||
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
|
||||
// let syntect_theme =
|
||||
// crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size);
|
||||
let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(ui.style());
|
||||
let text = string.as_str();
|
||||
|
||||
let mut layout_job = if syntax_highlighting_enabled && language != "txt" {
|
||||
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, language)
|
||||
// let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default();
|
||||
// settings.ts = syntect_theme;
|
||||
// syntax_highlighting::highlight_with(ui.ctx(), &ui.style().clone(), &theme, text, &language, &settings)
|
||||
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, &language)
|
||||
} else {
|
||||
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "")
|
||||
};
|
||||
|
||||
if syntax_highlighting_enabled && language != "txt" {
|
||||
for section in &mut layout_job.sections {
|
||||
section.format.font_id = font_id.to_owned();
|
||||
section.format.font_id = font_id.clone();
|
||||
}
|
||||
}
|
||||
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
layout_job
|
||||
}
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
fn handle_auto_indent(
|
||||
ui: &mut egui::Ui,
|
||||
active_tab: &mut crate::app::tab::Tab,
|
||||
text_edit_id: egui::Id,
|
||||
auto_indent: bool,
|
||||
) -> bool {
|
||||
if !auto_indent || !ui.input(|i| i.key_pressed(egui::Key::Enter) && i.modifiers.is_none()) {
|
||||
return false;
|
||||
}
|
||||
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
||||
.frame(false)
|
||||
.code_editor()
|
||||
.desired_width(desired_width)
|
||||
.desired_rows(0)
|
||||
.lock_focus(!show_find)
|
||||
.cursor_at_end(false)
|
||||
.layouter(&mut layouter)
|
||||
.id(egui::Id::new("main_text_editor"));
|
||||
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
if let Some(cursor_range) = state.cursor.char_range() {
|
||||
let cursor_pos = cursor_range.primary.index;
|
||||
let content = &active_tab.content;
|
||||
|
||||
// Find previous line's indentation
|
||||
let byte_idx = content
|
||||
.char_indices()
|
||||
.nth(cursor_pos)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(content.len());
|
||||
let before_cursor = &content[..byte_idx];
|
||||
let line_start = before_cursor.rfind('\n').map(|i| i + 1).unwrap_or(0);
|
||||
let current_line = &before_cursor[line_start..];
|
||||
let indentation: String = current_line
|
||||
.chars()
|
||||
.take_while(|&c| c == ' ' || c == '\t')
|
||||
.collect();
|
||||
|
||||
// Replace selection (or just insert at cursor) with newline + indentation
|
||||
let start_char = cursor_range.primary.index.min(cursor_range.secondary.index);
|
||||
let end_char = cursor_range.primary.index.max(cursor_range.secondary.index);
|
||||
|
||||
let start_byte = content
|
||||
.char_indices()
|
||||
.nth(start_char)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(content.len());
|
||||
let end_byte = content
|
||||
.char_indices()
|
||||
.nth(end_char)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(content.len());
|
||||
|
||||
let insert_text = format!("\n{}", indentation);
|
||||
active_tab
|
||||
.content
|
||||
.replace_range(start_byte..end_byte, &insert_text);
|
||||
|
||||
// Update cursor position
|
||||
let new_pos = start_char + insert_text.chars().count();
|
||||
state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(
|
||||
egui::text::CCursor::new(new_pos),
|
||||
)));
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
|
||||
// Consume the event so TextEdit doesn't process it
|
||||
ui.input_mut(|i| {
|
||||
i.events.retain(|e| {
|
||||
!matches!(
|
||||
e,
|
||||
egui::Event::Key {
|
||||
key: egui::Key::Enter,
|
||||
pressed: true,
|
||||
..
|
||||
}
|
||||
let output = if word_wrap {
|
||||
text_edit.show(ui)
|
||||
} else {
|
||||
egui::ScrollArea::horizontal()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::Vec2::new(estimated_width, ui.available_height()),
|
||||
egui::Layout::left_to_right(egui::Align::TOP),
|
||||
|ui| text_edit.show(ui),
|
||||
)
|
||||
})
|
||||
});
|
||||
.inner
|
||||
.inner
|
||||
};
|
||||
|
||||
return true;
|
||||
let content_changed = output.response.changed();
|
||||
let content_for_processing = if content_changed {
|
||||
active_tab.update_modified_state();
|
||||
Some(active_tab.content.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if content_changed {
|
||||
if let Err(e) = app.save_state_cache() {
|
||||
eprintln!("Failed to save state cache: {e}");
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn handle_tab_insertion(
|
||||
ui: &mut egui::Ui,
|
||||
active_tab: &mut crate::app::tab::Tab,
|
||||
text_edit_id: egui::Id,
|
||||
tab_char: bool,
|
||||
tab_width: usize,
|
||||
) -> bool {
|
||||
if tab_char || !ui.input(|i| i.key_pressed(egui::Key::Tab) && i.modifiers.is_none()) {
|
||||
return false;
|
||||
if content_changed && app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
}
|
||||
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
if let Some(cursor_range) = state.cursor.char_range() {
|
||||
let cursor_pos = cursor_range.primary.index;
|
||||
let content = &active_tab.content;
|
||||
let byte_idx = content
|
||||
.char_indices()
|
||||
.nth(cursor_pos)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(content.len());
|
||||
let insert_text = " ".repeat(tab_width);
|
||||
active_tab
|
||||
.content
|
||||
.replace_range(byte_idx..byte_idx, &insert_text);
|
||||
let new_pos = cursor_pos + insert_text.chars().count();
|
||||
state
|
||||
let current_cursor_pos = output
|
||||
.state
|
||||
.cursor
|
||||
.set_char_range(Some(egui::text::CCursorRange::one(
|
||||
egui::text::CCursor::new(new_pos),
|
||||
)));
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
ui.input_mut(|i| {
|
||||
i.events.retain(|e| {
|
||||
!matches!(
|
||||
e,
|
||||
egui::Event::Key {
|
||||
key: egui::Key::Tab,
|
||||
pressed: true,
|
||||
..
|
||||
.char_range()
|
||||
.map(|range| range.primary.index);
|
||||
|
||||
if let Some(content) = content_for_processing {
|
||||
let previous_content = app.previous_content.to_owned();
|
||||
let previous_cursor_pos = app.previous_cursor_char_index;
|
||||
|
||||
if !previous_content.is_empty() {
|
||||
if let (Some(prev_cursor_pos), Some(curr_cursor_pos)) =
|
||||
(previous_cursor_pos, current_cursor_pos)
|
||||
{
|
||||
app.process_incremental_change(
|
||||
&previous_content,
|
||||
&content,
|
||||
prev_cursor_pos,
|
||||
curr_cursor_pos,
|
||||
ui,
|
||||
);
|
||||
}
|
||||
)
|
||||
})
|
||||
});
|
||||
} else {
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
}
|
||||
|
||||
app.previous_content = content.to_owned();
|
||||
app.previous_cursor_char_index = current_cursor_pos;
|
||||
}
|
||||
|
||||
if app.font_settings_changed || app.text_needs_processing {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let content = active_tab.content.to_owned();
|
||||
app.process_text_for_rendering(&content, ui);
|
||||
}
|
||||
app.font_settings_changed = false;
|
||||
app.text_needs_processing = false;
|
||||
}
|
||||
|
||||
if let Some(cursor_pos) = current_cursor_pos {
|
||||
let cursor_moved = Some(cursor_pos) != app.previous_cursor_position;
|
||||
let text_changed = output.response.changed();
|
||||
|
||||
if cursor_moved || text_changed {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let content = &active_tab.content;
|
||||
let cursor_line = content
|
||||
.char_indices()
|
||||
.take_while(|(byte_pos, _)| *byte_pos < cursor_pos)
|
||||
.filter(|(_, ch)| *ch == '\n')
|
||||
.count();
|
||||
|
||||
let font_id = ui
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&egui::TextStyle::Monospace)
|
||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||
.to_owned();
|
||||
let line_height = ui.fonts(|fonts| fonts.row_height(&font_id));
|
||||
|
||||
let y_pos = output.response.rect.top() + (cursor_line as f32 * line_height);
|
||||
let cursor_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(output.response.rect.left(), y_pos),
|
||||
egui::vec2(2.0, line_height),
|
||||
);
|
||||
|
||||
let visible_area = ui.clip_rect();
|
||||
if !visible_area.intersects(cursor_rect) {
|
||||
ui.scroll_to_rect(cursor_rect, Some(egui::Align::Center));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
app.previous_cursor_position = Some(cursor_pos);
|
||||
}
|
||||
|
||||
if !output.response.has_focus()
|
||||
&& !show_preferences
|
||||
&& !show_about
|
||||
&& !show_shortcuts
|
||||
&& !show_find
|
||||
{
|
||||
output.response.request_focus();
|
||||
}
|
||||
|
||||
output.response
|
||||
}
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
use crate::util::safe_slice_to_pos;
|
||||
use eframe::egui;
|
||||
|
||||
/// Safely get a string slice up to a byte position, ensuring UTF-8 boundaries
|
||||
fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
||||
let pos = pos.min(content.len());
|
||||
let mut boundary_pos = pos;
|
||||
while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) {
|
||||
boundary_pos -= 1;
|
||||
}
|
||||
&content[..boundary_pos]
|
||||
}
|
||||
|
||||
pub(super) fn draw_find_highlights(
|
||||
ui: &mut egui::Ui,
|
||||
content: &str,
|
||||
@ -9,18 +18,26 @@ pub(super) fn draw_find_highlights(
|
||||
galley: &std::sync::Arc<egui::Galley>,
|
||||
text_area_left: f32,
|
||||
text_area_top: f32,
|
||||
_font_size: f32,
|
||||
font_size: f32,
|
||||
) {
|
||||
for (match_index, &(start_byte, end_byte)) in matches.iter().enumerate() {
|
||||
let font_id = ui
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&egui::TextStyle::Monospace)
|
||||
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||
.to_owned();
|
||||
|
||||
for (match_index, &(start_pos, end_pos)) in matches.iter().enumerate() {
|
||||
let is_current_match = current_match_index == Some(match_index);
|
||||
draw_single_highlight(
|
||||
ui,
|
||||
content,
|
||||
start_byte,
|
||||
end_byte,
|
||||
galley,
|
||||
start_pos,
|
||||
end_pos,
|
||||
text_area_left,
|
||||
text_area_top,
|
||||
galley,
|
||||
&font_id,
|
||||
is_current_match,
|
||||
);
|
||||
}
|
||||
@ -29,15 +46,70 @@ pub(super) fn draw_find_highlights(
|
||||
fn draw_single_highlight(
|
||||
ui: &mut egui::Ui,
|
||||
content: &str,
|
||||
start_byte: usize,
|
||||
end_byte: usize,
|
||||
galley: &std::sync::Arc<egui::Galley>,
|
||||
start_pos: usize,
|
||||
end_pos: usize,
|
||||
text_area_left: f32,
|
||||
text_area_top: f32,
|
||||
galley: &std::sync::Arc<egui::Galley>,
|
||||
font_id: &egui::FontId,
|
||||
is_current_match: bool,
|
||||
) {
|
||||
let start_char = safe_slice_to_pos(content, start_byte).chars().count();
|
||||
let end_char = safe_slice_to_pos(content, end_byte).chars().count();
|
||||
let text_up_to_start = safe_slice_to_pos(content, start_pos);
|
||||
let start_line = text_up_to_start.chars().filter(|&c| c == '\n').count();
|
||||
|
||||
if start_line >= galley.rows.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let line_start_byte_pos = text_up_to_start.rfind('\n').map(|pos| pos + 1).unwrap_or(0);
|
||||
let line_start_char_pos = safe_slice_to_pos(content, line_start_byte_pos)
|
||||
.chars()
|
||||
.count();
|
||||
let start_char_pos = safe_slice_to_pos(content, start_pos).chars().count();
|
||||
let start_col = start_char_pos - line_start_char_pos;
|
||||
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
if start_line >= lines.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let line_text = lines[start_line];
|
||||
let text_before_match: String = line_text.chars().take(start_col).collect();
|
||||
|
||||
let text_before_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
text_before_match,
|
||||
font_id.to_owned(),
|
||||
egui::Color32::WHITE,
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
|
||||
let galley_row = &galley.rows[start_line];
|
||||
let start_y = text_area_top + galley_row.min_y();
|
||||
let line_height = galley_row.height();
|
||||
let start_x = text_area_left + text_before_width;
|
||||
|
||||
let match_text = &content[start_pos..end_pos.min(content.len())];
|
||||
let match_width = ui.fonts(|fonts| {
|
||||
fonts
|
||||
.layout(
|
||||
match_text.to_string(),
|
||||
font_id.to_owned(),
|
||||
ui.visuals().text_color(),
|
||||
f32::INFINITY,
|
||||
)
|
||||
.size()
|
||||
.x
|
||||
});
|
||||
|
||||
let highlight_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(start_x, start_y),
|
||||
egui::vec2(match_width, line_height),
|
||||
);
|
||||
|
||||
let highlight_color = if is_current_match {
|
||||
ui.visuals().selection.bg_fill
|
||||
@ -46,36 +118,5 @@ fn draw_single_highlight(
|
||||
};
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
let mut current_char_idx = 0;
|
||||
|
||||
for row in &galley.rows {
|
||||
let row_start_char = current_char_idx;
|
||||
let row_end_char = row_start_char + row.char_count_excluding_newline();
|
||||
|
||||
current_char_idx += row.char_count_including_newline();
|
||||
|
||||
if row_end_char <= start_char || row_start_char >= end_char {
|
||||
continue;
|
||||
}
|
||||
|
||||
let highlight_start_char_in_row = start_char.max(row_start_char) - row_start_char;
|
||||
let highlight_end_char_in_row = end_char.min(row_end_char) - row_start_char;
|
||||
|
||||
let start_x = row.x_offset(highlight_start_char_in_row);
|
||||
let end_x = row.x_offset(highlight_end_char_in_row);
|
||||
|
||||
let rect = egui::Rect::from_min_max(
|
||||
egui::pos2(
|
||||
text_area_left + row.rect().min.x + start_x,
|
||||
text_area_top + row.rect().min.y,
|
||||
),
|
||||
egui::pos2(
|
||||
text_area_left + row.rect().min.x + end_x,
|
||||
text_area_top + row.rect().max.y,
|
||||
),
|
||||
);
|
||||
|
||||
painter.rect_filled(rect, 0.0, highlight_color);
|
||||
}
|
||||
painter.rect_filled(highlight_rect, 0.0, highlight_color);
|
||||
}
|
||||
|
||||
@ -2,10 +2,8 @@ use eframe::egui;
|
||||
|
||||
fn format_line_number(line_number: usize, line_side: bool, line_count_width: usize) -> String {
|
||||
if line_side {
|
||||
// Right side: left-align with trailing space for scrollbar clearance
|
||||
format!("{:<width$} ", line_number, width = line_count_width)
|
||||
format!("{:<width$}", line_number, width = line_count_width)
|
||||
} else {
|
||||
// Left side: right-align, no trailing space (separator provides gap)
|
||||
format!("{:>width$}", line_number, width = line_count_width)
|
||||
}
|
||||
}
|
||||
@ -24,12 +22,12 @@ pub(super) fn calculate_visual_line_mapping(
|
||||
continue;
|
||||
}
|
||||
|
||||
let galley = ui.fonts_mut(|fonts| {
|
||||
let galley = ui.fonts(|fonts| {
|
||||
fonts.layout(
|
||||
line.to_string(),
|
||||
font_id.to_owned(),
|
||||
egui::Color32::WHITE,
|
||||
available_width,
|
||||
available_width - font_id.size,
|
||||
)
|
||||
});
|
||||
|
||||
@ -57,23 +55,12 @@ pub(super) fn render_line_numbers(
|
||||
word_wrap: bool,
|
||||
line_side: bool,
|
||||
font_size: f32,
|
||||
monospace: bool,
|
||||
) {
|
||||
ui.vertical(|ui| {
|
||||
ui.disable();
|
||||
ui.set_width(line_number_width);
|
||||
|
||||
let spacing_adjustment = if monospace {
|
||||
0.0
|
||||
} else {
|
||||
match font_size {
|
||||
10.0 | 16.0 | 22.0 | 23.0 | 28.0 | 29.0 | 30.0 => -1.0,
|
||||
_ => 0.0,
|
||||
}
|
||||
};
|
||||
|
||||
ui.spacing_mut().item_spacing.y = spacing_adjustment;
|
||||
ui.add_space(if monospace { 1.0 } else { 2.0 }); // Text Editor default top margin
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
ui.add_space(2.0); // Text Editor default top margin
|
||||
let text_color = ui.visuals().weak_text_color();
|
||||
let bg_color = ui.visuals().extreme_bg_color;
|
||||
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
use crate::app::TextEditor;
|
||||
use eframe::egui;
|
||||
use egui_commonmark::CommonMarkViewer;
|
||||
|
||||
pub(super) fn markdown_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) {
|
||||
let content = if let Some(active_tab) = app.get_active_tab() {
|
||||
active_tab.content.clone()
|
||||
} else {
|
||||
ui.label("No file open");
|
||||
return;
|
||||
};
|
||||
|
||||
CommonMarkViewer::new().show(ui, &mut app.markdown_cache, &content);
|
||||
}
|
||||
@ -24,5 +24,3 @@ pub const DEFAULT_FONT_SIZE_STR: &str = "14";
|
||||
pub const PREVIEW_AREA_MAX_HEIGHT: f32 = 150.0;
|
||||
|
||||
pub const INNER_MARGIN: i8 = 8;
|
||||
|
||||
pub const SCROLLBAR_WIDTH: f32 = 25.0;
|
||||
|
||||
@ -1,932 +0,0 @@
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::focus_manager::{FocusTarget, priorities};
|
||||
use egui::Ui;
|
||||
use std::fs;
|
||||
use std::io::BufRead;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct FileTreeState {
|
||||
expanded_paths: std::collections::HashSet<PathBuf>,
|
||||
selected_path: Option<PathBuf>,
|
||||
renaming_path: Option<PathBuf>,
|
||||
rename_text: String,
|
||||
clipboard_paths: Vec<PathBuf>,
|
||||
clipboard_operation: Option<ClipboardOperation>,
|
||||
rename_focus_requested: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ClipboardOperation {
|
||||
Cut,
|
||||
Copy,
|
||||
}
|
||||
|
||||
impl FileTreeState {
|
||||
pub fn is_expanded(&self, path: &Path) -> bool {
|
||||
self.expanded_paths.contains(path)
|
||||
}
|
||||
|
||||
pub fn toggle_expand(&mut self, path: &Path) {
|
||||
if self.expanded_paths.contains(path) {
|
||||
self.expanded_paths.remove(path);
|
||||
} else {
|
||||
self.expanded_paths.insert(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_selected(&self, path: &Path) -> bool {
|
||||
self.selected_path.as_ref().map_or(false, |p| p == path)
|
||||
}
|
||||
|
||||
pub fn set_selected(&mut self, path: Option<PathBuf>) {
|
||||
self.selected_path = path;
|
||||
}
|
||||
|
||||
pub fn is_renaming(&self, path: &Path) -> bool {
|
||||
self.renaming_path.as_ref().map_or(false, |p| p == path)
|
||||
}
|
||||
|
||||
pub fn start_rename(&mut self, path: &Path, initial_name: &str) {
|
||||
self.renaming_path = Some(path.to_path_buf());
|
||||
self.rename_text = initial_name.to_string();
|
||||
self.rename_focus_requested = false;
|
||||
}
|
||||
|
||||
pub fn cancel_rename(&mut self) {
|
||||
self.renaming_path = None;
|
||||
self.rename_text.clear();
|
||||
self.rename_focus_requested = false;
|
||||
}
|
||||
|
||||
pub fn finish_rename(&mut self) -> Option<(PathBuf, String)> {
|
||||
if let Some(old_path) = self.renaming_path.take() {
|
||||
let new_name = self.rename_text.clone();
|
||||
self.rename_text.clear();
|
||||
self.rename_focus_requested = false;
|
||||
Some((old_path, new_name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_clipboard(&mut self, paths: Vec<PathBuf>, operation: ClipboardOperation) {
|
||||
self.clipboard_paths = paths;
|
||||
self.clipboard_operation = Some(operation);
|
||||
}
|
||||
|
||||
pub fn clear_clipboard(&mut self) {
|
||||
self.clipboard_paths.clear();
|
||||
self.clipboard_operation = None;
|
||||
}
|
||||
|
||||
pub fn get_clipboard(&self) -> Option<(&Vec<PathBuf>, &ClipboardOperation)> {
|
||||
self.clipboard_operation
|
||||
.as_ref()
|
||||
.map(|op| (&self.clipboard_paths, op))
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_tree_lines(ui: &mut Ui, depth: usize, is_last: bool, extend_to_icon: bool) {
|
||||
if depth == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let line_height = ui.text_style_height(&egui::TextStyle::Body);
|
||||
let char_width = 8.0;
|
||||
let base_width = (depth as f32 * 3.0 * char_width).ceil();
|
||||
|
||||
let (rect, _) = if extend_to_icon {
|
||||
ui.allocate_at_least(
|
||||
egui::vec2(base_width + 16.0, line_height),
|
||||
egui::Sense::hover(),
|
||||
)
|
||||
} else {
|
||||
ui.allocate_at_least(egui::vec2(base_width, line_height), egui::Sense::hover())
|
||||
};
|
||||
|
||||
let painter = ui.painter();
|
||||
let stroke = egui::Stroke::new(1.0, egui::Color32::from_gray(128));
|
||||
|
||||
let mut x_offset = rect.left();
|
||||
|
||||
for i in 0..depth {
|
||||
if i == depth - 1 {
|
||||
let line_x = x_offset + char_width * 1.5;
|
||||
|
||||
if is_last {
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(line_x, rect.top() - line_height * 0.5),
|
||||
egui::pos2(line_x, rect.bottom() - line_height * 0.5),
|
||||
],
|
||||
stroke,
|
||||
);
|
||||
let end_x = if extend_to_icon {
|
||||
rect.right() + char_width
|
||||
} else {
|
||||
x_offset + char_width * 4.0
|
||||
};
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(line_x, rect.bottom() - line_height * 0.5),
|
||||
egui::pos2(end_x, rect.bottom() - line_height * 0.5),
|
||||
],
|
||||
stroke,
|
||||
);
|
||||
} else {
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(line_x, rect.top() - line_height * 0.5),
|
||||
egui::pos2(line_x, rect.bottom() + line_height * 0.5),
|
||||
],
|
||||
stroke,
|
||||
);
|
||||
let end_x = if extend_to_icon {
|
||||
rect.right() + char_width
|
||||
} else {
|
||||
x_offset + char_width * 4.0
|
||||
};
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(line_x, rect.bottom() - line_height * 0.5),
|
||||
egui::pos2(end_x, rect.bottom() - line_height * 0.5),
|
||||
],
|
||||
stroke,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let line_x = x_offset + char_width * 1.5;
|
||||
painter.line_segment(
|
||||
[
|
||||
egui::pos2(line_x, rect.top() - line_height * 0.5),
|
||||
egui::pos2(line_x, rect.bottom() + line_height * 0.5),
|
||||
],
|
||||
stroke,
|
||||
);
|
||||
}
|
||||
x_offset += char_width * 3.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_text_file(path: &Path) -> bool {
|
||||
let language = crate::ui::central_panel::languages::get_language_from_extension(Some(path));
|
||||
|
||||
if language != "txt" {
|
||||
true
|
||||
} else if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
|
||||
matches!(
|
||||
extension.to_lowercase().as_str(),
|
||||
"txt" | "gitignore" | "conf" | "cfg" | "ini" | "log" | "csv" | "tsv"
|
||||
)
|
||||
} else {
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
metadata.len() < 1024 * 1024
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show_entry(path: &Path, app: &TextEditor, gitignore_patterns: &[String]) -> bool {
|
||||
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
|
||||
// Check if it's a hidden file
|
||||
if !app.show_hidden_files && file_name.starts_with('.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it matches gitignore patterns
|
||||
if app.follow_git && !gitignore_patterns.is_empty() {
|
||||
let relative_path = path
|
||||
.strip_prefix(&app.file_tree_root.as_ref().unwrap_or(&PathBuf::from("/")))
|
||||
.unwrap_or(path);
|
||||
|
||||
for pattern in gitignore_patterns {
|
||||
if matches_gitignore_pattern(relative_path, pattern) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn load_gitignore_patterns(root_path: &Path) -> Vec<String> {
|
||||
let gitignore_path = root_path.join(".gitignore");
|
||||
if !gitignore_path.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let file = match fs::File::open(&gitignore_path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let reader = std::io::BufReader::new(file);
|
||||
reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|line| {
|
||||
let trimmed = line.trim();
|
||||
!trimmed.is_empty() && !trimmed.starts_with('#')
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn matches_gitignore_pattern(path: &Path, pattern: &str) -> bool {
|
||||
let path_str = path.to_string_lossy();
|
||||
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
|
||||
// Remove leading slashes from pattern
|
||||
let pattern = pattern.trim_start_matches('/');
|
||||
|
||||
// Handle directory patterns (ending with /)
|
||||
if pattern.ends_with('/') {
|
||||
let dir_pattern = &pattern[..pattern.len() - 1];
|
||||
return path.is_dir() && matches_glob_pattern(&path_str, dir_pattern);
|
||||
}
|
||||
|
||||
// Handle wildcard patterns
|
||||
if pattern.contains('*') || pattern.contains('?') || pattern.contains('[') {
|
||||
return matches_glob_pattern(&path_str, pattern)
|
||||
|| matches_glob_pattern(file_name, pattern);
|
||||
}
|
||||
|
||||
// Exact match
|
||||
path_str == pattern || file_name == pattern
|
||||
}
|
||||
|
||||
fn matches_glob_pattern(text: &str, pattern: &str) -> bool {
|
||||
// Split pattern by * to handle simple cases
|
||||
let parts: Vec<&str> = pattern.split('*').collect();
|
||||
|
||||
if parts.len() == 1 {
|
||||
// No wildcards, exact match
|
||||
return text == pattern;
|
||||
}
|
||||
|
||||
if pattern == "*" {
|
||||
// Match anything
|
||||
return true;
|
||||
}
|
||||
|
||||
if pattern.starts_with('*') && pattern.ends_with('*') && parts.len() == 3 && parts[1].is_empty()
|
||||
{
|
||||
// Pattern like "*text*" - contains match
|
||||
return text.contains(parts[0]);
|
||||
}
|
||||
|
||||
if pattern.starts_with('*') && !pattern.ends_with('*') {
|
||||
// Pattern like "*suffix" - ends with match
|
||||
return text.ends_with(&pattern[1..]);
|
||||
}
|
||||
|
||||
if !pattern.starts_with('*') && pattern.ends_with('*') {
|
||||
// Pattern like "prefix*" - starts with match
|
||||
return text.starts_with(&pattern[..pattern.len() - 1]);
|
||||
}
|
||||
|
||||
// More complex patterns - use a simple state machine
|
||||
match_complex_glob(text, pattern)
|
||||
}
|
||||
|
||||
fn match_complex_glob(text: &str, pattern: &str) -> bool {
|
||||
let text_bytes = text.as_bytes();
|
||||
let pattern_bytes = pattern.as_bytes();
|
||||
|
||||
let mut text_pos = 0;
|
||||
let mut pattern_pos = 0;
|
||||
let mut star_pos = None;
|
||||
|
||||
while text_pos < text_bytes.len() {
|
||||
if pattern_pos < pattern_bytes.len()
|
||||
&& (pattern_bytes[pattern_pos] == text_bytes[text_pos]
|
||||
|| pattern_bytes[pattern_pos] == b'?')
|
||||
{
|
||||
text_pos += 1;
|
||||
pattern_pos += 1;
|
||||
} else if pattern_pos < pattern_bytes.len() && pattern_bytes[pattern_pos] == b'*' {
|
||||
star_pos = Some((text_pos, pattern_pos));
|
||||
pattern_pos += 1;
|
||||
} else if let Some((saved_text_pos, saved_pattern_pos)) = star_pos {
|
||||
// Try to advance text position and retry
|
||||
star_pos = Some((saved_text_pos + 1, saved_pattern_pos));
|
||||
text_pos = saved_text_pos + 1;
|
||||
pattern_pos = saved_pattern_pos + 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip remaining wildcards
|
||||
while pattern_pos < pattern_bytes.len() && pattern_bytes[pattern_pos] == b'*' {
|
||||
pattern_pos += 1;
|
||||
}
|
||||
|
||||
pattern_pos == pattern_bytes.len()
|
||||
}
|
||||
|
||||
fn show_directory_context_menu(
|
||||
ui: &mut Ui,
|
||||
path: &Path,
|
||||
app: &mut TextEditor,
|
||||
ctx: &egui::Context,
|
||||
) {
|
||||
ui.menu_button("New", |ui| {
|
||||
if ui.button("File").clicked() {
|
||||
create_new_file_at_path(path, app, ctx);
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Directory").clicked() {
|
||||
create_new_directory_at_path(path, app, ctx);
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Cut").clicked() {
|
||||
app.file_tree_state
|
||||
.set_clipboard(vec![path.to_path_buf()], ClipboardOperation::Cut);
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui.button("Copy").clicked() {
|
||||
app.file_tree_state
|
||||
.set_clipboard(vec![path.to_path_buf()], ClipboardOperation::Copy);
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui.button("Paste").clicked() {
|
||||
if let Some((paths, operation)) = app.file_tree_state.get_clipboard() {
|
||||
paste_items(paths.clone(), path, *operation, app, ctx);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Delete").clicked() {
|
||||
delete_path(path, app, ctx);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_file_context_menu(ui: &mut Ui, path: &Path, app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.menu_button("New", |ui| {
|
||||
if ui.button("File").clicked() {
|
||||
if let Some(parent) = path.parent() {
|
||||
create_new_file_at_path(parent, app, ctx);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Directory").clicked() {
|
||||
if let Some(parent) = path.parent() {
|
||||
create_new_directory_at_path(parent, app, ctx);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Cut").clicked() {
|
||||
app.file_tree_state
|
||||
.set_clipboard(vec![path.to_path_buf()], ClipboardOperation::Cut);
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui.button("Copy").clicked() {
|
||||
app.file_tree_state
|
||||
.set_clipboard(vec![path.to_path_buf()], ClipboardOperation::Copy);
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
if ui.button("Paste").clicked() {
|
||||
if let Some((paths, operation)) = app.file_tree_state.get_clipboard() {
|
||||
paste_items(paths.clone(), parent, *operation, app, ctx);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Delete").clicked() {
|
||||
delete_path(path, app, ctx);
|
||||
ui.close();
|
||||
}
|
||||
|
||||
if ui.button("Rename").clicked() {
|
||||
app.file_tree_state.start_rename(
|
||||
path,
|
||||
path.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.as_ref(),
|
||||
);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn find_tab_index_by_path(path: &Path, app: &TextEditor) -> Option<usize> {
|
||||
app.tabs.iter().position(|tab| {
|
||||
tab.file_path.as_ref().map_or(false, |tab_path| {
|
||||
if tab_path == path {
|
||||
return true;
|
||||
}
|
||||
match (tab_path.canonicalize(), path.canonicalize()) {
|
||||
(Ok(canonical_tab), Ok(canonical_path)) => canonical_tab == canonical_path,
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn create_new_file_at_path(parent_path: &Path, app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let mut counter = 1;
|
||||
let mut new_file_path = parent_path.join(format!("new_file_{}.txt", counter));
|
||||
|
||||
while new_file_path.exists() {
|
||||
counter += 1;
|
||||
new_file_path = parent_path.join(format!("new_file_{}.txt", counter));
|
||||
}
|
||||
|
||||
match fs::File::create(&new_file_path) {
|
||||
Ok(_) => {
|
||||
app.state_cache = true;
|
||||
// Start rename mode for the newly created file
|
||||
app.file_tree_state.start_rename(&new_file_path, "");
|
||||
ctx.request_repaint();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create new file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_new_directory_at_path(parent_path: &Path, app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let mut counter = 1;
|
||||
let mut new_dir_path = parent_path.join(format!("new_directory_{}", counter));
|
||||
|
||||
while new_dir_path.exists() {
|
||||
counter += 1;
|
||||
new_dir_path = parent_path.join(format!("new_directory_{}", counter));
|
||||
}
|
||||
|
||||
match fs::create_dir(&new_dir_path) {
|
||||
Ok(_) => {
|
||||
app.state_cache = true;
|
||||
// Start rename mode for the newly created directory
|
||||
app.file_tree_state.start_rename(&new_dir_path, "");
|
||||
ctx.request_repaint();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create new directory: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_items(
|
||||
source_paths: Vec<PathBuf>,
|
||||
destination_path: &Path,
|
||||
operation: ClipboardOperation,
|
||||
app: &mut TextEditor,
|
||||
ctx: &egui::Context,
|
||||
) {
|
||||
for source_path in source_paths {
|
||||
let file_name = source_path.file_name().unwrap_or_default();
|
||||
let mut target_path = destination_path.join(file_name);
|
||||
let mut counter = 1;
|
||||
|
||||
// Handle name conflicts
|
||||
while target_path.exists() {
|
||||
let stem = source_path
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
let extension = source_path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
|
||||
let new_name = if extension.is_empty() {
|
||||
format!("{}_copy_{}", stem, counter)
|
||||
} else {
|
||||
format!("{}_copy_{}.{}", stem, counter, extension)
|
||||
};
|
||||
|
||||
target_path = destination_path.join(new_name);
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
let result = match operation {
|
||||
ClipboardOperation::Copy => {
|
||||
if source_path.is_file() {
|
||||
fs::copy(&source_path, &target_path).map(|_| ())
|
||||
} else {
|
||||
copy_directory_recursive(&source_path, &target_path)
|
||||
}
|
||||
}
|
||||
ClipboardOperation::Cut => fs::rename(&source_path, &target_path),
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!(
|
||||
"Failed to {} {} to {}: {}",
|
||||
match operation {
|
||||
ClipboardOperation::Copy => "copy",
|
||||
ClipboardOperation::Cut => "move",
|
||||
},
|
||||
source_path.display(),
|
||||
target_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app.file_tree_state.clear_clipboard();
|
||||
app.state_cache = true;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
fn copy_directory_recursive(source: &Path, destination: &Path) -> Result<(), std::io::Error> {
|
||||
fs::create_dir(destination)?;
|
||||
|
||||
for entry in fs::read_dir(source)? {
|
||||
let entry = entry?;
|
||||
let entry_path = entry.path();
|
||||
let dest_path = destination.join(entry.file_name());
|
||||
|
||||
if entry_path.is_dir() {
|
||||
copy_directory_recursive(&entry_path, &dest_path)?;
|
||||
} else {
|
||||
fs::copy(&entry_path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_path(path: &Path, app: &mut TextEditor, ctx: &egui::Context) {
|
||||
// Close any open tabs for this file
|
||||
if let Some(tab_index) = find_tab_index_by_path(path, app) {
|
||||
app.tabs.remove(tab_index);
|
||||
if app.active_tab_index >= app.tabs.len() && app.active_tab_index > 0 {
|
||||
app.active_tab_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
let result = if path.is_file() {
|
||||
fs::remove_file(path)
|
||||
} else {
|
||||
fs::remove_dir_all(path)
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("Failed to delete {}: {}", path.display(), e);
|
||||
} else {
|
||||
app.state_cache = true;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_file_opened(path: &Path, app: &TextEditor) -> bool {
|
||||
find_tab_index_by_path(path, app).is_some()
|
||||
}
|
||||
|
||||
fn display_directory(
|
||||
ui: &mut Ui,
|
||||
path: &Path,
|
||||
depth: usize,
|
||||
is_last: bool,
|
||||
app: &mut TextEditor,
|
||||
ctx: &egui::Context,
|
||||
) -> Option<PathBuf> {
|
||||
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
|
||||
let mut clicked_path = None;
|
||||
|
||||
let is_expanded = app.file_tree_state.is_expanded(path);
|
||||
let is_renaming = app.file_tree_state.is_renaming(path);
|
||||
let has_opened_files = app.tabs.iter().any(|tab| {
|
||||
tab.file_path.as_ref().map_or(false, |tab_path| {
|
||||
match (tab_path.canonicalize(), path.canonicalize()) {
|
||||
(Ok(canonical_tab), Ok(canonical_dir)) => {
|
||||
canonical_tab.starts_with(&canonical_dir) && canonical_tab != canonical_dir
|
||||
}
|
||||
_ => tab_path.starts_with(path) && tab_path != path,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
draw_tree_lines(ui, depth, is_last, false);
|
||||
if is_renaming {
|
||||
let text_edit = egui::TextEdit::singleline(&mut app.file_tree_state.rename_text)
|
||||
.desired_width(100.0)
|
||||
.hint_text(dir_name);
|
||||
|
||||
let response = ui.add(text_edit);
|
||||
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
if let Some((old_path, new_name)) = app.file_tree_state.finish_rename() {
|
||||
if let Err(e) = crate::io::rename_file(app, &old_path, &new_name) {
|
||||
eprintln!("Failed to rename directory: {}", e);
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
} else if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
app.file_tree_state.cancel_rename();
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
// Request focus for the rename text edit if we haven't already
|
||||
if !app.file_tree_state.rename_focus_requested {
|
||||
app.focus_manager
|
||||
.request_focus(FocusTarget::FileTreeRename, priorities::HIGH);
|
||||
app.file_tree_state.rename_focus_requested = true;
|
||||
}
|
||||
} else {
|
||||
let text_color = if has_opened_files {
|
||||
ui.visuals().warn_fg_color
|
||||
//egui::Color32::from_rgb(100, 200, 255) // Light blue for directories with opened files
|
||||
} else {
|
||||
ui.visuals().text_color()
|
||||
};
|
||||
|
||||
let icon = if is_expanded { "📂" } else { "📁" };
|
||||
let display_text = format!("{} {}", icon, dir_name);
|
||||
|
||||
let response = ui.selectable_label(
|
||||
false,
|
||||
egui::RichText::new(display_text).strong().color(text_color),
|
||||
);
|
||||
|
||||
if response.clicked() {
|
||||
app.file_tree_state.toggle_expand(path);
|
||||
}
|
||||
|
||||
response.context_menu(|ui| {
|
||||
show_directory_context_menu(ui, path, app, ctx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if is_expanded {
|
||||
if let Ok(entries) = fs::read_dir(path) {
|
||||
// Load gitignore patterns if follow_git is enabled
|
||||
let gitignore_patterns = if app.follow_git && path.join(".git").exists() {
|
||||
load_gitignore_patterns(path)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut entries: Vec<_> = entries
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| should_show_entry(&entry.path(), app, &gitignore_patterns))
|
||||
.collect();
|
||||
|
||||
entries.sort_by(|a, b| {
|
||||
let a_is_dir = a.path().is_dir();
|
||||
let b_is_dir = b.path().is_dir();
|
||||
if a_is_dir == b_is_dir {
|
||||
a.file_name().cmp(&b.file_name())
|
||||
} else if a_is_dir {
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
std::cmp::Ordering::Greater
|
||||
}
|
||||
});
|
||||
|
||||
let total_entries = entries.len();
|
||||
for (index, entry) in entries.iter().enumerate() {
|
||||
let entry_path = entry.path();
|
||||
let is_last_entry = index == total_entries - 1;
|
||||
|
||||
if entry_path.is_dir() {
|
||||
if let Some(clicked) =
|
||||
display_directory(ui, &entry_path, depth + 1, is_last_entry, app, ctx)
|
||||
{
|
||||
clicked_path = Some(clicked);
|
||||
}
|
||||
} else if is_text_file(&entry_path) {
|
||||
if let Some(clicked) =
|
||||
display_file(ui, &entry_path, depth + 1, is_last_entry, app, ctx)
|
||||
{
|
||||
clicked_path = Some(clicked);
|
||||
}
|
||||
} else {
|
||||
display_file(ui, &entry_path, depth + 1, is_last_entry, app, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clicked_path
|
||||
}
|
||||
|
||||
fn display_file(
|
||||
ui: &mut Ui,
|
||||
path: &Path,
|
||||
depth: usize,
|
||||
is_last: bool,
|
||||
app: &mut TextEditor,
|
||||
ctx: &egui::Context,
|
||||
) -> Option<PathBuf> {
|
||||
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
|
||||
let is_selected = app.file_tree_state.is_selected(path);
|
||||
let is_opened = is_file_opened(path, app);
|
||||
let is_renaming = app.file_tree_state.is_renaming(path);
|
||||
|
||||
let mut clicked_path = None;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
draw_tree_lines(ui, depth, is_last, true);
|
||||
|
||||
if is_renaming {
|
||||
let text_edit = egui::TextEdit::singleline(&mut app.file_tree_state.rename_text)
|
||||
.desired_width(100.0)
|
||||
.hint_text(file_name);
|
||||
|
||||
let response = ui.add(text_edit);
|
||||
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
if let Some((old_path, new_name)) = app.file_tree_state.finish_rename() {
|
||||
if let Err(e) = crate::io::rename_file(app, &old_path, &new_name) {
|
||||
eprintln!("Failed to rename file: {}", e);
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
} else if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
app.file_tree_state.cancel_rename();
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
// Request focus for the rename text edit if we haven't already
|
||||
if !app.file_tree_state.rename_focus_requested {
|
||||
app.focus_manager
|
||||
.request_focus(FocusTarget::FileTreeRename, priorities::HIGH);
|
||||
app.file_tree_state.rename_focus_requested = true;
|
||||
}
|
||||
} else {
|
||||
let text_color = if is_selected {
|
||||
ui.visuals().error_fg_color
|
||||
} else if is_opened {
|
||||
ui.visuals().warn_fg_color
|
||||
} else {
|
||||
ui.visuals().text_color()
|
||||
};
|
||||
// let icon = get_nerd_font_icon(path.extension().and_then(|s| s.to_str()).unwrap_or(""), path);
|
||||
let icon = "📄";
|
||||
let display_text = format!("{} {}", icon, file_name);
|
||||
let response =
|
||||
ui.selectable_label(false, egui::RichText::new(display_text).color(text_color));
|
||||
|
||||
if response.clicked() {
|
||||
if let Some(tab_index) = find_tab_index_by_path(path, app) {
|
||||
app.switch_to_tab(tab_index);
|
||||
app.file_tree_state.set_selected(Some(path.to_path_buf()));
|
||||
clicked_path = Some(path.to_path_buf());
|
||||
app.file_tree_state.cancel_rename();
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
if response.double_clicked() {
|
||||
if is_opened {
|
||||
app.file_tree_state.start_rename(path, file_name);
|
||||
} else {
|
||||
if let Err(e) = crate::io::open_file_from_path(app, path.to_path_buf()) {
|
||||
eprintln!("Failed to open file: {}", e);
|
||||
}
|
||||
app.file_tree_state.set_selected(Some(path.to_path_buf()));
|
||||
clicked_path = Some(path.to_path_buf());
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
response.context_menu(|ui| {
|
||||
show_file_context_menu(ui, path, app, ctx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
clicked_path
|
||||
}
|
||||
|
||||
pub(crate) fn file_tree(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let panel = if app.file_tree_side {
|
||||
egui::SidePanel::right("file_tree")
|
||||
} else {
|
||||
egui::SidePanel::left("file_tree")
|
||||
};
|
||||
|
||||
panel
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.show_animated(ctx, app.show_file_tree, |ui| {
|
||||
ui.horizontal_top(|ui| {
|
||||
if ui.button("📁").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new()
|
||||
.set_directory(std::env::current_dir().unwrap_or_default())
|
||||
.pick_folder()
|
||||
{
|
||||
app.file_tree_root = Some(path.clone());
|
||||
app.state_cache = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
let display_dir = app
|
||||
.file_tree_root
|
||||
.clone()
|
||||
.filter(|path| path.exists())
|
||||
.or_else(|| std::env::current_dir().ok().filter(|path| path.exists()))
|
||||
.or_else(|| {
|
||||
std::env::var("HOME")
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
.filter(|path| path.exists())
|
||||
});
|
||||
|
||||
if let Some(dir) = display_dir {
|
||||
display_directory(ui, &dir, 0, true, app, ctx);
|
||||
} else {
|
||||
ui.label("Failed to get current directory or home directory");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_nerd_font_icon(extension: &str, path: &Path) -> &'static str {
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
match filename.to_lowercase().as_str() {
|
||||
"license" | "license.md" | "license.txt" => "", // License file
|
||||
"dockerfile" => "", // Docker
|
||||
"makefile" => "", // Makefile
|
||||
"readme" | "readme.md" => "", // README
|
||||
".gitignore" => "", // Git
|
||||
"cargo.toml" => "", // Rust/Cargo
|
||||
"package.json" => "", // Node/npm
|
||||
"cargo.lock" => "", // Cargo lock
|
||||
_ => match extension.to_lowercase().as_str() {
|
||||
"rs" => "", // Rust
|
||||
"py" => "", // Python
|
||||
"js" => "", // JavaScript
|
||||
"ts" => "", // TypeScript
|
||||
"jsx" | "tsx" => "", // React
|
||||
"html" => "", // HTML
|
||||
"css" => "", // CSS
|
||||
"scss" | "sass" => "", // SASS
|
||||
"json" => "", // JSON
|
||||
"toml" => "", // TOML
|
||||
"yaml" | "yml" => "", // YAML
|
||||
"xml" => "", // XML
|
||||
"c" => "", // C
|
||||
"cpp" | "cxx" | "cc" => "", // C++
|
||||
"h" | "hpp" => "", // Header files
|
||||
"go" => "", // Go
|
||||
"java" => "", // Java
|
||||
"kt" | "kts" => "", // Kotlin
|
||||
"rb" => "", // Ruby
|
||||
"php" => "", // PHP
|
||||
"cs" => "", // C#
|
||||
"swift" => "", // Swift
|
||||
"dart" => "", // Dart
|
||||
"lua" => "", // Lua
|
||||
"sh" | "bash" | "zsh" | "fish" => "", // Shell scripts
|
||||
|
||||
"md" | "markdown" => "", // Markdown
|
||||
"txt" => "", // Text
|
||||
"pdf" => "", // PDF
|
||||
"doc" | "docx" => "", // Word
|
||||
"xls" | "xlsx" => "", // Excel
|
||||
"ppt" | "pptx" => "", // PowerPoint
|
||||
|
||||
"png" | "jpg" | "jpeg" | "gif" | "bmp" => "", // Images
|
||||
"svg" => "", // SVG
|
||||
"mp3" | "wav" | "ogg" | "flac" => "", // Audio
|
||||
"mp4" | "mkv" | "avi" | "mov" | "webm" => "", // Video
|
||||
|
||||
"zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "", // Archives
|
||||
|
||||
"exe" | "msi" => "", // Windows executable
|
||||
"app" | "dmg" => "", // macOS application
|
||||
"db" | "sqlite" | "sqlite3" => "", // Databases
|
||||
"csv" => "", // CSV
|
||||
"ini" | "conf" | "config" => "", // Config files
|
||||
_ => "",
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::constants::*;
|
||||
use crate::ui::focus_manager::{FocusTarget, priorities};
|
||||
use eframe::egui;
|
||||
|
||||
pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
@ -60,7 +59,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let response = ui.add(
|
||||
egui::TextEdit::singleline(&mut app.find_query)
|
||||
.desired_width(250.0)
|
||||
.hint_text("Search..."),
|
||||
.hint_text("Enter search text..."),
|
||||
);
|
||||
|
||||
if response.changed() {
|
||||
@ -68,13 +67,13 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
|
||||
if just_opened || focus_requested || app.focus_find {
|
||||
app.focus_manager.request_focus(FocusTarget::FindInput, priorities::HIGH);
|
||||
response.request_focus();
|
||||
app.focus_find = false;
|
||||
}
|
||||
|
||||
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
app.find_next(ctx);
|
||||
app.focus_manager.request_focus(FocusTarget::FindInput, priorities::HIGH);
|
||||
response.request_focus();
|
||||
}
|
||||
});
|
||||
|
||||
@ -85,7 +84,7 @@ pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let _replace_response = ui.add(
|
||||
egui::TextEdit::singleline(&mut app.replace_query)
|
||||
.desired_width(250.0)
|
||||
.hint_text("Replace..."),
|
||||
.hint_text("Enter replacement text..."),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
use eframe::egui;
|
||||
|
||||
/// Represents the different focusable components in the application
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FocusTarget {
|
||||
/// Main text editor
|
||||
Editor,
|
||||
/// Find window input field
|
||||
FindInput,
|
||||
/// File tree rename input
|
||||
FileTreeRename,
|
||||
/// Font size input in preferences
|
||||
FontSizeInput,
|
||||
/// Tab width input in preferences
|
||||
TabWidthInput,
|
||||
}
|
||||
|
||||
/// Centralized focus management system to prevent focus conflicts between components
|
||||
pub struct FocusManager {
|
||||
/// The currently requested focus target
|
||||
current_target: Option<FocusTarget>,
|
||||
/// Priority of the current focus request (higher = more important)
|
||||
current_priority: i32,
|
||||
/// Whether focus should be forced (ignore other requests)
|
||||
force_focus: bool,
|
||||
}
|
||||
|
||||
impl Default for FocusManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_target: None,
|
||||
current_priority: 0,
|
||||
force_focus: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Request focus for a specific target with a given priority
|
||||
/// Higher priority requests will override lower priority ones
|
||||
pub fn request_focus(&mut self, target: FocusTarget, priority: i32) {
|
||||
if priority >= self.current_priority || self.force_focus {
|
||||
self.current_target = Some(target);
|
||||
self.current_priority = priority;
|
||||
self.force_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Force focus to a target, ignoring priority (use sparingly)
|
||||
pub fn force_focus(&mut self, target: FocusTarget) {
|
||||
self.current_target = Some(target);
|
||||
self.force_focus = true;
|
||||
}
|
||||
|
||||
/// Clear the current focus request
|
||||
pub fn clear_focus(&mut self) {
|
||||
self.current_target = None;
|
||||
self.current_priority = 0;
|
||||
self.force_focus = false;
|
||||
}
|
||||
|
||||
/// Get the current focus target
|
||||
pub fn get_current_target(&self) -> Option<FocusTarget> {
|
||||
self.current_target
|
||||
}
|
||||
|
||||
/// Check if a specific target currently has focus
|
||||
pub fn has_focus(&self, target: FocusTarget) -> bool {
|
||||
self.current_target == Some(target)
|
||||
}
|
||||
|
||||
/// Apply the current focus request to the UI context
|
||||
pub fn apply_focus(&mut self, ctx: &egui::Context) {
|
||||
if let Some(target) = self.current_target {
|
||||
let id = match target {
|
||||
FocusTarget::Editor => egui::Id::new("main_text_editor"),
|
||||
FocusTarget::FindInput => egui::Id::new("find_input"),
|
||||
FocusTarget::FileTreeRename => egui::Id::new("file_tree_rename"),
|
||||
FocusTarget::FontSizeInput => egui::Id::new("font_size_input"),
|
||||
FocusTarget::TabWidthInput => egui::Id::new("tab_width_input"),
|
||||
};
|
||||
|
||||
ctx.memory_mut(|mem| {
|
||||
mem.request_focus(id);
|
||||
});
|
||||
|
||||
// Clear the request after applying it
|
||||
self.clear_focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset focus state at the beginning of each frame
|
||||
pub fn reset(&mut self) {
|
||||
self.current_target = None;
|
||||
self.current_priority = 0;
|
||||
self.force_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Priority levels for focus requests (higher = more important)
|
||||
pub mod priorities {
|
||||
pub const LOW: i32 = 10;
|
||||
pub const NORMAL: i32 = 50;
|
||||
pub const HIGH: i32 = 100;
|
||||
pub const CRITICAL: i32 = 200;
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
use crate::app::TextEditor;
|
||||
use crate::app::actions::ShortcutAction;
|
||||
use crate::{app::TextEditor, io};
|
||||
use eframe::egui::{self, Frame};
|
||||
use egui::UiKind;
|
||||
|
||||
@ -49,25 +48,25 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.menu_button("File", |ui| {
|
||||
app.menu_interaction_active = true;
|
||||
if ui.button("New").clicked() {
|
||||
app.perform_action(ShortcutAction::NewFile);
|
||||
io::new_file(app);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Open...").clicked() {
|
||||
app.perform_action(ShortcutAction::OpenFile);
|
||||
io::open_file(app);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Save").clicked() {
|
||||
app.perform_action(ShortcutAction::SaveFile);
|
||||
io::save_file(app);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Save As...").clicked() {
|
||||
app.perform_action(ShortcutAction::SaveAsFile);
|
||||
io::save_as_file(app);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Preferences").clicked() {
|
||||
app.perform_action(ShortcutAction::Preferences);
|
||||
app.show_preferences = true;
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Exit").clicked() {
|
||||
@ -104,19 +103,12 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Select All").clicked() {
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id =
|
||||
egui::Id::new("main_text_editor").with(&id_source);
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) =
|
||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||
{
|
||||
if let Some(active_tab) = app.get_active_tab() {
|
||||
let text_len = active_tab.content.chars().count();
|
||||
let text_len = active_tab.content.len();
|
||||
let select_all_range = egui::text::CCursorRange::two(
|
||||
egui::text::CCursor::new(0),
|
||||
egui::text::CCursor::new(text_len),
|
||||
@ -125,44 +117,11 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
// Check if undo is available
|
||||
let can_undo = if let Some(active_tab) = app.get_active_tab() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||
{
|
||||
let current_state = (
|
||||
state.cursor.char_range().unwrap_or_default(),
|
||||
active_tab.content.to_string(),
|
||||
);
|
||||
state.undoer().undo(¤t_state).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if ui
|
||||
.add_enabled(can_undo, egui::Button::new("Undo"))
|
||||
.clicked()
|
||||
{
|
||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id =
|
||||
egui::Id::new("main_text_editor").with(&id_source);
|
||||
if ui.button("Undo").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) =
|
||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||
{
|
||||
@ -178,11 +137,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
active_tab.content = content.to_string();
|
||||
state.cursor.set_char_range(Some(*cursor_range));
|
||||
state.set_undoer(undoer);
|
||||
egui::TextEdit::store_state(
|
||||
ui.ctx(),
|
||||
text_edit_id,
|
||||
state,
|
||||
);
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
active_tab.update_modified_state();
|
||||
if app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
@ -190,43 +145,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
// Check if redo is available
|
||||
let can_redo = if let Some(active_tab) = app.get_active_tab() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id = egui::Id::new("main_text_editor").with(&id_source);
|
||||
if let Some(state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||
{
|
||||
let current_state = (
|
||||
state.cursor.char_range().unwrap_or_default(),
|
||||
active_tab.content.to_string(),
|
||||
);
|
||||
state.undoer().redo(¤t_state).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if ui
|
||||
.add_enabled(can_redo, egui::Button::new("Redo"))
|
||||
.clicked()
|
||||
{
|
||||
if let Some(active_tab) = app.get_active_tab_mut() {
|
||||
let id_source = active_tab
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
let text_edit_id =
|
||||
egui::Id::new("main_text_editor").with(&id_source);
|
||||
if ui.button("Redo").clicked() {
|
||||
let text_edit_id = egui::Id::new("main_text_editor");
|
||||
if let Some(mut state) =
|
||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||
{
|
||||
@ -242,11 +164,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
active_tab.content = content.to_string();
|
||||
state.cursor.set_char_range(Some(*cursor_range));
|
||||
state.set_undoer(undoer);
|
||||
egui::TextEdit::store_state(
|
||||
ui.ctx(),
|
||||
text_edit_id,
|
||||
state,
|
||||
);
|
||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||
active_tab.update_modified_state();
|
||||
if app.show_find && !app.find_query.is_empty() {
|
||||
app.update_find_matches();
|
||||
@ -254,23 +172,21 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
|
||||
ui.menu_button("View", |ui| {
|
||||
app.menu_interaction_active = true;
|
||||
|
||||
if ui
|
||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
||||
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut app.show_markdown, "Preview Markdown")
|
||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
@ -280,62 +196,27 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.checkbox(&mut app.show_hidden_files, "Show Hidden Files").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.menu_button("Layout", |ui| {
|
||||
ui.menu_button("Line Numbers", |ui| {
|
||||
if ui.checkbox(&mut app.show_line_numbers, "Show").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.radio_value(&mut app.line_side, false, "Left").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.radio_value(&mut app.line_side, true, "Right").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
ui.menu_button("File Tree", |ui| {
|
||||
if ui.checkbox(&mut app.show_file_tree, "Show").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.radio_value(&mut app.file_tree_side, false, "Left").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.radio_value(&mut app.file_tree_side, true, "Right").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
if ui.checkbox(&mut app.show_tab_bar, "Tab Bar").clicked() {
|
||||
if ui.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut app.show_bottom_bar, "Bottom Bar")
|
||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.checkbox(&mut app.show_terminal, "Terminal").clicked() {
|
||||
app.save_config();
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Reset Zoom").clicked() {
|
||||
app.zoom_factor = 1.0;
|
||||
ctx.set_zoom_factor(1.0);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut app.auto_hide_toolbar, "Hide Toolbar")
|
||||
.clicked()
|
||||
{
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.menu_button("Appearance", |ui| {
|
||||
app.menu_interaction_active = true;
|
||||
@ -377,18 +258,14 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Reset Zoom").clicked() {
|
||||
app.zoom_factor = 1.0;
|
||||
ctx.set_zoom_factor(1.0);
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.button("Focus Mode").clicked() {
|
||||
app.focus_mode = true;
|
||||
if ui.radio_value(&mut app.line_side, false, "Left").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
if ui.radio_value(&mut app.line_side, true, "Right").clicked() {
|
||||
app.save_config();
|
||||
ui.close_kind(UiKind::Menu);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@ -404,7 +281,7 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
});
|
||||
|
||||
if !app.show_tab_bar {
|
||||
if app.hide_tab_bar {
|
||||
let tab_title = if let Some(tab) = app.get_active_tab() {
|
||||
tab.get_display_title()
|
||||
} else {
|
||||
@ -412,10 +289,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
empty_tab.get_display_title()
|
||||
};
|
||||
|
||||
let window_width = ctx.viewport_rect().width();
|
||||
let window_width = ctx.screen_rect().width();
|
||||
let font_id = ui.style().text_styles[&egui::TextStyle::Body].to_owned();
|
||||
|
||||
let text_galley = ui.fonts_mut(|fonts| {
|
||||
let text_galley = ui.fonts(|fonts| {
|
||||
fonts.layout_job(egui::text::LayoutJob::simple_singleline(
|
||||
tab_title,
|
||||
font_id,
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::constants::*;
|
||||
use crate::ui::focus_manager::{FocusTarget, priorities};
|
||||
use eframe::egui;
|
||||
|
||||
pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let visuals = &ctx.style().visuals;
|
||||
let screen_rect = ctx.viewport_rect();
|
||||
let screen_rect = ctx.screen_rect();
|
||||
let window_width =
|
||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
||||
let window_height =
|
||||
@ -30,8 +29,11 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
})
|
||||
.show(ctx, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Editor Settings");
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui|{
|
||||
ui.vertical(|ui| {
|
||||
if ui
|
||||
.checkbox(&mut app.state_cache, "Maintain State")
|
||||
.on_hover_text("Unsaved changes will be cached between sessions")
|
||||
@ -45,19 +47,28 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
if ui
|
||||
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
|
||||
.changed()
|
||||
{
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
if ui
|
||||
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||
.on_hover_text(
|
||||
"Hide the top bar until you move your mouse to the upper edge",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
app.save_config();
|
||||
}
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
if ui.checkbox(&mut app.show_hidden_files, "Show Hidden Files").on_hover_text("Show files and directories starting with '.'").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
if ui.checkbox(&mut app.follow_git, "Git").on_hover_text("Respect .gitignore file").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
});
|
||||
ui.vertical(|ui|{
|
||||
if ui
|
||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
||||
.changed()
|
||||
@ -66,54 +77,10 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
if ui
|
||||
.checkbox(&mut app.auto_indent, "Auto Indent")
|
||||
.on_hover_text("Automatically indent new lines to match the previous line")
|
||||
.changed()
|
||||
{
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
if ui.checkbox(&mut app.show_markdown, "Preview Markdown").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(MEDIUM);
|
||||
ui.separator();
|
||||
ui.add_space(SMALL);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
if ui.checkbox(&mut app.show_line_numbers, "Line Numbers").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
ui.radio_value(&mut app.line_side, false, "Left");
|
||||
ui.add_space(SMALL);
|
||||
ui.radio_value(&mut app.line_side, true, "Right");
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
if ui.checkbox(&mut app.show_file_tree, "File Tree").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
ui.radio_value(&mut app.file_tree_side, false, "Left");
|
||||
ui.add_space(SMALL);
|
||||
ui.radio_value(&mut app.file_tree_side, true, "Right");
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
if ui.checkbox(&mut app.show_tab_bar, "Tab Bar").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
if ui.checkbox(&mut app.show_bottom_bar, "Bottom Bar").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL);
|
||||
if ui
|
||||
.checkbox(&mut app.auto_hide_toolbar, "Hide Toolbar")
|
||||
.on_hover_text("Hide the toolbar until cursor moves to top of the window")
|
||||
.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar")
|
||||
.on_hover_text(
|
||||
"Hide the tab bar and show tab title in menu bar instead",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
app.save_config();
|
||||
@ -121,14 +88,16 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(MEDIUM);
|
||||
ui.separator();
|
||||
ui.add_space(SMALL);
|
||||
ui.separator();
|
||||
ui.add_space(LARGE);
|
||||
ui.heading("Font Settings");
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Font Family:");
|
||||
ui.add_space(MEDIUM);
|
||||
ui.add_space(SMALL);
|
||||
ui.label("Font Size:");
|
||||
});
|
||||
|
||||
@ -180,10 +149,10 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
app.font_size_input = Some(font_size_text.to_owned());
|
||||
|
||||
if response.clicked() {
|
||||
app.focus_manager.request_focus(FocusTarget::FontSizeInput, priorities::NORMAL);
|
||||
response.request_focus();
|
||||
}
|
||||
|
||||
ui.label("pt");
|
||||
ui.label("px");
|
||||
|
||||
if response.lost_focus() {
|
||||
if let Ok(new_size) = font_size_text.parse::<f32>() {
|
||||
@ -201,50 +170,6 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
}
|
||||
})
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Tab Char:").on_hover_text("Use tab character instead of spaces for indentation");
|
||||
ui.add_space(MEDIUM);
|
||||
ui.label("Tab Width:").on_hover_text("Preferred number of spaces for indentation");
|
||||
});
|
||||
|
||||
ui.vertical(|ui| {
|
||||
if ui.checkbox(&mut app.tab_char, "").changed() {
|
||||
app.save_config();
|
||||
}
|
||||
ui.add_space(SMALL * 1.5);
|
||||
if app.tab_width_input.is_none() {
|
||||
app.tab_width_input = Some(app.tab_width.to_string());
|
||||
}
|
||||
let mut tab_width_text = app
|
||||
.tab_width_input
|
||||
.as_ref()
|
||||
.unwrap_or(&"4".to_string())
|
||||
.to_owned();
|
||||
ui.horizontal(|ui| {
|
||||
let response = ui.add(
|
||||
egui::TextEdit::singleline(&mut tab_width_text)
|
||||
.desired_width(FONT_SIZE_INPUT_WIDTH)
|
||||
.hint_text("4").id(egui::Id::new("tab_width_input")),
|
||||
);
|
||||
|
||||
app.tab_width_input = Some(tab_width_text.to_owned());
|
||||
|
||||
if response.clicked() {
|
||||
app.focus_manager.request_focus(FocusTarget::TabWidthInput, priorities::NORMAL);
|
||||
}
|
||||
|
||||
if response.lost_focus() {
|
||||
if let Ok(new_width) = tab_width_text.parse::<usize>() {
|
||||
let clamped_width = new_width.clamp(1, 8);
|
||||
if app.tab_width != clamped_width {
|
||||
app.tab_width = clamped_width;
|
||||
app.apply_font_settings(ctx);
|
||||
}
|
||||
}
|
||||
app.tab_width_input = None;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
@ -1,288 +0,0 @@
|
||||
use crate::app::TextEditor;
|
||||
use crate::ui::constants::*;
|
||||
use eframe::egui::{self, Frame, Id, ScrollArea, TextEdit};
|
||||
use nix::pty::{Winsize, openpty};
|
||||
use nix::unistd::{ForkResult, close, dup2, execvp, fork, setsid};
|
||||
use std::ffi::CString;
|
||||
use std::os::fd::{AsRawFd, OwnedFd};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct ShellState {
|
||||
pub pty: Option<PtyHandle>,
|
||||
pub output_buffer: Arc<Mutex<String>>,
|
||||
pub input_buffer: String,
|
||||
pub scroll_to_bottom: bool,
|
||||
pub input_id: Id,
|
||||
}
|
||||
|
||||
pub struct PtyHandle {
|
||||
pub master: OwnedFd,
|
||||
pub _reader_thread: std::thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Default for ShellState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pty: None,
|
||||
output_buffer: Arc::new(Mutex::new(String::new())),
|
||||
input_buffer: String::new(),
|
||||
scroll_to_bottom: false,
|
||||
input_id: Id::new("terminal_input"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellState {
|
||||
pub fn start_shell(&mut self) {
|
||||
if self.pty.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the user's shell
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
|
||||
|
||||
if let Ok(mut output) = self.output_buffer.lock() {
|
||||
output.push_str(&format!("Starting shell: {}\n", shell));
|
||||
}
|
||||
|
||||
// Open PTY
|
||||
let pty_result = openpty(
|
||||
Some(&Winsize {
|
||||
ws_row: 24,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
let pty = match pty_result {
|
||||
Ok(pty) => {
|
||||
if let Ok(mut output) = self.output_buffer.lock() {
|
||||
output.push_str("PTY created successfully\n");
|
||||
}
|
||||
pty
|
||||
}
|
||||
Err(e) => {
|
||||
if let Ok(mut output) = self.output_buffer.lock() {
|
||||
output.push_str(&format!("Failed to create PTY: {}\n", e));
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Fork to create child process
|
||||
match unsafe { fork() } {
|
||||
Ok(ForkResult::Parent { child }) => {
|
||||
// Parent process
|
||||
if let Ok(mut output) = self.output_buffer.lock() {
|
||||
output.push_str(&format!("Forked child process: {:?}\n", child));
|
||||
}
|
||||
|
||||
// Close the slave side
|
||||
let _ = close(pty.slave.as_raw_fd());
|
||||
|
||||
let master_fd = pty.master.as_raw_fd();
|
||||
|
||||
if let Ok(mut output) = self.output_buffer.lock() {
|
||||
output.push_str(&format!("Master fd: {}\n", master_fd));
|
||||
}
|
||||
|
||||
// Set master to non-blocking mode
|
||||
use nix::fcntl::{FcntlArg, OFlag, fcntl};
|
||||
let _ = fcntl(master_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK));
|
||||
|
||||
let output_buffer = Arc::clone(&self.output_buffer);
|
||||
|
||||
// Spawn reader thread that polls the master fd
|
||||
let reader_thread = std::thread::spawn(move || {
|
||||
let mut buf = [0u8; 8192];
|
||||
|
||||
loop {
|
||||
// Use nix::unistd::read to read from the fd
|
||||
match nix::unistd::read(master_fd, &mut buf) {
|
||||
Ok(0) => {
|
||||
if let Ok(mut output) = output_buffer.lock() {
|
||||
output.push_str("\n[EOF from shell]\n");
|
||||
}
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
let text = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||
if let Ok(mut output) = output_buffer.lock() {
|
||||
output.push_str(&text);
|
||||
if output.len() > 100_000 {
|
||||
output.drain(..50_000);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(nix::errno::Errno::EAGAIN)
|
||||
| Err(nix::errno::Errno::EWOULDBLOCK) => {
|
||||
// No data available, sleep briefly
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
Err(e) => {
|
||||
if let Ok(mut output) = output_buffer.lock() {
|
||||
output.push_str(&format!("\n[Read error: {:?}]\n", e));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.pty = Some(PtyHandle {
|
||||
master: pty.master,
|
||||
_reader_thread: reader_thread,
|
||||
});
|
||||
|
||||
self.scroll_to_bottom = true;
|
||||
}
|
||||
Ok(ForkResult::Child) => {
|
||||
// Child process
|
||||
// Close the master side
|
||||
let _ = close(pty.master.as_raw_fd());
|
||||
|
||||
// Create a new session
|
||||
let _ = setsid();
|
||||
|
||||
let slave_fd = pty.slave.as_raw_fd();
|
||||
|
||||
// Make this PTY the controlling terminal
|
||||
unsafe {
|
||||
libc::ioctl(slave_fd, libc::TIOCSCTTY, 0);
|
||||
}
|
||||
|
||||
// Redirect stdin, stdout, stderr to the slave PTY
|
||||
let _ = dup2(slave_fd, 0); // stdin
|
||||
let _ = dup2(slave_fd, 1); // stdout
|
||||
let _ = dup2(slave_fd, 2); // stderr
|
||||
|
||||
// Close the slave fd since it's been duplicated
|
||||
if slave_fd > 2 {
|
||||
let _ = close(slave_fd);
|
||||
}
|
||||
|
||||
// Set TERM environment variable
|
||||
unsafe {
|
||||
std::env::set_var("TERM", "xterm-256color");
|
||||
}
|
||||
|
||||
// Execute the shell with -i for interactive mode
|
||||
let shell_cstr = CString::new(shell.as_bytes()).unwrap();
|
||||
let arg_i = CString::new("-i").unwrap();
|
||||
let args = [shell_cstr.clone(), arg_i];
|
||||
let _ = execvp(&shell_cstr, &args);
|
||||
|
||||
// If execvp returns, it failed
|
||||
eprintln!("Failed to execute shell");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Ok(mut output) = self.output_buffer.lock() {
|
||||
output.push_str(&format!("Fork failed: {}\n", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_input(&mut self, text: &str) {
|
||||
if let Some(ref pty) = self.pty {
|
||||
let input_bytes = format!("{}\n", text).into_bytes();
|
||||
|
||||
// Use nix::unistd::write to write to the fd
|
||||
use std::os::fd::AsFd;
|
||||
let _ = nix::unistd::write(pty.master.as_fd(), &input_bytes);
|
||||
|
||||
self.scroll_to_bottom = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_output(&mut self) {
|
||||
if let Ok(mut output) = self.output_buffer.lock() {
|
||||
output.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shell_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
// Auto-start shell on first show
|
||||
if app.show_terminal && app.shell_state.pty.is_none() {
|
||||
app.shell_state.start_shell();
|
||||
}
|
||||
|
||||
let frame = Frame::NONE.fill(ctx.style().visuals.panel_fill);
|
||||
|
||||
egui::TopBottomPanel::bottom("shell_bar")
|
||||
.frame(frame)
|
||||
.min_height(200.0)
|
||||
.default_height(300.0)
|
||||
.resizable(true)
|
||||
.show_animated(ctx, app.show_terminal, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(SMALL);
|
||||
// Simplified header
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(SMALL);
|
||||
if ui.button("Clear").clicked() {
|
||||
app.shell_state.clear_output();
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Output area
|
||||
let output_height = ui.available_height() - 35.0;
|
||||
|
||||
ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.stick_to_bottom(app.shell_state.scroll_to_bottom)
|
||||
.max_height(output_height)
|
||||
.show(ui, |ui| {
|
||||
ui.style_mut().override_font_id = Some(egui::FontId::monospace(12.0));
|
||||
|
||||
let output_text = if let Ok(output) = app.shell_state.output_buffer.lock() {
|
||||
if output.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
output.clone()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
ui.label(output_text);
|
||||
});
|
||||
|
||||
if app.shell_state.scroll_to_bottom {
|
||||
app.shell_state.scroll_to_bottom = false;
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Input line
|
||||
ui.horizontal(|ui| {
|
||||
ui.style_mut().override_font_id = Some(egui::FontId::monospace(12.0));
|
||||
|
||||
let response = ui.add(
|
||||
TextEdit::singleline(&mut app.shell_state.input_buffer)
|
||||
.id(app.shell_state.input_id)
|
||||
.desired_width(f32::INFINITY),
|
||||
);
|
||||
|
||||
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
let input = app.shell_state.input_buffer.clone();
|
||||
app.shell_state.input_buffer.clear();
|
||||
app.shell_state.send_input(&input);
|
||||
// Re-focus after command
|
||||
ui.memory_mut(|mem| mem.request_focus(app.shell_state.input_id));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Request repaint when shell is active
|
||||
if app.show_terminal && app.shell_state.pty.is_some() {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
@ -3,163 +3,62 @@ use crate::ui::constants::*;
|
||||
use eframe::egui;
|
||||
|
||||
fn render_shortcuts_content(ui: &mut egui::Ui) {
|
||||
let description_color = ui.visuals().text_color().gamma_multiply(0.8);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
// Navigation section
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new("Navigation")
|
||||
.size(UI_HEADER_SIZE)
|
||||
.strong(),
|
||||
);
|
||||
});
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(MEDIUM);
|
||||
ui.columns(2, |columns| {
|
||||
let shortcuts = [
|
||||
("Ctrl + N", "New"),
|
||||
("Ctrl + O", "Open"),
|
||||
("Ctrl + S", "Save"),
|
||||
("Ctrl + Shift + S", "Save As"),
|
||||
("Ctrl + T", "New Tab"),
|
||||
("Ctrl + W", "Close Tab"),
|
||||
("Ctrl + Tab", "Next Tab"),
|
||||
("Ctrl + Shift + Tab", "Last Tab"),
|
||||
];
|
||||
|
||||
for (i, (shortcut, description)) in shortcuts.iter().enumerate() {
|
||||
let col = i % 2;
|
||||
columns[col].label(
|
||||
egui::RichText::new(*shortcut)
|
||||
.size(UI_TEXT_SIZE)
|
||||
.strong()
|
||||
.monospace(),
|
||||
);
|
||||
columns[col].label(
|
||||
egui::RichText::new(*description)
|
||||
.size(UI_TEXT_SIZE)
|
||||
.color(description_color),
|
||||
);
|
||||
|
||||
// Add space after each complete row (every 2 items)
|
||||
if i % 2 == 1 {
|
||||
columns[0].add_space(MEDIUM);
|
||||
columns[1].add_space(MEDIUM);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.label(egui::RichText::new("Ctrl + N: New").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + O: Open").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + S: Save").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + T: New Tab").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + W: Close Tab").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(UI_TEXT_SIZE));
|
||||
ui.add_space(VLARGE);
|
||||
ui.separator();
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
// Editing section
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong());
|
||||
});
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(MEDIUM);
|
||||
ui.columns(2, |columns| {
|
||||
let shortcuts = [
|
||||
("Ctrl + Z", "Undo"),
|
||||
("Ctrl + Shift + Z", "Redo"),
|
||||
("Ctrl + X", "Cut"),
|
||||
("Ctrl + C", "Copy"),
|
||||
("Ctrl + V", "Paste"),
|
||||
("Ctrl + A", "Select All"),
|
||||
("Ctrl + F", "Find"),
|
||||
("Ctrl + R", "Replace"),
|
||||
];
|
||||
|
||||
for (i, (shortcut, description)) in shortcuts.iter().enumerate() {
|
||||
let col = i % 2;
|
||||
columns[col].label(
|
||||
egui::RichText::new(*shortcut)
|
||||
.size(UI_TEXT_SIZE)
|
||||
.strong()
|
||||
.monospace(),
|
||||
);
|
||||
columns[col].label(
|
||||
egui::RichText::new(*description)
|
||||
.size(UI_TEXT_SIZE)
|
||||
.color(description_color),
|
||||
);
|
||||
|
||||
// Add space after each complete row (every 2 items)
|
||||
if i % 2 == 1 {
|
||||
columns[0].add_space(MEDIUM);
|
||||
columns[1].add_space(MEDIUM);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.label(egui::RichText::new("Ctrl + Z: Undo").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + Shift + Z: Redo").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + X: Cut").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + C: Copy").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + V: Paste").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + A: Select All").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + F: Find").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + R: Replace").size(UI_TEXT_SIZE));
|
||||
|
||||
ui.add_space(VLARGE);
|
||||
ui.separator();
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
// Views section
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong());
|
||||
});
|
||||
ui.add_space(MEDIUM);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(MEDIUM);
|
||||
ui.columns(2, |columns| {
|
||||
let shortcuts = [
|
||||
("Ctrl + H", "Toggle Auto Hide Toolbar"),
|
||||
("Ctrl + B", "Toggle Bottom Bar"),
|
||||
("Ctrl + K", "Toggle Word Wrap"),
|
||||
("Ctrl + M", "Toggle Markdown Preview"),
|
||||
("Ctrl + L", "Toggle Line Numbers"),
|
||||
("Ctrl + Shift + L", "Change Line Number Side"),
|
||||
("Ctrl + E", "Toggle File Tree"),
|
||||
("Ctrl + Shift + E", "Toggle File Tree Side"),
|
||||
("Ctrl + =/-", "Increase/Decrease Font Size"),
|
||||
("Ctrl + Shift + =/-", "Zoom In/Out"),
|
||||
("Ctrl + P", "Preferences"),
|
||||
("Ctrl + Alt + F", "Toggle Focus Mode"),
|
||||
];
|
||||
|
||||
for (i, (shortcut, description)) in shortcuts.iter().enumerate() {
|
||||
let col = i % 2;
|
||||
columns[col].label(
|
||||
egui::RichText::new(*shortcut)
|
||||
.size(UI_TEXT_SIZE)
|
||||
.strong()
|
||||
.monospace(),
|
||||
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(UI_TEXT_SIZE));
|
||||
ui.label(
|
||||
egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(UI_TEXT_SIZE),
|
||||
);
|
||||
columns[col].label(
|
||||
egui::RichText::new(*description)
|
||||
.size(UI_TEXT_SIZE)
|
||||
.color(description_color),
|
||||
);
|
||||
|
||||
// Add space after each complete row (every 2 items)
|
||||
if i % 2 == 1 {
|
||||
columns[0].add_space(MEDIUM);
|
||||
columns[1].add_space(MEDIUM);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + =/-: Increase/Decrease Font Size").size(UI_TEXT_SIZE));
|
||||
ui.label(egui::RichText::new("Ctrl + Shift + =/-: Zoom In/Out").size(UI_TEXT_SIZE));
|
||||
// ui.label(
|
||||
// egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode")
|
||||
// .size(14.0)
|
||||
// );
|
||||
// ui.label(
|
||||
// egui::RichText::new("Ctrl + .: Toggle Vim Mode")
|
||||
// .size(14.0)
|
||||
// );
|
||||
ui.add_space(VLARGE);
|
||||
ui.separator();
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
let visuals = &ctx.style().visuals;
|
||||
let screen_rect = ctx.viewport_rect();
|
||||
let screen_rect = ctx.screen_rect();
|
||||
|
||||
let window_width =
|
||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
||||
@ -197,8 +96,7 @@ pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||
);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
//ui.add_space(MEDIUM);
|
||||
ui.separator();
|
||||
ui.add_space(MEDIUM);
|
||||
let visuals = ui.visuals();
|
||||
let close_button = egui::Button::new("Close")
|
||||
.fill(visuals.widgets.inactive.bg_fill)
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
pub(crate) fn safe_slice_to_pos(content: &str, pos: usize) -> &str {
|
||||
let pos = pos.min(content.len());
|
||||
let mut boundary_pos = pos;
|
||||
while boundary_pos > 0 && !content.is_char_boundary(boundary_pos) {
|
||||
boundary_pos -= 1;
|
||||
}
|
||||
&content[..boundary_pos]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user