Skip to content

Commit

Permalink
merge 9143 by intarga
Browse files Browse the repository at this point in the history
minimal implementation of shada

currently only writes the header when closing

disable writing shada in integration tests

will probably need a sophisticated way of handling this eventually for
testing shada behaviour, but for now this is fine

switch from rmp-serde to bincode

rename shada to session

switch to multi-file approach, implement persisting command history

read command history from file

handle NotFound error when reading histfile before it has been created

persist and load search history

move session.rs from helix-term to helix-loader

persist file history

load file history

It was necessary make pos in file args an option to prevent it from
overwriting the file positions loaded from persistence.

Alignment is not quite right... I think we need to persist selections
instead of view positions, or disable center aligning

encode register history with bincode, and merge logic with file history

encoding was found to be necessary because registers can contain line
endings, which breaks the previous lines-of-text format

avoid exposing internals of register.rs

rename session to persistence

promote type/implementation-specific persistence logic to helix-view

store ViewPosition and Selection directly in FileHistoryEntry

fix quirky file persistence behaviour

fix integration tests

save cloning by passing by ref to persistence functions

persist clipboard

add on/off config options for persistence

fix bug: writes on untruncated histfiles

trim persistence files

add config option to exclude files form old_file_locs

add trim config options for persistence

add command to reload history

fix rebase breakage

add .*/COMMIT_EDITMSG to persistent file exclusions

useful in the case of bare git repos, where the git dir is not always
named .git, and so the previous exclusion wouldn't catch it.

default to <cache dir>/helix/state if state dir is None

run docgen

split persistence config options into own struct

add documentation for persistent state

only trim persistent state files if persistent state is enabled

add integration test for persistent state

avoid repeated loading of config to check persistence config in startup

address hanging TODOs

fix line feed handling in integration test for windows
  • Loading branch information
Axlefublr committed Dec 10, 2024
1 parent f8782f6 commit 0317302
Show file tree
Hide file tree
Showing 24 changed files with 785 additions and 40 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.

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ members = [
"xtask",
]

default-members = [
"helix-term"
]
default-members = ["helix-term"]

[profile.release]
lto = "thin"
Expand All @@ -42,6 +40,7 @@ tree-sitter = { version = "0.22" }
nucleo = "0.5.0"
slotmap = "1.0.7"
thiserror = "2.0"
regex = "1"

[workspace.package]
version = "24.7.0"
Expand Down
17 changes: 17 additions & 0 deletions book/src/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,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 @@ -93,3 +93,4 @@
| `:random`, `:rng`, `:rnd` | Randomize your selections |
| `:echo`, `:c` | Print to the messages line |
| `:echopy`, `:cc` | Put string into clipboard |
| `: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 @@ -20,7 +20,7 @@ helix-stdx = { path = "../helix-stdx" }
helix-loader = { path = "../helix-loader" }

ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
smallvec = "1.13"
smallvec = { version = "1.13", features = ["serde"] }
smartstring = "1.0.1"
unicode-segmentation = "1.12"
# unicode-width is changing width definitions
Expand All @@ -34,7 +34,7 @@ slotmap.workspace = true
tree-sitter.workspace = true
once_cell = "1.20"
arc-swap = "1"
regex = "1"
regex.workspace = true
bitflags = "2.6"
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 @@ -12,6 +12,7 @@ use crate::{
Assoc, ChangeSet, RopeGraphemes, RopeSlice,
};
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 @@ -50,7 +51,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 @@ -403,7 +404,7 @@ impl From<(usize, usize)> for 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
87 changes: 87 additions & 0 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 @@ -132,6 +165,22 @@ pub fn cache_dir() -> PathBuf {
path
}

pub fn state_dir() -> PathBuf {
// 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
}
}
}

pub fn config_file() -> PathBuf {
CONFIG_FILE.get().map(|path| path.to_path_buf()).unwrap()
}
Expand All @@ -140,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 @@ -152,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 0317302

Please sign in to comment.