add workspace config and manual LSP root management

fixup documentation

Co-authored-by: LeoniePhiline <22329650+LeoniePhiline@users.noreply.github.com>

fixup typo

Co-authored-by: LeoniePhiline <22329650+LeoniePhiline@users.noreply.github.com>
pull/6474/head
Pascal Kuthe 2 years ago committed by Blaž Hrastnik
parent d59b80514e
commit 2d10a429eb

@ -30,6 +30,9 @@ You can use a custom configuration file by specifying it with the `-c` or
Additionally, you can reload the configuration file by sending the USR1 Additionally, you can reload the configuration file by sending the USR1
signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`. signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.
## Editor ## Editor
### `[editor]` Section ### `[editor]` Section
@ -58,6 +61,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | | `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
### `[editor.statusline]` Section ### `[editor.statusline]` Section

@ -70,6 +70,7 @@
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
| `:config-reload` | Refresh user config. | | `:config-reload` | Refresh user config. |
| `:config-open` | Open the user config.toml file. | | `:config-open` | Open the user config.toml file. |
| `:config-open-workspace` | Open the workspace config.toml file. |
| `:log-open` | Open the helix log file. | | `:log-open` | Open the helix log file. |
| `:insert-output` | Run shell command, inserting output before each selection. | | `:insert-output` | Run shell command, inserting output before each selection. |
| `:append-output` | Run shell command, appending output after each selection. | | `:append-output` | Run shell command, appending output after each selection. |

@ -64,6 +64,7 @@ These configuration keys are available:
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | `` |
### File-type detection and the `file-types` key ### File-type detection and the `file-types` key

@ -36,55 +36,12 @@ pub mod unicode {
pub use unicode_width as width; pub use unicode_width as width;
} }
pub use helix_loader::find_workspace;
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> { pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace()) line.chars().position(|ch| !ch.is_whitespace())
} }
/// Find project root.
///
/// Order of detection:
/// * Top-most folder containing a root marker in current git repository
/// * Git repository root if no marker detected
/// * Top-most folder containing a root marker if not git repository detected
/// * Current working directory as fallback
pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::PathBuf {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let root = match root {
Some(root) => {
let root = std::path::Path::new(root);
if root.is_absolute() {
root.to_path_buf()
} else {
current_dir.join(root)
}
}
None => current_dir.clone(),
};
let mut top_marker = None;
for ancestor in root.ancestors() {
if root_markers
.iter()
.any(|marker| ancestor.join(marker).exists())
{
top_marker = Some(ancestor);
}
if ancestor.join(".git").exists() {
// Top marker is repo root if not root marker was detected yet
if top_marker.is_none() {
top_marker = Some(ancestor);
}
// Don't go higher than repo if we're in one
break;
}
}
// Return the found top marker or the current_dir as fallback
top_marker.map_or(current_dir, |a| a.to_path_buf())
}
pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice}; pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
// pub use tendril::StrTendril as Tendril; // pub use tendril::StrTendril as Tendril;

@ -20,7 +20,7 @@ use std::{
fmt, fmt,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
mem::{replace, transmute}, mem::{replace, transmute},
path::Path, path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::Arc, sync::Arc,
}; };
@ -127,6 +127,10 @@ pub struct LanguageConfiguration {
pub auto_pairs: Option<AutoPairs>, pub auto_pairs: Option<AutoPairs>,
pub rulers: Option<Vec<u16>>, // if set, override editor's rulers pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
/// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
/// Falling back to the current working directory if none are configured.
pub workspace_lsp_roots: Option<Vec<PathBuf>>,
} }
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash)]

