Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dbaa010
impl workspace trust
usering-around Oct 27, 2025
fa61d4b
fix untrust parent showing trust instead of untrust
usering-around Oct 27, 2025
f1f0b8b
fix not using kebab case for the workspace-trust config
usering-around Oct 27, 2025
e40b898
laziliy initialize the trust db path
usering-around Oct 28, 2025
d47a19b
docgen
usering-around Oct 28, 2025
a564f88
from the-mikedavis/workspace-trust: add select component
usering-around Oct 28, 2025
4b30e12
make trust dialog use the select component
usering-around Oct 28, 2025
5c7c492
add colors to select component
usering-around Oct 29, 2025
2654e6c
refactor database to have in-memory cache + add test
usering-around Oct 30, 2025
08547dc
fix not creating parent dir to the database
usering-around Oct 30, 2025
3d62265
make database use XDG_STATE_HOME with fallback to data directory on w…
usering-around Oct 30, 2025
20a07f3
refactor trust_dialog and choose_parent_dialog to return a Component …
usering-around Oct 30, 2025
490ddc9
fix ignoring workspace settings when using command line arguements
usering-around Oct 30, 2025
3bc7f4e
use consistent path in trust-dialog instead of searching for it multi…
usering-around Oct 30, 2025
d0d151f
use OnceLock<ArcSwap<T>> instead of ArcSwapOption<T> in SimpleDb
usering-around Nov 1, 2025
bc95923
change restricted to untrusted
usering-around Nov 1, 2025
8de5d7c
add new error for SimpleDb; do not panic on ser/de errors
usering-around Nov 1, 2025
5388810
initialize jobs early to fix hanging on dispatch_blocking in hooks
usering-around Nov 1, 2025
e92fce6
auto trust newly created user files
usering-around Nov 2, 2025
9758b8a
reload config & start lsp when first trusting a workspace
usering-around Nov 2, 2025
569801b
canonicalize path before trusting it
usering-around Nov 3, 2025
551c543
refactor trust_db to use helix_stdx's canonicalize instead of std
usering-around Nov 3, 2025
8e29c12
do not allow trusting paths which do not exist
usering-around Nov 3, 2025
af743ee
move FileCreated hook to application.rs
usering-around Nov 3, 2025
faa70b2
use doc_id instead of path
usering-around Nov 3, 2025
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
11 changes: 11 additions & 0 deletions Cargo.lock

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

