Skip to content

Add reload! and reload_all! #13167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
Expand Up @@ -48,8 +48,10 @@
| `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
| `:character-info`, `:char` | Get info about the character under the primary cursor. |
| `:reload`, `:rl` | Discard changes and reload from the source file. |
| `:reload-all`, `:rla` | Discard changes and reload all documents from the source files. |
| `:reload!`, `:rl!` | Discard changes and reload from the source file |
| `:reload`, `:rl` | Reload from the source file, if no changes were made. |
| `:reload-all!`, `:rla!` | Discard changes and reload all documents from the source files. |
| `:reload-all`, `:rla` | Reload all documents from the source files, if no changes were made. |
| `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the given language servers, or all language servers that are used by the current file if no arguments are supplied |
Expand Down
82 changes: 78 additions & 4 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1320,13 +1320,22 @@ fn get_character_info(
}

/// Reload the [`Document`] from its source file.
fn reload(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
fn reload_impl(
cx: &mut compositor::Context,
event: PromptEvent,
force: bool,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);

if !force && doc.is_modified() {
bail!("Cannot reload unsaved buffer");
}

doc.reload(view, &cx.editor.diff_providers).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})?;
Expand All @@ -1339,11 +1348,29 @@ fn reload(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyh
Ok(())
}

fn reload_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
fn force_reload(
cx: &mut compositor::Context,
_args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
reload_impl(cx, event, true)
}

fn reload(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
reload_impl(cx, event, false)
}

fn reload_all_impl(
cx: &mut compositor::Context,
event: PromptEvent,
force: bool,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

let mut unsaved_buffer_count = 0;

let scrolloff = cx.editor.config().scrolloff;
let view_id = view!(cx.editor).id;

Expand All @@ -1365,6 +1392,13 @@ fn reload_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) ->
for (doc_id, view_ids) in docs_view_ids {
let doc = doc_mut!(cx.editor, &doc_id);

if doc.is_modified() {
unsaved_buffer_count += 1;
if !force {
continue;
}
}

// Every doc is guaranteed to have at least 1 view at this point.
let view = view_mut!(cx.editor, view_ids[0]);

Expand All @@ -1391,9 +1425,27 @@ fn reload_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) ->
}
}

if !force && unsaved_buffer_count > 0 {
bail!(
"{}, unsaved buffer(s) remaining, all saved buffers reloaded",
unsaved_buffer_count
);
}

Ok(())
}

fn force_reload_all(
cx: &mut compositor::Context,
_args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
reload_all_impl(cx, event, true)
}

fn reload_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
reload_all_impl(cx, event, false)
}
/// Update the [`Document`] if it has been modified.
fn update(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
Expand Down Expand Up @@ -3104,21 +3156,43 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
..Signature::DEFAULT
},
},
TypableCommand{
name: "reload!",
aliases: &["rl!"],
doc: "Discard changes and reload from the source file",
fun: force_reload,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
},
TypableCommand {
name: "reload",
aliases: &["rl"],
doc: "Discard changes and reload from the source file.",
doc: "Reload from the source file, if no changes were made.",
fun: reload,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
},
TypableCommand {
name: "reload-all!",
aliases: &["rla!"],
doc: "Discard changes and reload all documents from the source files.",
fun: force_reload_all,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
},
TypableCommand {
name: "reload-all",
aliases: &["rla"],
doc: "Discard changes and reload all documents from the source files.",
doc: "Reload all documents from the source files, if no changes were made.",
fun: reload_all,
completer: CommandCompleter::none(),
signature: Signature {
Expand Down
148 changes: 148 additions & 0 deletions helix-term/tests/test/commands/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,154 @@ async fn test_hardlink_write() -> anyhow::Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_reload_no_force() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.with_input_text("hello#[ |]#")
.build()?;

test_key_sequences(
&mut app,
vec![
(Some("athere<esc>"), None),
(
Some(":reload<ret>"),
Some(&|app| {
assert!(app.editor.is_err());

let doc = app.editor.documents().next().unwrap();
assert!(doc.is_modified());
assert_eq!(doc.text(), &LineFeedHandling::Native.apply("hello there"));
}),
),
],
false,
)
.await?;

helpers::assert_file_has_content(&mut file, "")?;

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_reload_force() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.with_input_text("hello#[ |]#")
.build()?;

file.as_file_mut().write_all(b"goodbye!")?;

test_key_sequences(
&mut app,
vec![
(Some("athere<esc>"), None),
(
Some(":reload!<ret>"),
Some(&|app| {
assert!(!app.editor.is_err());

let doc = app.editor.documents().next().unwrap();
assert!(!doc.is_modified());
assert_eq!(doc.text(), "goodbye!");
}),
),
],
false,
)
.await?;

helpers::assert_file_has_content(&mut file, "goodbye!")?;

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_reload_all_no_force() -> anyhow::Result<()> {
let file1 = tempfile::NamedTempFile::new()?;
let mut file2 = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file1.path(), None)
.with_file(file2.path(), None)
.with_input_text("#[c|]#hange1")
.build()?;

file2.as_file_mut().write_all(b"change2")?;

test_key_sequence(
&mut app,
Some(":reload-all<ret>"),
Some(&|app| {
assert!(app.editor.is_err());

let (mut doc1_visited, mut doc2_visited) = (false, false);
for doc in app.editor.documents() {
if doc.path().unwrap() == file1.path() {
assert!(doc.is_modified());
assert_eq!(doc.text(), "change1");
doc1_visited = true;
} else if doc.path().unwrap() == file2.path() {
assert!(!doc.is_modified());
assert_eq!(doc.text(), "change2");
doc2_visited = true;
}
}
assert!(doc1_visited);
assert!(doc2_visited);
assert_eq!(app.editor.documents().count(), 2);
}),
false,
)
.await?;

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_reload_all_force() -> anyhow::Result<()> {
let file1 = tempfile::NamedTempFile::new()?;
let mut file2 = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file1.path(), None)
.with_file(file2.path(), None)
.with_input_text("#[c|]#hange1")
.build()?;

file2.as_file_mut().write_all(b"change2")?;

test_key_sequence(
&mut app,
Some(":reload-all!<ret>"),
Some(&|app| {
assert!(!app.editor.is_err());

let (mut doc1_visited, mut doc2_visited) = (false, false);
for doc in app.editor.documents() {
if doc.path().unwrap() == file1.path() {
assert!(!doc.is_modified());
assert_eq!(doc.text(), "");
doc1_visited = true;
} else if doc.path().unwrap() == file2.path() {
assert!(!doc.is_modified());
assert_eq!(doc.text(), "change2");
doc2_visited = true;
}
}
assert!(doc1_visited);
assert!(doc2_visited);
assert_eq!(app.editor.documents().count(), 2);
}),
false,
)
.await?;

Ok(())
}

async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;

Expand Down
Loading