Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
@@ -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. |
Expand All @@ -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. |
Expand Down
66 changes: 64 additions & 2 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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...");

Expand Down Expand Up @@ -2676,6 +2714,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"],
Expand Down Expand Up @@ -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]),
Expand All @@ -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]),
Expand Down
96 changes: 95 additions & 1 deletion helix-term/tests/test/commands/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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()?;
Expand Down Expand Up @@ -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();
Expand Down