Skip to content

Commit

Permalink
merge persistent state
Browse files Browse the repository at this point in the history
  • Loading branch information
gabydd committed Feb 5, 2025
2 parents e3fca43 + ea5d2df commit eca36b7
Show file tree
Hide file tree
Showing 24 changed files with 782 additions and 56 deletions.
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ tempfile = "3.16.0"
bitflags = "2.8"
unicode-segmentation = "1.2"
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
regex = "1"

[workspace.package]
version = "25.1.1"
Expand Down
17 changes: 17 additions & 0 deletions book/src/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,20 @@ end-of-line-diagnostics = "hint"
[editor.inline-diagnostics]
cursor-line = "warning" # show warnings and errors on the cursorline inline
```

### `[editor.persistence]` Section

Options for persisting editor state between sessions.

The state is formatted with bincode, and stored in files in the state directory (`~/.local/state/helix` on Unix, `~\Local Settings\Application Data\helix\state` on Windows). You can reset your persisted state (and recover from any corruption) by deleting these files.

| Key | Description | Default |
| --- | ----------- | ------- |
| `old-files` | whether to persist file locations between sessions ( when you reopen the a file, it will open at the place you last closed it) | `false` |
| `commands` | whether to persist command history between sessions | `false` |
| `search` | whether to persist search history between sessions | `false` |
| `clipboard` | whether to persist helix's internal clipboard between sessions | `false` |
| `old-files-exclusions` | a list of regexes defining file paths to exclude from persistence | `[".*/\.git/.*", ".*/COMMIT_EDITMSG"]` |
| `old-files-trim` | number of old-files entries to keep when helix trims the state files at startup | `100` |
| `commands-trim` | number of command history entries to keep when helix trims the state files at startup | `100` |
| `search-trim` | number of search history entries to keep when helix trims the state files at startup | `100` |
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@
| `:move`, `:mv` | Move the current buffer and its corresponding file to a different path |
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
| `:read`, `:r` | Load a file into buffer |
| `:reload-history` | Reload history files for persistent state |
4 changes: 2 additions & 2 deletions helix-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ helix-loader = { path = "../helix-loader" }
helix-parsec = { path = "../helix-parsec" }

ropey.workspace = true
smallvec = "1.13"
smallvec = { version = "1.13", features = ["serde"] }
smartstring = "1.0.1"
unicode-segmentation.workspace = true
# unicode-width is changing width definitions
Expand All @@ -35,7 +35,7 @@ slotmap.workspace = true
tree-sitter.workspace = true
once_cell = "1.20"
arc-swap = "1"
regex = "1"
regex.workspace = true
bitflags.workspace = true
ahash = "0.8.11"
hashbrown = { version = "0.14.5", features = ["raw"] }
Expand Down
5 changes: 3 additions & 2 deletions helix-core/src/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
};
use helix_stdx::range::is_subset;
use helix_stdx::rope::{self, RopeSliceExt};
use serde::{Deserialize, Serialize};
use smallvec::{smallvec, SmallVec};
use std::{borrow::Cow, iter, slice};
use tree_sitter::Node;
Expand Down Expand Up @@ -51,7 +52,7 @@ use tree_sitter::Node;
/// single grapheme inward from the range's edge. There are a
/// variety of helper methods on `Range` for working in terms of
/// that block cursor, all of which have `cursor` in their name.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Range {
/// The anchor of the range: the side that doesn't move when extending.
pub anchor: usize,
Expand Down Expand Up @@ -413,7 +414,7 @@ impl From<Range> for helix_stdx::Range {

/// A selection consists of one or more selection ranges.
/// invariant: A selection can never be empty (always contains at least primary range).
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Selection {
ranges: SmallVec<[Range; 1]>,
primary_index: usize,
Expand Down
1 change: 1 addition & 0 deletions helix-loader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ etcetera = "0.8"
tree-sitter.workspace = true
once_cell = "1.20"
log = "0.4"
bincode = "1.3.3"

# TODO: these two should be on !wasm32 only

Expand Down
94 changes: 83 additions & 11 deletions helix-loader/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod config;
pub mod grammar;
pub mod persistence;

use helix_stdx::{env::current_working_dir, path};

Expand All @@ -15,6 +16,14 @@ static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCe

static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();

static COMMAND_HISTFILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();

static SEARCH_HISTFILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();

static FILE_HISTFILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();

static CLIPBOARD_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();

pub fn initialize_config_file(specified_file: Option<PathBuf>) {
let config_file = specified_file.unwrap_or_else(default_config_file);
ensure_parent_dir(&config_file);
Expand All @@ -27,6 +36,30 @@ pub fn initialize_log_file(specified_file: Option<PathBuf>) {
LOG_FILE.set(log_file).ok();
}

pub fn initialize_command_histfile(specified_file: Option<PathBuf>) {
let command_histfile = specified_file.unwrap_or_else(default_command_histfile);
ensure_parent_dir(&command_histfile);
COMMAND_HISTFILE.set(command_histfile).ok();
}

pub fn initialize_search_histfile(specified_file: Option<PathBuf>) {
let search_histfile = specified_file.unwrap_or_else(default_search_histfile);
ensure_parent_dir(&search_histfile);
SEARCH_HISTFILE.set(search_histfile).ok();
}

pub fn initialize_file_histfile(specified_file: Option<PathBuf>) {
let file_histfile = specified_file.unwrap_or_else(default_file_histfile);
ensure_parent_dir(&file_histfile);
FILE_HISTFILE.set(file_histfile).ok();
}

pub fn initialize_clipboard_file(specified_file: Option<PathBuf>) {
let clipboard_file = specified_file.unwrap_or_else(default_clipboard_file);
ensure_parent_dir(&clipboard_file);
CLIPBOARD_FILE.set(clipboard_file).ok();
}

/// A list of runtime directories from highest to lowest priority
///
/// The priority is:
Expand Down Expand Up @@ -133,17 +166,18 @@ pub fn cache_dir() -> PathBuf {
}

pub fn state_dir() -> PathBuf {
#[cfg(unix)]
{
let strategy = choose_base_strategy().expect("Unable to find the state directory!");
let mut path = strategy.state_dir().unwrap();
path.push("helix");
path
}

#[cfg(windows)]
{
cache_dir()
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the state directory!");
match strategy.state_dir() {
Some(mut path) => {
path.push("helix");
path
}
None => {
let mut path = strategy.cache_dir();
path.push("helix/state");
path
}
}
}

Expand All @@ -155,6 +189,28 @@ pub fn log_file() -> PathBuf {
LOG_FILE.get().map(|path| path.to_path_buf()).unwrap()
}

pub fn command_histfile() -> PathBuf {
COMMAND_HISTFILE
.get()
.map(|path| path.to_path_buf())
.unwrap()
}

pub fn search_histfile() -> PathBuf {
SEARCH_HISTFILE
.get()
.map(|path| path.to_path_buf())
.unwrap()
}

pub fn file_histfile() -> PathBuf {
FILE_HISTFILE.get().map(|path| path.to_path_buf()).unwrap()
}

pub fn clipboard_file() -> PathBuf {
CLIPBOARD_FILE.get().map(|path| path.to_path_buf()).unwrap()
}

pub fn workspace_config_file() -> PathBuf {
find_workspace().0.join(".helix").join("config.toml")
}
Expand All @@ -167,6 +223,22 @@ pub fn default_log_file() -> PathBuf {
cache_dir().join("helix.log")
}

pub fn default_command_histfile() -> PathBuf {
state_dir().join("command_history")
}

pub fn default_search_histfile() -> PathBuf {
state_dir().join("search_history")
}

pub fn default_file_histfile() -> PathBuf {
state_dir().join("file_history")
}

pub fn default_clipboard_file() -> PathBuf {
state_dir().join("clipboard")
}

/// Merge two TOML documents, merging values from `right` onto `left`
///
/// When an array exists in both `left` and `right`, `right`'s array is
Expand Down
72 changes: 72 additions & 0 deletions helix-loader/src/persistence.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use bincode::{deserialize_from, serialize_into};
use serde::{Deserialize, Serialize};
use std::{
fs::{File, OpenOptions},
io::{self, BufReader},
path::PathBuf,
};

pub fn write_history<T: Serialize>(filepath: PathBuf, entries: &Vec<T>) {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(filepath)
.unwrap();

for entry in entries {
serialize_into(&file, &entry).unwrap();
}
}

pub fn push_history<T: Serialize>(filepath: PathBuf, entry: &T) {
let file = OpenOptions::new()
.append(true)
.create(true)
.open(filepath)
.unwrap();

serialize_into(file, entry).unwrap();
}

pub fn read_history<T: for<'a> Deserialize<'a>>(filepath: &PathBuf) -> Vec<T> {
match File::open(filepath) {
Ok(file) => {
let mut read = BufReader::new(file);
let mut entries = Vec::new();
// FIXME: Can we do better error handling here? It's unfortunate that bincode doesn't
// distinguish an empty reader from an actual error.
//
// Perhaps we could use the underlying bufreader to check for emptiness in the while
// condition, then we could know any errors from bincode should be surfaced or logged.
// BufRead has a method `has_data_left` that would work for this, but at the time of
// writing it is nightly-only and experimental :(
while let Ok(entry) = deserialize_from(&mut read) {
entries.push(entry);
}
entries
}
Err(e) => match e.kind() {
io::ErrorKind::NotFound => Vec::new(),
// Going through the potential errors listed from the docs:
// - `InvalidInput` can't happen since we aren't setting options
// - `AlreadyExists` can't happen since we aren't setting `create_new`
// - `PermissionDenied` could happen if someone really borked their file permissions
// in `~/.local`, but helix already panics in that case, and I think a panic is
// acceptable.
_ => unreachable!(),
},
}
}

pub fn trim_history<T: Clone + Serialize + for<'a> Deserialize<'a>>(
filepath: PathBuf,
limit: usize,
) {
let history: Vec<T> = read_history(&filepath);
if history.len() > limit {
let trim_start = history.len() - limit;
let trimmed_history = history[trim_start..].to_vec();
write_history(filepath, &trimmed_history);
}
}
Loading

0 comments on commit eca36b7

Please sign in to comment.