Added file saving

This commit is contained in:
Joshua Barretto 2025-06-16 23:19:56 +01:00
parent e9e938ca4a
commit bc2cff34d4
6 changed files with 80 additions and 16 deletions

View file

@ -34,6 +34,7 @@ pub enum Action {
CommandStart(&'static str), // Start a new command CommandStart(&'static str), // Start a new command
GotoLine(isize), // Go to the specified file line GotoLine(isize), // Go to the specified file line
SelectToken, // Fully select the token under the cursor SelectToken, // Fully select the token under the cursor
Save, // Save the current buffer
} }
#[derive(Debug)] #[derive(Debug)]
@ -349,4 +350,20 @@ impl RawEvent {
None None
} }
} }
pub fn to_save(&self) -> Option<Action> {
if matches!(
&self.0,
TerminalEvent::Key(KeyEvent {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
})
) {
Some(Action::Save)
} else {
None
}
}
} }

View file

@ -128,6 +128,7 @@ impl Text {
#[derive(Default)] #[derive(Default)]
pub struct Buffer { pub struct Buffer {
pub unsaved: bool,
pub text: Text, pub text: Text,
pub highlights: Option<Highlights>, pub highlights: Option<Highlights>,
pub cursors: HopSlotMap<CursorId, Cursor>, pub cursors: HopSlotMap<CursorId, Cursor>,
@ -137,11 +138,11 @@ pub struct Buffer {
impl Buffer { impl Buffer {
pub fn from_file(path: PathBuf) -> Result<Self, Error> { pub fn from_file(path: PathBuf) -> Result<Self, Error> {
let (dir, chars, s) = match std::fs::read_to_string(&path) { let (unsaved, dir, chars, s) = match std::fs::read_to_string(&path) {
Ok(s) => { Ok(s) => {
let mut path = path.canonicalize()?; let mut path = path.canonicalize()?;
path.pop(); path.pop();
(Some(path), s.chars().collect(), s) (false, Some(path), s.chars().collect(), s)
} }
// If the file doesn't exist, create a new file // If the file doesn't exist, create a new file
Err(err) if err.kind() == io::ErrorKind::NotFound => { Err(err) if err.kind() == io::ErrorKind::NotFound => {
@ -150,11 +151,12 @@ impl Buffer {
.filter(|p| p.to_str() != Some("")) .filter(|p| p.to_str() != Some(""))
.map(Path::to_owned) .map(Path::to_owned)
.or_else(|| std::env::current_dir().ok()); .or_else(|| std::env::current_dir().ok());
(dir, Vec::new(), String::new()) (true, dir, Vec::new(), String::new())
} }
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };
Ok(Self { Ok(Self {
unsaved,
highlights: Highlighter::from_file_name(&path).map(|h| h.highlight(&chars)), highlights: Highlighter::from_file_name(&path).map(|h| h.highlight(&chars)),
text: Text { chars }, text: Text { chars },
cursors: HopSlotMap::default(), cursors: HopSlotMap::default(),
@ -163,11 +165,22 @@ impl Buffer {
}) })
} }
pub fn name(&self) -> Option<&str> { pub fn save(&mut self) -> Result<(), Error> {
if self.unsaved {
std::fs::write(
self.path.as_ref().expect("buffer must have path to save"),
self.text.to_string(),
)?;
self.unsaved = false;
}
Ok(())
}
pub fn name(&self) -> Option<String> {
Some( Some(
match self.path.as_ref()?.file_name().and_then(|n| n.to_str()) { match self.path.as_ref()?.file_name().and_then(|n| n.to_str()) {
Some(name) => name, Some(name) => format!("{}{name}", if self.unsaved { "* " } else { "" }),
None => "<error>", None => "<error>".to_string(),
}, },
) )
} }
@ -180,6 +193,8 @@ impl Buffer {
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.unsaved = true;
self.text.chars.clear(); self.text.chars.clear();
self.update_highlights(); self.update_highlights();
// Reset cursors // Reset cursors
@ -299,6 +314,8 @@ impl Buffer {
} }
pub fn insert(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) { pub fn insert(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) {
self.unsaved = true;
let mut n = 0; let mut n = 0;
for c in chars { for c in chars {
self.text self.text
@ -332,6 +349,8 @@ impl Buffer {
// Assumes range is well-formed // Assumes range is well-formed
pub fn remove(&mut self, range: Range<usize>) { pub fn remove(&mut self, range: Range<usize>) {
self.unsaved = true;
// TODO: Bell if false? // TODO: Bell if false?
self.text.chars.drain(range.clone()); self.text.chars.drain(range.clone());
self.update_highlights(); self.update_highlights();
@ -409,6 +428,17 @@ impl TryFrom<Args> for State {
} }
impl State { impl State {
pub fn open_or_get(&mut self, path: PathBuf) -> Result<BufferId, Error> {
let true_path = path.canonicalize()?;
if let Some((buffer_id, _)) = self.buffers.iter().find(|(_, b)| {
b.path.as_ref().and_then(|p| p.canonicalize().ok()).as_ref() == Some(&true_path)
}) {
Ok(buffer_id)
} else {
Ok(self.buffers.insert(Buffer::from_file(path)?))
}
}
pub fn tick(&mut self) { pub fn tick(&mut self) {
self.tick += 1; self.tick += 1;
} }

View file

@ -36,6 +36,7 @@ pub struct Theme {
pub ui_bg: Color, pub ui_bg: Color,
pub select_bg: Color, pub select_bg: Color,
pub unfocus_select_bg: Color, pub unfocus_select_bg: Color,
pub search_result_bg: Color,
pub margin_bg: Color, pub margin_bg: Color,
pub margin_line_num: Color, pub margin_line_num: Color,
pub border: BorderTheme, pub border: BorderTheme,
@ -69,6 +70,7 @@ impl Default for Theme {
ui_bg: Color::AnsiValue(235), ui_bg: Color::AnsiValue(235),
select_bg: Color::AnsiValue(23), select_bg: Color::AnsiValue(23),
unfocus_select_bg: Color::AnsiValue(240), unfocus_select_bg: Color::AnsiValue(240),
search_result_bg: Color::AnsiValue(124),
margin_bg: Color::Reset, margin_bg: Color::Reset,
margin_line_num: Color::AnsiValue(245), margin_line_num: Color::AnsiValue(245),
border: BorderTheme::default(), border: BorderTheme::default(),

View file

@ -1,6 +1,6 @@
use super::*; use super::*;
use crate::{ use crate::{
state::{Buffer, BufferId, CursorId}, state::{Buffer, BufferId, Cursor, CursorId},
terminal::CursorStyle, terminal::CursorStyle,
}; };
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
@ -72,20 +72,20 @@ impl Element for Doc {
.or_else(|| e.to_open_opener(open_path)) .or_else(|| e.to_open_opener(open_path))
.or_else(|| e.to_open_finder()) .or_else(|| e.to_open_finder())
.or_else(|| e.to_move()) .or_else(|| e.to_move())
.or_else(|| e.to_save())
}) { }) {
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))), action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))),
action @ Some(Action::OpenOpener(_)) => Ok(Resp::handled(action.map(Into::into))), action @ Some(Action::OpenOpener(_)) => Ok(Resp::handled(action.map(Into::into))),
action @ Some(Action::OpenFinder) => { action @ Some(Action::OpenFinder) => {
self.search = Some(Search::new()); self.search = Some(Search::new(buffer.cursors[cursor_id]));
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::SwitchBuffer(new_buffer)) => { Some(Action::SwitchBuffer(new_buffer)) => {
self.switch_buffer(state, new_buffer); self.switch_buffer(state, new_buffer);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::OpenFile(path)) => match Buffer::from_file(path) { Some(Action::OpenFile(path)) => match state.open_or_get(path) {
Ok(buffer) => { Ok(buffer_id) => {
let buffer_id = state.buffers.insert(buffer);
self.switch_buffer(state, buffer_id); self.switch_buffer(state, buffer_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
@ -93,6 +93,12 @@ impl Element for Doc {
Action::Show(Some(format!("Could not open file")), format!("{err}")).into(), Action::Show(Some(format!("Could not open file")), format!("{err}")).into(),
))), ))),
}, },
Some(Action::Save) => {
let event = buffer.save().err().map(|err| {
Action::Show(Some("Could not save file".to_string()), err.to_string()).into()
});
Ok(Resp::handled(event))
}
_ => { _ => {
let Some(buffer) = state.buffers.get_mut(self.buffer) else { let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return Err(event); return Err(event);
@ -122,7 +128,7 @@ impl Visual for Doc {
.with(|f| { .with(|f| {
self.input.render( self.input.render(
state, state,
buffer.name(), buffer.name().as_deref(),
buffer, buffer,
cursor_id, cursor_id,
self.search.as_ref(), self.search.as_ref(),
@ -144,6 +150,8 @@ impl Visual for Doc {
} }
pub struct Search { pub struct Search {
old_cursor: Cursor,
buffer: Buffer, buffer: Buffer,
cursor_id: CursorId, cursor_id: CursorId,
input: Input, input: Input,
@ -154,9 +162,11 @@ pub struct Search {
} }
impl Search { impl Search {
fn new() -> Self { fn new(old_cursor: Cursor) -> Self {
let mut buffer = Buffer::default(); let mut buffer = Buffer::default();
Self { Self {
old_cursor,
cursor_id: buffer.start_session(), cursor_id: buffer.start_session(),
buffer, buffer,
input: Input::filter(), input: Input::filter(),
@ -192,7 +202,12 @@ impl Search {
let res = match event let res = match event
.to_action(|e| e.to_cancel().or_else(|| e.to_go()).or_else(|| e.to_move())) .to_action(|e| e.to_cancel().or_else(|| e.to_go()).or_else(|| e.to_move()))
{ {
Some(Action::Cancel | Action::Go) => return Ok(Resp::end(None)), Some(Action::Cancel) => {
buffer.cursors[cursor_id] = self.old_cursor;
input.refocus(buffer, cursor_id);
return Ok(Resp::end(None));
}
Some(Action::Go) => return Ok(Resp::end(None)),
Some(Action::Move(dir, false, _)) => { Some(Action::Move(dir, false, _)) => {
match dir { match dir {
Dir::Up => { Dir::Up => {

View file

@ -192,7 +192,7 @@ impl Input {
let bg = if let Some(s) = search { let bg = if let Some(s) = search {
match s.contains(pos) { match s.contains(pos) {
Some(true) => state.theme.select_bg, Some(true) => state.theme.select_bg,
Some(false) => state.theme.unfocus_select_bg, Some(false) => state.theme.search_result_bg,
None => Color::Reset, None => Color::Reset,
} }
} else if !selected { } else if !selected {

View file

@ -268,7 +268,7 @@ impl Visual for BufferId {
let Some(buffer) = state.buffers.get(*self) else { let Some(buffer) = state.buffers.get(*self) else {
return; return;
}; };
frame.text([0, 0], buffer.name().unwrap_or("<unknown>")); frame.text([0, 0], buffer.name().as_deref().unwrap_or("<unknown>"));
} }
} }