use std::path::Path;
use std::sync::mpsc as std_mpsc;
use crate::config::ConfigLoadResult;
use crate::editor::{DeferredAction, Editor};
use crate::editor::document::Document;
use crate::editor::pane::AreaRect;
use crate::input;
use crate::input::command::Command;
use crate::input::keymap;
use crate::key::{KeyCode, KeyInput};
use crate::lsp::{self, LspClient, LspMessage};
fn find_font_file(name: &str) -> Option<std::path::PathBuf> {
let home = std::env::var("HOME").unwrap_or_default();
let dirs = [
format!("{home}/Library/Fonts"),
"/Library/Fonts".to_string(),
"/System/Library/Fonts".to_string(),
"/System/Library/Fonts/Supplemental".to_string(),
];
let name_lower = name.to_lowercase();
for dir in &dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let fname = entry.file_name().to_string_lossy().to_lowercase();
if fname.contains(&name_lower)
&& (fname.ends_with(".ttf") || fname.ends_with(".otf"))
{
return Some(entry.path());
}
}
}
}
None
}
fn setup_custom_font(ctx: &egui::Context, font_path: &std::path::Path) -> Result<(), String> {
let font_data = std::fs::read(font_path)
.map_err(|e| format!("Failed to read font: {e}"))?;
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"custom_mono".to_owned(),
egui::FontData::from_owned(font_data).into(),
);
fonts.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "custom_mono".to_owned());
fonts.families
.entry(egui::FontFamily::Proportional)
.or_default()
.push("custom_mono".to_owned());
ctx.set_fonts(fonts);
Ok(())
}
pub struct GuiApp {
editor: Editor,
runtime: tokio::runtime::Runtime,
lsp_client: Option<LspClient>,
lsp_rx: std_mpsc::Receiver<LspMessage>,
lsp_tx: std_mpsc::Sender<LspMessage>,
file_uri: Option<String>,
last_notified_version: i64,
last_pane_rects: Vec<(usize, egui::Rect)>,
}
impl GuiApp {
pub fn new(cc: &eframe::CreationContext<'_>, path: Option<String>, config_result: ConfigLoadResult) -> Self {
let document = match path {
Some(ref p) => Document::open(p).unwrap_or_else(|_| Document::new_empty()),
None => Document::new_empty(),
};
let (lsp_tx, lsp_rx) = std_mpsc::channel();
let runtime = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
let mut editor = Editor::with_config(document, config_result.config);
if let Some(warning) = config_result.warning {
editor.status_message = Some(warning);
}
let mut app = Self {
editor,
runtime,
lsp_client: None,
lsp_rx,
lsp_tx,
file_uri: None,
last_notified_version: 0,
last_pane_rects: Vec::new(),
};
if let Some(ref font_name) = app.editor.config.gui_font_family {
match find_font_file(font_name) {
Some(path) => {
if let Err(e) = setup_custom_font(&cc.egui_ctx, &path) {
app.editor.status_message = Some(e);
}
}
None => {
app.editor.status_message = Some(format!("Font not found: {font_name}"));
}
}
}
if let Some(ref p) = path {
let path = std::path::PathBuf::from(p);
if let Ok(canonical) = std::fs::canonicalize(&path) {
app.editor.document.path = Some(canonical.clone());
app.start_lsp(&canonical);
}
}
app
}
fn start_lsp(&mut self, file_path: &Path) {
let root = lsp::find_project_root(file_path);
let tx = self.lsp_tx.clone();
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
match self.runtime.block_on(LspClient::start(&root, event_tx)) {
Ok(client) => {
self.lsp_client = Some(client);
self.editor.status_message = Some(format!("LSP: starting (root: {})", root.display()));
self.runtime.spawn(async move {
while let Some(event) = event_rx.recv().await {
if let crate::lsp::AppEvent::Lsp(msg) = event
&& tx.send(msg).is_err() {
break;
}
}
});
}
Err(e) => {
self.editor.status_message = Some(format!("LSP: failed to start: {e}"));
}
}
}
fn process_lsp_messages(&mut self) {
while let Ok(msg) = self.lsp_rx.try_recv() {
self.handle_lsp_message(msg);
}
}
fn handle_lsp_message(&mut self, msg: LspMessage) {
match msg {
LspMessage::Response { id, result, error } => {
let is_init = self.lsp_client.as_ref()
.is_some_and(|lsp| id == lsp.initialize_id && !lsp.initialized);
if is_init {
if error.is_some() {
self.editor.status_message = Some("LSP: initialize failed".to_string());
return;
}
let path_data = self.editor.document.path.as_ref().map(|path| {
let uri = lsp::path_to_uri(path);
let text = self.editor.document.rope.to_string();
let version = self.editor.document.version;
(uri, text, version)
});
if let Some(lsp) = &mut self.lsp_client {
let _ = self.runtime.block_on(lsp.send_initialized());
if let Some((ref uri, ref text, version)) = path_data {
let _ = self.runtime.block_on(lsp.did_open(uri, text, version));
}
}
if let Some((uri, _, _)) = path_data {
self.file_uri = Some(uri);
}
self.editor.status_message = Some("LSP: ready".to_string());
return;
}
if Some(id) == self.editor.pending_completion_id {
self.editor.pending_completion_id = None;
if let Some(result) = result {
let items = lsp::parse_completions(&result);
if !items.is_empty() {
self.editor.completions = items;
self.editor.completion_index = 0;
self.editor.showing_completion = true;
}
}
return;
}
if Some(id) == self.editor.pending_goto_id {
self.editor.pending_goto_id = None;
if let Some(result) = result {
let locations = lsp::parse_goto_definition(&result);
if let Some(loc) = locations.first() {
self.editor.push_jump();
let current_uri = self.file_uri.as_deref().unwrap_or("");
if loc.uri == current_uri {
self.editor.cursor.row = loc.start_line as usize;
self.editor.cursor.col = loc.start_col as usize;
self.editor.clamp_cursor();
self.editor.scroll();
} else {
let name = loc.uri.rsplit('/').next().unwrap_or(&loc.uri);
self.editor.status_message = Some(format!(
"Definition in {}:{}:{}",
name, loc.start_line + 1, loc.start_col + 1
));
}
} else {
self.editor.status_message = Some("No definition found".to_string());
}
}
return;
}
if Some(id) == self.editor.pending_hover_id {
self.editor.pending_hover_id = None;
if let Some(result) = result {
if let Some(text) = lsp::parse_hover(&result) {
self.editor.hover_text = Some(text);
self.editor.showing_hover = true;
} else {
self.editor.status_message = Some("No hover info".to_string());
}
}
return;
}
if Some(id) == self.editor.pending_references_id {
self.editor.pending_references_id = None;
if let Some(result) = result {
let locations = lsp::parse_references(&result);
if !locations.is_empty() {
self.editor.references = locations;
self.editor.reference_index = 0;
self.editor.showing_references = true;
} else {
self.editor.status_message = Some("No references found".to_string());
}
}
return;
}
if Some(id) == self.editor.pending_format_id {
self.editor.pending_format_id = None;
if let Some(result) = result {
self.apply_format_edits(&result);
}
return;
}
if Some(id) == self.editor.pending_code_action_id {
self.editor.pending_code_action_id = None;
if let Some(result) = result {
let actions = lsp::parse_code_actions(&result);
if !actions.is_empty() {
self.editor.code_actions = actions;
self.editor.code_action_index = 0;
self.editor.showing_code_actions = true;
} else {
self.editor.status_message = Some("No code actions available".to_string());
}
}
return;
}
if Some(id) == self.editor.pending_workspace_symbol_id {
self.editor.pending_workspace_symbol_id = None;
if let Some(result) = result {
let symbols = lsp::parse_workspace_symbols(&result);
self.editor.workspace_symbol_results = symbols;
self.editor.workspace_symbol_index = 0;
}
return;
}
if Some(id) == self.editor.pending_rename_id {
self.editor.pending_rename_id = None;
if let Some(ref err) = error {
let msg = err.get("message").and_then(|m| m.as_str()).unwrap_or("Rename failed");
self.editor.status_message = Some(format!("Rename error: {msg}"));
return;
}
if let Some(result) = result {
self.apply_rename_edits(&result);
}
}
}
LspMessage::Notification { method, params } => {
if method == "textDocument/publishDiagnostics" {
self.editor.diagnostics = lsp::parse_diagnostics(¶ms);
}
}
LspMessage::ServerRequest { id, method, .. } => {
if let Some(lsp) = &mut self.lsp_client
&& (method == "window/workDoneProgress/create" || method == "client/registerCapability") {
let _ = self.runtime.block_on(lsp.respond(&id, serde_json::Value::Null));
}
}
}
}
fn apply_format_edits(&mut self, result: &serde_json::Value) {
let edits = match result.as_array() {
Some(arr) => arr,
None => {
self.editor.status_message = Some("Formatted (no changes)".to_string());
return;
}
};
if edits.is_empty() {
self.editor.status_message = Some("Formatted (no changes)".to_string());
return;
}
self.editor.history.save(&self.editor.document.rope, self.editor.cursor);
let mut text_edits: Vec<(usize, usize, usize, usize, String)> = edits
.iter()
.filter_map(|edit| {
let range = edit.get("range")?;
let start = range.get("start")?;
let end = range.get("end")?;
let new_text = edit.get("newText")?.as_str()?.to_string();
Some((
start.get("line")?.as_u64()? as usize,
start.get("character")?.as_u64()? as usize,
end.get("line")?.as_u64()? as usize,
end.get("character")?.as_u64()? as usize,
new_text,
))
})
.collect();
text_edits.sort_by(|a, b| (b.2, b.3).cmp(&(a.2, a.3)));
for (start_line, start_col, end_line, end_col, new_text) in &text_edits {
let line_count = self.editor.document.rope.len_lines();
if *start_line >= line_count { continue; }
let end_line = (*end_line).min(line_count.saturating_sub(1));
let start_idx = self.editor.document.rope.line_to_char(*start_line) + start_col;
let end_idx = self.editor.document.rope.line_to_char(end_line) + end_col;
let end_idx = end_idx.min(self.editor.document.rope.len_chars());
let start_idx = start_idx.min(self.editor.document.rope.len_chars());
if start_idx < end_idx { self.editor.document.rope.remove(start_idx..end_idx); }
if !new_text.is_empty() { self.editor.document.rope.insert(start_idx, new_text); }
}
self.editor.document.modified = true;
self.editor.document.bump_version();
self.editor.clamp_cursor();
self.editor.status_message = Some("Formatted".to_string());
}
fn apply_rename_edits(&mut self, result: &serde_json::Value) {
let file_uri = match &self.file_uri {
Some(uri) => uri.clone(),
None => return,
};
let edits = lsp::parse_rename_edits(result, &file_uri);
if edits.is_empty() {
self.editor.status_message = Some("No edits to apply".to_string());
return;
}
self.editor.history.save(&self.editor.document.rope, self.editor.cursor);
let count = edits.len();
for edit in &edits {
let start_line_char = self.editor.document.rope.line_to_char(edit.start_line as usize);
let end_line_char = self.editor.document.rope.line_to_char(edit.end_line as usize);
let start_idx = start_line_char + edit.start_col as usize;
let end_idx = end_line_char + edit.end_col as usize;
let end_idx = end_idx.min(self.editor.document.rope.len_chars());
if start_idx < end_idx { self.editor.document.rope.remove(start_idx..end_idx); }
if !edit.new_text.is_empty() { self.editor.document.rope.insert(start_idx, &edit.new_text); }
}
self.editor.document.modified = true;
self.editor.document.bump_version();
self.editor.clamp_cursor();
self.editor.status_message = Some(format!("Renamed: {count} occurrence(s)"));
}
fn handle_key(&mut self, key: KeyInput, ctx: &egui::Context) {
if self.editor.recording_macro.is_some() {
let is_stop = matches!(key.code, KeyCode::Char('q'))
&& !key.ctrl
&& self.editor.mode == crate::input::mode::Mode::Normal;
if !is_stop {
self.editor.macro_buffer.push(key);
}
}
if let Some(cmd) = keymap::map_key(&mut self.editor, key) {
let trigger_completion = matches!(cmd, Command::TriggerCompletion);
let trigger_goto = matches!(cmd, Command::GotoDefinition);
let trigger_hover = matches!(cmd, Command::Hover);
let trigger_refs = matches!(cmd, Command::FindReferences);
let trigger_ref_jump = matches!(cmd, Command::ReferenceJump);
let trigger_file_finder = matches!(cmd, Command::OpenFileFinder);
let trigger_code_action = matches!(cmd, Command::CodeAction);
let trigger_code_action_accept = matches!(cmd, Command::CodeActionAccept);
let trigger_ws_symbol = matches!(cmd, Command::WorkspaceSymbol);
let trigger_ws_confirm = matches!(cmd, Command::WorkspaceSymbolConfirm);
if !matches!(
cmd,
Command::TriggerCompletion
| Command::AcceptCompletion
| Command::CancelCompletion
| Command::CompletionNext
| Command::CompletionPrev
) && self.editor.showing_completion {
self.editor.cancel_completion();
}
let deferred = input::execute(&mut self.editor, cmd);
if trigger_completion { self.request_completion(); }
if trigger_goto { self.request_goto_definition(); }
if trigger_hover { self.request_hover(); }
if trigger_refs { self.request_references(); }
if trigger_ref_jump { self.jump_to_reference(); }
if trigger_file_finder {
let entries = self.scan_project_files();
self.editor.open_file_finder(entries);
}
if trigger_code_action { self.request_code_action(); }
if trigger_code_action_accept { self.accept_code_action(); }
if trigger_ws_symbol {
self.editor.open_workspace_symbols();
}
if trigger_ws_confirm {
self.jump_to_workspace_symbol();
}
if self.editor.workspace_symbol_needs_request {
self.editor.workspace_symbol_needs_request = false;
self.request_workspace_symbols();
}
if let Some(action) = deferred {
self.handle_deferred(action);
}
}
self.notify_lsp_change();
ctx.request_repaint();
}
fn handle_deferred(&mut self, action: DeferredAction) {
match action {
DeferredAction::Rename(new_name) => { self.request_rename(&new_name); }
DeferredAction::DidSave => { self.notify_lsp_did_save(); }
DeferredAction::OpenFile(path) => { self.open_file(&path); }
DeferredAction::SyncFileUri => { self.sync_file_uri(); }
DeferredAction::ShellCommand(cmd) => {
match std::process::Command::new("sh").arg("-c").arg(&cmd).output() {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr_out = String::from_utf8_lossy(&output.stderr);
let msg = if !stdout.is_empty() {
stdout.lines().next().unwrap_or("").to_string()
} else if !stderr_out.is_empty() {
stderr_out.lines().next().unwrap_or("").to_string()
} else {
format!("Exit: {}", output.status.code().unwrap_or(-1))
};
self.editor.status_message = Some(msg);
}
Err(e) => {
self.editor.status_message = Some(format!("Error: {e}"));
}
}
}
DeferredAction::FormatDocument => { self.request_formatting(); }
DeferredAction::PlayMacro(ch) => { self.play_macro(ch); }
}
}
fn handle_mouse(&mut self, ctx: &egui::Context) {
let font_size = self.editor.config.gui_font_size;
let char_width = font_size * 0.6;
let line_height = font_size * 1.4;
let scroll_delta = ctx.input(|i| i.smooth_scroll_delta);
if scroll_delta.y.abs() > 0.1 {
let lines = (scroll_delta.y.abs() / line_height).ceil() as usize;
let lines = lines.max(3);
if scroll_delta.y > 0.0 {
self.editor.scroll_viewport_up(lines);
} else {
self.editor.scroll_viewport_down(lines);
}
ctx.request_repaint();
}
let clicked = ctx.input(|i| i.pointer.primary_clicked());
if !clicked {
return;
}
let click_pos = match ctx.input(|i| i.pointer.interact_pos()) {
Some(pos) => pos,
None => return,
};
if self.editor.showing_completion
|| self.editor.showing_hover
|| self.editor.showing_references
|| self.editor.showing_code_actions
|| self.editor.showing_diagnostics
|| self.editor.showing_file_finder
|| self.editor.showing_workspace_symbols
{
return;
}
let clicked_pane = self.last_pane_rects.iter().find(|(_, rect)| rect.contains(click_pos));
let (pane_id, pane_rect) = match clicked_pane {
Some(&(id, rect)) => (id, rect),
None => return,
};
if pane_id != self.editor.active_pane_id {
self.editor.save_active_pane();
self.editor.load_pane(pane_id);
self.sync_file_uri();
}
let gutter_width = self.editor.gutter_width();
let gutter_px = gutter_width as f32 * char_width;
let editor_rows = (self.editor.view.height) as f32 * line_height;
let editor_area_bottom = pane_rect.min.y + editor_rows;
if click_pos.y >= editor_area_bottom {
ctx.request_repaint();
return;
}
if click_pos.x < pane_rect.min.x + gutter_px {
ctx.request_repaint();
return;
}
let screen_col_f = (click_pos.x - pane_rect.min.x - gutter_px) / char_width;
let screen_row_f = (click_pos.y - pane_rect.min.y) / line_height;
let screen_col = screen_col_f.max(0.0) as usize;
let screen_row = screen_row_f.max(0.0) as usize;
if self.editor.config.wrap {
let text_width = self.editor.view.width.saturating_sub(gutter_width);
let screen_map = crate::editor::wrap::build_screen_map(
&self.editor.document.rope,
self.editor.view.offset_row,
self.editor.view.offset_wrap,
text_width,
self.editor.view.height,
);
if let Some(seg) = screen_map.get(screen_row) {
self.editor.cursor.row = seg.doc_row;
self.editor.cursor.col = seg.char_start + screen_col;
}
} else {
self.editor.cursor.row = self.editor.view.offset_row + screen_row;
self.editor.cursor.col = self.editor.view.offset_col + screen_col;
}
self.editor.clamp_cursor();
self.editor.scroll();
ctx.request_repaint();
}
fn play_macro(&mut self, ch: char) {
let keys = match self.editor.macros.get(&ch) {
Some(keys) => keys.clone(),
None => {
self.editor.status_message = Some(format!("Macro @{ch} is empty"));
return;
}
};
self.editor.last_macro = Some(ch);
for key in &keys {
if let Some(cmd) = keymap::map_key(&mut self.editor, *key) {
let deferred = input::execute(&mut self.editor, cmd);
if let Some(action) = deferred {
match action {
DeferredAction::PlayMacro(_) => {}
other => self.handle_deferred(other),
}
}
}
self.notify_lsp_change();
if self.editor.should_quit { break; }
}
}
fn notify_lsp_change(&mut self) {
let version = self.editor.document.version;
if version == self.last_notified_version {
return;
}
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let text = self.editor.document.rope.to_string();
let _ = self.runtime.block_on(lsp.did_change(uri, &text, version));
self.last_notified_version = version;
}
}
fn notify_lsp_did_save(&mut self) {
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let _ = self.runtime.block_on(lsp.did_save(uri));
}
}
fn request_completion(&mut self) {
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let line = self.editor.cursor.row as u32;
let character = self.editor.cursor.col as u32;
if let Ok(id) = self.runtime.block_on(lsp.completion(uri, line, character)) {
self.editor.pending_completion_id = Some(id);
}
}
}
fn request_goto_definition(&mut self) {
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let line = self.editor.cursor.row as u32;
let character = self.editor.cursor.col as u32;
if let Ok(id) = self.runtime.block_on(lsp.goto_definition(uri, line, character)) {
self.editor.pending_goto_id = Some(id);
}
}
}
fn request_hover(&mut self) {
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let line = self.editor.cursor.row as u32;
let character = self.editor.cursor.col as u32;
if let Ok(id) = self.runtime.block_on(lsp.hover(uri, line, character)) {
self.editor.pending_hover_id = Some(id);
}
}
}
fn request_references(&mut self) {
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let line = self.editor.cursor.row as u32;
let character = self.editor.cursor.col as u32;
if let Ok(id) = self.runtime.block_on(lsp.references(uri, line, character)) {
self.editor.pending_references_id = Some(id);
}
}
}
fn request_rename(&mut self, new_name: &str) {
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let line = self.editor.cursor.row as u32;
let character = self.editor.cursor.col as u32;
if let Ok(id) = self.runtime.block_on(lsp.rename(uri, line, character, new_name)) {
self.editor.pending_rename_id = Some(id);
}
}
}
fn request_workspace_symbols(&mut self) {
if let Some(lsp) = &mut self.lsp_client {
if !lsp.initialized { return; }
let query = self.editor.workspace_symbol_query.clone();
if let Ok(id) = self.runtime.block_on(lsp.workspace_symbol(&query)) {
self.editor.pending_workspace_symbol_id = Some(id);
}
}
}
fn jump_to_workspace_symbol(&mut self) {
if let Some(sym) = self.editor.workspace_symbol_selected() {
self.editor.workspace_symbol_cancel();
self.editor.push_jump();
let current_uri = self.file_uri.as_deref().unwrap_or("");
if sym.uri == current_uri {
self.editor.cursor.row = sym.start_line as usize;
self.editor.cursor.col = sym.start_col as usize;
self.editor.clamp_cursor();
self.editor.scroll();
} else if let Some(path) = lsp::uri_to_path(&sym.uri) {
let target_line = sym.start_line;
let target_col = sym.start_col;
self.open_file(&path);
self.editor.cursor.row = target_line as usize;
self.editor.cursor.col = target_col as usize;
self.editor.clamp_cursor();
self.editor.scroll();
} else {
let name = sym.uri.rsplit('/').next().unwrap_or(&sym.uri);
self.editor.status_message = Some(format!(
"Symbol in {}:{}:{}",
name, sym.start_line + 1, sym.start_col + 1
));
}
}
}
fn request_code_action(&mut self) {
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let line = self.editor.cursor.row as u32;
let character = self.editor.cursor.col as u32;
let diagnostics = self.editor.diagnostics.clone();
if let Ok(id) = self.runtime.block_on(lsp.code_action(uri, line, character, &diagnostics)) {
self.editor.pending_code_action_id = Some(id);
}
}
}
fn accept_code_action(&mut self) {
if self.editor.code_actions.is_empty() {
self.editor.dismiss_code_actions();
return;
}
let action = self.editor.code_actions[self.editor.code_action_index].clone();
self.editor.dismiss_code_actions();
if let Some(ref edit) = action.edit {
self.apply_workspace_edit(edit);
}
self.editor.status_message = Some(format!("Applied: {}", action.title));
self.notify_lsp_change();
}
fn apply_workspace_edit(&mut self, edit: &serde_json::Value) {
let file_uri = match &self.file_uri {
Some(uri) => uri.clone(),
None => return,
};
let mut text_edits: Vec<lsp::LspTextEdit> = Vec::new();
if let Some(changes) = edit.get("changes").and_then(|c| c.as_object())
&& let Some(file_edits) = changes.get(&file_uri).and_then(|e| e.as_array()) {
for e in file_edits {
if let Some(te) = lsp::parse_text_edit(e) { text_edits.push(te); }
}
}
if text_edits.is_empty()
&& let Some(doc_changes) = edit.get("documentChanges").and_then(|c| c.as_array()) {
for dc in doc_changes {
let uri = dc.get("textDocument").and_then(|td| td.get("uri")).and_then(|u| u.as_str());
if uri == Some(&file_uri)
&& let Some(edit_arr) = dc.get("edits").and_then(|e| e.as_array()) {
for e in edit_arr {
if let Some(te) = lsp::parse_text_edit(e) { text_edits.push(te); }
}
}
}
}
if text_edits.is_empty() { return; }
self.editor.history.save(&self.editor.document.rope, self.editor.cursor);
text_edits.sort_by(|a, b| b.start_line.cmp(&a.start_line).then(b.start_col.cmp(&a.start_col)));
for te in &text_edits {
let line_count = self.editor.document.rope.len_lines();
if (te.start_line as usize) >= line_count { continue; }
let end_line = (te.end_line as usize).min(line_count.saturating_sub(1));
let start_idx = self.editor.document.rope.line_to_char(te.start_line as usize) + te.start_col as usize;
let end_idx = self.editor.document.rope.line_to_char(end_line) + te.end_col as usize;
let end_idx = end_idx.min(self.editor.document.rope.len_chars());
let start_idx = start_idx.min(self.editor.document.rope.len_chars());
if start_idx < end_idx { self.editor.document.rope.remove(start_idx..end_idx); }
if !te.new_text.is_empty() { self.editor.document.rope.insert(start_idx, &te.new_text); }
}
self.editor.document.modified = true;
self.editor.document.bump_version();
self.editor.clamp_cursor();
}
fn request_formatting(&mut self) {
if let (Some(lsp), Some(uri)) = (&mut self.lsp_client, &self.file_uri) {
if !lsp.initialized { return; }
let params = serde_json::json!({
"textDocument": { "uri": uri },
"options": { "tabSize": 4, "insertSpaces": true }
});
match self.runtime.block_on(lsp.send_request("textDocument/formatting", params)) {
Ok(id) => { self.editor.pending_format_id = Some(id); }
Err(_) => { self.editor.status_message = Some("Format request failed".to_string()); }
}
}
}
fn jump_to_reference(&mut self) {
if self.editor.references.is_empty() { return; }
let loc = &self.editor.references[self.editor.reference_index];
let current_uri = self.file_uri.as_deref().unwrap_or("");
if loc.uri == current_uri {
self.editor.cursor.row = loc.start_line as usize;
self.editor.cursor.col = loc.start_col as usize;
self.editor.clamp_cursor();
self.editor.scroll();
self.editor.dismiss_popup();
} else {
let name = loc.uri.rsplit('/').next().unwrap_or(&loc.uri);
self.editor.status_message = Some(format!("Reference in {}:{}:{}", name, loc.start_line + 1, loc.start_col + 1));
self.editor.dismiss_popup();
}
}
fn sync_file_uri(&mut self) {
if let (Some(lsp), Some(old_uri)) = (&mut self.lsp_client, &self.file_uri)
&& lsp.initialized {
let _ = self.runtime.block_on(lsp.send_notification(
"textDocument/didClose",
serde_json::json!({"textDocument": {"uri": old_uri}}),
));
}
self.last_notified_version = self.editor.document.version;
if let Some(path) = &self.editor.document.path {
if let Some(lsp) = &mut self.lsp_client
&& lsp.initialized {
let uri = lsp::path_to_uri(path);
let text = self.editor.document.rope.to_string();
let version = self.editor.document.version;
let _ = self.runtime.block_on(lsp.did_open(&uri, &text, version));
self.file_uri = Some(uri);
}
} else {
self.file_uri = None;
}
}
fn open_file(&mut self, rel_path: &str) {
let root = if let Some(path) = &self.editor.document.path {
lsp::find_project_root(path)
} else {
std::env::current_dir().unwrap_or_default()
};
let full_path = root.join(rel_path);
if let Some(idx) = self.editor.find_buffer_by_path(&full_path) {
if idx != self.editor.current_buffer {
self.editor.switch_buffer(idx);
self.sync_file_uri();
}
return;
}
match Document::open(&full_path.to_string_lossy()) {
Ok(doc) => {
self.editor.add_buffer(doc);
self.editor.status_message = Some(format!("\"{}\"", self.editor.document.file_name()));
self.last_notified_version = self.editor.document.version;
if let Some(lsp) = &mut self.lsp_client
&& lsp.initialized {
let uri = lsp::path_to_uri(&full_path);
let text = self.editor.document.rope.to_string();
let version = self.editor.document.version;
let _ = self.runtime.block_on(lsp.did_open(&uri, &text, version));
self.file_uri = Some(uri);
}
}
Err(e) => {
self.editor.status_message = Some(format!("Error opening file: {e}"));
}
}
}
fn scan_project_files(&self) -> Vec<String> {
let root = if let Some(path) = &self.editor.document.path {
lsp::find_project_root(path)
} else {
std::env::current_dir().unwrap_or_default()
};
let mut files = Vec::new();
Self::walk_dir(&root, &root, &mut files);
files.sort();
files
}
fn walk_dir(root: &Path, dir: &Path, out: &mut Vec<String>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') { continue; }
if matches!(name_str.as_ref(), "target" | "node_modules" | "build" | "dist" | "__pycache__") { continue; }
if path.is_dir() {
Self::walk_dir(root, &path, out);
} else if path.is_file()
&& let Ok(rel) = path.strip_prefix(root) {
out.push(rel.to_string_lossy().to_string());
}
}
}
}
impl eframe::App for GuiApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if self.editor.font_family_changed {
self.editor.font_family_changed = false;
match &self.editor.config.gui_font_family {
Some(font_name) => {
match find_font_file(font_name) {
Some(path) => {
if let Err(e) = setup_custom_font(ctx, &path) {
self.editor.status_message = Some(e);
}
}
None => {
ctx.set_fonts(egui::FontDefinitions::default());
self.editor.status_message =
Some(format!("Font not found: {font_name} (reset to default)"));
self.editor.config.gui_font_family = None;
}
}
}
None => {
ctx.set_fonts(egui::FontDefinitions::default());
}
}
}
self.process_lsp_messages();
let events: Vec<egui::Event> = ctx.input(|i| i.events.clone());
for event in &events {
match event {
egui::Event::Key { key, pressed: true, modifiers, .. } => {
let key_input = egui_key_to_key_input(*key, *modifiers);
if let Some(ki) = key_input {
self.handle_key(ki, ctx);
}
}
egui::Event::Text(text) => {
if matches!(self.editor.mode,
crate::input::mode::Mode::Insert
| crate::input::mode::Mode::Command
| crate::input::mode::Mode::Search
) {
for ch in text.chars() {
if !ch.is_control() {
let ki = KeyInput { code: KeyCode::Char(ch), ctrl: false };
self.handle_key(ki, ctx);
}
}
} else {
for ch in text.chars() {
if !ch.is_control() {
let ki = KeyInput { code: KeyCode::Char(ch), ctrl: false };
self.handle_key(ki, ctx);
}
}
}
}
_ => {}
}
}
let avail = ctx.available_rect();
let font_size = self.editor.config.gui_font_size;
let char_width = font_size * 0.6;
let line_height = font_size * 1.4;
let cols = (avail.width() / char_width) as u16;
let rows = (avail.height() / line_height) as u16;
let tab_rows: u16 = if self.editor.buffers.len() > 1 { 1 } else { 0 };
let command_rows: u16 = 1;
let pane_area = AreaRect::new(0, tab_rows, cols, rows.saturating_sub(tab_rows + command_rows));
self.editor.editor_area = pane_area;
let pane_rects = self.editor.pane_layout.layout(pane_area);
for &(pane_id, rect) in &pane_rects {
let editor_height = rect.height.saturating_sub(1);
let editor_width = rect.width;
if pane_id == self.editor.active_pane_id {
self.editor.view.width = editor_width;
self.editor.view.height = editor_height;
} else if let Some(pane) = self.editor.panes.iter_mut().find(|p| p.id == pane_id) {
pane.view.width = editor_width;
pane.view.height = editor_height;
}
}
self.editor.scroll();
self.editor.update_highlights();
self.handle_mouse(ctx);
self.last_pane_rects = crate::gui::render(&self.editor, ctx);
if self.editor.should_quit {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
}
fn egui_key_to_key_input(key: egui::Key, modifiers: egui::Modifiers) -> Option<KeyInput> {
let ctrl = modifiers.ctrl || modifiers.mac_cmd;
let code = match key {
egui::Key::Escape => Some(KeyCode::Esc),
egui::Key::Enter => Some(KeyCode::Enter),
egui::Key::Backspace => Some(KeyCode::Backspace),
egui::Key::Tab => {
if modifiers.shift {
Some(KeyCode::BackTab)
} else {
Some(KeyCode::Tab)
}
}
egui::Key::ArrowUp => Some(KeyCode::Up),
egui::Key::ArrowDown => Some(KeyCode::Down),
egui::Key::ArrowLeft => Some(KeyCode::Left),
egui::Key::ArrowRight => Some(KeyCode::Right),
_ if ctrl => {
match key {
egui::Key::A => Some(KeyCode::Char('a')),
egui::Key::B => Some(KeyCode::Char('b')),
egui::Key::C => Some(KeyCode::Char('c')),
egui::Key::D => Some(KeyCode::Char('d')),
egui::Key::F => Some(KeyCode::Char('f')),
egui::Key::I => Some(KeyCode::Char('i')),
egui::Key::O => Some(KeyCode::Char('o')),
egui::Key::P => Some(KeyCode::Char('p')),
egui::Key::R => Some(KeyCode::Char('r')),
egui::Key::T => Some(KeyCode::Char('t')),
egui::Key::U => Some(KeyCode::Char('u')),
egui::Key::W => Some(KeyCode::Char('w')),
egui::Key::X => Some(KeyCode::Char('x')),
egui::Key::Space => Some(KeyCode::Char(' ')),
_ => None,
}
}
_ => None,
};
code.map(|c| KeyInput { code: c, ctrl })
}