8 changes: 4 additions & 4 deletions helix-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ impl std::fmt::Display for LanguageLoaderError {
impl std::error::Error for LanguageLoaderError {}

/// Language configuration based on user configured languages.toml.
pub fn user_lang_config() -> Result<Configuration, toml::de::Error> {
helix_loader::config::user_lang_config()?.try_into()
pub fn user_lang_config(use_local: bool) -> Result<Configuration, toml::de::Error> {
helix_loader::config::user_lang_config(use_local)?.try_into()
}

/// Language configuration loader based on user configured languages.toml.
pub fn user_lang_loader() -> Result<Loader, LanguageLoaderError> {
let config: Configuration = helix_loader::config::user_lang_config()
pub fn user_lang_loader(use_local: bool) -> Result<Loader, LanguageLoaderError> {
let config: Configuration = helix_loader::config::user_lang_config(use_local)
.map_err(LanguageLoaderError::DeserializeError)?
.try_into()
.map_err(LanguageLoaderError::DeserializeError)?;
Expand Down
1 change: 1 addition & 0 deletions helix-loader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ threadpool = { version = "1.0" }
tempfile.workspace = true

tree-house.workspace = true
fs2 = "0.4.3"
36 changes: 19 additions & 17 deletions helix-loader/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@ pub fn default_lang_config() -> toml::Value {
}

/// User configured languages.toml file, merged with the default config.
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
let config = [
crate::config_dir(),
crate::find_workspace().0.join(".helix"),
]
.into_iter()
.map(|path| path.join("languages.toml"))
.filter_map(|file| {
std::fs::read_to_string(file)
.map(|config| toml::from_str(&config))
.ok()
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.fold(default_lang_config(), |a, b| {
crate::merge_toml_values(a, b, 3)
});
pub fn user_lang_config(use_local: bool) -> Result<toml::Value, toml::de::Error> {
let mut dirs = vec![crate::config_dir()];
if use_local {
dirs.push(crate::find_workspace().0.join(".helix"));
}

let config = dirs
.into_iter()
.map(|path| path.join("languages.toml"))
.filter_map(|file| {
std::fs::read_to_string(file)
.map(|config| toml::from_str(&config))
.ok()
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.fold(default_lang_config(), |a, b| {
crate::merge_toml_values(a, b, 3)
});

Ok(config)
}
14 changes: 7 additions & 7 deletions helix-loader/src/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub fn fetch_grammars() -> Result<()> {
ensure_git_is_available()?;

// We do not need to fetch local grammars.
let mut grammars = get_grammar_configs()?;
let mut grammars = get_grammar_configs(false)?;
grammars.retain(|grammar| !matches!(grammar.source, GrammarSource::Local { .. }));

println!("Fetching {} grammars", grammars.len());
Expand Down Expand Up @@ -144,10 +144,10 @@ pub fn fetch_grammars() -> Result<()> {
Ok(())
}

pub fn build_grammars(target: Option<String>) -> Result<()> {
pub fn build_grammars(target: Option<String>, use_local_config: bool) -> Result<()> {
ensure_git_is_available()?;

let grammars = get_grammar_configs()?;
let grammars = get_grammar_configs(use_local_config)?;
println!("Building {} grammars", grammars.len());
let results = run_parallel(grammars, move |grammar| {
build_grammar(grammar, target.as_deref())
Expand Down Expand Up @@ -191,8 +191,8 @@ pub fn build_grammars(target: Option<String>) -> Result<()> {
// Grammars are configured in the default and user `languages.toml` and are
// merged. The `grammar_selection` key of the config is then used to filter
// down all grammars into a subset of the user's choosing.
fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
let config: Configuration = crate::config::user_lang_config()
fn get_grammar_configs(use_local_config: bool) -> Result<Vec<GrammarConfiguration>> {
let config: Configuration = crate::config::user_lang_config(use_local_config)
.context("Could not parse languages.toml")?
.try_into()?;

Expand All @@ -213,8 +213,8 @@ fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
Ok(grammars)
}

pub fn get_grammar_names() -> Result<Option<HashSet<String>>> {
let config: Configuration = crate::config::user_lang_config()
pub fn get_grammar_names(use_local_config: bool) -> Result<Option<HashSet<String>>> {
let config: Configuration = crate::config::user_lang_config(use_local_config)
.context("Could not parse languages.toml")?
.try_into()?;

Expand Down
23 changes: 18 additions & 5 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 trust_db;

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

Expand Down Expand Up @@ -132,6 +133,14 @@ pub fn cache_dir() -> PathBuf {
path
}

pub fn data_dir() -> PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the data directory!");
let mut path = strategy.data_dir();
path.push("helix");
path
}

pub fn config_file() -> PathBuf {
CONFIG_FILE.get().map(|path| path.to_path_buf()).unwrap()
}
Expand Down Expand Up @@ -247,14 +256,18 @@ pub fn find_workspace() -> (PathBuf, bool) {
find_workspace_in(current_dir)
}

pub fn is_workspace(path: impl AsRef<Path>) -> bool {
let path = path.as_ref();
path.join(".git").exists()
|| path.join(".svn").exists()
|| path.join(".jj").exists()
|| path.join(".helix").exists()
}

pub fn find_workspace_in(dir: impl AsRef<Path>) -> (PathBuf, bool) {
let dir = dir.as_ref();
for ancestor in dir.ancestors() {
if ancestor.join(".git").exists()
|| ancestor.join(".svn").exists()
|| ancestor.join(".jj").exists()
|| ancestor.join(".helix").exists()
{
if is_workspace(ancestor) {
return (ancestor.to_owned(), false);
}
}
Expand Down
145 changes: 145 additions & 0 deletions helix-loader/src/trust_db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use std::{
collections::HashMap,
fs::File,
io::ErrorKind,
path::{Path, PathBuf},
};

use fs2::FileExt;
use serde::{Deserialize, Serialize};

use crate::{data_dir, ensure_parent_dir, is_workspace};

#[derive(Serialize, Deserialize, Default)]
struct TrustDb {
trust: Option<HashMap<PathBuf, Trust>>,
}

#[derive(Serialize, Deserialize, PartialEq)]
pub enum Trust {
Trusted,
Untrusted,
}

impl TrustDb {
fn is_workspace_trusted(&self, path: impl AsRef<Path>) -> Option<bool> {
self.trust.as_ref().and_then(|map| {
path.as_ref().ancestors().find_map(|path| {
if is_workspace(path) || path.is_file() {
map.get(path).map(|trust| matches!(trust, Trust::Trusted))
} else {
None
}
})
})
}

fn lock() -> std::io::Result<File> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(trust_db_lock_file())?;
file.lock_exclusive()?;
Ok(file)
}

fn inspect<F, R>(f: F) -> std::io::Result<R>
where
F: FnOnce(TrustDb) -> R,
{
let lock = TrustDb::lock()?;
let contents = match std::fs::read_to_string(trust_db_file()) {
Ok(s) => s,
Err(e) => {
if e.kind() == ErrorKind::NotFound {
toml::to_string(&TrustDb::default()).unwrap()
} else {
return Err(e);
}
}
};
let toml: TrustDb = toml::from_str(&contents).unwrap_or_else(|_| {
panic!(
"Trust database is corrupted. Try to fix {} or delete it",
trust_db_file().display()
)
});
let r = f(toml);
drop(lock);
Ok(r)
}

fn modify<F, R>(f: F) -> std::io::Result<R>
where
F: FnOnce(&mut TrustDb) -> R,
{
let lock = TrustDb::lock()?;
let contents = match std::fs::read_to_string(trust_db_file()) {
Ok(s) => s,
Err(e) => {
if e.kind() == ErrorKind::NotFound {
toml::to_string(&TrustDb::default()).unwrap()
} else {
return Err(e);
}
}
};
let mut toml: TrustDb = toml::from_str(&contents).unwrap_or_else(|_| {
panic!(
"Trust database is corrupted. Try to fix {} or delete it",
trust_db_file().display()
)
});
let r = f(&mut toml);
let toml_updated =
toml::to_string(&toml).expect("toml serialization of trust database failed?");
std::fs::write(trust_db_file(), toml_updated)?;
drop(lock);
Ok(r)
}
}

fn trust_db_file() -> PathBuf {
data_dir().join("trust_db.toml")
}

fn trust_db_lock_file() -> PathBuf {
trust_db_file().with_extension("lock")
}

pub fn is_workspace_trusted(path: impl AsRef<Path>) -> std::io::Result<Option<bool>> {
let Ok(path) = path.as_ref().canonicalize() else {
return Ok(Some(false));
};
TrustDb::inspect(|db| db.is_workspace_trusted(path))
}

pub fn trust_path(path: impl AsRef<Path>) -> std::io::Result<bool> {
let Ok(path) = path.as_ref().canonicalize() else {
return Ok(false);
};
TrustDb::modify(|db| {
db.trust
.get_or_insert(HashMap::new())
.insert(path, Trust::Trusted)
!= Some(Trust::Trusted)
})
}

pub fn untrust_path(path: impl AsRef<Path>) -> std::io::Result<bool> {
let Ok(path) = path.as_ref().canonicalize() else {
return Ok(false);
};
TrustDb::modify(|db| {
db.trust
.get_or_insert(HashMap::new())
.insert(path, Trust::Untrusted)
!= Some(Trust::Untrusted)
})
}

pub fn initialize_trust_db() {
ensure_parent_dir(&trust_db_file());
}
2 changes: 1 addition & 1 deletion helix-term/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use helix_loader::grammar::{build_grammars, fetch_grammars};
fn main() {
if std::env::var("HELIX_DISABLE_AUTO_GRAMMAR_BUILD").is_err() {
fetch_grammars().expect("Failed to fetch tree-sitter grammars");
build_grammars(Some(std::env::var("TARGET").unwrap()))
build_grammars(Some(std::env::var("TARGET").unwrap()), true)
.expect("Failed to compile tree-sitter grammars");
}

Expand Down
3 changes: 2 additions & 1 deletion helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,11 @@ impl Application {
let default_config = Config::load_default()
.map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?;

let use_local_lang = doc!(self.editor).is_trusted.unwrap_or_default();
// Update the syntax language loader before setting the theme. Setting the theme will
// call `Loader::set_scopes` which must be done before the documents are re-parsed for
// the sake of locals highlighting.
let lang_loader = helix_core::config::user_lang_loader()?;
let lang_loader = helix_core::config::user_lang_loader(use_local_lang)?;
self.editor.syn_loader.store(Arc::new(lang_loader));
Self::load_configured_theme(
&mut self.editor,
Expand Down
Loading
Loading