Added saving, save-as
This commit is contained in:
parent
1456571671
commit
2b4464debd
8 changed files with 210 additions and 121 deletions
|
|
@ -10,6 +10,8 @@
|
|||
- [x] Multiple panes
|
||||
- [x] Pane creation/deletion
|
||||
- [x] Opener
|
||||
- [x] Save
|
||||
- [x] Save as
|
||||
- [x] Find
|
||||
- [x] Search in buffer switcher
|
||||
- [x] File saving
|
||||
|
|
@ -38,6 +40,7 @@ Buffers represent open files and are tracked independently of view panes.
|
|||
```
|
||||
Ctrl + o = Open buffer
|
||||
Ctrl + s = Save buffer
|
||||
Ctrl + Shift + s = Save buffer as
|
||||
Ctrl + b = Switch buffer
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ pub enum Action {
|
|||
OpenSwitcher,
|
||||
// Open the file opener
|
||||
OpenOpener(PathBuf),
|
||||
OpenSaver(PathBuf),
|
||||
// Open the finder, with the given default query
|
||||
OpenFinder(Option<String>),
|
||||
// Switch the current pane to the given buffer
|
||||
|
|
@ -57,6 +58,8 @@ pub enum Action {
|
|||
OpenFile(PathBuf, Option<usize>),
|
||||
// Create a new file and switch the current pane to it
|
||||
CreateFile(PathBuf),
|
||||
// Save the file in the current pane to the possibly-new path
|
||||
SaveFileAs(PathBuf),
|
||||
// Start a new command
|
||||
CommandStart(&'static str),
|
||||
// Go to the specified file line
|
||||
|
|
@ -73,6 +76,8 @@ pub enum Action {
|
|||
Save,
|
||||
// Save the current buffer, forcefully
|
||||
Overwrite,
|
||||
// Save the current buffer as a new path, forcefully
|
||||
OverwriteFileAs(PathBuf),
|
||||
// Reload the current file from disk, losing unsaved changes
|
||||
Reload,
|
||||
// (action, pos, is_ctrl, drag_id)
|
||||
|
|
@ -329,7 +334,7 @@ impl RawEvent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_open_opener(&self, path: &PathBuf) -> Option<Action> {
|
||||
pub fn to_open_browser(&self, path: &PathBuf) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
|
|
@ -340,6 +345,16 @@ impl RawEvent {
|
|||
})
|
||||
) {
|
||||
Some(Action::OpenOpener(path.clone()))
|
||||
} else if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Char('s'),
|
||||
modifiers,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
}) if *modifiers == KeyModifiers::CONTROL | KeyModifiers::SHIFT
|
||||
) {
|
||||
Some(Action::OpenSaver(path.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
15
src/state.rs
15
src/state.rs
|
|
@ -229,22 +229,31 @@ impl Buffer {
|
|||
Ok(Self::file(unsaved, chars, path))
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Error> {
|
||||
pub fn save_as(&mut self, path: PathBuf) -> Result<(), Error> {
|
||||
// Ensure trailing newline exists
|
||||
if self.text.chars.last().map_or(false, |c| *c != '\n') {
|
||||
self.insert(self.text.chars.len(), ['\n']);
|
||||
}
|
||||
let path = self.path.as_ref().expect("buffer must have path to save");
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::write(path, self.text.to_string())?;
|
||||
std::fs::write(&path, self.text.to_string())?;
|
||||
self.path = Some(path);
|
||||
self.diverged = false;
|
||||
self.opened_at = Some(SystemTime::now());
|
||||
self.unsaved = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Error> {
|
||||
if let Some(path) = self.path.take() {
|
||||
self.save_as(path)
|
||||
} else {
|
||||
// TODO: Not okay!
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<String> {
|
||||
Some(
|
||||
match self.path.as_ref()?.file_name().and_then(|n| n.to_str()) {
|
||||
|
|
|
|||
|
|
@ -76,15 +76,15 @@ impl Element for Doc {
|
|||
|
||||
match event.to_action(|e| {
|
||||
e.to_open_switcher()
|
||||
.or_else(|| e.to_open_opener(&open_path))
|
||||
.or_else(|| e.to_open_browser(&open_path))
|
||||
.or_else(|| e.to_open_finder(None))
|
||||
.or_else(|| e.to_move())
|
||||
.or_else(|| e.to_save())
|
||||
.or_else(|| e.to_path_search())
|
||||
}) {
|
||||
action @ Some(Action::OpenSwitcher) | action @ Some(Action::OpenOpener(_)) => {
|
||||
Ok(Resp::handled(action.map(Into::into)))
|
||||
}
|
||||
action @ Some(Action::OpenSwitcher)
|
||||
| action @ Some(Action::OpenOpener(_))
|
||||
| action @ Some(Action::OpenSaver(_)) => Ok(Resp::handled(action.map(Into::into))),
|
||||
ref action @ Some(Action::OpenFinder(ref query)) => {
|
||||
self.finder = Some(Finder::new(
|
||||
buffer.cursors[*cursor_id],
|
||||
|
|
@ -134,13 +134,6 @@ impl Element for Doc {
|
|||
Action::Show(Some(format!("Could not create file")), format!("{err}")).into(),
|
||||
))),
|
||||
},
|
||||
Some(Action::Overwrite) => Ok(Resp::handled(buffer.save().err().map(|err| {
|
||||
Action::Show(Some("Could not save file".to_string()), err.to_string()).into()
|
||||
}))),
|
||||
Some(Action::Reload) => {
|
||||
buffer.reload();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Save) => Ok(Resp::handled(if buffer.diverged {
|
||||
Some(
|
||||
Action::Confirm(
|
||||
|
|
@ -150,18 +143,37 @@ impl Element for Doc {
|
|||
.into(),
|
||||
)
|
||||
} else if buffer.path.is_none() {
|
||||
Some(
|
||||
Action::Show(
|
||||
None,
|
||||
"Error: buffer does not have a path (TODO: implement save_as)".to_string(),
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
Some(Action::OpenSaver(std::env::current_dir().expect("no cwd")).into())
|
||||
} else {
|
||||
buffer.save().err().map(|err| {
|
||||
Action::Show(Some("Could not save file".to_string()), err.to_string()).into()
|
||||
})
|
||||
})),
|
||||
Some(Action::Overwrite) => Ok(Resp::handled(buffer.save().err().map(|err| {
|
||||
Action::Show(Some("Could not save file".to_string()), err.to_string()).into()
|
||||
}))),
|
||||
Some(Action::SaveFileAs(path)) => Ok(Resp::handled(if path.exists() {
|
||||
Some(
|
||||
Action::Confirm(
|
||||
format!("File already exists on disk. Are you sure you wish to overwrite it (y/n)?"),
|
||||
Box::new(Action::OverwriteFileAs(path)),
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
buffer.save_as(path).err().map(|err| {
|
||||
Action::Show(Some("Could not save file".to_string()), err.to_string()).into()
|
||||
})
|
||||
})),
|
||||
Some(Action::OverwriteFileAs(path)) => {
|
||||
Ok(Resp::handled(buffer.save_as(path).err().map(|err| {
|
||||
Action::Show(Some("Could not save file".to_string()), err.to_string()).into()
|
||||
})))
|
||||
}
|
||||
Some(Action::Reload) => {
|
||||
buffer.reload();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
_ => {
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub use self::{
|
|||
doc::{Doc, Finder},
|
||||
input::Input,
|
||||
panes::Panes,
|
||||
prompt::{Confirm, Opener, Prompt, Show, Switcher},
|
||||
prompt::{Confirm, FileBrowser, FileBrowserMode, Prompt, Show, Switcher},
|
||||
root::Root,
|
||||
search::Searcher,
|
||||
status::Status,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ pub enum PaneKind {
|
|||
}
|
||||
|
||||
enum PaneTask {
|
||||
Opener(Opener),
|
||||
FileBrowser(FileBrowser),
|
||||
Switcher(Switcher),
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +21,17 @@ impl Element for Pane {
|
|||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
||||
match event.to_action(|_| None) {
|
||||
Some(Action::OpenOpener(path)) => {
|
||||
self.task = Some(PaneTask::Opener(Opener::new(path)));
|
||||
self.task = Some(PaneTask::FileBrowser(FileBrowser::new(
|
||||
path,
|
||||
FileBrowserMode::Opener,
|
||||
)));
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::OpenSaver(path)) => {
|
||||
self.task = Some(PaneTask::FileBrowser(FileBrowser::new(
|
||||
path,
|
||||
FileBrowserMode::Save,
|
||||
)));
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::OpenSwitcher) => {
|
||||
|
|
@ -36,7 +46,7 @@ impl Element for Pane {
|
|||
_ => {
|
||||
let event = if let Some(task) = &mut self.task {
|
||||
let resp = match task {
|
||||
PaneTask::Opener(opener) => opener.handle(state, event),
|
||||
PaneTask::FileBrowser(browser) => browser.handle(state, event),
|
||||
PaneTask::Switcher(switcher) => switcher.handle(state, event),
|
||||
};
|
||||
match resp {
|
||||
|
|
@ -64,8 +74,8 @@ impl Element for Pane {
|
|||
impl Visual for Pane {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let remaining_space = match &mut self.task {
|
||||
Some(PaneTask::Opener(opener)) => {
|
||||
opener.render(state, frame);
|
||||
Some(PaneTask::FileBrowser(browser)) => {
|
||||
browser.render(state, frame);
|
||||
None
|
||||
}
|
||||
Some(PaneTask::Switcher(switcher)) => {
|
||||
|
|
@ -225,7 +235,10 @@ impl Panes {
|
|||
if path.is_dir() {
|
||||
(
|
||||
state.new_anonymous(),
|
||||
Some(PaneTask::Opener(Opener::new(path.clone()))),
|
||||
Some(PaneTask::FileBrowser(FileBrowser::new(
|
||||
path.clone(),
|
||||
FileBrowserMode::Opener,
|
||||
))),
|
||||
)
|
||||
} else {
|
||||
(state.create(path.clone()).ok()?, None)
|
||||
|
|
|
|||
|
|
@ -301,17 +301,23 @@ impl Visual for BufferId {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct Opener {
|
||||
pub enum FileBrowserMode {
|
||||
Opener,
|
||||
Save,
|
||||
}
|
||||
|
||||
pub struct FileBrowser {
|
||||
pub options: Options<FileOption>,
|
||||
// Filter
|
||||
pub buffer: Buffer,
|
||||
pub cursor_id: CursorId,
|
||||
pub input: Input,
|
||||
preview: Option<(Buffer, CursorId, Input)>,
|
||||
mode: FileBrowserMode,
|
||||
}
|
||||
|
||||
impl Opener {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
impl FileBrowser {
|
||||
pub fn new(path: PathBuf, mode: FileBrowserMode) -> Self {
|
||||
let mut buffer = Buffer::default();
|
||||
let cursor_id = buffer.start_session();
|
||||
match path.display().to_string().as_str() {
|
||||
|
|
@ -327,6 +333,7 @@ impl Opener {
|
|||
buffer,
|
||||
input: Input::filter(),
|
||||
preview: None,
|
||||
mode,
|
||||
};
|
||||
this.update_completions();
|
||||
this
|
||||
|
|
@ -420,7 +427,7 @@ impl Opener {
|
|||
}
|
||||
}
|
||||
|
||||
impl Element<()> for Opener {
|
||||
impl Element<()> for FileBrowser {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||
let path_str = self.buffer.text.to_string();
|
||||
let res = match event.to_action(|e| e.to_cancel().or_else(|| e.to_char().map(Action::Char))) {
|
||||
|
|
@ -448,8 +455,14 @@ impl Element<()> for Opener {
|
|||
self.set_string(&format!("{}/", file.path.display()));
|
||||
Ok(Resp::handled(None))
|
||||
},
|
||||
FileKind::File => Ok(Resp::end(Some(Action::OpenFile(file.path, None).into()))),
|
||||
FileKind::New => Ok(Resp::end(Some(Action::CreateFile(file.path).into()))),
|
||||
FileKind::File => match &self.mode {
|
||||
FileBrowserMode::Opener => Ok(Resp::end(Some(Action::OpenFile(file.path, None).into()))),
|
||||
FileBrowserMode::Save => Ok(Resp::end(Some(Action::SaveFileAs(file.path).into()))),
|
||||
},
|
||||
FileKind::New => match &self.mode {
|
||||
FileBrowserMode::Opener => Ok(Resp::end(Some(Action::CreateFile(file.path).into()))),
|
||||
FileBrowserMode::Save => Ok(Resp::end(Some(Action::SaveFileAs(file.path).into()))),
|
||||
},
|
||||
FileKind::Unknown => Ok(Resp::handled(None)),
|
||||
}
|
||||
Ok(None) => Ok(Resp::handled(None)),
|
||||
|
|
@ -521,7 +534,7 @@ impl Visual for FileOption {
|
|||
}
|
||||
}
|
||||
|
||||
impl Visual for Opener {
|
||||
impl Visual for FileBrowser {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
self.preview = self.options.selected().and_then(|f| {
|
||||
self.preview
|
||||
|
|
@ -557,8 +570,12 @@ impl Visual for Opener {
|
|||
[frame.size()[0], path_input_sz],
|
||||
)
|
||||
.with(|f| {
|
||||
let title = match &self.mode {
|
||||
FileBrowserMode::Opener => "Open file",
|
||||
FileBrowserMode::Save => "Save file",
|
||||
};
|
||||
self.input
|
||||
.render(state, None, &self.buffer, self.cursor_id, None, f)
|
||||
.render(state, Some(title), &self.buffer, self.cursor_id, None, f)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
188
src/ui/root.rs
188
src/ui/root.rs
|
|
@ -45,101 +45,121 @@ impl Element<()> for Root {
|
|||
.map(Event::Action)
|
||||
.unwrap_or(event);
|
||||
|
||||
// Pass the event down through the list of tasks until we meet one that can handle it
|
||||
let mut task_idx = self.tasks.len();
|
||||
let event = loop {
|
||||
task_idx = match task_idx.checked_sub(1) {
|
||||
Some(task_idx) => task_idx,
|
||||
None => {
|
||||
break match self.panes.handle(state, event) {
|
||||
Ok(resp) => resp.event,
|
||||
Err(event) => Some(event),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let res = match &mut self.tasks[task_idx] {
|
||||
Task::Prompt(p) => p.handle(state, event),
|
||||
Task::Show(s) => s.handle(state, event),
|
||||
Task::Confirm(c) => c.handle(state, event),
|
||||
Task::Searcher(s) => s.handle(state, event),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(resp) => {
|
||||
// If the task has requested that it should end, kill it and all of its children
|
||||
if resp.is_end() {
|
||||
self.tasks.truncate(task_idx);
|
||||
loop {
|
||||
// Pass the event down through the list of tasks until we meet one that can handle it
|
||||
let mut task_idx = self.tasks.len();
|
||||
event = loop {
|
||||
task_idx = match task_idx.checked_sub(1) {
|
||||
Some(task_idx) => task_idx,
|
||||
None => {
|
||||
break match self.panes.handle(state, event) {
|
||||
Ok(resp) => match resp.event {
|
||||
Some(new_event) => new_event,
|
||||
None => return Ok(Resp::handled(None)),
|
||||
},
|
||||
Err(event) => event,
|
||||
};
|
||||
}
|
||||
event = if let Some(event) = resp.event {
|
||||
event
|
||||
} else {
|
||||
break None;
|
||||
};
|
||||
}
|
||||
Err(e) => event = e,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Handle 'top-level' actions
|
||||
if let Some(action) = event.as_ref().and_then(|e| {
|
||||
e.to_action(|e| {
|
||||
let res = match &mut self.tasks[task_idx] {
|
||||
Task::Prompt(p) => p.handle(state, event),
|
||||
Task::Show(s) => s.handle(state, event),
|
||||
Task::Confirm(c) => c.handle(state, event),
|
||||
Task::Searcher(s) => s.handle(state, event),
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(resp) => {
|
||||
// If the task has requested that it should end, kill it and all of its children
|
||||
if resp.is_end() {
|
||||
self.tasks.truncate(task_idx);
|
||||
}
|
||||
event = if let Some(event) = resp.event {
|
||||
event
|
||||
} else {
|
||||
return Ok(Resp::handled(None));
|
||||
};
|
||||
}
|
||||
Err(e) => event = e,
|
||||
}
|
||||
};
|
||||
|
||||
// Handle 'top-level' actions
|
||||
event = if let Some(action) = event.to_action(|e| {
|
||||
e.to_open_prompt()
|
||||
.or_else(|| e.to_cancel())
|
||||
.or_else(|| e.to_command_start())
|
||||
})
|
||||
}) {
|
||||
match action {
|
||||
Action::OpenPrompt => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks.push(Task::Prompt(Prompt::new("")));
|
||||
}
|
||||
Action::OpenSearcher(path, needle) => {
|
||||
self.tasks.clear(); // Overrides all
|
||||
self.tasks.push(Task::Searcher(Searcher::new(path, needle)));
|
||||
}
|
||||
Action::CommandStart(cmd) => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks
|
||||
.push(Task::Prompt(Prompt::new(&format!("{cmd} "))));
|
||||
}
|
||||
Action::Cancel => {
|
||||
let unsaved = state.buffers.values().filter(|b| b.unsaved).count();
|
||||
if state.buffers.is_empty() {
|
||||
return Ok(Resp::end(None));
|
||||
} else {
|
||||
self.tasks.push(Task::Confirm(Confirm {
|
||||
label: Label(if unsaved == 0 {
|
||||
format!("Are you sure you wish to quit? (y/n). You have multiple documents open!")
|
||||
} else {
|
||||
format!("Are you sure you wish to quit? (y/n). Note that {} files are unsaved!", unsaved)
|
||||
}),
|
||||
action: Action::Quit,
|
||||
}));
|
||||
}) {
|
||||
match action {
|
||||
Action::OpenPrompt => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks.push(Task::Prompt(Prompt::new("")));
|
||||
break Ok(Resp::handled(None));
|
||||
}
|
||||
}
|
||||
Action::Confirm(q, action) => self.tasks.push(Task::Confirm(Confirm {
|
||||
label: Label(q),
|
||||
action: *action,
|
||||
})),
|
||||
Action::Show(title, text) => self.tasks.push(Task::Show(Show {
|
||||
title,
|
||||
label: Label(text),
|
||||
})),
|
||||
Action::Quit => return Ok(Resp::end(None)),
|
||||
action => {
|
||||
return self
|
||||
Action::OpenSearcher(path, needle) => {
|
||||
self.tasks.clear(); // Overrides all
|
||||
self.tasks.push(Task::Searcher(Searcher::new(path, needle)));
|
||||
break Ok(Resp::handled(None));
|
||||
}
|
||||
Action::CommandStart(cmd) => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks
|
||||
.push(Task::Prompt(Prompt::new(&format!("{cmd} "))));
|
||||
break Ok(Resp::handled(None));
|
||||
}
|
||||
Action::Cancel => {
|
||||
let unsaved = state.buffers.values().filter(|b| b.unsaved).count();
|
||||
if state.buffers.is_empty() {
|
||||
break Ok(Resp::end(None));
|
||||
} else {
|
||||
self.tasks.push(Task::Confirm(Confirm {
|
||||
label: Label(if unsaved == 0 {
|
||||
format!("Are you sure you wish to quit? (y/n). You have multiple documents open!")
|
||||
} else {
|
||||
format!("Are you sure you wish to quit? (y/n). Note that {} files are unsaved!", unsaved)
|
||||
}),
|
||||
action: Action::Quit,
|
||||
}));
|
||||
break Ok(Resp::handled(None));
|
||||
}
|
||||
}
|
||||
Action::Confirm(q, action) => {
|
||||
self.tasks.push(Task::Confirm(Confirm {
|
||||
label: Label(q),
|
||||
action: *action,
|
||||
}));
|
||||
break Ok(Resp::handled(None));
|
||||
}
|
||||
Action::Show(title, text) => {
|
||||
self.tasks.push(Task::Show(Show {
|
||||
title,
|
||||
label: Label(text),
|
||||
}));
|
||||
break Ok(Resp::handled(None));
|
||||
}
|
||||
Action::Quit => break Ok(Resp::end(None)),
|
||||
action => match self
|
||||
.panes
|
||||
.handle(state, Event::Action(action))
|
||||
.map(|r| r.into_can_end());
|
||||
.map(|r| r.into_can_end::<()>())
|
||||
{
|
||||
Ok(resp) if resp.is_end() => return Ok(resp),
|
||||
Ok(resp) => {
|
||||
if let Some(new_event) = resp.event {
|
||||
new_event
|
||||
} else {
|
||||
// Nothing to do
|
||||
break Ok(Resp::handled(None));
|
||||
}
|
||||
}
|
||||
Err(event) => break Err(event),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
break Err(event);
|
||||
}
|
||||
} else if let Some(event) = event {
|
||||
return Err(event);
|
||||
}
|
||||
|
||||
// Root element swallows all other events
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue