Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ba260f685 | |||
| a3cc8b96f0 |
12
Cargo.toml
12
Cargo.toml
@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ced"
|
name = "ced"
|
||||||
version = "0.3.3"
|
version = "0.1.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = "0.33.3"
|
eframe = "0.32"
|
||||||
egui = "0.33.3"
|
egui = "0.32"
|
||||||
egui_extras = { version = "0.33.3", features = ["syntect"] }
|
egui_extras = { version = "0.32", features = ["syntect"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.141"
|
serde_json = "1.0.141"
|
||||||
rfd = "0.15.4"
|
rfd = "0.15.4"
|
||||||
@ -17,7 +17,3 @@ syntect = "5.2.0"
|
|||||||
plist = "1.7.4"
|
plist = "1.7.4"
|
||||||
diffy = "0.4.2"
|
diffy = "0.4.2"
|
||||||
uuid = { version = "1.0", features = ["v4"] }
|
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.).
|
* 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.
|
* 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` + `+`/`-`).
|
* Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`).
|
||||||
* Ricers rejoice, your `pywal` colors will be used!
|
* Ricers rejoice, your `pywal` colors will be used!
|
||||||
* Weirdly smooth typing experience.
|
* Weirdly smooth typing experience.
|
||||||
@ -24,7 +22,7 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel
|
|||||||
##### Ubuntu/Debian
|
##### Ubuntu/Debian
|
||||||
`sudo apt install git rust`
|
`sudo apt install git rust`
|
||||||
|
|
||||||
### Install
|
#### Install
|
||||||
```bash
|
```bash
|
||||||
git clone https://code.lampnet.io/candle/ced
|
git clone https://code.lampnet.io/candle/ced
|
||||||
cd ced && cargo build --release
|
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.
|
`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`.
|
`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
|
word_wrap = false
|
||||||
theme = "System"
|
theme = "System"
|
||||||
line_side = false
|
line_side = false
|
||||||
show_file_tree = true
|
|
||||||
file_tree_side = false
|
|
||||||
font_family = "Monospace"
|
font_family = "Monospace"
|
||||||
font_size = 16.0
|
font_size = 16.0
|
||||||
syntax_highlighting = true
|
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. |
|
| `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. |
|
| `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_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. |
|
| `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. |
|
| `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. |
|
| `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_family` | `"Proportional"` | The font family used for the editor text. |
|
||||||
| `font_size` | `14.0` | `8.0-32.0` The font size for text editing. |
|
| `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 config;
|
||||||
pub mod shortcuts;
|
pub mod shortcuts;
|
||||||
pub mod state;
|
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;
|
use super::theme::Theme;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
#[serde(default = "default_state_cache")]
|
||||||
pub state_cache: bool,
|
pub state_cache: bool,
|
||||||
|
#[serde(default = "default_auto_hide_toolbar")]
|
||||||
pub auto_hide_toolbar: bool,
|
pub auto_hide_toolbar: bool,
|
||||||
pub show_tab_bar: bool,
|
#[serde(default = "default_hide_tab_bar")]
|
||||||
pub show_bottom_bar: bool,
|
pub hide_tab_bar: bool,
|
||||||
pub show_file_tree: bool,
|
#[serde(default = "default_show_line_numbers")]
|
||||||
pub show_line_numbers: bool,
|
pub show_line_numbers: bool,
|
||||||
|
#[serde(default = "default_word_wrap")]
|
||||||
pub word_wrap: bool,
|
pub word_wrap: bool,
|
||||||
|
#[serde(default = "Theme::default")]
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
|
#[serde(default = "default_line_side")]
|
||||||
pub line_side: bool,
|
pub line_side: bool,
|
||||||
pub file_tree_side: bool,
|
#[serde(default = "default_font_family")]
|
||||||
pub show_hidden_files: bool,
|
|
||||||
pub show_terminal: bool,
|
|
||||||
pub follow_git: bool,
|
|
||||||
pub tab_char: bool,
|
|
||||||
pub tab_width: usize,
|
|
||||||
pub font_family: String,
|
pub font_family: String,
|
||||||
|
#[serde(default = "default_font_size")]
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
|
#[serde(default = "default_syntax_highlighting")]
|
||||||
pub syntax_highlighting: bool,
|
pub syntax_highlighting: bool,
|
||||||
pub auto_indent: bool,
|
|
||||||
pub focus_mode: bool,
|
|
||||||
// pub vim_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 {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state_cache: false,
|
state_cache: default_state_cache(),
|
||||||
auto_hide_toolbar: false,
|
auto_hide_toolbar: default_auto_hide_toolbar(),
|
||||||
show_tab_bar: false,
|
hide_tab_bar: default_hide_tab_bar(),
|
||||||
show_bottom_bar: true,
|
show_line_numbers: default_show_line_numbers(),
|
||||||
show_file_tree: false,
|
word_wrap: default_word_wrap(),
|
||||||
show_line_numbers: false,
|
|
||||||
word_wrap: true,
|
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
line_side: false,
|
line_side: default_line_side(),
|
||||||
file_tree_side: false,
|
font_family: default_font_family(),
|
||||||
show_hidden_files: false,
|
font_size: default_font_size(),
|
||||||
show_terminal: false,
|
syntax_highlighting: default_syntax_highlighting(),
|
||||||
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,
|
|
||||||
// vim_mode: false,
|
// vim_mode: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,35 @@
|
|||||||
use crate::app::actions::ShortcutAction;
|
|
||||||
use crate::app::state::TextEditor;
|
use crate::app::state::TextEditor;
|
||||||
|
use crate::io;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum ShortcutAction {
|
||||||
|
NewFile,
|
||||||
|
OpenFile,
|
||||||
|
SaveFile,
|
||||||
|
SaveAsFile,
|
||||||
|
NewTab,
|
||||||
|
CloseTab,
|
||||||
|
ToggleLineNumbers,
|
||||||
|
ToggleLineSide,
|
||||||
|
ToggleWordWrap,
|
||||||
|
ToggleAutoHideToolbar,
|
||||||
|
ToggleFind,
|
||||||
|
FocusFind,
|
||||||
|
NextTab,
|
||||||
|
PrevTab,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
GlobalZoomIn,
|
||||||
|
GlobalZoomOut,
|
||||||
|
ResetZoom,
|
||||||
|
Escape,
|
||||||
|
Preferences,
|
||||||
|
ToggleVimMode,
|
||||||
|
}
|
||||||
|
|
||||||
type ShortcutDefinition = (egui::Modifiers, egui::Key, ShortcutAction);
|
type ShortcutDefinition = (egui::Modifiers, egui::Key, ShortcutAction);
|
||||||
|
|
||||||
fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
||||||
@ -28,11 +56,6 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::W,
|
egui::Key::W,
|
||||||
ShortcutAction::CloseTab,
|
ShortcutAction::CloseTab,
|
||||||
),
|
),
|
||||||
(
|
|
||||||
egui::Modifiers::CTRL | egui::Modifiers::ALT,
|
|
||||||
egui::Key::F,
|
|
||||||
ShortcutAction::ToggleFocusMode,
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||||
egui::Key::F,
|
egui::Key::F,
|
||||||
@ -43,11 +66,6 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::F,
|
egui::Key::F,
|
||||||
ShortcutAction::ToggleFind,
|
ShortcutAction::ToggleFind,
|
||||||
),
|
),
|
||||||
(
|
|
||||||
egui::Modifiers::CTRL,
|
|
||||||
egui::Key::R,
|
|
||||||
ShortcutAction::ToggleReplace,
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||||
egui::Key::L,
|
egui::Key::L,
|
||||||
@ -68,21 +86,6 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::H,
|
egui::Key::H,
|
||||||
ShortcutAction::ToggleAutoHideToolbar,
|
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::Modifiers::CTRL | egui::Modifiers::SHIFT,
|
||||||
egui::Key::Tab,
|
egui::Key::Tab,
|
||||||
@ -143,23 +146,145 @@ fn get_shortcuts() -> Vec<ShortcutDefinition> {
|
|||||||
egui::Key::Escape,
|
egui::Key::Escape,
|
||||||
ShortcutAction::Escape,
|
ShortcutAction::Escape,
|
||||||
),
|
),
|
||||||
(
|
|
||||||
egui::Modifiers::CTRL,
|
|
||||||
egui::Key::M,
|
|
||||||
ShortcutAction::ToggleMarkdown,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_action(action: ShortcutAction, editor: &mut TextEditor) -> bool {
|
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::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) {
|
pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let mut font_zoom_occurred = false;
|
let mut font_zoom_occurred = false;
|
||||||
let mut global_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| {
|
ctx.input_mut(|i| {
|
||||||
for (modifiers, key, action) in get_shortcuts() {
|
for (modifiers, key, action) in get_shortcuts() {
|
||||||
@ -174,12 +299,6 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
execute_action(action, editor);
|
execute_action(action, editor);
|
||||||
global_zoom_occurred = true;
|
global_zoom_occurred = true;
|
||||||
}
|
}
|
||||||
ShortcutAction::PageUp => {
|
|
||||||
page_up_pressed = true;
|
|
||||||
}
|
|
||||||
ShortcutAction::PageDown => {
|
|
||||||
page_down_pressed = true;
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
execute_action(action, editor);
|
execute_action(action, editor);
|
||||||
}
|
}
|
||||||
@ -197,14 +316,6 @@ pub fn handle(editor: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ctx.set_zoom_factor(editor.zoom_factor);
|
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 {
|
if editor.should_select_current_match {
|
||||||
editor.select_current_match(ctx);
|
editor.select_current_match(ctx);
|
||||||
editor.should_select_current_match = false;
|
editor.should_select_current_match = false;
|
||||||
|
|||||||
@ -1,21 +1,15 @@
|
|||||||
use super::editor::TextEditor;
|
use super::editor::TextEditor;
|
||||||
use crate::app::shortcuts;
|
use crate::app::shortcuts;
|
||||||
use crate::ui::about_window::about_window;
|
use crate::ui::about_window::about_window;
|
||||||
use crate::ui::bottom_bar::bottom_bar;
|
|
||||||
use crate::ui::central_panel::central_panel;
|
use crate::ui::central_panel::central_panel;
|
||||||
use crate::ui::file_tree::file_tree;
|
|
||||||
use crate::ui::find_window::find_window;
|
use crate::ui::find_window::find_window;
|
||||||
use crate::ui::menu_bar::menu_bar;
|
use crate::ui::menu_bar::menu_bar;
|
||||||
use crate::ui::preferences_window::preferences_window;
|
use crate::ui::preferences_window::preferences_window;
|
||||||
use crate::ui::shortcuts_window::shortcuts_window;
|
use crate::ui::shortcuts_window::shortcuts_window;
|
||||||
use crate::ui::tab_bar::tab_bar;
|
use crate::ui::tab_bar::tab_bar;
|
||||||
use crate::ui::shell_bar::shell_bar;
|
|
||||||
|
|
||||||
impl eframe::App for TextEditor {
|
impl eframe::App for TextEditor {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
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())
|
if ctx.input(|i| i.viewport().close_requested())
|
||||||
&& !self.force_quit_confirmed
|
&& !self.force_quit_confirmed
|
||||||
&& !self.clean_quit_requested
|
&& !self.clean_quit_requested
|
||||||
@ -28,35 +22,21 @@ impl eframe::App for TextEditor {
|
|||||||
|
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.get_title()));
|
ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.get_title()));
|
||||||
|
|
||||||
if !self.focus_mode {
|
|
||||||
menu_bar(self, ctx);
|
menu_bar(self, ctx);
|
||||||
}
|
|
||||||
|
|
||||||
if self.show_tab_bar && !self.focus_mode {
|
if !self.hide_tab_bar {
|
||||||
tab_bar(self, ctx);
|
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);
|
central_panel(self, ctx);
|
||||||
|
|
||||||
if self.show_about && !self.focus_mode {
|
if self.show_about {
|
||||||
about_window(self, ctx);
|
about_window(self, ctx);
|
||||||
}
|
}
|
||||||
if self.show_shortcuts && !self.focus_mode {
|
if self.show_shortcuts {
|
||||||
shortcuts_window(self, ctx);
|
shortcuts_window(self, ctx);
|
||||||
}
|
}
|
||||||
if self.show_preferences && !self.focus_mode {
|
if self.show_preferences {
|
||||||
preferences_window(self, ctx);
|
preferences_window(self, ctx);
|
||||||
}
|
}
|
||||||
if self.show_find {
|
if self.show_find {
|
||||||
@ -66,9 +46,6 @@ impl eframe::App for TextEditor {
|
|||||||
self.show_unsaved_changes_dialog(ctx);
|
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;
|
self.prev_show_find = self.show_find;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,22 +11,12 @@ impl TextEditor {
|
|||||||
show_line_numbers: config.show_line_numbers,
|
show_line_numbers: config.show_line_numbers,
|
||||||
word_wrap: config.word_wrap,
|
word_wrap: config.word_wrap,
|
||||||
auto_hide_toolbar: config.auto_hide_toolbar,
|
auto_hide_toolbar: config.auto_hide_toolbar,
|
||||||
show_tab_bar: config.show_tab_bar,
|
hide_tab_bar: config.hide_tab_bar,
|
||||||
show_bottom_bar: config.show_bottom_bar,
|
|
||||||
show_file_tree: config.show_file_tree,
|
|
||||||
show_terminal: config.show_terminal,
|
|
||||||
theme: config.theme,
|
theme: config.theme,
|
||||||
line_side: config.line_side,
|
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_family: config.font_family,
|
||||||
font_size: config.font_size,
|
font_size: config.font_size,
|
||||||
syntax_highlighting: config.syntax_highlighting,
|
syntax_highlighting: config.syntax_highlighting,
|
||||||
auto_indent: config.auto_indent,
|
|
||||||
focus_mode: config.focus_mode,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,8 +80,6 @@ impl TextEditor {
|
|||||||
|
|
||||||
editor.apply_font_settings(&cc.egui_ctx);
|
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
|
editor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,23 +88,13 @@ impl TextEditor {
|
|||||||
state_cache: self.state_cache,
|
state_cache: self.state_cache,
|
||||||
auto_hide_toolbar: self.auto_hide_toolbar,
|
auto_hide_toolbar: self.auto_hide_toolbar,
|
||||||
show_line_numbers: self.show_line_numbers,
|
show_line_numbers: self.show_line_numbers,
|
||||||
show_tab_bar: self.show_tab_bar,
|
hide_tab_bar: self.hide_tab_bar,
|
||||||
show_bottom_bar: self.show_bottom_bar,
|
|
||||||
show_file_tree: self.show_file_tree,
|
|
||||||
show_terminal: self.show_terminal,
|
|
||||||
word_wrap: self.word_wrap,
|
word_wrap: self.word_wrap,
|
||||||
theme: self.theme,
|
theme: self.theme,
|
||||||
line_side: self.line_side,
|
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_family: self.font_family.to_string(),
|
||||||
font_size: self.font_size,
|
font_size: self.font_size,
|
||||||
syntax_highlighting: self.syntax_highlighting,
|
syntax_highlighting: self.syntax_highlighting,
|
||||||
auto_indent: self.auto_indent,
|
|
||||||
focus_mode: self.focus_mode,
|
|
||||||
// vim_mode: self.vim_mode,
|
// vim_mode: self.vim_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
use super::editor::TextEditor;
|
use super::editor::TextEditor;
|
||||||
use super::editor::TextProcessingResult;
|
use super::editor::TextProcessingResult;
|
||||||
use crate::app::{tab::Tab, theme::Theme};
|
use crate::app::{tab::Tab, theme::Theme};
|
||||||
use egui_commonmark::CommonMarkCache;
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
impl Default for TextEditor {
|
impl Default for TextEditor {
|
||||||
@ -15,32 +14,19 @@ impl Default for TextEditor {
|
|||||||
show_shortcuts: false,
|
show_shortcuts: false,
|
||||||
show_find: false,
|
show_find: false,
|
||||||
show_preferences: false,
|
show_preferences: false,
|
||||||
show_markdown: false,
|
|
||||||
pending_unsaved_action: None,
|
pending_unsaved_action: None,
|
||||||
force_quit_confirmed: false,
|
force_quit_confirmed: false,
|
||||||
clean_quit_requested: false,
|
clean_quit_requested: false,
|
||||||
show_line_numbers: false,
|
show_line_numbers: false,
|
||||||
word_wrap: true,
|
word_wrap: true,
|
||||||
auto_hide_toolbar: false,
|
auto_hide_toolbar: false,
|
||||||
show_tab_bar: false,
|
hide_tab_bar: true,
|
||||||
show_bottom_bar: true,
|
|
||||||
show_file_tree: false,
|
|
||||||
show_terminal: false,
|
|
||||||
file_tree_root: None,
|
|
||||||
file_tree_state: crate::ui::file_tree::FileTreeState::default(),
|
|
||||||
syntax_highlighting: false,
|
syntax_highlighting: false,
|
||||||
auto_indent: true,
|
|
||||||
theme: Theme::default(),
|
theme: Theme::default(),
|
||||||
line_side: false,
|
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_family: "Proportional".to_string(),
|
||||||
font_size: 14.0,
|
font_size: 14.0,
|
||||||
font_size_input: None,
|
font_size_input: None,
|
||||||
tab_width_input: None,
|
|
||||||
zoom_factor: 1.0,
|
zoom_factor: 1.0,
|
||||||
menu_interaction_active: false,
|
menu_interaction_active: false,
|
||||||
tab_bar_rect: None,
|
tab_bar_rect: None,
|
||||||
@ -59,15 +45,10 @@ impl Default for TextEditor {
|
|||||||
previous_content: String::new(),
|
previous_content: String::new(),
|
||||||
previous_cursor_char_index: None,
|
previous_cursor_char_index: None,
|
||||||
current_cursor_line: 0,
|
current_cursor_line: 0,
|
||||||
current_cursor_index: 0,
|
|
||||||
previous_cursor_line: 0,
|
previous_cursor_line: 0,
|
||||||
font_settings_changed: false,
|
font_settings_changed: false,
|
||||||
text_needs_processing: false,
|
text_needs_processing: false,
|
||||||
should_select_current_match: 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::tab::Tab;
|
||||||
use crate::app::theme::Theme;
|
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 eframe::egui;
|
||||||
use egui_commonmark::CommonMarkCache;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
@ -46,32 +39,19 @@ pub struct TextEditor {
|
|||||||
pub(crate) show_shortcuts: bool,
|
pub(crate) show_shortcuts: bool,
|
||||||
pub(crate) show_find: bool,
|
pub(crate) show_find: bool,
|
||||||
pub(crate) show_preferences: bool,
|
pub(crate) show_preferences: bool,
|
||||||
pub(crate) show_markdown: bool,
|
|
||||||
pub(crate) pending_unsaved_action: Option<UnsavedAction>,
|
pub(crate) pending_unsaved_action: Option<UnsavedAction>,
|
||||||
pub(crate) force_quit_confirmed: bool,
|
pub(crate) force_quit_confirmed: bool,
|
||||||
pub(crate) clean_quit_requested: bool,
|
pub(crate) clean_quit_requested: bool,
|
||||||
pub(crate) show_line_numbers: bool,
|
pub(crate) show_line_numbers: bool,
|
||||||
pub(crate) word_wrap: bool,
|
pub(crate) word_wrap: bool,
|
||||||
pub(crate) auto_hide_toolbar: bool,
|
pub(crate) auto_hide_toolbar: bool,
|
||||||
pub(crate) show_tab_bar: bool,
|
pub(crate) hide_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) syntax_highlighting: bool,
|
pub(crate) syntax_highlighting: bool,
|
||||||
pub(crate) auto_indent: bool,
|
|
||||||
pub(crate) theme: Theme,
|
pub(crate) theme: Theme,
|
||||||
pub(crate) line_side: bool,
|
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_family: String,
|
||||||
pub(crate) font_size: f32,
|
pub(crate) font_size: f32,
|
||||||
pub(crate) font_size_input: Option<String>,
|
pub(crate) font_size_input: Option<String>,
|
||||||
pub(crate) tab_width_input: Option<String>,
|
|
||||||
pub(crate) zoom_factor: f32,
|
pub(crate) zoom_factor: f32,
|
||||||
pub(crate) menu_interaction_active: bool,
|
pub(crate) menu_interaction_active: bool,
|
||||||
pub(crate) tab_bar_rect: Option<egui::Rect>,
|
pub(crate) tab_bar_rect: Option<egui::Rect>,
|
||||||
@ -89,179 +69,9 @@ pub struct TextEditor {
|
|||||||
pub(crate) previous_content: String,
|
pub(crate) previous_content: String,
|
||||||
pub(crate) previous_cursor_char_index: Option<usize>,
|
pub(crate) previous_cursor_char_index: Option<usize>,
|
||||||
pub(crate) current_cursor_line: usize,
|
pub(crate) current_cursor_line: usize,
|
||||||
pub(crate) current_cursor_index: usize,
|
|
||||||
pub(crate) previous_cursor_line: usize,
|
pub(crate) previous_cursor_line: usize,
|
||||||
pub(crate) font_settings_changed: bool,
|
pub(crate) font_settings_changed: bool,
|
||||||
pub(crate) text_needs_processing: bool,
|
pub(crate) text_needs_processing: bool,
|
||||||
pub(crate) should_select_current_match: bool,
|
pub(crate) should_select_current_match: bool,
|
||||||
pub(crate) previous_cursor_position: Option<usize>,
|
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 super::editor::TextEditor;
|
||||||
use crate::util::safe_slice_to_pos;
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
impl TextEditor {
|
impl TextEditor {
|
||||||
@ -115,15 +114,10 @@ impl TextEditor {
|
|||||||
if let Some(active_tab) = self.get_active_tab() {
|
if let Some(active_tab) = self.get_active_tab() {
|
||||||
let content = &active_tab.content;
|
let content = &active_tab.content;
|
||||||
|
|
||||||
let start_char = safe_slice_to_pos(content, start_byte).chars().count();
|
let start_char = Self::safe_slice_to_pos(content, start_byte).chars().count();
|
||||||
let end_char = safe_slice_to_pos(content, end_byte).chars().count();
|
let end_char = Self::safe_slice_to_pos(content, end_byte).chars().count();
|
||||||
|
|
||||||
let id_source = active_tab
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
.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(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||||
let selection_range = egui::text::CCursorRange::two(
|
let selection_range = egui::text::CCursorRange::two(
|
||||||
egui::text::CCursor::new(start_char),
|
egui::text::CCursor::new(start_char),
|
||||||
@ -158,16 +152,12 @@ impl TextEditor {
|
|||||||
self.update_find_matches();
|
self.update_find_matches();
|
||||||
|
|
||||||
if let Some(active_tab) = self.get_active_tab() {
|
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()
|
.chars()
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
let id_source = active_tab
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
.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(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||||
state
|
state
|
||||||
.cursor
|
.cursor
|
||||||
@ -216,14 +206,7 @@ impl TextEditor {
|
|||||||
|
|
||||||
self.current_match_index = None;
|
self.current_match_index = None;
|
||||||
|
|
||||||
if let Some(active_tab) = self.get_active_tab() {
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
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(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
if let Some(mut state) = egui::TextEdit::load_state(ctx, text_edit_id) {
|
||||||
state
|
state
|
||||||
.cursor
|
.cursor
|
||||||
@ -234,4 +217,3 @@ impl TextEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
use super::editor::{TextEditor, TextProcessingResult};
|
use super::editor::{TextEditor, TextProcessingResult};
|
||||||
use crate::util::safe_slice_to_pos;
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
impl TextEditor {
|
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) {
|
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;
|
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 font_id = self.get_font_id();
|
||||||
let longest_line_pixel_width = if longest_line_length > 0 {
|
let longest_line_pixel_width = if longest_line_length > 0 {
|
||||||
let longest_line_text = lines[longest_line_index];
|
let longest_line_text = lines[longest_line_index];
|
||||||
ui.fonts_mut(|fonts| {
|
ui.fonts(|fonts| {
|
||||||
fonts
|
fonts
|
||||||
.layout_no_wrap(
|
.layout(
|
||||||
longest_line_text.to_string(),
|
longest_line_text.to_string(),
|
||||||
font_id,
|
font_id,
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
|
f32::INFINITY,
|
||||||
)
|
)
|
||||||
.size()
|
.size()
|
||||||
.x
|
.x
|
||||||
@ -74,6 +83,15 @@ impl TextEditor {
|
|||||||
new_cursor_pos: usize,
|
new_cursor_pos: usize,
|
||||||
ui: &egui::Ui,
|
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() {
|
if old_content.len() == new_content.len() {
|
||||||
self.handle_character_replacement(
|
self.handle_character_replacement(
|
||||||
old_content,
|
old_content,
|
||||||
@ -103,6 +121,25 @@ impl TextEditor {
|
|||||||
self.previous_cursor_line = self.current_cursor_line;
|
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(
|
fn handle_character_replacement(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -131,43 +168,41 @@ impl TextEditor {
|
|||||||
new_cursor_pos: usize,
|
new_cursor_pos: usize,
|
||||||
ui: &egui::Ui,
|
ui: &egui::Ui,
|
||||||
) {
|
) {
|
||||||
let old_char_count = old_content.chars().count();
|
let min_len = old_content.len().min(new_content.len());
|
||||||
let new_char_count = new_content.chars().count();
|
let mut common_prefix = 0;
|
||||||
|
let mut common_suffix = 0;
|
||||||
let safe_new_cursor = new_cursor_pos.min(new_char_count);
|
for i in 0..min_len {
|
||||||
|
if old_content.as_bytes()[i] == new_content.as_bytes()[i] {
|
||||||
let new_byte_pos = new_content.char_indices()
|
common_prefix += 1;
|
||||||
.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]
|
|
||||||
} else {
|
} 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();
|
let newlines_added = added_text.bytes().filter(|&b| b == b'\n').count();
|
||||||
|
|
||||||
if newlines_added > 0 {
|
if newlines_added > 0 {
|
||||||
let mut current_result = self.get_text_processing_result();
|
let mut current_result = self.get_text_processing_result();
|
||||||
current_result.line_count += newlines_added;
|
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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
let addition_end_line = new_content[..addition_end_byte]
|
let addition_end_line = Self::safe_slice_to_pos(old_content, added_end)
|
||||||
.bytes()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
@ -183,7 +218,7 @@ impl TextEditor {
|
|||||||
self.update_processing_result(current_result);
|
self.update_processing_result(current_result);
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
let current_line_length = current_line.chars().count();
|
||||||
self.update_line_if_longer(
|
self.update_line_if_longer(
|
||||||
self.current_cursor_line,
|
self.current_cursor_line,
|
||||||
@ -233,11 +268,11 @@ impl TextEditor {
|
|||||||
let mut current_result = self.get_text_processing_result();
|
let mut current_result = self.get_text_processing_result();
|
||||||
current_result.line_count = current_result.line_count.saturating_sub(newlines_removed);
|
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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.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()
|
.bytes()
|
||||||
.filter(|&b| b == b'\n')
|
.filter(|&b| b == b'\n')
|
||||||
.count();
|
.count();
|
||||||
@ -296,7 +331,7 @@ impl TextEditor {
|
|||||||
{
|
{
|
||||||
content[line_start_boundary..line_end_boundary].to_string()
|
content[line_start_boundary..line_end_boundary].to_string()
|
||||||
} else {
|
} 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 {
|
if line_length > current_result.longest_line_length {
|
||||||
let font_id = self.get_font_id();
|
let font_id = self.get_font_id();
|
||||||
let pixel_width = ui.fonts_mut(|fonts| {
|
let pixel_width = ui.fonts(|fonts| {
|
||||||
fonts
|
fonts
|
||||||
.layout_no_wrap(
|
.layout(
|
||||||
line_content.to_string(),
|
line_content.to_string(),
|
||||||
font_id,
|
font_id,
|
||||||
egui::Color32::WHITE,
|
egui::Color32::WHITE,
|
||||||
|
f32::INFINITY,
|
||||||
)
|
)
|
||||||
.size()
|
.size()
|
||||||
.x
|
.x
|
||||||
|
|||||||
@ -36,7 +36,7 @@ impl TextEditor {
|
|||||||
self.update_find_matches();
|
self.update_find_matches();
|
||||||
}
|
}
|
||||||
self.text_needs_processing = true;
|
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() {
|
if let Err(e) = self.save_state_cache() {
|
||||||
eprintln!("Failed to save state cache: {e}");
|
eprintln!("Failed to save state cache: {e}");
|
||||||
}
|
}
|
||||||
@ -49,10 +49,7 @@ impl TextEditor {
|
|||||||
if self.show_find && !self.find_query.is_empty() {
|
if self.show_find && !self.find_query.is_empty() {
|
||||||
self.update_find_matches();
|
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.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() {
|
if let Err(e) = self.save_state_cache() {
|
||||||
eprintln!("Failed to save state cache: {e}");
|
eprintln!("Failed to save state cache: {e}");
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use eframe::egui;
|
|||||||
pub struct EditorDimensions {
|
pub struct EditorDimensions {
|
||||||
pub text_width: f32,
|
pub text_width: f32,
|
||||||
pub line_number_width: f32,
|
pub line_number_width: f32,
|
||||||
|
pub total_reserved_width: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextEditor {
|
impl TextEditor {
|
||||||
@ -46,7 +47,6 @@ impl TextEditor {
|
|||||||
egui::TextStyle::Monospace,
|
egui::TextStyle::Monospace,
|
||||||
egui::FontId::new(self.font_size, font_family),
|
egui::FontId::new(self.font_size, font_family),
|
||||||
);
|
);
|
||||||
self.font_size_input = Some(self.font_size.to_string());
|
|
||||||
|
|
||||||
ctx.set_style(style);
|
ctx.set_style(style);
|
||||||
self.font_settings_changed = true;
|
self.font_settings_changed = true;
|
||||||
@ -60,32 +60,39 @@ impl TextEditor {
|
|||||||
return EditorDimensions {
|
return EditorDimensions {
|
||||||
text_width: total_available_width,
|
text_width: total_available_width,
|
||||||
line_number_width: 0.0,
|
line_number_width: 0.0,
|
||||||
|
total_reserved_width: 0.0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let processing_result = self.get_text_processing_result();
|
let processing_result = self.get_text_processing_result();
|
||||||
let line_count = processing_result.line_count;
|
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 line_count_digits = line_count.to_string().len();
|
||||||
let sample_text = "9".repeat(line_count_digits);
|
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
|
fonts
|
||||||
.layout_no_wrap(sample_text, monospace_font_id, egui::Color32::WHITE)
|
.layout(sample_text, font_id, egui::Color32::WHITE, f32::INFINITY)
|
||||||
.size()
|
.size()
|
||||||
.x
|
.x
|
||||||
});
|
});
|
||||||
|
|
||||||
let line_number_width = if self.line_side {
|
let line_number_width = if self.line_side {
|
||||||
base_line_number_width + crate::ui::constants::SCROLLBAR_WIDTH
|
base_line_number_width + 20.0
|
||||||
} else {
|
} else {
|
||||||
base_line_number_width
|
base_line_number_width + 8.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let text_width = total_available_width.max(100.0); // Minimum 100px for text
|
// Separator space (7.0 for separator + 3.0 spacing = 10.0 total)
|
||||||
|
let separator_width = 10.0;
|
||||||
|
|
||||||
|
let total_reserved_width = line_number_width + separator_width;
|
||||||
|
let text_width = (total_available_width - total_reserved_width).max(100.0); // Minimum 100px for text
|
||||||
|
|
||||||
EditorDimensions {
|
EditorDimensions {
|
||||||
text_width,
|
text_width,
|
||||||
line_number_width,
|
line_number_width,
|
||||||
|
total_reserved_width,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,149 +103,10 @@ impl TextEditor {
|
|||||||
return self.calculate_editor_dimensions(ui).text_width;
|
return self.calculate_editor_dimensions(ui).text_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
let longest_line_width = processing_result.longest_line_pixel_width;
|
let longest_line_width =
|
||||||
let font_id = self.get_font_id();
|
processing_result.longest_line_pixel_width + (self.font_size * 2.0);
|
||||||
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 dimensions = self.calculate_editor_dimensions(ui);
|
let dimensions = self.calculate_editor_dimensions(ui);
|
||||||
(longest_line_width + extra_space).max(dimensions.text_width)
|
longest_line_width.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 app;
|
||||||
mod io;
|
mod io;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod util;
|
use app::{config::Config, TextEditor};
|
||||||
use app::{TextEditor, config::Config};
|
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
pub(crate) mod about_window;
|
pub(crate) mod about_window;
|
||||||
pub(crate) mod bottom_bar;
|
|
||||||
pub(crate) mod central_panel;
|
pub(crate) mod central_panel;
|
||||||
pub(crate) mod constants;
|
pub(crate) mod constants;
|
||||||
pub(crate) mod find_window;
|
pub(crate) mod find_window;
|
||||||
pub(crate) mod file_tree;
|
|
||||||
pub(crate) mod focus_manager;
|
|
||||||
pub(crate) mod menu_bar;
|
pub(crate) mod menu_bar;
|
||||||
pub(crate) mod preferences_window;
|
pub(crate) mod preferences_window;
|
||||||
pub(crate) mod shortcuts_window;
|
pub(crate) mod shortcuts_window;
|
||||||
pub(crate) mod tab_bar;
|
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,21 @@
|
|||||||
mod editor;
|
mod editor;
|
||||||
mod find_highlight;
|
mod find_highlight;
|
||||||
pub mod languages;
|
mod languages;
|
||||||
mod line_numbers;
|
mod line_numbers;
|
||||||
mod markdown;
|
|
||||||
|
|
||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
use crate::ui::constants::*;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
use egui::UiKind;
|
||||||
|
|
||||||
use self::editor::editor_view_ui;
|
use self::editor::editor_view_ui;
|
||||||
use self::languages::get_language_from_extension;
|
use self::line_numbers::{get_visual_line_mapping, render_line_numbers};
|
||||||
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) {
|
pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let show_line_numbers = app.show_line_numbers;
|
let show_line_numbers = app.show_line_numbers;
|
||||||
let word_wrap = app.word_wrap;
|
let word_wrap = app.word_wrap;
|
||||||
let line_side = app.line_side;
|
let line_side = app.line_side;
|
||||||
let font_size = app.font_size;
|
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()
|
let _output = egui::CentralPanel::default()
|
||||||
.frame(egui::Frame::NONE)
|
.frame(egui::Frame::NONE)
|
||||||
@ -39,110 +25,42 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ui.painter().rect_filled(panel_rect, 0.0, bg_color);
|
ui.painter().rect_filled(panel_rect, 0.0, bg_color);
|
||||||
let editor_height = panel_rect.height();
|
let editor_height = panel_rect.height();
|
||||||
|
|
||||||
// Handle markdown split view
|
if !show_line_numbers || app.get_active_tab().is_none() {
|
||||||
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 {
|
|
||||||
let _scroll_response =
|
let _scroll_response =
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.auto_shrink([false; 2])
|
.auto_shrink([false; 2])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let full_rect = ui.available_rect_before_wrap();
|
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| {
|
ui.scope_builder(egui::UiBuilder::new().max_rect(full_rect), |ui| {
|
||||||
editor_view_ui(ui, app);
|
editor_view_ui(ui, app);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
handle_empty(ui, app, &context_response);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_count = app.get_text_processing_result().line_count;
|
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 separator_widget = |ui: &mut egui::Ui| {
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 {
|
let visual_line_mapping = if word_wrap {
|
||||||
app.get_active_tab()
|
let available_text_width = editor_dimensions.text_width;
|
||||||
.map(|active_tab| {
|
if let Some(active_tab) = app.get_active_tab() {
|
||||||
calculate_visual_line_mapping(
|
get_visual_line_mapping(
|
||||||
ui,
|
ui,
|
||||||
&active_tab.content,
|
&active_tab.content,
|
||||||
actual_editor_width - (if line_side { 8.0 } else { 20.0 }),
|
available_text_width,
|
||||||
font_id,
|
font_size,
|
||||||
)
|
)
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
vec![]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
let line_numbers_widget = |ui: &mut egui::Ui| {
|
let line_numbers_widget = |ui: &mut egui::Ui| {
|
||||||
@ -152,26 +70,48 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
&visual_line_mapping,
|
&visual_line_mapping,
|
||||||
line_number_width,
|
line_number_width,
|
||||||
word_wrap,
|
word_wrap,
|
||||||
line_side,
|
|
||||||
font_size,
|
font_size,
|
||||||
monospace,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let separator_widget = |ui: &mut egui::Ui| {
|
||||||
|
ui.add_space(SMALL);
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false; 2])
|
||||||
|
.show(ui, |ui| {
|
||||||
if line_side {
|
if line_side {
|
||||||
|
let text_editor_width =
|
||||||
|
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
|
||||||
ui.allocate_ui_with_layout(
|
ui.allocate_ui_with_layout(
|
||||||
egui::vec2(available_width, editor_height),
|
egui::vec2(text_editor_width, editor_height),
|
||||||
egui::Layout::left_to_right(egui::Align::TOP),
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|ui| {
|
|ui| {
|
||||||
ui.allocate_ui_with_layout(
|
ui.allocate_ui_with_layout(
|
||||||
egui::vec2(actual_editor_width, editor_height),
|
egui::vec2(editor_dimensions.text_width, editor_height),
|
||||||
egui::Layout::left_to_right(egui::Align::TOP),
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|ui| {
|
|ui| {
|
||||||
let full_rect: egui::Rect = ui.available_rect_before_wrap();
|
let full_rect = ui.available_rect_before_wrap();
|
||||||
|
let context_response = ui.allocate_response(
|
||||||
|
full_rect.size(),
|
||||||
|
egui::Sense::click(),
|
||||||
|
);
|
||||||
|
|
||||||
ui.scope_builder(
|
ui.scope_builder(
|
||||||
egui::UiBuilder::new().max_rect(full_rect),
|
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);
|
separator_widget(ui);
|
||||||
@ -179,18 +119,27 @@ pub(crate) fn central_panel(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
let text_editor_width =
|
||||||
|
editor_dimensions.text_width + editor_dimensions.total_reserved_width;
|
||||||
ui.allocate_ui_with_layout(
|
ui.allocate_ui_with_layout(
|
||||||
egui::vec2(available_width, editor_height),
|
egui::vec2(text_editor_width, editor_height),
|
||||||
egui::Layout::left_to_right(egui::Align::TOP),
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
|ui| {
|
|ui| {
|
||||||
line_numbers_widget(ui);
|
line_numbers_widget(ui);
|
||||||
separator_widget(ui);
|
separator_widget(ui);
|
||||||
|
|
||||||
let editor_area = ui.available_rect_before_wrap();
|
let editor_area = ui.available_rect_before_wrap();
|
||||||
|
let context_response =
|
||||||
|
ui.allocate_response(editor_area.size(), egui::Sense::click());
|
||||||
|
|
||||||
ui.scope_builder(
|
ui.scope_builder(
|
||||||
egui::UiBuilder::new().max_rect(editor_area),
|
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 +147,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,41 +1,38 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::focus_manager::{FocusTarget, priorities};
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui_extras::syntax_highlighting::{self};
|
use egui_extras::syntax_highlighting::{self};
|
||||||
|
|
||||||
use super::find_highlight;
|
use super::find_highlight;
|
||||||
|
|
||||||
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response {
|
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 show_find = app.show_find;
|
||||||
|
let _prev_show_find = app.prev_show_find;
|
||||||
let show_preferences = app.show_preferences;
|
let show_preferences = app.show_preferences;
|
||||||
let show_about = app.show_about;
|
let show_about = app.show_about;
|
||||||
let show_shortcuts = app.show_shortcuts;
|
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 word_wrap = app.word_wrap;
|
||||||
let font_size = app.font_size;
|
let font_size = app.font_size;
|
||||||
let font_id = app.get_font_id();
|
let font_id = app.get_font_id();
|
||||||
let syntax_highlighting_enabled = app.syntax_highlighting;
|
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 reset_zoom_key = egui::Id::new("editor_reset_zoom");
|
||||||
let editor_rect = ui.available_rect_before_wrap();
|
let should_reset_zoom = ui
|
||||||
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
.ctx()
|
||||||
|
.memory_mut(|mem| mem.data.get_temp::<bool>(reset_zoom_key).unwrap_or(false));
|
||||||
|
|
||||||
handle_zoom_reset(ui, app);
|
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 {
|
let estimated_width = if !word_wrap {
|
||||||
(app.calculate_content_based_width(ui), f32::INFINITY)
|
app.calculate_content_based_width(ui)
|
||||||
} else {
|
} else {
|
||||||
(0.0, ui.available_width())
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let find_data = if show_find && !app.find_matches.is_empty() {
|
let find_data = if show_find && !app.find_matches.is_empty() {
|
||||||
@ -54,355 +51,35 @@ 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?");
|
return ui.label("No file open, how did you get here?");
|
||||||
};
|
};
|
||||||
|
|
||||||
let draw_highlights = |ui: &mut egui::Ui| {
|
let bg_color = ui.visuals().extreme_bg_color;
|
||||||
draw_editor_highlights(ui, &find_data, &font_id, font_size, desired_width);
|
let editor_rect = ui.available_rect_before_wrap();
|
||||||
};
|
ui.painter().rect_filled(editor_rect, 0.0, bg_color);
|
||||||
|
|
||||||
let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref());
|
if let Some((content, matches, current_match_index)) = &find_data {
|
||||||
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
|
let font_id = ui
|
||||||
let layout_job = editor_layouter(
|
.style()
|
||||||
ui,
|
.text_styles
|
||||||
string,
|
.get(&egui::TextStyle::Monospace)
|
||||||
wrap_width,
|
.unwrap_or(&egui::FontId::monospace(font_size))
|
||||||
syntax_highlighting_enabled,
|
.to_owned();
|
||||||
&language,
|
|
||||||
&font_id,
|
|
||||||
);
|
|
||||||
ui.fonts_mut(|f| f.layout_job(layout_job))
|
|
||||||
};
|
|
||||||
|
|
||||||
let id_source = active_tab
|
let desired_width = if word_wrap {
|
||||||
.file_path
|
ui.available_width()
|
||||||
.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 {
|
} else {
|
||||||
egui::ScrollArea::horizontal()
|
f32::INFINITY
|
||||||
.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);
|
let temp_galley = ui.fonts(|fonts| {
|
||||||
|
|
||||||
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| {
|
|
||||||
fonts.layout(
|
fonts.layout(
|
||||||
content.to_owned(),
|
content.to_owned(),
|
||||||
font_id.to_owned(),
|
font_id.to_owned(),
|
||||||
ui.visuals().text_color(),
|
ui.visuals().text_color(),
|
||||||
wrap_width - 8.0,
|
desired_width,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let cursor_pos = ui.cursor().min;
|
let text_area_left = editor_rect.left() + 4.0;
|
||||||
let text_area_left = cursor_pos.x + 4.0; // Text Editor default margins
|
let text_area_top = editor_rect.top() + 2.0;
|
||||||
let text_area_top = cursor_pos.y + 2.0;
|
|
||||||
|
|
||||||
find_highlight::draw_find_highlights(
|
find_highlight::draw_find_highlights(
|
||||||
ui,
|
ui,
|
||||||
@ -415,157 +92,167 @@ fn draw_editor_highlights(
|
|||||||
font_size,
|
font_size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn editor_layouter(
|
let desired_width = if word_wrap {
|
||||||
ui: &egui::Ui,
|
ui.available_width()
|
||||||
string: &dyn egui::TextBuffer,
|
} else {
|
||||||
wrap_width: f32,
|
f32::INFINITY
|
||||||
syntax_highlighting_enabled: bool,
|
};
|
||||||
language: &str,
|
|
||||||
font_id: &egui::FontId,
|
let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref());
|
||||||
) -> egui::text::LayoutJob {
|
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 theme = egui_extras::syntax_highlighting::CodeTheme::from_style(ui.style());
|
||||||
let text = string.as_str();
|
let text = string.as_str();
|
||||||
|
|
||||||
let mut layout_job = if syntax_highlighting_enabled && language != "txt" {
|
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 {
|
} else {
|
||||||
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "")
|
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "")
|
||||||
};
|
};
|
||||||
|
|
||||||
if syntax_highlighting_enabled && language != "txt" {
|
if syntax_highlighting_enabled && language != "txt" {
|
||||||
for section in &mut layout_job.sections {
|
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.wrap.max_width = wrap_width;
|
||||||
layout_job
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
}
|
};
|
||||||
|
|
||||||
fn handle_auto_indent(
|
let text_edit = egui::TextEdit::multiline(&mut active_tab.content)
|
||||||
ui: &mut egui::Ui,
|
.frame(false)
|
||||||
active_tab: &mut crate::app::tab::Tab,
|
.code_editor()
|
||||||
text_edit_id: egui::Id,
|
.desired_width(desired_width)
|
||||||
auto_indent: bool,
|
.desired_rows(0)
|
||||||
) -> bool {
|
.lock_focus(!show_find)
|
||||||
if !auto_indent || !ui.input(|i| i.key_pressed(egui::Key::Enter) && i.modifiers.is_none()) {
|
.cursor_at_end(false)
|
||||||
return 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) {
|
let output = if word_wrap {
|
||||||
if let Some(cursor_range) = state.cursor.char_range() {
|
text_edit.show(ui)
|
||||||
let cursor_pos = cursor_range.primary.index;
|
} else {
|
||||||
let content = &active_tab.content;
|
egui::ScrollArea::horizontal()
|
||||||
|
.auto_shrink([false; 2])
|
||||||
// Find previous line's indentation
|
.show(ui, |ui| {
|
||||||
let byte_idx = content
|
ui.allocate_ui_with_layout(
|
||||||
.char_indices()
|
egui::Vec2::new(estimated_width, ui.available_height()),
|
||||||
.nth(cursor_pos)
|
egui::Layout::left_to_right(egui::Align::TOP),
|
||||||
.map(|(i, _)| i)
|
|ui| text_edit.show(ui),
|
||||||
.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,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
});
|
.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(
|
if content_changed && app.show_find && !app.find_query.is_empty() {
|
||||||
ui: &mut egui::Ui,
|
app.update_find_matches();
|
||||||
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 let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
let current_cursor_pos = output
|
||||||
if let Some(cursor_range) = state.cursor.char_range() {
|
.state
|
||||||
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
|
|
||||||
.cursor
|
.cursor
|
||||||
.set_char_range(Some(egui::text::CCursorRange::one(
|
.char_range()
|
||||||
egui::text::CCursor::new(new_pos),
|
.map(|range| range.primary.index);
|
||||||
)));
|
|
||||||
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
if let Some(content) = content_for_processing {
|
||||||
ui.input_mut(|i| {
|
let previous_content = app.previous_content.to_owned();
|
||||||
i.events.retain(|e| {
|
let previous_cursor_pos = app.previous_cursor_char_index;
|
||||||
!matches!(
|
|
||||||
e,
|
if !previous_content.is_empty() {
|
||||||
egui::Event::Key {
|
if let (Some(prev_cursor_pos), Some(curr_cursor_pos)) =
|
||||||
key: egui::Key::Tab,
|
(previous_cursor_pos, current_cursor_pos)
|
||||||
pressed: true,
|
{
|
||||||
..
|
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 !word_wrap {
|
||||||
|
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;
|
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(
|
pub(super) fn draw_find_highlights(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
content: &str,
|
content: &str,
|
||||||
@ -9,18 +18,26 @@ pub(super) fn draw_find_highlights(
|
|||||||
galley: &std::sync::Arc<egui::Galley>,
|
galley: &std::sync::Arc<egui::Galley>,
|
||||||
text_area_left: f32,
|
text_area_left: f32,
|
||||||
text_area_top: 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);
|
let is_current_match = current_match_index == Some(match_index);
|
||||||
draw_single_highlight(
|
draw_single_highlight(
|
||||||
ui,
|
ui,
|
||||||
content,
|
content,
|
||||||
start_byte,
|
start_pos,
|
||||||
end_byte,
|
end_pos,
|
||||||
galley,
|
|
||||||
text_area_left,
|
text_area_left,
|
||||||
text_area_top,
|
text_area_top,
|
||||||
|
galley,
|
||||||
|
&font_id,
|
||||||
is_current_match,
|
is_current_match,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -29,15 +46,70 @@ pub(super) fn draw_find_highlights(
|
|||||||
fn draw_single_highlight(
|
fn draw_single_highlight(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
content: &str,
|
content: &str,
|
||||||
start_byte: usize,
|
start_pos: usize,
|
||||||
end_byte: usize,
|
end_pos: usize,
|
||||||
galley: &std::sync::Arc<egui::Galley>,
|
|
||||||
text_area_left: f32,
|
text_area_left: f32,
|
||||||
text_area_top: f32,
|
text_area_top: f32,
|
||||||
|
galley: &std::sync::Arc<egui::Galley>,
|
||||||
|
font_id: &egui::FontId,
|
||||||
is_current_match: bool,
|
is_current_match: bool,
|
||||||
) {
|
) {
|
||||||
let start_char = safe_slice_to_pos(content, start_byte).chars().count();
|
let text_up_to_start = safe_slice_to_pos(content, start_pos);
|
||||||
let end_char = safe_slice_to_pos(content, end_byte).chars().count();
|
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 {
|
let highlight_color = if is_current_match {
|
||||||
ui.visuals().selection.bg_fill
|
ui.visuals().selection.bg_fill
|
||||||
@ -46,36 +118,5 @@ fn draw_single_highlight(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let painter = ui.painter();
|
let painter = ui.painter();
|
||||||
|
painter.rect_filled(highlight_rect, 0.0, highlight_color);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,47 @@
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
fn format_line_number(line_number: usize, line_side: bool, line_count_width: usize) -> String {
|
thread_local! {
|
||||||
if line_side {
|
static VISUAL_LINE_MAPPING_CACHE: std::cell::RefCell<Option<(String, f32, Vec<Option<usize>>)>> = std::cell::RefCell::new(None);
|
||||||
// Right side: left-align with trailing space for scrollbar clearance
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn calculate_visual_line_mapping(
|
pub(super) fn get_visual_line_mapping(
|
||||||
ui: &egui::Ui,
|
ui: &egui::Ui,
|
||||||
content: &str,
|
content: &str,
|
||||||
available_width: f32,
|
available_width: f32,
|
||||||
font_id: egui::FontId,
|
font_size: f32,
|
||||||
|
) -> Vec<Option<usize>> {
|
||||||
|
let should_recalculate = VISUAL_LINE_MAPPING_CACHE.with(|cache| {
|
||||||
|
if let Some((cached_content, cached_width, _)) = cache.borrow().as_ref() {
|
||||||
|
content != cached_content || available_width != *cached_width
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if should_recalculate {
|
||||||
|
let visual_lines = calculate_visual_line_mapping(ui, content, available_width, font_size);
|
||||||
|
VISUAL_LINE_MAPPING_CACHE.with(|cache| {
|
||||||
|
*cache.borrow_mut() = Some((content.to_owned(), available_width, visual_lines));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
VISUAL_LINE_MAPPING_CACHE.with(|cache| {
|
||||||
|
cache
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, _, mapping)| mapping.to_owned())
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_visual_line_mapping(
|
||||||
|
ui: &egui::Ui,
|
||||||
|
content: &str,
|
||||||
|
available_width: f32,
|
||||||
|
font_size: f32,
|
||||||
) -> Vec<Option<usize>> {
|
) -> Vec<Option<usize>> {
|
||||||
let mut visual_lines = Vec::new();
|
let mut visual_lines = Vec::new();
|
||||||
|
let font_id = egui::FontId::monospace(font_size);
|
||||||
|
|
||||||
for (line_num, line) in content.lines().enumerate() {
|
for (line_num, line) in content.lines().enumerate() {
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
@ -24,7 +49,7 @@ pub(super) fn calculate_visual_line_mapping(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let galley = ui.fonts_mut(|fonts| {
|
let galley = ui.fonts(|fonts| {
|
||||||
fonts.layout(
|
fonts.layout(
|
||||||
line.to_string(),
|
line.to_string(),
|
||||||
font_id.to_owned(),
|
font_id.to_owned(),
|
||||||
@ -34,6 +59,7 @@ pub(super) fn calculate_visual_line_mapping(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let wrapped_line_count = galley.rows.len().max(1);
|
let wrapped_line_count = galley.rows.len().max(1);
|
||||||
|
|
||||||
visual_lines.push(Some(line_num + 1));
|
visual_lines.push(Some(line_num + 1));
|
||||||
|
|
||||||
for _ in 1..wrapped_line_count {
|
for _ in 1..wrapped_line_count {
|
||||||
@ -41,11 +67,6 @@ pub(super) fn calculate_visual_line_mapping(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if content.ends_with('\n') && !content.is_empty() {
|
|
||||||
let line_num = content.lines().count();
|
|
||||||
visual_lines.push(Some(line_num + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
visual_lines
|
visual_lines
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,25 +76,12 @@ pub(super) fn render_line_numbers(
|
|||||||
visual_line_mapping: &[Option<usize>],
|
visual_line_mapping: &[Option<usize>],
|
||||||
line_number_width: f32,
|
line_number_width: f32,
|
||||||
word_wrap: bool,
|
word_wrap: bool,
|
||||||
line_side: bool,
|
|
||||||
font_size: f32,
|
font_size: f32,
|
||||||
monospace: bool,
|
|
||||||
) {
|
) {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.disable();
|
|
||||||
ui.set_width(line_number_width);
|
ui.set_width(line_number_width);
|
||||||
|
ui.spacing_mut().item_spacing.y = 0.0;
|
||||||
|
|
||||||
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
|
|
||||||
let text_color = ui.visuals().weak_text_color();
|
let text_color = ui.visuals().weak_text_color();
|
||||||
let bg_color = ui.visuals().extreme_bg_color;
|
let bg_color = ui.visuals().extreme_bg_color;
|
||||||
|
|
||||||
@ -83,28 +91,28 @@ pub(super) fn render_line_numbers(
|
|||||||
let font_id = egui::FontId::monospace(font_size);
|
let font_id = egui::FontId::monospace(font_size);
|
||||||
let line_count_width = line_count.to_string().len();
|
let line_count_width = line_count.to_string().len();
|
||||||
|
|
||||||
let line_texts = if word_wrap {
|
if word_wrap {
|
||||||
visual_line_mapping
|
for line_number_opt in visual_line_mapping {
|
||||||
.into_iter()
|
let text = if let Some(line_number) = line_number_opt {
|
||||||
.map(|line_number_opt| {
|
format!("{:>width$}", line_number, width = line_count_width)
|
||||||
line_number_opt.map_or_else(
|
|
||||||
|| " ".repeat(line_count_width),
|
|
||||||
|line_number| format_line_number(line_number, line_side, line_count_width),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
} else {
|
} else {
|
||||||
(1..=line_count)
|
" ".repeat(line_count_width)
|
||||||
.map(|i| format_line_number(i, line_side, line_count_width))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for text in line_texts {
|
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(text)
|
egui::RichText::new(text)
|
||||||
.font(font_id.to_owned())
|
.font(font_id.to_owned())
|
||||||
.color(text_color),
|
.color(text_color),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for i in 1..=line_count {
|
||||||
|
let text = format!("{:>width$}", i, width = line_count_width);
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(text)
|
||||||
|
.font(font_id.to_owned())
|
||||||
|
.color(text_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 PREVIEW_AREA_MAX_HEIGHT: f32 = 150.0;
|
||||||
|
|
||||||
pub const INNER_MARGIN: i8 = 8;
|
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::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
use crate::ui::constants::*;
|
||||||
use crate::ui::focus_manager::{FocusTarget, priorities};
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
pub(crate) fn find_window(app: &mut TextEditor, ctx: &egui::Context) {
|
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(
|
let response = ui.add(
|
||||||
egui::TextEdit::singleline(&mut app.find_query)
|
egui::TextEdit::singleline(&mut app.find_query)
|
||||||
.desired_width(250.0)
|
.desired_width(250.0)
|
||||||
.hint_text("Search..."),
|
.hint_text("Enter search text..."),
|
||||||
);
|
);
|
||||||
|
|
||||||
if response.changed() {
|
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 {
|
if just_opened || focus_requested || app.focus_find {
|
||||||
app.focus_manager.request_focus(FocusTarget::FindInput, priorities::HIGH);
|
response.request_focus();
|
||||||
app.focus_find = false;
|
app.focus_find = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
app.find_next(ctx);
|
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(
|
let _replace_response = ui.add(
|
||||||
egui::TextEdit::singleline(&mut app.replace_query)
|
egui::TextEdit::singleline(&mut app.replace_query)
|
||||||
.desired_width(250.0)
|
.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::TextEditor, io};
|
||||||
use crate::app::actions::ShortcutAction;
|
|
||||||
use eframe::egui::{self, Frame};
|
use eframe::egui::{self, Frame};
|
||||||
use egui::UiKind;
|
use egui::UiKind;
|
||||||
|
|
||||||
@ -49,25 +48,25 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
ui.menu_button("File", |ui| {
|
ui.menu_button("File", |ui| {
|
||||||
app.menu_interaction_active = true;
|
app.menu_interaction_active = true;
|
||||||
if ui.button("New").clicked() {
|
if ui.button("New").clicked() {
|
||||||
app.perform_action(ShortcutAction::NewFile);
|
io::new_file(app);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Open...").clicked() {
|
if ui.button("Open...").clicked() {
|
||||||
app.perform_action(ShortcutAction::OpenFile);
|
io::open_file(app);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Save").clicked() {
|
if ui.button("Save").clicked() {
|
||||||
app.perform_action(ShortcutAction::SaveFile);
|
io::save_file(app);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Save As...").clicked() {
|
if ui.button("Save As...").clicked() {
|
||||||
app.perform_action(ShortcutAction::SaveAsFile);
|
io::save_as_file(app);
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Preferences").clicked() {
|
if ui.button("Preferences").clicked() {
|
||||||
app.perform_action(ShortcutAction::Preferences);
|
app.show_preferences = true;
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Exit").clicked() {
|
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);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.button("Select All").clicked() {
|
if ui.button("Select All").clicked() {
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
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(mut state) =
|
if let Some(mut state) =
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
||||||
{
|
{
|
||||||
if let Some(active_tab) = app.get_active_tab() {
|
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(
|
let select_all_range = egui::text::CCursorRange::two(
|
||||||
egui::text::CCursor::new(0),
|
egui::text::CCursor::new(0),
|
||||||
egui::text::CCursor::new(text_len),
|
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);
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
// Check if undo is available
|
if ui.button("Undo").clicked() {
|
||||||
let can_undo = if let Some(active_tab) = app.get_active_tab() {
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
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 let Some(mut state) =
|
if let Some(mut state) =
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
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();
|
active_tab.content = content.to_string();
|
||||||
state.cursor.set_char_range(Some(*cursor_range));
|
state.cursor.set_char_range(Some(*cursor_range));
|
||||||
state.set_undoer(undoer);
|
state.set_undoer(undoer);
|
||||||
egui::TextEdit::store_state(
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
ui.ctx(),
|
|
||||||
text_edit_id,
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
active_tab.update_modified_state();
|
active_tab.update_modified_state();
|
||||||
if app.show_find && !app.find_query.is_empty() {
|
if app.show_find && !app.find_query.is_empty() {
|
||||||
app.update_find_matches();
|
app.update_find_matches();
|
||||||
@ -190,43 +145,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
// Check if redo is available
|
if ui.button("Redo").clicked() {
|
||||||
let can_redo = if let Some(active_tab) = app.get_active_tab() {
|
let text_edit_id = egui::Id::new("main_text_editor");
|
||||||
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 let Some(mut state) =
|
if let Some(mut state) =
|
||||||
egui::TextEdit::load_state(ui.ctx(), text_edit_id)
|
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();
|
active_tab.content = content.to_string();
|
||||||
state.cursor.set_char_range(Some(*cursor_range));
|
state.cursor.set_char_range(Some(*cursor_range));
|
||||||
state.set_undoer(undoer);
|
state.set_undoer(undoer);
|
||||||
egui::TextEdit::store_state(
|
egui::TextEdit::store_state(ui.ctx(), text_edit_id, state);
|
||||||
ui.ctx(),
|
|
||||||
text_edit_id,
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
active_tab.update_modified_state();
|
active_tab.update_modified_state();
|
||||||
if app.show_find && !app.find_query.is_empty() {
|
if app.show_find && !app.find_query.is_empty() {
|
||||||
app.update_find_matches();
|
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.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.menu_button("View", |ui| {
|
ui.menu_button("View", |ui| {
|
||||||
app.menu_interaction_active = true;
|
app.menu_interaction_active = true;
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.show_markdown, "Preview Markdown")
|
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
@ -280,62 +196,27 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui.checkbox(&mut app.show_hidden_files, "Show Hidden Files").clicked() {
|
if ui.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar").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() {
|
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.show_bottom_bar, "Bottom Bar")
|
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
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);
|
ui.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
if ui
|
|
||||||
.checkbox(&mut app.auto_hide_toolbar, "Hide Toolbar")
|
ui.separator();
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
app.save_config();
|
|
||||||
ui.close_kind(UiKind::Menu);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.menu_button("Appearance", |ui| {
|
ui.menu_button("Appearance", |ui| {
|
||||||
app.menu_interaction_active = true;
|
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.close_kind(UiKind::Menu);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
if ui.radio_value(&mut app.line_side, false, "Left").clicked() {
|
||||||
if ui.button("Reset Zoom").clicked() {
|
app.save_config();
|
||||||
app.zoom_factor = 1.0;
|
ui.close_kind(UiKind::Menu);
|
||||||
ctx.set_zoom_factor(1.0);
|
}
|
||||||
ui.close_kind(UiKind::Menu);
|
if ui.radio_value(&mut app.line_side, true, "Right").clicked() {
|
||||||
}
|
|
||||||
if ui.button("Focus Mode").clicked() {
|
|
||||||
app.focus_mode = true;
|
|
||||||
app.save_config();
|
app.save_config();
|
||||||
ui.close_kind(UiKind::Menu);
|
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() {
|
let tab_title = if let Some(tab) = app.get_active_tab() {
|
||||||
tab.get_display_title()
|
tab.get_display_title()
|
||||||
} else {
|
} else {
|
||||||
@ -412,10 +289,10 @@ pub(crate) fn menu_bar(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
empty_tab.get_display_title()
|
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 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(
|
fonts.layout_job(egui::text::LayoutJob::simple_singleline(
|
||||||
tab_title,
|
tab_title,
|
||||||
font_id,
|
font_id,
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
use crate::app::TextEditor;
|
use crate::app::TextEditor;
|
||||||
use crate::ui::constants::*;
|
use crate::ui::constants::*;
|
||||||
use crate::ui::focus_manager::{FocusTarget, priorities};
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let visuals = &ctx.style().visuals;
|
let visuals = &ctx.style().visuals;
|
||||||
let screen_rect = ctx.viewport_rect();
|
let screen_rect = ctx.screen_rect();
|
||||||
let window_width =
|
let window_width =
|
||||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_WIDTH);
|
||||||
let window_height =
|
let window_height =
|
||||||
@ -30,6 +29,9 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
})
|
})
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.heading("Editor Settings");
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
if ui
|
if ui
|
||||||
@ -45,19 +47,28 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.add_space(SMALL);
|
ui.add_space(SMALL);
|
||||||
if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() {
|
if ui
|
||||||
|
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
}
|
}
|
||||||
ui.add_space(SMALL);
|
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() {
|
if ui
|
||||||
app.save_config();
|
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
|
||||||
}
|
.on_hover_text(
|
||||||
ui.add_space(SMALL);
|
"Hide the top bar until you move your mouse to the upper edge",
|
||||||
if ui.checkbox(&mut app.follow_git, "Git").on_hover_text("Respect .gitignore file").changed() {
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
app.save_config();
|
app.save_config();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
|
if ui.checkbox(&mut app.word_wrap, "Word Wrap").changed() {
|
||||||
|
app.save_config();
|
||||||
|
}
|
||||||
|
ui.add_space(SMALL);
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
|
||||||
.changed()
|
.changed()
|
||||||
@ -66,54 +77,10 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
ui.add_space(SMALL);
|
ui.add_space(SMALL);
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut app.auto_indent, "Auto Indent")
|
.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar")
|
||||||
.on_hover_text("Automatically indent new lines to match the previous line")
|
.on_hover_text(
|
||||||
.changed()
|
"Hide the tab bar and show tab title in menu bar instead",
|
||||||
{
|
)
|
||||||
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")
|
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
app.save_config();
|
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.add_space(SMALL);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(LARGE);
|
||||||
|
ui.heading("Font Settings");
|
||||||
|
ui.add_space(MEDIUM);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.label("Font Family:");
|
ui.label("Font Family:");
|
||||||
ui.add_space(MEDIUM);
|
ui.add_space(SMALL);
|
||||||
ui.label("Font Size:");
|
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());
|
app.font_size_input = Some(font_size_text.to_owned());
|
||||||
|
|
||||||
if response.clicked() {
|
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 response.lost_focus() {
|
||||||
if let Ok(new_size) = font_size_text.parse::<f32>() {
|
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);
|
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;
|
use eframe::egui;
|
||||||
|
|
||||||
fn render_shortcuts_content(ui: &mut egui::Ui) {
|
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.vertical_centered(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new("Navigation")
|
egui::RichText::new("Navigation")
|
||||||
.size(UI_HEADER_SIZE)
|
.size(UI_HEADER_SIZE)
|
||||||
.strong(),
|
.strong(),
|
||||||
);
|
);
|
||||||
});
|
ui.label(egui::RichText::new("Ctrl + N: New").size(UI_TEXT_SIZE));
|
||||||
ui.add_space(MEDIUM);
|
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.horizontal(|ui| {
|
ui.label(egui::RichText::new("Ctrl + Shift + S: Save As").size(UI_TEXT_SIZE));
|
||||||
ui.add_space(MEDIUM);
|
ui.label(egui::RichText::new("Ctrl + T: New Tab").size(UI_TEXT_SIZE));
|
||||||
ui.columns(2, |columns| {
|
ui.label(egui::RichText::new("Ctrl + W: Close Tab").size(UI_TEXT_SIZE));
|
||||||
let shortcuts = [
|
ui.label(egui::RichText::new("Ctrl + Tab: Next Tab").size(UI_TEXT_SIZE));
|
||||||
("Ctrl + N", "New"),
|
ui.label(egui::RichText::new("Ctrl + Shift + Tab: Last Tab").size(UI_TEXT_SIZE));
|
||||||
("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.add_space(VLARGE);
|
ui.add_space(VLARGE);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.add_space(MEDIUM);
|
|
||||||
|
|
||||||
// Editing section
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong());
|
ui.label(egui::RichText::new("Editing").size(UI_HEADER_SIZE).strong());
|
||||||
});
|
ui.label(egui::RichText::new("Ctrl + Z: Undo").size(UI_TEXT_SIZE));
|
||||||
ui.add_space(MEDIUM);
|
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.horizontal(|ui| {
|
ui.label(egui::RichText::new("Ctrl + C: Copy").size(UI_TEXT_SIZE));
|
||||||
ui.add_space(MEDIUM);
|
ui.label(egui::RichText::new("Ctrl + V: Paste").size(UI_TEXT_SIZE));
|
||||||
ui.columns(2, |columns| {
|
ui.label(egui::RichText::new("Ctrl + A: Select All").size(UI_TEXT_SIZE));
|
||||||
let shortcuts = [
|
ui.label(egui::RichText::new("Ctrl + D: Delete Line").size(UI_TEXT_SIZE));
|
||||||
("Ctrl + Z", "Undo"),
|
ui.label(egui::RichText::new("Ctrl + F: Find").size(UI_TEXT_SIZE));
|
||||||
("Ctrl + Shift + Z", "Redo"),
|
ui.label(egui::RichText::new("Ctrl + R: Replace").size(UI_TEXT_SIZE));
|
||||||
("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.add_space(VLARGE);
|
ui.add_space(VLARGE);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.add_space(MEDIUM);
|
|
||||||
|
|
||||||
// Views section
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong());
|
ui.label(egui::RichText::new("Views").size(UI_HEADER_SIZE).strong());
|
||||||
});
|
ui.label(egui::RichText::new("Ctrl + L: Toggle Line Numbers").size(UI_TEXT_SIZE));
|
||||||
ui.add_space(MEDIUM);
|
ui.label(
|
||||||
|
egui::RichText::new("Ctrl + Shift + L: Change Line Number Side").size(UI_TEXT_SIZE),
|
||||||
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(),
|
|
||||||
);
|
);
|
||||||
columns[col].label(
|
ui.label(egui::RichText::new("Ctrl + K: Toggle Word Wrap").size(UI_TEXT_SIZE));
|
||||||
egui::RichText::new(*description)
|
ui.label(egui::RichText::new("Ctrl + H: Toggle Auto Hide Toolbar").size(UI_TEXT_SIZE));
|
||||||
.size(UI_TEXT_SIZE)
|
ui.label(egui::RichText::new("Ctrl + P: Preferences").size(UI_TEXT_SIZE));
|
||||||
.color(description_color),
|
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(
|
||||||
// Add space after each complete row (every 2 items)
|
// egui::RichText::new("Ctrl + Shift + .: Toggle Vim Mode")
|
||||||
if i % 2 == 1 {
|
// .size(14.0)
|
||||||
columns[0].add_space(MEDIUM);
|
// );
|
||||||
columns[1].add_space(MEDIUM);
|
// ui.label(
|
||||||
}
|
// egui::RichText::new("Ctrl + .: Toggle Vim Mode")
|
||||||
}
|
// .size(14.0)
|
||||||
});
|
// );
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(VLARGE);
|
ui.add_space(VLARGE);
|
||||||
|
ui.separator();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
pub(crate) fn shortcuts_window(app: &mut TextEditor, ctx: &egui::Context) {
|
||||||
let visuals = &ctx.style().visuals;
|
let visuals = &ctx.style().visuals;
|
||||||
let screen_rect = ctx.viewport_rect();
|
let screen_rect = ctx.screen_rect();
|
||||||
|
|
||||||
let window_width =
|
let window_width =
|
||||||
(screen_rect.width() * WINDOW_WIDTH_RATIO).clamp(WINDOW_MIN_WIDTH, WINDOW_MAX_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.vertical_centered(|ui| {
|
||||||
//ui.add_space(MEDIUM);
|
ui.add_space(MEDIUM);
|
||||||
ui.separator();
|
|
||||||
let visuals = ui.visuals();
|
let visuals = ui.visuals();
|
||||||
let close_button = egui::Button::new("Close")
|
let close_button = egui::Button::new("Close")
|
||||||
.fill(visuals.widgets.inactive.bg_fill)
|
.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