on master help sign in
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>

commit2d10a429ebf7abe5af184b6227346377dc0523e8 authorPascal Kuthe <pascal.kuthe@semimod.de> dateTue 31 Jan 2023, 00:31 (4 years ago) parentd59b805
16 files changed+295−182
book/src/configuration.md+4 book/src/generated/typable-cmd.md+1 book/src/languages.md+1 helix-core/src/lib.rs+2−45 helix-core/src/syntax.rs+5−1 helix-loader/src/config.rs+3−5 helix-loader/src/lib.rs+18−26 helix-lsp/src/client.rs+8−4 helix-lsp/src/lib.rs+53−3 helix-term/src/commands.rs+2−4 helix-term/src/commands/typed.rs+25−1 helix-term/src/config.rs+108−28 helix-term/src/keymap.rs+36−43 helix-term/src/main.rs+14−13 helix-term/tests/test/helpers.rs+8−4 helix-view/src/editor.rs+7−5
book/src/configuration.md+4
@@ -30,6 +30,9 @@ You can use a custom configuration file by specifying it with the `-c` or
3030Additionally, you can reload the configuration file by sending the USR1
3131signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.
3232
33+Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
34+Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.
35+
3336## Editor
3437
3538### `[editor]` Section
@@ -58,6 +61,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
5861| `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` |
5962| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
6063| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` |
64+| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
6165
6266### `[editor.statusline]` Section
6367
book/src/generated/typable-cmd.md+1
@@ -70,6 +70,7 @@
7070| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
7171| `:config-reload` | Refresh user config. |
7272| `:config-open` | Open the user config.toml file. |
73+| `:config-open-workspace` | Open the workspace config.toml file. |
7374| `:log-open` | Open the helix log file. |
7475| `:insert-output` | Run shell command, inserting output before each selection. |
7576| `:append-output` | Run shell command, appending output after each selection. |
book/src/languages.md+1
@@ -64,6 +64,7 @@ These configuration keys are available:
6464| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
6565| `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 |
6666| `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` |
67+| `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. | `` |
6768
6869### File-type detection and the `file-types` key
6970
helix-core/src/lib.rs+2−45
@@ -36,55 +36,12 @@ pub mod unicode {
3636 pub use unicode_width as width;
3737}
3838
39+pub use helix_loader::find_workspace;
40+
3941pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
4042 line.chars().position(|ch| !ch.is_whitespace())
4143}
4244
43−/// Find project root.
44−///
45−/// Order of detection:
46−/// * Top-most folder containing a root marker in current git repository
47−/// * Git repository root if no marker detected
48−/// * Top-most folder containing a root marker if not git repository detected
49−/// * Current working directory as fallback
50−pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::PathBuf {
51− let current_dir = std::env::current_dir().expect("unable to determine current directory");
52−
53− let root = match root {
54− Some(root) => {
55− let root = std::path::Path::new(root);
56− if root.is_absolute() {
57− root.to_path_buf()
58− } else {
59− current_dir.join(root)
60− }
61− }
62− None => current_dir.clone(),
63− };
64−
65− let mut top_marker = None;
66− for ancestor in root.ancestors() {
67− if root_markers
68− .iter()
69− .any(|marker| ancestor.join(marker).exists())
70− {
71− top_marker = Some(ancestor);
72− }
73−
74− if ancestor.join(".git").exists() {
75− // Top marker is repo root if not root marker was detected yet
76− if top_marker.is_none() {
77− top_marker = Some(ancestor);
78− }
79− // Don't go higher than repo if we're in one
80− break;
81− }
82− }
83−
84− // Return the found top marker or the current_dir as fallback
85− top_marker.map_or(current_dir, |a| a.to_path_buf())
86−}
87−
8845pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
8946
9047// pub use tendril::StrTendril as Tendril;
helix-core/src/syntax.rs+5−1
@@ -20,7 +20,7 @@ use std::{
2020 fmt,
2121 hash::{Hash, Hasher},
2222 mem::{replace, transmute},
23− path::Path,
23+ path::{Path, PathBuf},
2424 str::FromStr,
2525 sync::Arc,
2626};
@@ -127,6 +127,10 @@ pub struct LanguageConfiguration {
127127 pub auto_pairs: Option<AutoPairs>,
128128
129129 pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
130+
131+ /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
132+ /// Falling back to the current working directory if none are configured.
133+ pub workspace_lsp_roots: Option<Vec<PathBuf>>,
130134}
131135
132136#[derive(Debug, PartialEq, Eq, Hash)]
helix-loader/src/config.rs+3−5
@@ -9,9 +9,8 @@ pub fn default_lang_config() -> toml::Value {
99
1010/// User configured languages.toml file, merged with the default config.
1111pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
12− let config = crate::local_config_dirs()
12+ let config = [crate::config_dir(), crate::find_workspace().join(".helix")]
1313 .into_iter()
14− .chain([crate::config_dir()].into_iter())
1514 .map(|path| path.join("languages.toml"))
1615 .filter_map(|file| {
1716 std::fs::read_to_string(file)
@@ -20,8 +19,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
2019 })
2120 .collect::<Result<Vec<_>, _>>()?
2221 .into_iter()
23− .chain([default_lang_config()].into_iter())
24− .fold(toml::Value::Table(toml::value::Table::default()), |a, b| {
22+ .fold(default_lang_config(), |a, b| {
2523 // combines for example
2624 // b:
2725 // [[language]]
@@ -38,7 +36,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
3836 // language-server = { command = "/usr/bin/taplo" }
3937 //
4038 // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
41− crate::merge_toml_values(b, a, 3)
39+ crate::merge_toml_values(a, b, 3)
4240 });
4341
4442 Ok(config)
helix-loader/src/lib.rs+18−26
@@ -42,7 +42,7 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
4242 let mut rt_dirs = Vec::new();
4343 if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
4444 // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
45− let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
45+ let path = PathBuf::from(dir).parent().unwrap().join(RT_DIR);
4646 log::debug!("runtime dir: {}", path.to_string_lossy());
4747 rt_dirs.push(path);
4848 }
@@ -113,15 +113,6 @@ pub fn config_dir() -> PathBuf {
113113 path
114114}
115115
116−pub fn local_config_dirs() -> Vec<PathBuf> {
117− let directories = find_local_config_dirs()
118− .into_iter()
119− .map(|path| path.join(".helix"))
120− .collect();
121− log::debug!("Located configuration folders: {:?}", directories);
122− directories
123−}
124−
125116pub fn cache_dir() -> PathBuf {
126117 // TODO: allow env var override
127118 let strategy = choose_base_strategy().expect("Unable to find the config directory!");
@@ -137,6 +128,10 @@ pub fn config_file() -> PathBuf {
137128 .unwrap_or_else(|| config_dir().join("config.toml"))
138129}
139130
131+pub fn workspace_config_file() -> PathBuf {
132+ find_workspace().join(".helix").join("config.toml")
133+}
134+
140135pub fn lang_config_file() -> PathBuf {
141136 config_dir().join("languages.toml")
142137}
@@ -145,22 +140,6 @@ pub fn log_file() -> PathBuf {
145140 cache_dir().join("helix.log")
146141}
147142
148−pub fn find_local_config_dirs() -> Vec<PathBuf> {
149− let current_dir = std::env::current_dir().expect("unable to determine current directory");
150− let mut directories = Vec::new();
151−
152− for ancestor in current_dir.ancestors() {
153− if ancestor.join(".git").exists() {
154− directories.push(ancestor.to_path_buf());
155− // Don't go higher than repo if we're in one
156− break;
157− } else if ancestor.join(".helix").is_dir() {
158− directories.push(ancestor.to_path_buf());
159− }
160− }
161− directories
162−}
163−
164143/// Merge two TOML documents, merging values from `right` onto `left`
165144///
166145/// When an array exists in both `left` and `right`, `right`'s array is
@@ -302,3 +281,16 @@ mod merge_toml_tests {
302281 )
303282 }
304283}
284+
285+/// Finds the current workspace folder.
286+/// Used as a ceiling dir for root resolve, for the filepicker and other related
287+pub fn find_workspace() -> PathBuf {
288+ let current_dir = std::env::current_dir().expect("unable to determine current directory");
289+ for ancestor in current_dir.ancestors() {
290+ if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
291+ return ancestor.to_owned();
292+ }
293+ }
294+
295+ current_dir
296+}
helix-lsp/src/client.rs+8−4
@@ -1,22 +1,22 @@
11use crate::{
2− jsonrpc,
2+ find_root, jsonrpc,
33 transport::{Payload, Transport},
44 Call, Error, OffsetEncoding, Result,
55};
66
7−use helix_core::{find_root, ChangeSet, Rope};
7+use helix_core::{ChangeSet, Rope};
88use helix_loader::{self, VERSION_AND_GIT_HASH};
99use lsp::PositionEncodingKind;
1010use lsp_types as lsp;
1111use serde::Deserialize;
1212use serde_json::Value;
13−use std::collections::HashMap;
1413use std::future::Future;
1514use std::process::Stdio;
1615use std::sync::{
1716 atomic::{AtomicU64, Ordering},
1817 Arc,
1918};
19+use std::{collections::HashMap, path::PathBuf};
2020use tokio::{
2121 io::{BufReader, BufWriter},
2222 process::{Child, Command},
@@ -49,6 +49,7 @@ impl Client {
4949 config: Option<Value>,
5050 server_environment: HashMap<String, String>,
5151 root_markers: &[String],
52+ manual_roots: &[PathBuf],
5253 id: usize,
5354 req_timeout: u64,
5455 doc_path: Option<&std::path::PathBuf>,
@@ -77,8 +78,11 @@ impl Client {
7778 Transport::start(reader, writer, stderr, id);
7879
7980 let root_path = find_root(
80− doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())),
81+ doc_path
82+ .and_then(|x| x.parent().and_then(|x| x.to_str()))
83+ .unwrap_or("."),
8184 root_markers,
85+ manual_roots,
8286 );
8387
8488 let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();
helix-lsp/src/lib.rs+53−3
@@ -10,11 +10,15 @@ pub use lsp::{Position, Url};
1010pub use lsp_types as lsp;
1111
1212use futures_util::stream::select_all::SelectAll;
13−use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration};
13+use helix_core::{
14+ find_workspace,
15+ syntax::{LanguageConfiguration, LanguageServerConfiguration},
16+};
1417use tokio::sync::mpsc::UnboundedReceiver;
1518
1619use std::{
1720 collections::{hash_map::Entry, HashMap},
21+ path::PathBuf,
1822 sync::{
1923 atomic::{AtomicUsize, Ordering},
2024 Arc,
@@ -641,6 +645,7 @@ impl Registry {
641645 &mut self,
642646 language_config: &LanguageConfiguration,
643647 doc_path: Option<&std::path::PathBuf>,
648+ root_dirs: &[PathBuf],
644649 ) -> Result<Option<Arc<Client>>> {
645650 let config = match &language_config.language_server {
646651 Some(config) => config,
@@ -656,7 +661,7 @@ impl Registry {
656661 let id = self.counter.fetch_add(1, Ordering::Relaxed);
657662
658663 let NewClientResult(client, incoming) =
659− start_client(id, language_config, config, doc_path)?;
664+ start_client(id, language_config, config, doc_path, root_dirs)?;
660665 self.incoming.push(UnboundedReceiverStream::new(incoming));
661666
662667 let (_, old_client) = entry.insert((id, client.clone()));
@@ -684,6 +689,7 @@ impl Registry {
684689 &mut self,
685690 language_config: &LanguageConfiguration,
686691 doc_path: Option<&std::path::PathBuf>,
692+ root_dirs: &[PathBuf],
687693 ) -> Result<Option<Arc<Client>>> {
688694 let config = match &language_config.language_server {
689695 Some(config) => config,
@@ -697,7 +703,7 @@ impl Registry {
697703 let id = self.counter.fetch_add(1, Ordering::Relaxed);
698704
699705 let NewClientResult(client, incoming) =
700− start_client(id, language_config, config, doc_path)?;
706+ start_client(id, language_config, config, doc_path, root_dirs)?;
701707 self.incoming.push(UnboundedReceiverStream::new(incoming));
702708
703709 entry.insert((id, client.clone()));
@@ -798,6 +804,7 @@ fn start_client(
798804 config: &LanguageConfiguration,
799805 ls_config: &LanguageServerConfiguration,
800806 doc_path: Option<&std::path::PathBuf>,
807+ root_dirs: &[PathBuf],
801808) -> Result<NewClientResult> {
802809 let (client, incoming, initialize_notify) = Client::start(
803810 &ls_config.command,
@@ -805,6 +812,7 @@ fn start_client(
805812 config.config.clone(),
806813 ls_config.environment.clone(),
807814 &config.roots,
815+ config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
808816 id,
809817 ls_config.timeout,
810818 doc_path,
@@ -842,6 +850,48 @@ fn start_client(
842850 Ok(NewClientResult(client, incoming))
843851}
844852
853+/// Find an LSP root of a file using the following mechansim:
854+/// * start at `file` (either an absolute path or relative to CWD)
855+/// * find the top most directory containing a root_marker
856+/// * inside the current workspace
857+/// * stop the search at the first root_dir that contains `file` or the workspace (obtained from `helix_core::find_workspace`)
858+/// * root_dirs only apply inside the workspace. For files outside of the workspace they are ignored
859+/// * outside the current workspace: keep searching to the top of the file hiearchy
860+pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) -> PathBuf {
861+ let file = std::path::Path::new(file);
862+ let workspace = find_workspace();
863+ let file = if file.is_absolute() {
864+ file.to_path_buf()
865+ } else {
866+ let current_dir = std::env::current_dir().expect("unable to determine current directory");
867+ current_dir.join(file)
868+ };
869+
870+ let inside_workspace = file.strip_prefix(&workspace).is_ok();
871+
872+ let mut top_marker = None;
873+ for ancestor in file.ancestors() {
874+ if root_markers
875+ .iter()
876+ .any(|marker| ancestor.join(marker).exists())
877+ {
878+ top_marker = Some(ancestor);
879+ }
880+
881+ if inside_workspace
882+ && (ancestor == workspace
883+ || root_dirs
884+ .iter()
885+ .any(|root_dir| root_dir == ancestor.strip_prefix(&workspace).unwrap()))
886+ {
887+ return top_marker.unwrap_or(ancestor).to_owned();
888+ }
889+ }
890+
891+ // If no root was found use the workspace as a fallback
892+ workspace
893+}
894+
845895#[cfg(test)]
846896mod tests {
847897 use super::{lsp, util::*, OffsetEncoding};
helix-term/src/commands.rs+2−4
@@ -12,7 +12,7 @@ pub use typed::*;
1212use helix_core::{
1313 char_idx_at_visual_offset, comment,
1414 doc_formatter::TextFormat,
15− encoding, find_first_non_whitespace_char, find_root, graphemes,
15+ encoding, find_first_non_whitespace_char, find_workspace, graphemes,
1616 history::UndoKind,
1717 increment, indent,
1818 indent::IndentStyle,
@@ -2419,9 +2419,7 @@ fn append_mode(cx: &mut Context) {
24192419}
24202420
24212421fn file_picker(cx: &mut Context) {
2422− // We don't specify language markers, root will be the root of the current
2423− // git repo or the current dir if we're not in a repo
2424− let root = find_root(None, &[]);
2422+ let root = find_workspace();
24252423 let picker = ui::file_picker(root, &cx.editor.config());
24262424 cx.push_layer(Box::new(overlayed(picker)));
24272425}
helix-term/src/commands/typed.rs+25−1
@@ -1371,13 +1371,16 @@ fn lsp_restart(
13711371 return Ok(());
13721372 }
13731373
1374+ let editor_config = cx.editor.config.load();
13741375 let (_view, doc) = current!(cx.editor);
13751376 let config = doc
13761377 .language_config()
13771378 .context("LSP not defined for the current document")?;
13781379
13791380 let scope = config.scope.clone();
1380− cx.editor.language_servers.restart(config, doc.path())?;
1381+ cx.editor
1382+ .language_servers
1383+ .restart(config, doc.path(), &editor_config.workspace_lsp_roots)?;
13811384
13821385 // This collect is needed because refresh_language_server would need to re-borrow editor.
13831386 let document_ids_to_refresh: Vec<DocumentId> = cx
@@ -1970,6 +1973,20 @@ fn open_config(
19701973 Ok(())
19711974}
19721975
1976+fn open_workspace_config(
1977+ cx: &mut compositor::Context,
1978+ _args: &[Cow<str>],
1979+ event: PromptEvent,
1980+) -> anyhow::Result<()> {
1981+ if event != PromptEvent::Validate {
1982+ return Ok(());
1983+ }
1984+
1985+ cx.editor
1986+ .open(&helix_loader::workspace_config_file(), Action::Replace)?;
1987+ Ok(())
1988+}
1989+
19731990fn open_log(
19741991 cx: &mut compositor::Context,
19751992 _args: &[Cow<str>],
@@ -2646,6 +2663,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
26462663 fun: open_config,
26472664 signature: CommandSignature::none(),
26482665 },
2666+ TypableCommand {
2667+ name: "config-open-workspace",
2668+ aliases: &[],
2669+ doc: "Open the workspace config.toml file.",
2670+ fun: open_workspace_config,
2671+ signature: CommandSignature::none(),
2672+ },
26492673 TypableCommand {
26502674 name: "log-open",
26512675 aliases: &[],
helix-term/src/config.rs+108−28
@@ -1,27 +1,34 @@
1−use crate::keymap::{default::default, merge_keys, Keymap};
1+use crate::keymap;
2+use crate::keymap::{merge_keys, Keymap};
3+use helix_loader::merge_toml_values;
24use helix_view::document::Mode;
35use serde::Deserialize;
46use std::collections::HashMap;
57use std::fmt::Display;
8+use std::fs;
69use std::io::Error as IOError;
7−use std::path::PathBuf;
810use toml::de::Error as TomlError;
911
10−#[derive(Debug, Clone, PartialEq, Deserialize)]
11−#[serde(deny_unknown_fields)]
12+#[derive(Debug, Clone, PartialEq)]
1213pub struct Config {
1314 pub theme: Option<String>,
14− #[serde(default = "default")]
1515 pub keys: HashMap<Mode, Keymap>,
16− #[serde(default)]
1716 pub editor: helix_view::editor::Config,
1817}
1918
19+#[derive(Debug, Clone, PartialEq, Deserialize)]
20+#[serde(deny_unknown_fields)]
21+pub struct ConfigRaw {
22+ pub theme: Option<String>,
23+ pub keys: Option<HashMap<Mode, Keymap>>,
24+ pub editor: Option<toml::Value>,
25+}
26+
2027impl Default for Config {
2128 fn default() -> Config {
2229 Config {
2330 theme: None,
24− keys: default(),
31+ keys: keymap::default(),
2532 editor: helix_view::editor::Config::default(),
2633 }
2734 }
@@ -33,6 +40,12 @@ pub enum ConfigLoadError {
3340 Error(IOError),
3441}
3542
43+impl Default for ConfigLoadError {
44+ fn default() -> Self {
45+ ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder"))
46+ }
47+}
48+
3649impl Display for ConfigLoadError {
3750 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3851 match self {
@@ -43,17 +56,72 @@ impl Display for ConfigLoadError {
4356}
4457
4558impl Config {
46− pub fn load(config_path: PathBuf) -> Result<Config, ConfigLoadError> {
47− match std::fs::read_to_string(config_path) {
48− Ok(config) => toml::from_str(&config)
49− .map(merge_keys)
50− .map_err(ConfigLoadError::BadConfig),
51− Err(err) => Err(ConfigLoadError::Error(err)),
52− }
59+ pub fn load(
60+ global: Result<String, ConfigLoadError>,
61+ local: Result<String, ConfigLoadError>,
62+ ) -> Result<Config, ConfigLoadError> {
63+ let global_config: Result<ConfigRaw, ConfigLoadError> =
64+ global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
65+ let local_config: Result<ConfigRaw, ConfigLoadError> =
66+ local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
67+ let res = match (global_config, local_config) {
68+ (Ok(global), Ok(local)) => {
69+ let mut keys = keymap::default();
70+ if let Some(global_keys) = global.keys {
71+ merge_keys(&mut keys, global_keys)
72+ }
73+ if let Some(local_keys) = local.keys {
74+ merge_keys(&mut keys, local_keys)
75+ }
76+
77+ let editor = match (global.editor, local.editor) {
78+ (None, None) => helix_view::editor::Config::default(),
79+ (None, Some(val)) | (Some(val), None) => {
80+ val.try_into().map_err(ConfigLoadError::BadConfig)?
81+ }
82+ (Some(global), Some(local)) => merge_toml_values(global, local, 3)
83+ .try_into()
84+ .map_err(ConfigLoadError::BadConfig)?,
85+ };
86+
87+ Config {
88+ theme: local.theme.or(global.theme),
89+ keys,
90+ editor,
91+ }
92+ }
93+ // if any configs are invalid return that first
94+ (_, Err(ConfigLoadError::BadConfig(err)))
95+ | (Err(ConfigLoadError::BadConfig(err)), _) => {
96+ return Err(ConfigLoadError::BadConfig(err))
97+ }
98+ (Ok(config), Err(_)) | (Err(_), Ok(config)) => {
99+ let mut keys = keymap::default();
100+ if let Some(keymap) = config.keys {
101+ merge_keys(&mut keys, keymap);
102+ }
103+ Config {
104+ theme: config.theme,
105+ keys,
106+ editor: config.editor.map_or_else(
107+ || Ok(helix_view::editor::Config::default()),
108+ |val| val.try_into().map_err(ConfigLoadError::BadConfig),
109+ )?,
110+ }
111+ }
112+ // these are just two io errors return the one for the global config
113+ (Err(err), Err(_)) => return Err(err),
114+ };
115+
116+ Ok(res)
53117 }
54118
55119 pub fn load_default() -> Result<Config, ConfigLoadError> {
56− Config::load(helix_loader::config_file())
120+ let global_config =
121+ fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error);
122+ let local_config = fs::read_to_string(helix_loader::workspace_config_file())
123+ .map_err(ConfigLoadError::Error);
124+ Config::load(global_config, local_config)
57125 }
58126}
59127
@@ -61,6 +129,12 @@ impl Config {
61129mod tests {
62130 use super::*;
63131
132+ impl Config {
133+ fn load_test(config: &str) -> Config {
134+ Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap()
135+ }
136+ }
137+
64138 #[test]
65139 fn parsing_keymaps_config_file() {
66140 use crate::keymap;
@@ -77,18 +151,24 @@ mod tests {
77151 A-F12 = "move_next_word_end"
78152 "#;
79153
154+ let mut keys = keymap::default();
155+ merge_keys(
156+ &mut keys,
157+ hashmap! {
158+ Mode::Insert => Keymap::new(keymap!({ "Insert mode"
159+ "y" => move_line_down,
160+ "S-C-a" => delete_selection,
161+ })),
162+ Mode::Normal => Keymap::new(keymap!({ "Normal mode"
163+ "A-F12" => move_next_word_end,
164+ })),
165+ },
166+ );
167+
80168 assert_eq!(
81− toml::from_str::<Config>(sample_keymaps).unwrap(),
169+ Config::load_test(sample_keymaps),
82170 Config {
83− keys: hashmap! {
84− Mode::Insert => Keymap::new(keymap!({ "Insert mode"
85− "y" => move_line_down,
86− "S-C-a" => delete_selection,
87− })),
88− Mode::Normal => Keymap::new(keymap!({ "Normal mode"
89− "A-F12" => move_next_word_end,
90− })),
91− },
171+ keys,
92172 ..Default::default()
93173 }
94174 );
@@ -97,11 +177,11 @@ mod tests {
97177 #[test]
98178 fn keys_resolve_to_correct_defaults() {
99179 // From serde default
100− let default_keys = toml::from_str::<Config>("").unwrap().keys;
101− assert_eq!(default_keys, default());
180+ let default_keys = Config::load_test("").keys;
181+ assert_eq!(default_keys, keymap::default());
102182
103183 // From the Default trait
104184 let default_keys = Config::default().keys;
105− assert_eq!(default_keys, default());
185+ assert_eq!(default_keys, keymap::default());
106186 }
107187}
helix-term/src/keymap.rs+36−43
@@ -2,7 +2,6 @@ pub mod default;
22pub mod macros;
33
44pub use crate::commands::MappableCommand;
5−use crate::config::Config;
65use arc_swap::{
76 access::{DynAccess, DynGuard},
87 ArcSwap,
@@ -16,7 +15,7 @@ use std::{
1615 sync::Arc,
1716};
1817
19−use default::default;
18+pub use default::default;
2019use macros::key;
2120
2221#[derive(Debug, Clone)]
@@ -417,12 +416,10 @@ impl Default for Keymaps {
417416}
418417
419418/// Merge default config keys with user overwritten keys for custom user config.
420−pub fn merge_keys(mut config: Config) -> Config {
421− let mut delta = std::mem::replace(&mut config.keys, default());
422− for (mode, keys) in &mut config.keys {
419+pub fn merge_keys(dst: &mut HashMap<Mode, Keymap>, mut delta: HashMap<Mode, Keymap>) {
420+ for (mode, keys) in dst {
423421 keys.merge(delta.remove(mode).unwrap_or_default())
424422 }
425− config
426423}
427424
428425#[cfg(test)]
@@ -449,26 +446,24 @@ mod tests {
449446
450447 #[test]
451448 fn merge_partial_keys() {
452− let config = Config {
453− keys: hashmap! {
454− Mode::Normal => Keymap::new(
455− keymap!({ "Normal mode"
456− "i" => normal_mode,
457− "无" => insert_mode,
458− "z" => jump_backward,
459− "g" => { "Merge into goto mode"
460− "$" => goto_line_end,
461− "g" => delete_char_forward,
462− },
463− })
464− )
465− },
466− ..Default::default()
449+ let keymap = hashmap! {
450+ Mode::Normal => Keymap::new(
451+ keymap!({ "Normal mode"
452+ "i" => normal_mode,
453+ "无" => insert_mode,
454+ "z" => jump_backward,
455+ "g" => { "Merge into goto mode"
456+ "$" => goto_line_end,
457+ "g" => delete_char_forward,
458+ },
459+ })
460+ )
467461 };
468− let mut merged_config = merge_keys(config.clone());
469− assert_ne!(config, merged_config);
462+ let mut merged_keyamp = default();
463+ merge_keys(&mut merged_keyamp, keymap.clone());
464+ assert_ne!(keymap, merged_keyamp);
470465
471− let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone())));
466+ let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone())));
472467 assert_eq!(
473468 keymap.get(Mode::Normal, key!('i')),
474469 KeymapResult::Matched(MappableCommand::normal_mode),
@@ -486,7 +481,7 @@ mod tests {
486481 "Leaf should replace node"
487482 );
488483
489− let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
484+ let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
490485 // Assumes that `g` is a node in default keymap
491486 assert_eq!(
492487 keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
@@ -506,30 +501,28 @@ mod tests {
506501 "Old leaves in subnode should be present in merged node"
507502 );
508503
509− assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1);
510− assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0);
504+ assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1);
505+ assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0);
511506 }
512507
513508 #[test]
514509 fn order_should_be_set() {
515− let config = Config {
516− keys: hashmap! {
517− Mode::Normal => Keymap::new(
518− keymap!({ "Normal mode"
519− "space" => { ""
520− "s" => { ""
521− "v" => vsplit,
522− "c" => hsplit,
523− },
510+ let keymap = hashmap! {
511+ Mode::Normal => Keymap::new(
512+ keymap!({ "Normal mode"
513+ "space" => { ""
514+ "s" => { ""
515+ "v" => vsplit,
516+ "c" => hsplit,
524517 },
525− })
526− )
527− },
528− ..Default::default()
518+ },
519+ })
520+ )
529521 };
530− let mut merged_config = merge_keys(config.clone());
531− assert_ne!(config, merged_config);
532− let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
522+ let mut merged_keyamp = default();
523+ merge_keys(&mut merged_keyamp, keymap.clone());
524+ assert_ne!(keymap, merged_keyamp);
525+ let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
533526 // Make sure mapping works
534527 assert_eq!(
535528 keymap
helix-term/src/main.rs+14−13
@@ -3,7 +3,7 @@ use crossterm::event::EventStream;
33use helix_loader::VERSION_AND_GIT_HASH;
44use helix_term::application::Application;
55use helix_term::args::Args;
6−use helix_term::config::Config;
6+use helix_term::config::{Config, ConfigLoadError};
77use std::path::PathBuf;
88
99fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@@ -126,18 +126,19 @@ FLAGS:
126126
127127 helix_loader::initialize_config_file(args.config_file.clone());
128128
129− let config = match std::fs::read_to_string(helix_loader::config_file()) {
130− Ok(config) => toml::from_str(&config)
131− .map(helix_term::keymap::merge_keys)
132− .unwrap_or_else(|err| {
133− eprintln!("Bad config: {}", err);
134− eprintln!("Press <ENTER> to continue with default config");
135− use std::io::Read;
136− let _ = std::io::stdin().read(&mut []);
137− Config::default()
138− }),
139− Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
140− Err(err) => return Err(Error::new(err)),
129+ let config = match Config::load_default() {
130+ Ok(config) => config,
131+ Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
132+ Config::default()
133+ }
134+ Err(ConfigLoadError::Error(err)) => return Err(Error::new(err)),
135+ Err(ConfigLoadError::BadConfig(err)) => {
136+ eprintln!("Bad config: {}", err);
137+ eprintln!("Press <ENTER> to continue with default config");
138+ use std::io::Read;
139+ let _ = std::io::stdin().read(&mut []);
140+ Config::default()
141+ }
141142 };
142143
143144 let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {
helix-term/tests/test/helpers.rs+8−4
@@ -1,6 +1,7 @@
11use std::{
22 fs::File,
33 io::{Read, Write},
4+ mem::replace,
45 path::PathBuf,
56 time::Duration,
67};
@@ -222,10 +223,11 @@ pub fn temp_file_with_contents<S: AsRef<str>>(
222223
223224/// Generates a config with defaults more suitable for integration tests
224225pub fn test_config() -> Config {
225− merge_keys(Config {
226+ Config {
226227 editor: test_editor_config(),
228+ keys: helix_term::keymap::default(),
227229 ..Default::default()
228− })
230+ }
229231}
230232
231233pub fn test_editor_config() -> helix_view::editor::Config {
@@ -300,8 +302,10 @@ impl AppBuilder {
300302
301303 // Remove this attribute once `with_config` is used in a test:
302304 #[allow(dead_code)]
303− pub fn with_config(mut self, config: Config) -> Self {
304− self.config = helix_term::keymap::merge_keys(config);
305+ pub fn with_config(mut self, mut config: Config) -> Self {
306+ let keys = replace(&mut config.keys, helix_term::keymap::default());
307+ merge_keys(&mut config.keys, keys);
308+ self.config = config;
305309 self
306310 }
307311
helix-view/src/editor.rs+7−5
@@ -282,6 +282,8 @@ pub struct Config {
282282 /// Whether to color modes with different colors. Defaults to `false`.
283283 pub color_modes: bool,
284284 pub soft_wrap: SoftWrap,
285+ /// Workspace specific lsp ceiling dirs
286+ pub workspace_lsp_roots: Vec<PathBuf>,
285287}
286288
287289#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -747,6 +749,7 @@ impl Default for Config {
747749 soft_wrap: SoftWrap::default(),
748750 text_width: 80,
749751 completion_replace: false,
752+ workspace_lsp_roots: Vec::new(),
750753 }
751754 }
752755}
@@ -1087,15 +1090,14 @@ impl Editor {
10871090 }
10881091
10891092 // if doc doesn't have a URL it's a scratch buffer, ignore it
1090− let (lang, path) = {
1091− let doc = self.document(doc_id)?;
1092− (doc.language.clone(), doc.path().cloned())
1093− };
1093+ let doc = self.document(doc_id)?;
1094+ let (lang, path) = (doc.language.clone(), doc.path().cloned());
1095+ let root_dirs = &doc.config.load().workspace_lsp_roots;
10941096
10951097 // try to find a language server based on the language name
10961098 let language_server = lang.as_ref().and_then(|language| {
10971099 self.language_servers
1098− .get(language, path.as_ref())
1100+ .get(language, path.as_ref(), root_dirs)
10991101 .map_err(|e| {
11001102 log::error!(
11011103 "Failed to initialize the LSP for `{}` {{ {} }}",