@ -9,9 +9,8 @@ pub fn default_lang_config() -> toml::Value {
/// User configured languages.toml file, merged with the default config. /// User configured languages.toml file, merged with the default config.
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> { pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
let config = crate::local_config_dirs() let config = [crate::config_dir(), crate::find_workspace().join(".helix")]
.into_iter() .into_iter()
.chain([crate::config_dir()].into_iter())
.map(|path| path.join("languages.toml")) .map(|path| path.join("languages.toml"))
.filter_map(|file| { .filter_map(|file| {
std::fs::read_to_string(file) std::fs::read_to_string(file)
@ -20,8 +19,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
}) })
.collect::<Result<Vec<_>, _>>()? .collect::<Result<Vec<_>, _>>()?
.into_iter() .into_iter()
.chain([default_lang_config()].into_iter()) .fold(default_lang_config(), |a, b| {
.fold(toml::Value::Table(toml::value::Table::default()), |a, b| {
// combines for example // combines for example
// b: // b:
// [[language]] // [[language]]
@ -38,7 +36,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
// language-server = { command = "/usr/bin/taplo" } // language-server = { command = "/usr/bin/taplo" }
// //
// thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
crate::merge_toml_values(b, a, 3) crate::merge_toml_values(a, b, 3)
}); });
Ok(config) Ok(config)

@ -42,7 +42,7 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
let mut rt_dirs = Vec::new(); let mut rt_dirs = Vec::new();
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); let path = PathBuf::from(dir).parent().unwrap().join(RT_DIR);
log::debug!("runtime dir: {}", path.to_string_lossy()); log::debug!("runtime dir: {}", path.to_string_lossy());
rt_dirs.push(path); rt_dirs.push(path);
} }
@ -113,15 +113,6 @@ pub fn config_dir() -> PathBuf {
path path
} }
pub fn local_config_dirs() -> Vec<PathBuf> {
let directories = find_local_config_dirs()
.into_iter()
.map(|path| path.join(".helix"))
.collect();
log::debug!("Located configuration folders: {:?}", directories);
directories
}
pub fn cache_dir() -> PathBuf { pub fn cache_dir() -> PathBuf {
// TODO: allow env var override // TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!"); let strategy = choose_base_strategy().expect("Unable to find the config directory!");
@ -137,6 +128,10 @@ pub fn config_file() -> PathBuf {
.unwrap_or_else(|| config_dir().join("config.toml")) .unwrap_or_else(|| config_dir().join("config.toml"))
} }
pub fn workspace_config_file() -> PathBuf {
find_workspace().join(".helix").join("config.toml")
}
pub fn lang_config_file() -> PathBuf { pub fn lang_config_file() -> PathBuf {
config_dir().join("languages.toml") config_dir().join("languages.toml")
} }
@ -145,22 +140,6 @@ pub fn log_file() -> PathBuf {
cache_dir().join("helix.log") cache_dir().join("helix.log")
} }
pub fn find_local_config_dirs() -> Vec<PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let mut directories = Vec::new();
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() {
directories.push(ancestor.to_path_buf());
// Don't go higher than repo if we're in one
break;
} else if ancestor.join(".helix").is_dir() {
directories.push(ancestor.to_path_buf());
}
}
directories
}
/// Merge two TOML documents, merging values from `right` onto `left` /// Merge two TOML documents, merging values from `right` onto `left`
/// ///
/// When an array exists in both `left` and `right`, `right`'s array is /// When an array exists in both `left` and `right`, `right`'s array is
@ -302,3 +281,16 @@ mod merge_toml_tests {
) )
} }
} }
/// Finds the current workspace folder.
/// Used as a ceiling dir for root resolve, for the filepicker and other related
pub fn find_workspace() -> PathBuf {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return ancestor.to_owned();
}
}
current_dir
}

