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
GotoLine(isize), // Go to the specified file line
SelectToken, // Fully select the token under the cursor
Save, // Save the current buffer
}
#[derive(Debug)]
@ -349,4 +350,20 @@ impl RawEvent {
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)]
pub struct Buffer {
pub unsaved: bool,
pub text: Text,
pub highlights: Option<Highlights>,
pub cursors: HopSlotMap<CursorId, Cursor>,
@ -137,11 +138,11 @@ pub struct Buffer {
impl Buffer {
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) => {
let mut path = path.canonicalize()?;
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
Err(err) if err.kind() == io::ErrorKind::NotFound => {
@ -150,11 +151,12 @@ impl Buffer {
.filter(|p| p.to_str() != Some(""))
.map(Path::to_owned)
.or_else(|| std::env::current_dir().ok());
(dir, Vec::new(), String::new())
(true, dir, Vec::new(), String::new())
}
Err(err) => return Err(err.into()),
};
Ok(Self {
unsaved,
highlights: Highlighter::from_file_name(&path).map(|h| h.highlight(&chars)),
text: Text { chars },
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(
match self.path.as_ref()?.file_name().and_then(|n| n.to_str()) {
Some(name) => name,
None => "<error>",
Some(name) => format!("{}{name}", if self.unsaved { "* " } else { "" }),
None => "<error>".to_string(),
},
)
}
@ -180,6 +193,8 @@ impl Buffer {
}
pub fn clear(&mut self) {
self.unsaved = true;
self.text.chars.clear();
self.update_highlights();
// Reset cursors
@ -299,6 +314,8 @@ impl Buffer {
}
pub fn insert(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) {
self.unsaved = true;
let mut n = 0;
for c in chars {
self.text
@ -332,6 +349,8 @@ impl Buffer {
// Assumes range is well-formed
pub fn remove(&mut self, range: Range<usize>) {
self.unsaved = true;
// TODO: Bell if false?
self.text.chars.drain(range.clone());
self.update_highlights();
@ -409,6 +428,17 @@ impl TryFrom<Args> for 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) {
self.tick += 1;
}

View file

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

View file

@ -1,6 +1,6 @@
use super::*;
use crate::{
state::{Buffer, BufferId, CursorId},
state::{Buffer, BufferId, Cursor, CursorId},
terminal::CursorStyle,
};
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_finder())
.or_else(|| e.to_move())
.or_else(|| e.to_save())
}) {
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::OpenFinder) => {
self.search = Some(Search::new());
self.search = Some(Search::new(buffer.cursors[cursor_id]));
Ok(Resp::handled(None))
}
Some(Action::SwitchBuffer(new_buffer)) => {
self.switch_buffer(state, new_buffer);
Ok(Resp::handled(None))
}
Some(Action::OpenFile(path)) => match Buffer::from_file(path) {
Ok(buffer) => {
let buffer_id = state.buffers.insert(buffer);
Some(Action::OpenFile(path)) => match state.open_or_get(path) {
Ok(buffer_id) => {
self.switch_buffer(state, buffer_id);
Ok(Resp::handled(None))
}
@ -93,6 +93,12 @@ impl Element for Doc {
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 {
return Err(event);
@ -122,7 +128,7 @@ impl Visual for Doc {
.with(|f| {
self.input.render(
state,
buffer.name(),
buffer.name().as_deref(),
buffer,
cursor_id,
self.search.as_ref(),
@ -144,6 +150,8 @@ impl Visual for Doc {
}
pub struct Search {
old_cursor: Cursor,
buffer: Buffer,
cursor_id: CursorId,
input: Input,
@ -154,9 +162,11 @@ pub struct Search {
}
impl Search {
fn new() -> Self {
fn new(old_cursor: Cursor) -> Self {
let mut buffer = Buffer::default();
Self {
old_cursor,
cursor_id: buffer.start_session(),
buffer,
input: Input::filter(),
@ -192,7 +202,12 @@ impl Search {
let res = match event
.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, _)) => {
match dir {
Dir::Up => {

View file

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

View file

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