Unnamed repository; edit this file 'description' to name the repository.
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | book/src/SUMMARY.md | 1 | ||||
| -rw-r--r-- | book/src/generated/typable-cmd.md | 2 | ||||
| -rw-r--r-- | book/src/workspace-trust.md | 23 | ||||
| -rw-r--r-- | helix-core/src/config.rs | 10 | ||||
| -rw-r--r-- | helix-loader/src/config.rs | 41 | ||||
| -rw-r--r-- | helix-loader/src/grammar.rs | 4 | ||||
| -rw-r--r-- | helix-loader/src/lib.rs | 20 | ||||
| -rw-r--r-- | helix-loader/src/workspace_trust.rs | 188 | ||||
| -rw-r--r-- | helix-term/Cargo.toml | 2 | ||||
| -rw-r--r-- | helix-term/src/application.rs | 2 | ||||
| -rw-r--r-- | helix-term/src/commands/typed.rs | 45 | ||||
| -rw-r--r-- | helix-term/src/config.rs | 19 | ||||
| -rw-r--r-- | helix-term/src/handlers.rs | 2 | ||||
| -rw-r--r-- | helix-term/src/handlers/workspace_trust.rs | 100 | ||||
| -rw-r--r-- | helix-term/src/health.rs | 4 | ||||
| -rw-r--r-- | helix-term/src/main.rs | 17 | ||||
| -rw-r--r-- | helix-view/src/editor.rs | 15 |
18 files changed, 455 insertions, 41 deletions
@@ -1556,6 +1556,7 @@ dependencies = [ "nucleo", "once_cell", "open", + "parking_lot", "pulldown-cmark", "same-file", "serde", diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index e6bc5b73..9845dfa9 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -16,6 +16,7 @@ - [Command line](./command-line.md) - [Commands](./commands.md) - [Language support](./lang-support.md) + - [Workspace trust](./workspace-trust.md) - [Ecosystem](./ecosystem.md) - [Migrating from Vim](./from-vim.md) - [Helix mode in other software](./other-software.md) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 5e4624bb..c2c7e2de 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -97,3 +97,5 @@ | `:read`, `:r` | Load a file into buffer | | `:echo` | Prints the given arguments to the statusline. | | `:noop` | Does nothing. | +| `:workspace-trust` | Add current workspace to the list of trusted workspaces. | +| `:workspace-untrust` | Remove current workspace from the list of trusted workspaces. | diff --git a/book/src/workspace-trust.md b/book/src/workspace-trust.md new file mode 100644 index 00000000..1975b6e9 --- /dev/null +++ b/book/src/workspace-trust.md @@ -0,0 +1,23 @@ +# Workspace trust + +Helix has a number of potentially dangerous features, namely LSP and ability to use local to workspace configurations. Those features can lead to unexpected code execution. To protect against code execution in dangerous contexts, Helix has a workspace trust protection, which will prevent these potentially dangerous features from running automatically. + +Helix will not trust any workspace by default. + +By default, it will prompt about trust when you open new file in a workspace where you didn't make a decision about trust yet. + +If you decide not to trust a workspace and don't want to be prompted about trust every time you start a new session in it, you can exclude the workspace by choosing `Never` option in trust selection window. + +You can always make current workspace trusted by running `:workspace-trust` command, and untrust it with `:workspace-untrust`. + +Lists of trusted and excluded workspaces, delimited by newline characters, are stored in `~/.local/share/helix/trusted_workspaces` and `~/.local/share/helix/excluded_workspaces` correspondingly. +<!-- TODO: Windows paths --> + +# Configuration + +You can return to the old behaviour of loading every local `.helix/config.toml` and `.helix/languages.toml` and starting LSP's without an explicit permission by setting following option: + +```toml +[editor] +insecure = true +``` diff --git a/helix-core/src/config.rs b/helix-core/src/config.rs index 308f9a60..79bdcad1 100644 --- a/helix-core/src/config.rs +++ b/helix-core/src/config.rs @@ -37,14 +37,14 @@ 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(insecure: bool) -> Result<Configuration, toml::de::Error> { + helix_loader::config::user_lang_config(insecure)?.try_into() } /// Language configuration loader based on user configured languages.toml. -pub fn user_lang_loader() -> Result<Loader, LanguageLoaderError> { - let config_val = - helix_loader::config::user_lang_config().map_err(LanguageLoaderError::DeserializeError)?; +pub fn user_lang_loader(insecure: bool) -> Result<Loader, LanguageLoaderError> { + let config_val = helix_loader::config::user_lang_config(insecure) + .map_err(LanguageLoaderError::DeserializeError)?; let config = config_val.clone().try_into().map_err(|e| { if let Some(languages) = config_val.get("language").and_then(|v| v.as_array()) { for lang in languages.iter() { diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index 1f414de6..04bb55bf 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -1,5 +1,7 @@ use std::str::from_utf8; +use crate::workspace_trust::{quick_query_workspace, TrustStatus}; + /// Default built-in languages.toml. pub fn default_lang_config() -> toml::Value { let default_config = include_bytes!("../../languages.toml"); @@ -8,23 +10,28 @@ 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(insecure: bool) -> Result<toml::Value, toml::de::Error> { + let global_config = crate::lang_config_file(); + let workspace_config = crate::workspace_lang_config_file(); + + let files = if let TrustStatus::Trusted = quick_query_workspace(insecure) { + vec![global_config, workspace_config] + } else { + vec![global_config] + }; + + let config = files + .iter() + .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) } diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index ca2e8ca3..3574e589 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -195,7 +195,7 @@ pub fn build_grammars(target: Option<String>) -> Result<()> { // 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() + let config: Configuration = crate::config::user_lang_config(false) .context("Could not parse languages.toml")? .try_into()?; @@ -217,7 +217,7 @@ fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> { } pub fn get_grammar_names() -> Result<Option<HashSet<String>>> { - let config: Configuration = crate::config::user_lang_config() + let config: Configuration = crate::config::user_lang_config(false) .context("Could not parse languages.toml")? .try_into()?; diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 9872b77a..2e3b760d 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -1,5 +1,6 @@ pub mod config; pub mod grammar; +pub mod workspace_trust; use helix_stdx::{env::current_working_dir, path}; @@ -132,6 +133,13 @@ pub fn cache_dir() -> PathBuf { path } +pub fn data_dir() -> PathBuf { + 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() } @@ -144,6 +152,10 @@ pub fn workspace_config_file() -> PathBuf { find_workspace().0.join(".helix").join("config.toml") } +pub fn workspace_lang_config_file() -> PathBuf { + find_workspace().0.join(".helix").join("languages.toml") +} + pub fn lang_config_file() -> PathBuf { config_dir().join("languages.toml") } @@ -152,6 +164,14 @@ pub fn default_log_file() -> PathBuf { cache_dir().join("helix.log") } +pub fn workspace_trust_file() -> PathBuf { + data_dir().join("trusted_workspaces") +} + +pub fn workspace_exclude_file() -> PathBuf { + data_dir().join("excluded_workspaces") +} + /// Merge two TOML documents, merging values from `right` onto `left` /// /// `merge_depth` sets the nesting depth up to which values are merged instead diff --git a/helix-loader/src/workspace_trust.rs b/helix-loader/src/workspace_trust.rs new file mode 100644 index 00000000..40bc198a --- /dev/null +++ b/helix-loader/src/workspace_trust.rs @@ -0,0 +1,188 @@ +use std::{collections::HashSet, fs, path::PathBuf}; + +use crate::{data_dir, workspace_exclude_file, workspace_trust_file}; + +pub struct WorkspaceTrust { + trusted: HashSet<PathBuf>, + excluded: Option<HashSet<PathBuf>>, +} + +#[derive(Clone, Copy)] +pub enum TrustStatus { + Untrusted, + Trusted, +} + +impl WorkspaceTrust { + /// Loads `WorkspaceTrust`. + /// + /// Should be used only when there is a need to change trust status + /// of a particular workspace. + /// + /// For querying trust status of a workspace use `quick_query_workspace()` or + /// `quick_query_workspace_with_explicit_untrust()` + pub fn load(with_exclusion: bool) -> Self { + let mut trusted = HashSet::new(); + + match fs::read_to_string(workspace_trust_file()) { + Ok(workspace_trust_file) => { + for line in workspace_trust_file.split('\n') { + if !line.is_empty() { + let path = PathBuf::from(line); + trusted.insert(path); + } + } + } + Err(e) => log::error!("workspace file couldn't be read: {:?}", e), + }; + + let excluded = if with_exclusion { + let mut untrusted = HashSet::new(); + + match fs::read_to_string(workspace_exclude_file()) { + Ok(workspace_untrust_file) => { + for line in workspace_untrust_file.split('\n') { + if !line.is_empty() { + let path = PathBuf::from(line); + untrusted.insert(path); + } + } + } + Err(e) => log::error!("workspace file couldn't be read: {:?}", e), + }; + + Some(untrusted) + } else { + None + }; + WorkspaceTrust { trusted, excluded } + } + + fn write_trust_to_file(&self) { + let mut trust_text = String::new(); + for workspace in self.trusted.iter() { + if let Some(path_str) = workspace.to_str() { + trust_text += &format!("{path_str}\n"); + } + } + // let chains aren't supported in current MSRV + if let Ok(false) = fs::exists(data_dir()) { + if let Err(e) = fs::create_dir_all(data_dir()) { + log::error!("Couldn't create helix's data directory: {:?}", e); + }; + } + if let Err(e) = fs::write(workspace_trust_file(), trust_text) { + log::error!("Error during write of workspace_trust file: {:?}", e); + } + } + + fn write_exclusion_to_file(&self) { + if let Some(untrusted) = &self.excluded { + let mut trust_text = String::new(); + for workspace in untrusted.iter() { + if let Some(path_str) = workspace.to_str() { + trust_text += &format!("{path_str}\n"); + } + } + // let chains aren't supported in current MSRV + if let Ok(false) = fs::exists(data_dir()) { + if let Err(e) = fs::create_dir_all(data_dir()) { + log::error!("Couldn't create helix's data directory: {:?}", e); + }; + } + if let Err(e) = fs::write(workspace_exclude_file(), trust_text) { + log::error!("Error during write of workspace_trust file: {:?}", e); + } + } else { + log::error!("Called write_untrust_to_file() when self.untrusted is None"); + } + } + + /// Mark current workspace trusted + pub fn trust_workspace(&mut self) { + let workspace = crate::find_workspace().0; + self.trusted.insert(workspace); + self.write_trust_to_file(); + } + + /// Remove trusted mark from current workspace + pub fn untrust_workspace(&mut self) { + let workspace = crate::find_workspace().0; + self.trusted.remove(&workspace); + self.write_trust_to_file(); + } + + /// Mark current workspace excluded. + /// + /// Should be called only if `WorkspaceTrust` was created with `WorkspaceTrust::load(true)` + pub fn exclude_workspace(&mut self) { + let workspace = crate::find_workspace().0; + self.trusted.remove(&workspace); + if let Some(excluded) = &mut self.excluded { + excluded.insert(workspace); + self.write_exclusion_to_file(); + } else { + log::error!("Called untrust_workspace_permanent() when self.untrusted is None"); + } + } +} + +#[derive(Default, Clone, Copy, Debug)] +pub enum TrustUntrustStatus { + DenyAlways, + #[default] + DenyOnce, + AllowAlways, +} + +pub fn quick_query_workspace(insecure: bool) -> TrustStatus { + if insecure { + return TrustStatus::Trusted; + } + + let workspace = crate::find_workspace().0; + match fs::read_to_string(workspace_trust_file()) { + Ok(workspace_trust_file) => { + for line in workspace_trust_file.split('\n') { + if PathBuf::from(line) == workspace { + return TrustStatus::Trusted; + } + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => (), + Err(err) => log::error!("workspace file couldn't be read: {err:?}"), + }; + TrustStatus::Untrusted +} + +pub fn quick_query_workspace_with_explicit_untrust(insecure: bool) -> TrustUntrustStatus { + if insecure { + return TrustUntrustStatus::AllowAlways; + } + + let workspace = crate::find_workspace().0; + match fs::read_to_string(workspace_trust_file()) { + Ok(workspace_trust_file) => { + for line in workspace_trust_file.split('\n') { + if PathBuf::from(line) == workspace { + return TrustUntrustStatus::AllowAlways; + } + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => (), + Err(err) => log::error!("workspace_trust file couldn't be read: {err:?}"), + }; + + match fs::read_to_string(workspace_exclude_file()) { + Ok(workspace_untrust_file) => { + for line in workspace_untrust_file.split('\n') { + if PathBuf::from(line) == workspace { + return TrustUntrustStatus::DenyAlways; + } + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => (), + Err(err) => log::error!("workspace_untrust file couldn't be read: {err:?}"), + }; + TrustUntrustStatus::DenyOnce +} diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index afd666c5..9332290a 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -92,6 +92,8 @@ serde = { version = "1.0", features = ["derive"] } dashmap = "6.0" +parking_lot.workspace = true + [target.'cfg(windows)'.dependencies] crossterm = { version = "0.28", features = ["event-stream"] } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index ae3b9392..ed03edfc 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -421,7 +421,7 @@ impl Application { // 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(default_config.editor.insecure)?; self.editor.syn_loader.store(Arc::new(lang_loader)); Self::load_configured_theme( &mut self.editor, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 6685691f..6354e68f 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3976,6 +3976,22 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ ..Signature::DEFAULT }, }, + TypableCommand { + name: "workspace-trust", + aliases: &[], + doc: "Add current workspace to the list of trusted workspaces.", + fun: trust_workspace, + completer: CommandCompleter::none(), + signature: Signature { positionals: (0, None), ..Signature::DEFAULT }, + }, + TypableCommand { + name: "workspace-untrust", + aliases: &[], + doc: "Remove current workspace from the list of trusted workspaces.", + fun: untrust_workspace, + completer: CommandCompleter::none(), + signature: Signature { positionals: (0, None), ..Signature::DEFAULT }, + } ]; pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> = @@ -4386,3 +4402,32 @@ fn complete_expansion_kind(content: &str, offset: usize) -> Vec<ui::prompt::Comp .map(|(name, _)| (offset.., (*name).into())) .collect() } + +fn trust_workspace( + cx: &mut compositor::Context, + args: Args<'_>, + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + helix_loader::workspace_trust::WorkspaceTrust::load(false).trust_workspace(); + + cx.editor.config_events.0.send(ConfigEvent::Refresh)?; + // HACK + lsp_restart(cx, args, event) +} + +fn untrust_workspace( + _cx: &mut compositor::Context, + _args: Args<'_>, + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + helix_loader::workspace_trust::WorkspaceTrust::load(false).untrust_workspace(); + Ok(()) +} diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index dd051984..09edbfaf 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -57,11 +57,11 @@ impl Display for ConfigLoadError { impl Config { pub fn load( - global: Result<String, ConfigLoadError>, + global: Result<&String, ConfigLoadError>, local: Result<String, ConfigLoadError>, ) -> Result<Config, ConfigLoadError> { let global_config: Result<ConfigRaw, ConfigLoadError> = - global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); + global.and_then(|file| toml::from_str(file).map_err(ConfigLoadError::BadConfig)); let local_config: Result<ConfigRaw, ConfigLoadError> = local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); let res = match (global_config, local_config) { @@ -119,10 +119,19 @@ impl Config { pub fn load_default() -> Result<Config, ConfigLoadError> { let global_config = - fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error); + fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error)?; let local_config = fs::read_to_string(helix_loader::workspace_config_file()) .map_err(ConfigLoadError::Error); - Config::load(global_config, local_config) + + let phony_config = ConfigLoadError::Error(IOError::other("hacky placeholder")); + let global_parsed = Config::load(Ok(&global_config), Err(phony_config))?; + if let helix_loader::workspace_trust::TrustStatus::Trusted = + helix_loader::workspace_trust::quick_query_workspace(global_parsed.editor.insecure) + { + Config::load(Ok(&global_config), local_config) + } else { + Ok(global_parsed) + } } } @@ -132,7 +141,7 @@ mod tests { impl Config { fn load_test(config: &str) -> Config { - Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap() + Config::load(Ok(&config.to_owned()), Err(ConfigLoadError::default())).unwrap() } } diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs index 3b2b4c03..c421c49f 100644 --- a/helix-term/src/handlers.rs +++ b/helix-term/src/handlers.rs @@ -24,6 +24,7 @@ mod document_links; mod prompt; mod signature_help; mod snippet; +mod workspace_trust; pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers { events::register(); @@ -58,5 +59,6 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers { document_colors::register_hooks(&handlers); document_links::register_hooks(&handlers); prompt::register_hooks(&handlers); + workspace_trust::register_hooks(&handlers); handlers } diff --git a/helix-term/src/handlers/workspace_trust.rs b/helix-term/src/handlers/workspace_trust.rs new file mode 100644 index 00000000..68e9d8bd --- /dev/null +++ b/helix-term/src/handlers/workspace_trust.rs @@ -0,0 +1,100 @@ +use std::{collections::HashSet, path::PathBuf}; + +use helix_event::register_hook; +use helix_loader::workspace_trust::{ + quick_query_workspace_with_explicit_untrust, TrustUntrustStatus, WorkspaceTrust, +}; +use helix_view::{events::DocumentDidOpen, handlers::Handlers, DocumentId}; +use once_cell::sync::Lazy; +use parking_lot::Mutex; + +use crate::{compositor::Compositor, job, ui}; + +const ID: &str = "workspace-trust-select"; + +/// A set of canonicalized workspace paths which have been prompted for trust at runtime. +static PROMPTED_WORKSPACES: Lazy<Mutex<HashSet<PathBuf>>> = + Lazy::new(|| Mutex::new(HashSet::new())); + +pub(super) fn register_hooks(_handlers: &Handlers) { + register_hook!(move |event: &mut DocumentDidOpen<'_>| { + let doc = doc!(event.editor, &event.doc); + + // If there is no servers to be loaded, then the workspace might not be trusted yet + if doc.language_servers().next().is_none() { + if let TrustUntrustStatus::DenyOnce = + quick_query_workspace_with_explicit_untrust(event.editor.config().insecure) + { + let (workspace, _) = helix_loader::find_workspace(); + job::dispatch_blocking(|_editor, compositor| prompt(workspace, compositor)); + } + } + Ok(()) + }); +} + +pub fn prompt(path: PathBuf, compositor: &mut Compositor) { + let mut workspaces = PROMPTED_WORKSPACES.lock(); + if workspaces.contains(&path) { + return; + } else { + workspaces.insert(path.clone()); + } + let select = select(); + compositor.replace_or_push(ID, select); +} + +const TRUST_MESSAGE: &str = "Trust this workspace? + +Trusted workspaces may load local config files and auto-start language servers. Config and language servers can execute arbitrary code. Only trust workspaces which you know contain harmless config and code."; + +fn select() -> ui::Select<TrustUntrustStatus> { + ui::Select::new( + TRUST_MESSAGE, + [ + TrustUntrustStatus::DenyOnce, + TrustUntrustStatus::DenyAlways, + TrustUntrustStatus::AllowAlways, + ], + (), + move |editor, option, event| { + if event == ui::PromptEvent::Validate { + let mut trust = WorkspaceTrust::load(true); + match option { + TrustUntrustStatus::DenyAlways => { + trust.exclude_workspace(); + } + TrustUntrustStatus::DenyOnce => { + // Do nothing + } + TrustUntrustStatus::AllowAlways => { + trust.trust_workspace(); + + let documents: Vec<DocumentId> = editor.documents.keys().cloned().collect(); + for document_id in documents.iter() { + editor.launch_language_servers(*document_id); + } + + let _ = editor + .config_events + .0 + .send(helix_view::editor::ConfigEvent::Refresh); + } + } + } + }, + ) +} + +impl crate::ui::menu::Item for TrustUntrustStatus { + type Data = (); + + fn format(&self, _data: &Self::Data) -> tui::widgets::Row<'_> { + match self { + TrustUntrustStatus::DenyAlways => "Never", + TrustUntrustStatus::DenyOnce => "Not now", + TrustUntrustStatus::AllowAlways => "Always", + } + .into() + } +} diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 52112bf9..c73df787 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -163,7 +163,7 @@ fn languages(selection: Option<HashSet<String>>) -> std::io::Result<()> { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); - let mut syn_loader_conf = match user_lang_config() { + let mut syn_loader_conf = match user_lang_config(false) { Ok(conf) => conf, Err(err) => { let stderr = std::io::stderr(); @@ -283,7 +283,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); - let syn_loader_conf = match user_lang_config() { + let syn_loader_conf = match user_lang_config(false) { Ok(conf) => conf, Err(err) => { let stderr = std::io::stderr(); diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index bdca0c01..0bc87cf4 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -140,14 +140,15 @@ FLAGS: } }; - let lang_loader = helix_core::config::user_lang_loader().unwrap_or_else(|err| { - eprintln!("{}", err); - eprintln!("Press <ENTER> to continue with default language config"); - use std::io::Read; - // This waits for an enter press. - let _ = std::io::stdin().read(&mut []); - helix_core::config::default_lang_loader() - }); + let lang_loader = + helix_core::config::user_lang_loader(config.editor.insecure).unwrap_or_else(|err| { + eprintln!("{}", err); + eprintln!("Press <ENTER> to continue with default language config"); + use std::io::Read; + // This waits for an enter press. + let _ = std::io::stdin().read(&mut []); + helix_core::config::default_lang_loader() + }); // TODO: use the thread local executor to spawn the application task separately from the work pool let mut app = Application::new(args, config, lang_loader).context("unable to start Helix")?; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e5ae45c5..2146bc87 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -15,6 +15,7 @@ use crate::{ Document, DocumentId, View, ViewId, }; use helix_event::dispatch; +use helix_loader::workspace_trust::TrustStatus; use helix_vcs::DiffProviderRegistry; use futures_util::stream::select_all::SelectAll; @@ -431,6 +432,8 @@ pub struct Config { /// Whether to enable Kitty Keyboard Protocol pub kitty_keyboard_protocol: KittyKeyboardProtocolConfig, pub buffer_picker: BufferPickerConfig, + /// Whether to implicitly trust every workspace or not + pub insecure: bool, } #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Clone, Copy)] @@ -1153,6 +1156,7 @@ impl Default for Config { rainbow_brackets: false, kitty_keyboard_protocol: Default::default(), buffer_picker: BufferPickerConfig::default(), + insecure: false, } } } @@ -1633,7 +1637,7 @@ impl Editor { } /// Launch a language server for a given document - fn launch_language_servers(&mut self, doc_id: DocumentId) { + pub fn launch_language_servers(&mut self, doc_id: DocumentId) { if !self.config().lsp.enable { return; } @@ -1648,6 +1652,15 @@ impl Editor { let config = doc.config.load(); let root_dirs = &config.workspace_lsp_roots; + if let TrustStatus::Untrusted = + helix_loader::workspace_trust::quick_query_workspace(self.config.load().insecure) + { + self.set_status( + "Current workspace is not trusted. Run `:workspace-trust` to enable all features.", + ); + return; + }; + // store only successfully started language servers let language_servers = lang.as_ref().map_or_else(HashMap::default, |language| { self.language_servers |