@ -1,22 +1,22 @@
use crate::{ use crate::{
jsonrpc, find_root, jsonrpc,
transport::{Payload, Transport}, transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result, Call, Error, OffsetEncoding, Result,
}; };
use helix_core::{find_root, ChangeSet, Rope}; use helix_core::{ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH}; use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::PositionEncodingKind; use lsp::PositionEncodingKind;
use lsp_types as lsp; use lsp_types as lsp;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap;
use std::future::Future; use std::future::Future;
use std::process::Stdio; use std::process::Stdio;
use std::sync::{ use std::sync::{
atomic::{AtomicU64, Ordering}, atomic::{AtomicU64, Ordering},
Arc, Arc,
}; };
use std::{collections::HashMap, path::PathBuf};
use tokio::{ use tokio::{
io::{BufReader, BufWriter}, io::{BufReader, BufWriter},
process::{Child, Command}, process::{Child, Command},
@ -49,6 +49,7 @@ impl Client {
config: Option<Value>, config: Option<Value>,
server_environment: HashMap<String, String>, server_environment: HashMap<String, String>,
root_markers: &[String], root_markers: &[String],
manual_roots: &[PathBuf],
id: usize, id: usize,
req_timeout: u64, req_timeout: u64,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
@ -77,8 +78,11 @@ impl Client {
Transport::start(reader, writer, stderr, id); Transport::start(reader, writer, stderr, id);
let root_path = find_root( let root_path = find_root(
doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())), doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers, root_markers,
manual_roots,
); );
let root_uri = lsp::Url::from_file_path(root_path.clone()).ok(); let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();

@ -10,11 +10,15 @@ pub use lsp::{Position, Url};
pub use lsp_types as lsp; pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration}; use helix_core::{
find_workspace,
syntax::{LanguageConfiguration, LanguageServerConfiguration},
};
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedReceiver;
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::{hash_map::Entry, HashMap},
path::PathBuf,
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Arc, Arc,
@ -641,6 +645,7 @@ impl Registry {
&mut self, &mut self,
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
) -> Result<Option<Arc<Client>>> { ) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server { let config = match &language_config.language_server {
Some(config) => config, Some(config) => config,
@ -656,7 +661,7 @@ impl Registry {
let id = self.counter.fetch_add(1, Ordering::Relaxed); let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) = let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?; start_client(id, language_config, config, doc_path, root_dirs)?;
self.incoming.push(UnboundedReceiverStream::new(incoming)); self.incoming.push(UnboundedReceiverStream::new(incoming));
let (_, old_client) = entry.insert((id, client.clone())); let (_, old_client) = entry.insert((id, client.clone()));
@ -684,6 +689,7 @@ impl Registry {
&mut self, &mut self,
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
) -> Result<Option<Arc<Client>>> { ) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server { let config = match &language_config.language_server {
Some(config) => config, Some(config) => config,
@ -697,7 +703,7 @@ impl Registry {
let id = self.counter.fetch_add(1, Ordering::Relaxed); let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) = let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?; start_client(id, language_config, config, doc_path, root_dirs)?;
self.incoming.push(UnboundedReceiverStream::new(incoming)); self.incoming.push(UnboundedReceiverStream::new(incoming));
entry.insert((id, client.clone())); entry.insert((id, client.clone()));
@ -798,6 +804,7 @@ fn start_client(
config: &LanguageConfiguration, config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration, ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
) -> Result<NewClientResult> { ) -> Result<NewClientResult> {
let (client, incoming, initialize_notify) = Client::start( let (client, incoming, initialize_notify) = Client::start(
&ls_config.command, &ls_config.command,
@ -805,6 +812,7 @@ fn start_client(
config.config.clone(), config.config.clone(),
ls_config.environment.clone(), ls_config.environment.clone(),
&config.roots, &config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
id, id,
ls_config.timeout, ls_config.timeout,
doc_path, doc_path,
@ -842,6 +850,48 @@ fn start_client(
Ok(NewClientResult(client, incoming)) Ok(NewClientResult(client, incoming))
} }
/// Find an LSP root of a file using the following mechansim:
/// * start at `file` (either an absolute path or relative to CWD)
/// * find the top most directory containing a root_marker
/// * inside the current workspace
/// * stop the search at the first root_dir that contains `file` or the workspace (obtained from `helix_core::find_workspace`)
/// * root_dirs only apply inside the workspace. For files outside of the workspace they are ignored
/// * outside the current workspace: keep searching to the top of the file hiearchy
pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) -> PathBuf {
let file = std::path::Path::new(file);
let workspace = find_workspace();
let file = if file.is_absolute() {
file.to_path_buf()
} else {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
current_dir.join(file)
};
let inside_workspace = file.strip_prefix(&workspace).is_ok();
let mut top_marker = None;
for ancestor in file.ancestors() {
if root_markers
.iter()
.any(|marker| ancestor.join(marker).exists())
{
top_marker = Some(ancestor);
}
if inside_workspace
&& (ancestor == workspace
|| root_dirs
.iter()
.any(|root_dir| root_dir == ancestor.strip_prefix(&workspace).unwrap()))
{
return top_marker.unwrap_or(ancestor).to_owned();
}
}
// If no root was found use the workspace as a fallback
workspace
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{lsp, util::*, OffsetEncoding}; use super::{lsp, util::*, OffsetEncoding};

@ -12,7 +12,7 @@ pub use typed::*;
use helix_core::{ use helix_core::{
char_idx_at_visual_offset, comment, char_idx_at_visual_offset, comment,
doc_formatter::TextFormat, doc_formatter::TextFormat,
encoding, find_first_non_whitespace_char, find_root, graphemes, encoding, find_first_non_whitespace_char, find_workspace, graphemes,
history::UndoKind, history::UndoKind,
increment, indent, increment, indent,
indent::IndentStyle, indent::IndentStyle,
@ -2419,9 +2419,7 @@ fn append_mode(cx: &mut Context) {
} }
fn file_picker(cx: &mut Context) { fn file_picker(cx: &mut Context) {
// We don't specify language markers, root will be the root of the current let root = find_workspace();
// git repo or the current dir if we're not in a repo
let root = find_root(None, &[]);
let picker = ui::file_picker(root, &cx.editor.config()); let picker = ui::file_picker(root, &cx.editor.config());
cx.push_layer(Box::new(overlayed(picker))); cx.push_layer(Box::new(overlayed(picker)));
} }

@ -1371,13 +1371,16 @@ fn lsp_restart(
return Ok(()); return Ok(());
} }
let editor_config = cx.editor.config.load();
let (_view, doc) = current!(cx.editor); let (_view, doc) = current!(cx.editor);
let config = doc let config = doc
.language_config() .language_config()
.context("LSP not defined for the current document")?; .context("LSP not defined for the current document")?;
let scope = config.scope.clone(); let scope = config.scope.clone();
cx.editor.language_servers.restart(config, doc.path())?; cx.editor
.language_servers
.restart(config, doc.path(), &editor_config.workspace_lsp_roots)?;
// This collect is needed because refresh_language_server would need to re-borrow editor. // This collect is needed because refresh_language_server would need to re-borrow editor.
let document_ids_to_refresh: Vec<DocumentId> = cx let document_ids_to_refresh: Vec<DocumentId> = cx
@ -1970,6 +1973,20 @@ fn open_config(
Ok(()) Ok(())
} }
fn open_workspace_config(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
cx.editor
.open(&helix_loader::workspace_config_file(), Action::Replace)?;
Ok(())
}
fn open_log( fn open_log(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[Cow<str>], _args: &[Cow<str>],
@ -2646,6 +2663,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: open_config, fun: open_config,
signature: CommandSignature::none(), signature: CommandSignature::none(),
}, },
TypableCommand {
name: "config-open-workspace",
aliases: &[],
doc: "Open the workspace config.toml file.",
fun: open_workspace_config,
signature: CommandSignature::none(),
},
TypableCommand { TypableCommand {
name: "log-open", name: "log-open",
aliases: &[], aliases: &[],

@ -1,27 +1,34 @@
use crate::keymap::{default::default, merge_keys, Keymap}; use crate::keymap;
use crate::keymap::{merge_keys, Keymap};
use helix_loader::merge_toml_values;
use helix_view::document::Mode; use helix_view::document::Mode;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::fs;
use std::io::Error as IOError; use std::io::Error as IOError;
use std::path::PathBuf;
use toml::de::Error as TomlError; use toml::de::Error as TomlError;
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Config { pub struct Config {
pub theme: Option<String>, pub theme: Option<String>,
#[serde(default = "default")]
pub keys: HashMap<Mode, Keymap>, pub keys: HashMap<Mode, Keymap>,
#[serde(default)]
pub editor: helix_view::editor::Config, pub editor: helix_view::editor::Config,
} }
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfigRaw {
pub theme: Option<String>,
pub keys: Option<HashMap<Mode, Keymap>>,
pub editor: Option<toml::Value>,
}
impl Default for Config { impl Default for Config {
fn default() -> Config { fn default() -> Config {
Config { Config {
theme: None, theme: None,
keys: default(), keys: keymap::default(),
editor: helix_view::editor::Config::default(), editor: helix_view::editor::Config::default(),
} }
} }
@ -33,6 +40,12 @@ pub enum ConfigLoadError {
Error(IOError), Error(IOError),
} }
impl Default for ConfigLoadError {
fn default() -> Self {
ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder"))
}
}
impl Display for ConfigLoadError { impl Display for ConfigLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -43,17 +56,72 @@ impl Display for ConfigLoadError {
} }
impl Config { impl Config {
pub fn load(config_path: PathBuf) -> Result<Config, ConfigLoadError> { pub fn load(
match std::fs::read_to_string(config_path) { global: Result<String, ConfigLoadError>,
Ok(config) => toml::from_str(&config) local: Result<String, ConfigLoadError>,
.map(merge_keys) ) -> Result<Config, ConfigLoadError> {
.map_err(ConfigLoadError::BadConfig), let global_config: Result<ConfigRaw, ConfigLoadError> =
Err(err) => Err(ConfigLoadError::Error(err)), 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) {
(Ok(global), Ok(local)) => {
let mut keys = keymap::default();
if let Some(global_keys) = global.keys {
merge_keys(&mut keys, global_keys)
}
if let Some(local_keys) = local.keys {
merge_keys(&mut keys, local_keys)
}
let editor = match (global.editor, local.editor) {
(None, None) => helix_view::editor::Config::default(),
(None, Some(val)) | (Some(val), None) => {
val.try_into().map_err(ConfigLoadError::BadConfig)?
}
(Some(global), Some(local)) => merge_toml_values(global, local, 3)
.try_into()
.map_err(ConfigLoadError::BadConfig)?,
};
Config {
theme: local.theme.or(global.theme),
keys,
editor,
}
}
// if any configs are invalid return that first
(_, Err(ConfigLoadError::BadConfig(err)))
| (Err(ConfigLoadError::BadConfig(err)), _) => {
return Err(ConfigLoadError::BadConfig(err))
}
(Ok(config), Err(_)) | (Err(_), Ok(config)) => {
let mut keys = keymap::default();
if let Some(keymap) = config.keys {
merge_keys(&mut keys, keymap);
}
Config {
theme: config.theme,
keys,
editor: config.editor.map_or_else(
|| Ok(helix_view::editor::Config::default()),
|val| val.try_into().map_err(ConfigLoadError::BadConfig),
)?,
}
}
// these are just two io errors return the one for the global config
(Err(err), Err(_)) => return Err(err),
};
Ok(res)
} }
pub fn load_default() -> Result<Config, ConfigLoadError> { pub fn load_default() -> Result<Config, ConfigLoadError> {
Config::load(helix_loader::config_file()) let global_config =
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)
} }
} }
@ -61,6 +129,12 @@ impl Config {
mod tests { mod tests {
use super::*; use super::*;
impl Config {
fn load_test(config: &str) -> Config {
Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap()
}
}
#[test] #[test]
fn parsing_keymaps_config_file() { fn parsing_keymaps_config_file() {
use crate::keymap; use crate::keymap;
@ -77,18 +151,24 @@ mod tests {
A-F12 = "move_next_word_end" A-F12 = "move_next_word_end"
"#; "#;
let mut keys = keymap::default();
merge_keys(
&mut keys,
hashmap! {
Mode::Insert => Keymap::new(keymap!({ "Insert mode"
"y" => move_line_down,
"S-C-a" => delete_selection,
})),
Mode::Normal => Keymap::new(keymap!({ "Normal mode"
"A-F12" => move_next_word_end,
})),
},
);
assert_eq!( assert_eq!(
toml::from_str::<Config>(sample_keymaps).unwrap(), Config::load_test(sample_keymaps),
Config { Config {
keys: hashmap! { keys,
Mode::Insert => Keymap::new(keymap!({ "Insert mode"
"y" => move_line_down,
"S-C-a" => delete_selection,
})),
Mode::Normal => Keymap::new(keymap!({ "Normal mode"
"A-F12" => move_next_word_end,
})),
},
..Default::default() ..Default::default()
} }
); );
@ -97,11 +177,11 @@ mod tests {
#[test] #[test]
fn keys_resolve_to_correct_defaults() { fn keys_resolve_to_correct_defaults() {
// From serde default // From serde default
let default_keys = toml::from_str::<Config>("").unwrap().keys; let default_keys = Config::load_test("").keys;
assert_eq!(default_keys, default()); assert_eq!(default_keys, keymap::default());
// From the Default trait // From the Default trait
let default_keys = Config::default().keys; let default_keys = Config::default().keys;
assert_eq!(default_keys, default()); assert_eq!(default_keys, keymap::default());
} }
} }

@ -2,7 +2,6 @@ pub mod default;
pub mod macros; pub mod macros;
pub use crate::commands::MappableCommand; pub use crate::commands::MappableCommand;
use crate::config::Config;
use arc_swap::{ use arc_swap::{
access::{DynAccess, DynGuard}, access::{DynAccess, DynGuard},
ArcSwap, ArcSwap,
@ -16,7 +15,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use default::default; pub use default::default;
use macros::key; use macros::key;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -417,12 +416,10 @@ impl Default for Keymaps {
} }
/// Merge default config keys with user overwritten keys for custom user config. /// Merge default config keys with user overwritten keys for custom user config.
pub fn merge_keys(mut config: Config) -> Config { pub fn merge_keys(dst: &mut HashMap<Mode, Keymap>, mut delta: HashMap<Mode, Keymap>) {
let mut delta = std::mem::replace(&mut config.keys, default()); for (mode, keys) in dst {
for (mode, keys) in &mut config.keys {
keys.merge(delta.remove(mode).unwrap_or_default()) keys.merge(delta.remove(mode).unwrap_or_default())
} }
config
} }
#[cfg(test)] #[cfg(test)]
@ -449,26 +446,24 @@ mod tests {
#[test] #[test]
fn merge_partial_keys() { fn merge_partial_keys() {
let config = Config { let keymap = hashmap! {
keys: hashmap! { Mode::Normal => Keymap::new(
Mode::Normal => Keymap::new( keymap!({ "Normal mode"
keymap!({ "Normal mode" "i" => normal_mode,
"i" => normal_mode, "无" => insert_mode,
"无" => insert_mode, "z" => jump_backward,
"z" => jump_backward, "g" => { "Merge into goto mode"
"g" => { "Merge into goto mode" "$" => goto_line_end,
"$" => goto_line_end, "g" => delete_char_forward,
"g" => delete_char_forward, },
}, })
}) )
)
},
..Default::default()
}; };
let mut merged_config = merge_keys(config.clone()); let mut merged_keyamp = default();
assert_ne!(config, merged_config); merge_keys(&mut merged_keyamp, keymap.clone());
assert_ne!(keymap, merged_keyamp);
let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone())));
assert_eq!( assert_eq!(
keymap.get(Mode::Normal, key!('i')), keymap.get(Mode::Normal, key!('i')),
KeymapResult::Matched(MappableCommand::normal_mode), KeymapResult::Matched(MappableCommand::normal_mode),
@ -486,7 +481,7 @@ mod tests {
"Leaf should replace node" "Leaf should replace node"
); );
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap // Assumes that `g` is a node in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(), keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
@ -506,30 +501,28 @@ mod tests {
"Old leaves in subnode should be present in merged node" "Old leaves in subnode should be present in merged node"
); );
assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0);
} }
#[test] #[test]
fn order_should_be_set() { fn order_should_be_set() {
let config = Config { let keymap = hashmap! {
keys: hashmap! { Mode::Normal => Keymap::new(
Mode::Normal => Keymap::new( keymap!({ "Normal mode"
keymap!({ "Normal mode" "space" => { ""
"space" => { "" "s" => { ""
"s" => { "" "v" => vsplit,
"v" => vsplit, "c" => hsplit,
"c" => hsplit,
},
}, },
}) },
) })
}, )
..Default::default()
}; };
let mut merged_config = merge_keys(config.clone()); let mut merged_keyamp = default();
assert_ne!(config, merged_config); merge_keys(&mut merged_keyamp, keymap.clone());
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); assert_ne!(keymap, merged_keyamp);
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works // Make sure mapping works
assert_eq!( assert_eq!(
keymap keymap

@ -3,7 +3,7 @@ use crossterm::event::EventStream;
use helix_loader::VERSION_AND_GIT_HASH; use helix_loader::VERSION_AND_GIT_HASH;
use helix_term::application::Application; use helix_term::application::Application;
use helix_term::args::Args; use helix_term::args::Args;
use helix_term::config::Config; use helix_term::config::{Config, ConfigLoadError};
use std::path::PathBuf; use std::path::PathBuf;
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@ -126,18 +126,19 @@ FLAGS:
helix_loader::initialize_config_file(args.config_file.clone()); helix_loader::initialize_config_file(args.config_file.clone());
let config = match std::fs::read_to_string(helix_loader::config_file()) { let config = match Config::load_default() {
Ok(config) => toml::from_str(&config) Ok(config) => config,
.map(helix_term::keymap::merge_keys) Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
.unwrap_or_else(|err| { Config::default()
eprintln!("Bad config: {}", err); }
eprintln!("Press <ENTER> to continue with default config"); Err(ConfigLoadError::Error(err)) => return Err(Error::new(err)),
use std::io::Read; Err(ConfigLoadError::BadConfig(err)) => {
let _ = std::io::stdin().read(&mut []); eprintln!("Bad config: {}", err);
Config::default() eprintln!("Press <ENTER> to continue with default config");
}), use std::io::Read;
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), let _ = std::io::stdin().read(&mut []);
Err(err) => return Err(Error::new(err)), Config::default()
}
}; };
let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| { let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {

@ -1,6 +1,7 @@
use std::{ use std::{
fs::File, fs::File,
io::{Read, Write}, io::{Read, Write},
mem::replace,
path::PathBuf, path::PathBuf,
time::Duration, time::Duration,
}; };
@ -222,10 +223,11 @@ pub fn temp_file_with_contents<S: AsRef<str>>(
/// Generates a config with defaults more suitable for integration tests /// Generates a config with defaults more suitable for integration tests
pub fn test_config() -> Config { pub fn test_config() -> Config {
merge_keys(Config { Config {
editor: test_editor_config(), editor: test_editor_config(),
keys: helix_term::keymap::default(),
..Default::default() ..Default::default()
}) }
} }
pub fn test_editor_config() -> helix_view::editor::Config { pub fn test_editor_config() -> helix_view::editor::Config {
@ -300,8 +302,10 @@ impl AppBuilder {
// Remove this attribute once `with_config` is used in a test: // Remove this attribute once `with_config` is used in a test:
#[allow(dead_code)] #[allow(dead_code)]
pub fn with_config(mut self, config: Config) -> Self { pub fn with_config(mut self, mut config: Config) -> Self {
self.config = helix_term::keymap::merge_keys(config); let keys = replace(&mut config.keys, helix_term::keymap::default());
merge_keys(&mut config.keys, keys);
self.config = config;
self self
} }

@ -282,6 +282,8 @@ pub struct Config {
/// Whether to color modes with different colors. Defaults to `false`. /// Whether to color modes with different colors. Defaults to `false`.
pub color_modes: bool, pub color_modes: bool,
pub soft_wrap: SoftWrap, pub soft_wrap: SoftWrap,
/// Workspace specific lsp ceiling dirs
pub workspace_lsp_roots: Vec<PathBuf>,
} }
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -747,6 +749,7 @@ impl Default for Config {
soft_wrap: SoftWrap::default(), soft_wrap: SoftWrap::default(),
text_width: 80, text_width: 80,
completion_replace: false, completion_replace: false,
workspace_lsp_roots: Vec::new(),
} }
} }
} }
@ -1087,15 +1090,14 @@ impl Editor {
} }
// if doc doesn't have a URL it's a scratch buffer, ignore it // if doc doesn't have a URL it's a scratch buffer, ignore it
let (lang, path) = { let doc = self.document(doc_id)?;
let doc = self.document(doc_id)?; let (lang, path) = (doc.language.clone(), doc.path().cloned());
(doc.language.clone(), doc.path().cloned()) let root_dirs = &doc.config.load().workspace_lsp_roots;
};
// try to find a language server based on the language name // try to find a language server based on the language name
let language_server = lang.as_ref().and_then(|language| { let language_server = lang.as_ref().and_then(|language| {
self.language_servers self.language_servers
.get(language, path.as_ref()) .get(language, path.as_ref(), root_dirs)
.map_err(|e| { .map_err(|e| {
log::error!( log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}", "Failed to initialize the LSP for `{}` {{ {} }}",

Loading…
Cancel
Save