Unnamed repository; edit this file 'description' to name the repository.
Split :exit command from :write-quit (#11849)
Co-authored-by: Phyfl <[email protected]> Co-authored-by: Michael Davis <[email protected]>
William Poromaa 4 months ago
parent 4547133 · commit f6f8634
-rw-r--r--book/src/generated/typable-cmd.md6
-rw-r--r--helix-term/src/commands/typed.rs66
-rw-r--r--helix-term/tests/test/commands/write.rs96
3 files changed, 163 insertions, 5 deletions
diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md
index 3b0d6713..3d2ce4e3 100644
--- a/book/src/generated/typable-cmd.md
+++ b/book/src/generated/typable-cmd.md
@@ -1,5 +1,7 @@
| Name | Description |
| --- | --- |
+| `:exit`, `:x`, `:xit` | Write changes to disk if the buffer is modified and then quit. Accepts an optional path (:exit some/path.txt). |
+| `:exit!`, `:x!`, `:xit!` | Force write changes to disk, creating necessary subdirectories, if the buffer is modified and then quit. Accepts an optional path (:exit! some/path.txt). |
| `:quit`, `:q` | Close the current view. |
| `:quit!`, `:q!` | Force close the current view, ignoring unsaved changes. |
| `:open`, `:o`, `:edit`, `:e` | Open a file from disk into the current view. |
@@ -21,8 +23,8 @@
| `: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. |
| `: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!`, `: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. 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) |
| `: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-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. |
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 6e7f7a21..867ca4ac 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -68,6 +68,44 @@ impl CommandCompleter {
}
}
+fn exit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
+ if event != PromptEvent::Validate {
+ return Ok(());
+ }
+
+ if doc!(cx.editor).is_modified() {
+ write_impl(
+ cx,
+ args.first(),
+ WriteOptions {
+ force: false,
+ auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
+ },
+ )?;
+ }
+ cx.block_try_flush_writes()?;
+ quit(cx, Args::default(), event)
+}
+
+fn force_exit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
+ if event != PromptEvent::Validate {
+ return Ok(());
+ }
+
+ if doc!(cx.editor).is_modified() {
+ write_impl(
+ cx,
+ args.first(),
+ WriteOptions {
+ force: true,
+ auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
+ },
+ )?;
+ }
+ cx.block_try_flush_writes()?;
+ quit(cx, Args::default(), event)
+}
+
fn quit(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
log::debug!("quitting...");
@@ -2677,6 +2715,30 @@ const WRITE_NO_FORMAT_FLAG: Flag = Flag {
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
+ name: "exit",
+ aliases: &["x", "xit"],
+ doc: "Write changes to disk if the buffer is modified and then quit. Accepts an optional path (:exit some/path.txt).",
+ fun: exit,
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ flags: &[WRITE_NO_FORMAT_FLAG],
+ ..Signature::DEFAULT
+ },
+ },
+ TypableCommand {
+ name: "exit!",
+ aliases: &["x!", "xit!"],
+ doc: "Force write changes to disk, creating necessary subdirectories, if the buffer is modified and then quit. Accepts an optional path (:exit! some/path.txt).",
+ fun: force_exit,
+ completer: CommandCompleter::positional(&[completers::filename]),
+ signature: Signature {
+ positionals: (0, Some(1)),
+ flags: &[WRITE_NO_FORMAT_FLAG],
+ ..Signature::DEFAULT
+ },
+ },
+ TypableCommand {
name: "quit",
aliases: &["q"],
doc: "Close the current view.",
@@ -2910,7 +2972,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
},
TypableCommand {
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)",
fun: write_quit,
completer: CommandCompleter::positional(&[completers::filename]),
@@ -2922,7 +2984,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
},
TypableCommand {
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)",
fun: force_write_quit,
completer: CommandCompleter::positional(&[completers::filename]),
diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs
index 0cf09e1e..4f44cd36 100644
--- a/helix-term/tests/test/commands/write.rs
+++ b/helix-term/tests/test/commands/write.rs
@@ -10,6 +10,100 @@ use helix_view::doc;
use super::*;
#[tokio::test(flavor = "multi_thread")]
+async fn test_exit_w_buffer_w_path() -> anyhow::Result<()> {
+ let mut file = tempfile::NamedTempFile::new()?;
+ let mut app = helpers::AppBuilder::new()
+ .with_file(file.path(), None)
+ .build()?;
+ // Check for write operation on given path and edited buffer
+ test_key_sequence(
+ &mut app,
+ Some("iBecause of the obvious threat to untold numbers of citizens due to the crisis that is even now developing, this radio station will remain on the air day and night.<ret><esc>:x<ret>"),
+ None,
+ true,
+ )
+ .await?;
+
+ reload_file(&mut file).unwrap();
+ let mut file_content = String::new();
+ file.as_file_mut().read_to_string(&mut file_content)?;
+
+ assert_eq!(
+ LineFeedHandling::Native.apply("Because of the obvious threat to untold numbers of citizens due to the crisis that is even now developing, this radio station will remain on the air day and night.\n"),
+ file_content
+ );
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_exit_wo_buffer_w_path() -> anyhow::Result<()> {
+ let mut file = tempfile::NamedTempFile::new()?;
+ let mut app = helpers::AppBuilder::new()
+ .with_file(file.path(), None)
+ .build()?;
+
+ helpers::run_event_loop_until_idle(&mut app).await;
+
+ file.as_file_mut()
+ .write_all("extremely important content".as_bytes())?;
+ file.as_file_mut().flush()?;
+ file.as_file_mut().sync_all()?;
+
+ test_key_sequence(&mut app, Some(":x<ret>"), None, true).await?;
+
+ reload_file(&mut file).unwrap();
+ let mut file_content = String::new();
+ file.read_to_string(&mut file_content)?;
+ // check that nothing is written to file
+ assert_eq!("extremely important content", file_content);
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_exit_wo_buffer_wo_path() -> anyhow::Result<()> {
+ test_key_sequence(
+ &mut AppBuilder::new().build()?,
+ Some(":x<ret>"),
+ Some(&|app| {
+ assert!(!app.editor.is_err());
+ }),
+ true,
+ )
+ .await?;
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_exit_w_buffer_wo_file() -> anyhow::Result<()> {
+ let mut file = tempfile::NamedTempFile::new()?;
+ test_key_sequence(
+ // try to write without destination
+ &mut AppBuilder::new().build()?,
+ Some("itest<esc>:x<ret>"),
+ None,
+ false,
+ )
+ .await?;
+ test_key_sequence(
+ // try to write with path succeeds
+ &mut AppBuilder::new().build()?,
+ Some(format!("iMicCheck<esc>:x {}<ret>", file.path().to_string_lossy()).as_ref()),
+ Some(&|app| {
+ assert!(!app.editor.is_err());
+ }),
+ true,
+ )
+ .await?;
+
+ helpers::assert_file_has_content(&mut file, &LineFeedHandling::Native.apply("MicCheck"))?;
+
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
async fn test_write_quit_fail() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
let mut app = helpers::AppBuilder::new()
@@ -144,7 +238,7 @@ async fn test_overwrite_protection() -> anyhow::Result<()> {
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
- test_key_sequence(&mut app, Some(":x<ret>"), None, false).await?;
+ test_key_sequence(&mut app, Some("iOverwriteData<esc>:x<ret>"), None, false).await?;
reload_file(&mut file).unwrap();
let mut file_content = String::new();