Compare commits

...

6 Commits

Author SHA1 Message Date
Anthony Rubick 36754e35ef
Merge 0df5f5f446 into 4281228da3 2025-07-24 15:29:55 -07:00
Valtteri Koskivuori 4281228da3
fix(queries): Fix filesystem permissions for snakemake (#14061) 2025-07-24 13:09:40 -04:00
Anthony Rubick 0df5f5f446
doc(auto-reloading): document configuration of auto-reloading 2025-07-15 18:56:25 -07:00
Anthony Rubick 1d27b50318
fix: auto-reloading on focus
Open question: should we prompt users here too? currently don't
2025-07-15 18:28:01 -07:00
Anthony Rubick bf02b4dbea
fix: typos
I has originally named this feature `auto_read`, but changed the name to `auto_reload`, this changes some references I forgot to update to the new name
2025-07-15 17:56:38 -07:00
Anthony Rubick 104bb7443f
feat: automatic reload when file changes externally
Implements the idea raised by @porridgewithraisins in this comment:
https://github.com/helix-editor/helix/issues/1125#issuecomment-3073782827

The implementation was modeled after the implementation of the autosave feature.

Here are some example snippets of configuration for this feature:

- disable auto-reloading (default)
```toml
[editor]
auto-reload = false
```
or
```toml
[editor.auto-reload]
focus-gained = false
```
- auto-reload on focus
```toml
[editor]
auto-reload = true
```
or
```toml
[editor.auto-reload]
focus-gained = true
```
- auto-reload at some periodically at time interval (5 seconds in this example)
```toml
[editor.auto-reload]
periodic.enable = true
periodic.interval = 5000
```
- of course, you could have it reload on focus and at an interval too:
```toml
[editor.auto-reload]
focus-gained = true
periodic.enable = true
periodic.interval = 5000
```
2025-07-15 17:47:54 -07:00
14 changed files with 272 additions and 1 deletions

View File

@ -7,6 +7,7 @@
- [`[editor.cursor-shape]` Section](#editorcursor-shape-section)
- [`[editor.file-picker]` Section](#editorfile-picker-section)
- [`[editor.auto-pairs]` Section](#editorauto-pairs-section)
- [`[editor.auto-reload]` Section](#editorauto-reload-section)
- [`[editor.auto-save]` Section](#editorauto-save-section)
- [`[editor.search]` Section](#editorsearch-section)
- [`[editor.whitespace]` Section](#editorwhitespace-section)
@ -265,6 +266,16 @@ name = "rust"
'<' = '>'
```
### `[editor.auto-reload]` Section
Controls auto reloading of externally modified files.
| Key | Description | Default |
|--|--|---------|
| `focus-gained` | Enable automatic reloading of externally modified files when Helix is focused. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `periodic.enable` | Enable periodic auto reloading of externally modified files | `false` |
| `periodic.interval` | Time interval in milliseconds between auto reload checks | `3000` |
### `[editor.auto-save]` Section
Control auto save behavior.

View File

@ -1406,7 +1406,11 @@ 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<()> {
pub fn reload_all(
cx: &mut compositor::Context,
_args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

View File

@ -5,6 +5,7 @@ use helix_event::AsyncHook;
use crate::config::Config;
use crate::events;
use crate::handlers::auto_reload::AutoReloadHandler;
use crate::handlers::auto_save::AutoSaveHandler;
use crate::handlers::signature_help::SignatureHelpHandler;
@ -12,6 +13,7 @@ pub use helix_view::handlers::{word_index, Handlers};
use self::document_colors::DocumentColorsHandler;
pub(super) mod auto_reload;
mod auto_save;
pub mod completion;
mod diagnostics;
@ -25,6 +27,7 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
let event_tx = completion::CompletionHandler::new(config).spawn();
let signature_hints = SignatureHelpHandler::new().spawn();
let auto_save = AutoSaveHandler::new().spawn();
let auto_reload = AutoReloadHandler::new().spawn();
let document_colors = DocumentColorsHandler::default().spawn();
let word_index = word_index::Handler::spawn();
@ -32,6 +35,7 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
signature_hints,
auto_save,
auto_reload,
document_colors,
word_index,
};
@ -40,6 +44,7 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
completion::register_hooks(&handlers);
signature_help::register_hooks(&handlers);
auto_save::register_hooks(&handlers);
auto_reload::register_hooks(&handlers);
diagnostics::register_hooks(&handlers);
snippet::register_hooks(&handlers);
document_colors::register_hooks(&handlers);

View File

@ -0,0 +1,168 @@
use std::borrow::Cow;
use std::fs;
use std::sync::atomic::{self, AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use helix_core::command_line::Args;
use helix_event::{register_hook, send_blocking};
use helix_view::document::Mode;
use helix_view::events::DocumentDidOpen;
use helix_view::handlers::{AutoReloadEvent, Handlers};
use helix_view::{Document, Editor};
use tokio::time::Instant;
use crate::compositor::Compositor;
use crate::events::OnModeSwitch;
use crate::job;
use crate::ui::{Prompt, PromptEvent};
use crate::{commands, ui};
#[derive(Debug)]
pub(super) struct AutoReloadHandler {
reload_pending: Arc<AtomicBool>,
}
impl AutoReloadHandler {
pub fn new() -> AutoReloadHandler {
AutoReloadHandler {
reload_pending: Default::default(),
}
}
}
impl helix_event::AsyncHook for AutoReloadHandler {
type Event = AutoReloadEvent;
fn handle_event(
&mut self,
event: Self::Event,
existing_debounce: Option<Instant>,
) -> Option<Instant> {
match event {
Self::Event::CheckForChanges { after } => {
Some(Instant::now() + Duration::from_millis(after))
}
Self::Event::LeftInsertMode | Self::Event::EditorFocused => {
if existing_debounce.is_some() {
// If the event happened more recently than the debounce, let the
// debounce run down before checking for changes.
existing_debounce
} else {
// Otherwise if there is a reload pending, check immediately.
if self.reload_pending.load(Ordering::Relaxed) {
self.finish_debounce();
}
None
}
}
}
}
fn finish_debounce(&mut self) {
let reload_pending = self.reload_pending.clone();
job::dispatch_blocking(move |editor, compositor| {
if editor.mode() == Mode::Insert {
// Avoid reloading while in insert mode since this mixes up
// the modification indicator and prevents future saves.
reload_pending.store(true, atomic::Ordering::Relaxed);
} else {
prompt_to_reload_if_needed(editor, compositor);
reload_pending.store(false, atomic::Ordering::Relaxed);
}
});
}
}
/// Requests a reload if any documents have been modified externally.
fn prompt_to_reload_if_needed(editor: &mut Editor, compositor: &mut Compositor) {
// If there are no externally modified documents, we can do nothing.
if count_externally_modified_documents(editor.documents()) == 0 {
// Reset the debounce timer to allow for the next check.
let config = editor.config.load();
if config.auto_reload.periodic.enable {
let interval = config.auto_reload.periodic.interval;
send_blocking(
&editor.handlers.auto_reload,
AutoReloadEvent::CheckForChanges { after: interval },
);
}
return;
}
let prompt = Prompt::new(
Cow::Borrowed("Some files have been modified externally, press Enter to reload them."),
None,
ui::completers::none,
|cx, _, event| {
if event == PromptEvent::Update {
return;
}
if let Err(err) =
commands::typed::reload_all(cx, Args::default(), PromptEvent::Validate)
{
cx.editor
.set_error(format!("Failed to reload document: {err}"));
} else {
cx.editor.set_status("Reloaded modified documents");
}
// Reset the debounce timer to allow for the next check.
let config = cx.editor.config.load();
if config.auto_reload.periodic.enable {
let interval = config.auto_reload.periodic.interval;
send_blocking(
&cx.editor.handlers.auto_reload,
AutoReloadEvent::CheckForChanges { after: interval },
);
}
},
);
// Show the prompt to the user.
compositor.push(Box::new(prompt));
}
pub fn count_externally_modified_documents<'a>(docs: impl Iterator<Item = &'a Document>) -> usize {
docs // Filter out documents that have unsaved changes.
.filter(|doc| !doc.is_modified())
// Get the documents that have been modified externally.
.filter(|doc| {
let last_saved_time = doc.get_last_saved_time();
let Some(path) = doc.path() else {
return false;
};
// Check if the file has been modified externally
if let Ok(metadata) = fs::metadata(path) {
if let Ok(modified_time) = metadata.modified() {
if modified_time > last_saved_time {
return true;
}
}
}
false
})
.count()
}
pub(super) fn register_hooks(handlers: &Handlers) {
let tx = handlers.auto_reload.clone();
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
let config = event.editor.config.load();
if config.auto_reload.periodic.enable {
let interval = config.auto_reload.periodic.interval;
send_blocking(&tx, AutoReloadEvent::CheckForChanges { after: interval });
}
Ok(())
});
let tx = handlers.auto_reload.clone();
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
if event.old_mode == Mode::Insert {
send_blocking(&tx, AutoReloadEvent::LeftInsertMode)
}
Ok(())
});
}

View File

@ -1468,6 +1468,24 @@ impl Component for EditorView {
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
Event::FocusGained => {
if context.editor.config().auto_reload.focus_gained {
if crate::handlers::auto_reload::count_externally_modified_documents(
context.editor.documents(),
) > 0
{
if let Err(e) = commands::typed::reload_all(
context,
helix_core::command_line::Args::default(),
super::PromptEvent::Validate,
) {
context.editor.set_error(format!("{}", e));
} else {
context
.editor
.set_status("Reloaded files due to external changes");
}
}
}
self.terminal_focused = true;
EventResult::Consumed(None)
}

View File

@ -1200,6 +1200,11 @@ impl Document {
};
}
/// Return the last saved time of the document.
pub fn get_last_saved_time(&self) -> SystemTime {
self.last_saved_time
}
// Detect if the file is readonly and change the readonly field if necessary (unix only)
pub fn detect_readonly(&mut self) {
// Allows setting the flag for files the user cannot modify, like root files

View File

@ -63,6 +63,7 @@ use arc_swap::{
};
pub const DEFAULT_AUTO_SAVE_DELAY: u64 = 3000;
pub const DEFAULT_AUTO_RELOAD_INTERVAL: u64 = 3000;
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
@ -290,6 +291,10 @@ pub struct Config {
/// Time delay defaults to false with 3000ms delay. Focus lost defaults to false.
#[serde(deserialize_with = "deserialize_auto_save")]
pub auto_save: AutoSave,
/// Automatic reload of the modified documents on a periodic time interval and/or when the editor gains focus.
/// Time interval defaults to false with 3000ms delay. Focus gained defaults to false.
#[serde(deserialize_with = "deserialize_auto_reload")]
pub auto_reload: AutoReload,
/// Set a global text_width
pub text_width: usize,
/// Time in milliseconds since last keypress before idle timers trigger.
@ -886,6 +891,52 @@ where
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AutoReload {
/// Whether to check for file changes when the editor is focused. Defaults to false.
#[serde(default)]
pub focus_gained: bool,
/// Autosave periodically at some interval. Defaults to disabled.
#[serde(default)]
pub periodic: AutoReloadPeriodic,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AutoReloadPeriodic {
#[serde(default)]
/// Enable auto reload periodically. Defaults to false.
pub enable: bool,
#[serde(default = "default_auto_reload_interval")]
/// Time interval in milliseconds. Defaults to [DEFAULT_AUTO_RELOAD_INTERVAL].
pub interval: u64,
}
pub fn default_auto_reload_interval() -> u64 {
DEFAULT_AUTO_RELOAD_INTERVAL
}
fn deserialize_auto_reload<'de, D>(deserializer: D) -> Result<AutoReload, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize, Serialize)]
#[serde(untagged, deny_unknown_fields, rename_all = "kebab-case")]
enum AutoReloadToml {
FocusGained(bool),
AutoReload(AutoReload),
}
match AutoReloadToml::deserialize(deserializer)? {
AutoReloadToml::FocusGained(focus_gained) => Ok(AutoReload {
focus_gained,
..Default::default()
}),
AutoReloadToml::AutoReload(auto_reload) => Ok(auto_reload),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct WhitespaceCharacters {
@ -1016,6 +1067,7 @@ impl Default for Config {
auto_format: true,
default_yank_register: '"',
auto_save: AutoSave::default(),
auto_reload: AutoReload::default(),
idle_timeout: Duration::from_millis(250),
completion_timeout: Duration::from_millis(250),
preview_completion_insert: true,

View File

@ -17,11 +17,19 @@ pub enum AutoSaveEvent {
LeftInsertMode,
}
#[derive(Debug)]
pub enum AutoReloadEvent {
CheckForChanges { after: u64 },
EditorFocused,
LeftInsertMode,
}
pub struct Handlers {
// only public because most of the actual implementation is in helix-term right now :/
pub completions: CompletionHandler,
pub signature_hints: Sender<lsp::SignatureHelpEvent>,
pub auto_save: Sender<AutoSaveEvent>,
pub auto_reload: Sender<AutoReloadEvent>,
pub document_colors: Sender<lsp::DocumentColorsEvent>,
pub word_index: word_index::Handler,
}

0
runtime/queries/snakemake/LICENSE 100755 → 100644
View File

View File

View File

View File

View File

View File