botserver/src/console/editor.rs
Rodrigo Rodriguez (Pragmatismo) 5da86bbef2 Fix clippy warnings: match arms, async/await, Debug impls, formatting
- Fix match arms with identical bodies by consolidating patterns
- Fix case-insensitive file extension comparisons using eq_ignore_ascii_case
- Fix unnecessary Debug formatting in log/format macros
- Fix clone_from usage instead of clone assignment
- Fix let...else patterns where appropriate
- Fix format! append to String using write! macro
- Fix unwrap_or with function calls to use unwrap_or_else
- Add missing fields to manual Debug implementations
- Fix duplicate code in if blocks
- Add type aliases for complex types
- Rename struct fields to avoid common prefixes
- Various other clippy warning fixes

Note: Some 'unused async' warnings remain for functions that are
called with .await but don't contain await internally - these are
kept async for API compatibility.
2025-12-26 08:59:25 -03:00

237 lines
8.1 KiB
Rust

use crate::shared::state::AppState;
use color_eyre::Result;
use std::sync::Arc;
pub struct Editor {
file_path: String,
bucket: String,
key: String,
content: String,
cursor_pos: usize,
scroll_offset: usize,
visible_lines: usize,
modified: bool,
}
impl std::fmt::Debug for Editor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Editor")
.field("file_path", &self.file_path)
.field("bucket", &self.bucket)
.field("key", &self.key)
.field("content_len", &self.content.len())
.field("cursor_pos", &self.cursor_pos)
.field("scroll_offset", &self.scroll_offset)
.field("visible_lines", &self.visible_lines)
.field("modified", &self.modified)
.finish()
}
}
impl Editor {
pub async fn load(app_state: &Arc<AppState>, bucket: &str, path: &str) -> Result<Self> {
let content = if let Some(drive) = &app_state.drive {
match drive.get_object().bucket(bucket).key(path).send().await {
Ok(response) => {
let bytes = response.body.collect().await?.into_bytes();
String::from_utf8_lossy(&bytes).to_string()
}
Err(_) => String::new(),
}
} else {
String::new()
};
Ok(Self {
file_path: format!("{}/{}", bucket, path),
bucket: bucket.to_string(),
key: path.to_string(),
content,
cursor_pos: 0,
scroll_offset: 0,
visible_lines: 20,
modified: false,
})
}
pub async fn save(&mut self, app_state: &Arc<AppState>) -> Result<()> {
if let Some(drive) = &app_state.drive {
drive
.put_object()
.bucket(&self.bucket)
.key(&self.key)
.body(self.content.as_bytes().to_vec().into())
.send()
.await?;
self.modified = false;
}
Ok(())
}
pub fn file_path(&self) -> &str {
&self.file_path
}
pub fn set_visible_lines(&mut self, lines: usize) {
self.visible_lines = lines.max(5);
}
pub fn visible_lines(&self) -> usize {
self.visible_lines
}
fn get_cursor_line(&self) -> usize {
self.content[..self.cursor_pos].lines().count()
}
fn ensure_cursor_visible(&mut self) {
let cursor_line = self.get_cursor_line();
if cursor_line < self.scroll_offset {
self.scroll_offset = cursor_line;
}
let visible = self.visible_lines.saturating_sub(3);
if cursor_line >= self.scroll_offset + visible {
self.scroll_offset = cursor_line.saturating_sub(visible) + 1;
}
}
pub fn render(&self, cursor_blink: bool) -> String {
let lines: Vec<&str> = self.content.lines().collect();
let total_lines = lines.len().max(1);
let visible_lines = self.visible_lines();
let cursor_line = self.get_cursor_line();
let cursor_col = self.content[..self.cursor_pos]
.lines()
.last()
.map(|line| line.len())
.unwrap_or(0);
let start = self.scroll_offset;
let end = (start + visible_lines).min(total_lines);
let mut display_lines = Vec::new();
for i in start..end {
let line_num = i + 1;
let line_content = if i < lines.len() { lines[i] } else { "" };
let is_cursor_line = i == cursor_line;
let cursor_indicator = if is_cursor_line && cursor_blink {
let spaces = " ".repeat(cursor_col);
format!("{}", spaces)
} else {
String::new()
};
display_lines.push(format!(
" {:4}{}{}",
line_num, line_content, cursor_indicator
));
}
if display_lines.is_empty() {
let cursor_indicator = if cursor_blink { "" } else { "" };
display_lines.push(format!(" 1 │ {}", cursor_indicator));
}
display_lines.push("".to_string());
display_lines
.push("─────────────────────────────────────────────────────────────".to_string());
let status = if self.modified { "MODIFIED" } else { "SAVED" };
display_lines.push(format!(
" {} {} │ Line: {}, Col: {}",
status,
self.file_path,
cursor_line + 1,
cursor_col + 1
));
display_lines.push(" Ctrl+S: Save │ Ctrl+W: Close │ Esc: Close without saving".to_string());
display_lines.join("\n")
}
pub fn move_up(&mut self) {
if let Some(prev_line_end) = self.content[..self.cursor_pos].rfind('\n') {
if let Some(prev_prev_line_end) = self.content[..prev_line_end].rfind('\n') {
let target_pos = prev_prev_line_end
+ 1
+ (self.cursor_pos - prev_line_end - 1)
.min(self.content[prev_prev_line_end + 1..prev_line_end].len());
self.cursor_pos = target_pos;
} else {
self.cursor_pos = (self.cursor_pos - prev_line_end - 1).min(prev_line_end);
}
}
self.ensure_cursor_visible();
}
pub fn move_down(&mut self) {
if let Some(next_line_start) = self.content[self.cursor_pos..].find('\n') {
let current_line_start = self.content[..self.cursor_pos]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or(0);
let next_line_absolute = self.cursor_pos + next_line_start + 1;
if let Some(next_next_line_start) = self.content[next_line_absolute..].find('\n') {
let target_pos = next_line_absolute
+ (self.cursor_pos - current_line_start).min(next_next_line_start);
self.cursor_pos = target_pos;
} else {
let target_pos = next_line_absolute
+ (self.cursor_pos - current_line_start)
.min(self.content[next_line_absolute..].len());
self.cursor_pos = target_pos;
}
}
self.ensure_cursor_visible();
}
pub fn page_up(&mut self) {
for _ in 0..self.visible_lines.saturating_sub(2) {
if self.content[..self.cursor_pos].rfind('\n').is_none() {
break;
}
self.move_up();
}
}
pub fn page_down(&mut self) {
for _ in 0..self.visible_lines.saturating_sub(2) {
if self.content[self.cursor_pos..].find('\n').is_none() {
break;
}
self.move_down();
}
}
pub fn move_left(&mut self) {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
}
}
pub fn move_right(&mut self) {
if self.cursor_pos < self.content.len() {
self.cursor_pos += 1;
}
}
pub fn insert_char(&mut self, c: char) {
self.modified = true;
self.content.insert(self.cursor_pos, c);
self.cursor_pos += 1;
}
pub fn backspace(&mut self) {
if self.cursor_pos > 0 {
self.modified = true;
self.content.remove(self.cursor_pos - 1);
self.cursor_pos -= 1;
}
}
pub fn insert_newline(&mut self) {
self.modified = true;
self.content.insert(self.cursor_pos, '\n');
self.cursor_pos += 1;
self.ensure_cursor_visible();
}
pub fn goto_line(&mut self, line: usize) {
let lines: Vec<&str> = self.content.lines().collect();
let target_line = line.saturating_sub(1).min(lines.len().saturating_sub(1));
self.cursor_pos = 0;
for (i, line_content) in lines.iter().enumerate() {
if i == target_line {
break;
}
self.cursor_pos += line_content.len() + 1;
}
self.ensure_cursor_visible();
}
}