pull/13898/merge
Rene Leonhardt 2025-07-24 20:25:11 +01:00 committed by GitHub
commit 40a38073ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 18 deletions

View File

@ -21,8 +21,9 @@
| `:line-ending` | Set the document's default line ending. Options: crlf, lf. | | `:line-ending` | Set the document's default line ending. Options: crlf, lf. |
| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. | | `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. | | `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) | | `:write-quit`, `:wq` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) | | `:write-quit!`, `:wq!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
| `:exit`, `:xit`, `:x`, `:x!` | Save the current view if named and modified (but not externally), then quit. |
| `:write-all`, `:wa` | Write changes from all buffers to disk. | | `:write-all`, `:wa` | Write changes from all buffers to disk. |
| `:write-all!`, `:wa!` | Forcefully write changes from all buffers to disk creating necessary subdirectories. | | `:write-all!`, `:wa!` | Forcefully write changes from all buffers to disk creating necessary subdirectories. |
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. | | `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. |

View File

@ -728,6 +728,28 @@ fn force_write_quit(
force_quit(cx, Args::default(), event) force_quit(cx, Args::default(), event)
} }
/// exit command: Write only if named and modified (but not externally), then quit.
fn write_exit(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let (_, doc) = current!(cx.editor);
// Current document named and modified?
// Write changes if not externally modified already.
if doc.is_modified() {
if doc.path().is_some() && !doc.externally_overwritten() {
write_impl(cx, None, false)?;
cx.block_try_flush_writes()?;
} else {
doc.reset_modified();
}
}
quit(cx, Args::default(), event)
}
/// Results in an error if there are modified buffers remaining and sets editor /// Results in an error if there are modified buffers remaining and sets editor
/// error, otherwise returns `Ok(())`. If the current document is unmodified, /// error, otherwise returns `Ok(())`. If the current document is unmodified,
/// and there are modified documents, switches focus to one of them. /// and there are modified documents, switches focus to one of them.
@ -2906,7 +2928,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
}, },
TypableCommand { TypableCommand {
name: "write-quit", name: "write-quit",
aliases: &["wq", "x"], aliases: &["wq"],
doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)", doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
fun: write_quit, fun: write_quit,
completer: CommandCompleter::positional(&[completers::filename]), completer: CommandCompleter::positional(&[completers::filename]),
@ -2918,7 +2940,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
}, },
TypableCommand { TypableCommand {
name: "write-quit!", name: "write-quit!",
aliases: &["wq!", "x!"], aliases: &["wq!"],
doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)", doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
fun: force_write_quit, fun: force_write_quit,
completer: CommandCompleter::positional(&[completers::filename]), completer: CommandCompleter::positional(&[completers::filename]),
@ -2928,6 +2950,17 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
..Signature::DEFAULT ..Signature::DEFAULT
}, },
}, },
TypableCommand {
name: "exit",
aliases: &["xit", "x", "x!"],
doc: "Save the current view if named and modified (but not externally), then quit.",
fun: write_exit,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
},
TypableCommand { TypableCommand {
name: "write-all", name: "write-all",
aliases: &["wa"], aliases: &["wa"],

View File

@ -143,8 +143,10 @@ async fn test_overwrite_protection() -> anyhow::Result<()> {
file.as_file_mut().flush()?; file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?; file.as_file_mut().sync_all()?;
let last_saved_time = file.path().metadata()?.modified()?;
test_key_sequence(&mut app, Some(":x<ret>"), None, false).await?; // Exit empty unmodified view, externally changed file shouldn't be overwritten
test_key_sequence(&mut app, Some(":x<ret>"), None, true).await?;
reload_file(&mut file).unwrap(); reload_file(&mut file).unwrap();
let mut file_content = String::new(); let mut file_content = String::new();
@ -152,6 +154,46 @@ async fn test_overwrite_protection() -> anyhow::Result<()> {
assert_eq!("extremely important content", file_content); assert_eq!("extremely important content", file_content);
let saved_time = file.path().metadata()?.modified()?;
assert_eq!(saved_time, last_saved_time);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_overwrite_if_not_modified_since() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
file.as_file_mut().write_all("34".as_bytes())?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
helpers::run_event_loop_until_idle(&mut app).await;
// Modify and exit allowing saving because file hasn't been changed externally since opening
test_key_sequence(&mut app, Some("i12<esc>:x<ret>"), None, true).await?;
reload_file(&mut file).unwrap();
let mut file_content = String::new();
file.read_to_string(&mut file_content)?;
assert_eq!("1234", file_content);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_exit_if_not_named() -> anyhow::Result<()> {
let mut app = helpers::AppBuilder::new().build()?;
helpers::run_event_loop_until_idle(&mut app).await;
// Modify and exit without saving because file hasn't been named yet
test_key_sequence(&mut app, Some("i12<esc>:x<ret>"), None, true).await?;
Ok(()) Ok(())
} }

View File

@ -1183,23 +1183,21 @@ impl Document {
pub fn pickup_last_saved_time(&mut self) { pub fn pickup_last_saved_time(&mut self) {
self.last_saved_time = match self.path() { self.last_saved_time = match self.path() {
Some(path) => match path.metadata() { Some(path) => path.metadata().and_then(|m| m.modified()).unwrap_or_else(|err| {
Ok(metadata) => match metadata.modified() { log::debug!("Could not fetch file system's mtime, falling back to current system time: {}", err);
Ok(mtime) => mtime, SystemTime::now()
Err(err) => { }),
log::debug!("Could not fetch file system's mtime, falling back to current system time: {}", err);
SystemTime::now()
}
},
Err(err) => {
log::debug!("Could not fetch file system's mtime, falling back to current system time: {}", err);
SystemTime::now()
}
},
None => SystemTime::now(), None => SystemTime::now(),
}; };
} }
/// Path given and externally modified since `last_saved_time`?
pub fn externally_overwritten(&self) -> bool {
self.path()
.and_then(|p| p.metadata().ok().and_then(|m| m.modified().ok()))
.is_some_and(|mtime| mtime > self.last_saved_time)
}
// Detect if the file is readonly and change the readonly field if necessary (unix only) // Detect if the file is readonly and change the readonly field if necessary (unix only)
pub fn detect_readonly(&mut self) { pub fn detect_readonly(&mut self) {
// Allows setting the flag for files the user cannot modify, like root files // Allows setting the flag for files the user cannot modify, like root files