on master help sign in
add workspace config and manual LSP root management
2d10a42Pascal Kuthe
book/src/configuration.md+4
@@ -30,6 +30,9 @@ You can use a custom configuration file by specifying it with the `-c` or
30Additionally, you can reload the configuration file by sending the USR1
30Additionally, you can reload the configuration file by sending the USR1
31signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.
31signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.
32
32
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+
33## Editor
36## Editor
34
37
35### `[editor]` Section
38### `[editor]` Section
@@ -58,6 +61,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
58| `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` |
61| `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` |
59| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
62| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
60| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` |
63| `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` | `[]` |
61
65
62### `[editor.statusline]` Section
66### `[editor.statusline]` Section
63
67
book/src/generated/typable-cmd.md+1
@@ -70,6 +70,7 @@
70| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
70| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
71| `:config-reload` | Refresh user config. |
71| `:config-reload` | Refresh user config. |
72| `:config-open` | Open the user config.toml file. |
72| `:config-open` | Open the user config.toml file. |
73+| `:config-open-workspace` | Open the workspace config.toml file. |
73| `:log-open` | Open the helix log file. |
74| `:log-open` | Open the helix log file. |
74| `:insert-output` | Run shell command, inserting output before each selection. |
75| `:insert-output` | Run shell command, inserting output before each selection. |
75| `:append-output` | Run shell command, appending output after each selection. |
76| `:append-output` | Run shell command, appending output after each selection. |
book/src/languages.md+1
@@ -64,6 +64,7 @@ These configuration keys are available:
64| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
64| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
65| `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 |
65| `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 |
66| `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` |
66| `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. | `` |
67
68
68### File-type detection and the `file-types` key
69### File-type detection and the `file-types` key
69
70
helix-core/src/lib.rs+2−45
@@ -36,55 +36,12 @@ pub mod unicode {
36 pub use unicode_width as width;
36 pub use unicode_width as width;
37}
37}
38
38
39+pub use helix_loader::find_workspace;
40+
39pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
41pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
40 line.chars().position(|ch| !ch.is_whitespace())
42 line.chars().position(|ch| !ch.is_whitespace())
41}
43}
42
44
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−
88pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
45pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
89
46
90// pub use tendril::StrTendril as Tendril;
47// pub use tendril::StrTendril as Tendril;
helix-core/src/syntax.rs+5−1
@@ -20,7 +20,7 @@ use std::{
20 fmt,
20 fmt,
21 hash::{Hash, Hasher},
21 hash::{Hash, Hasher},
22 mem::{replace, transmute},
22 mem::{replace, transmute},
23− path::Path,
23+ path::{Path, PathBuf},
24 str::FromStr,
24 str::FromStr,
25 sync::Arc,
25 sync::Arc,
26};
26};
@@ -127,6 +127,10 @@ pub struct LanguageConfiguration {
127 pub auto_pairs: Option<AutoPairs>,
127 pub auto_pairs: Option<AutoPairs>,
128
128
129 pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
129 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>>,
130}
134}
131
135
132#[derive(Debug, PartialEq, Eq, Hash)]
136#[derive(Debug, PartialEq, Eq, Hash)]
helix-loader/src/config.rs+3−5
@@ -9,9 +9,8 @@ pub fn default_lang_config() -> toml::Value {
9
9
10/// User configured languages.toml file, merged with the default config.
10/// User configured languages.toml file, merged with the default config.
11pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
11pub 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")]
13 .into_iter()
13 .into_iter()
14− .chain([crate::config_dir()].into_iter())
15 .map(|path| path.join("languages.toml"))
14 .map(|path| path.join("languages.toml"))
16 .filter_map(|file| {
15 .filter_map(|file| {
17 std::fs::read_to_string(file)
16 std::fs::read_to_string(file)
@@ -20,8 +19,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
20 })
19 })
21 .collect::<Result<Vec<_>, _>>()?
20 .collect::<Result<Vec<_>, _>>()?
22 .into_iter()
21 .into_iter()
23− .chain([default_lang_config()].into_iter())
22+ .fold(default_lang_config(), |a, b| {
24− .fold(toml::Value::Table(toml::value::Table::default()), |a, b| {
25 // combines for example
23 // combines for example
26 // b:
24 // b:
27 // [[language]]
25 // [[language]]
@@ -38,7 +36,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
38 // language-server = { command = "/usr/bin/taplo" }
36 // language-server = { command = "/usr/bin/taplo" }
39 //
37 //
40 // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
38 // 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)
42 });
40 });
43
41
44 Ok(config)
42 Ok(config)
helix-loader/src/lib.rs+18−26
@@ -42,7 +42,7 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
42 let mut rt_dirs = Vec::new();
42 let mut rt_dirs = Vec::new();
43 if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
43 if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
44 // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
44 // 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);
46 log::debug!("runtime dir: {}", path.to_string_lossy());
46 log::debug!("runtime dir: {}", path.to_string_lossy());
47 rt_dirs.push(path);
47 rt_dirs.push(path);
48 }
48 }
@@ -113,15 +113,6 @@ pub fn config_dir() -> PathBuf {
113 path
113 path
114}
114}
115
115
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−
125pub fn cache_dir() -> PathBuf {
116pub fn cache_dir() -> PathBuf {
126 // TODO: allow env var override
117 // TODO: allow env var override
127 let strategy = choose_base_strategy().expect("Unable to find the config directory!");
118 let strategy = choose_base_strategy().expect("Unable to find the config directory!");
@@ -137,6 +128,10 @@ pub fn config_file() -> PathBuf {
137 .unwrap_or_else(|| config_dir().join("config.toml"))
128 .unwrap_or_else(|| config_dir().join("config.toml"))
138}
129}
139
130
131+pub fn workspace_config_file() -> PathBuf {
132+ find_workspace().join(".helix").join("config.toml")
133+}
134+
140pub fn lang_config_file() -> PathBuf {
135pub fn lang_config_file() -> PathBuf {
141 config_dir().join("languages.toml")
136 config_dir().join("languages.toml")
142}
137}
@@ -145,22 +140,6 @@ pub fn log_file() -> PathBuf {
145 cache_dir().join("helix.log")
140 cache_dir().join("helix.log")
146}
141}
147
142
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−
164/// Merge two TOML documents, merging values from `right` onto `left`
143/// Merge two TOML documents, merging values from `right` onto `left`
165///
144///
166/// When an array exists in both `left` and `right`, `right`'s array is
145/// When an array exists in both `left` and `right`, `right`'s array is
@@ -302,3 +281,16 @@ mod merge_toml_tests {
302 )
281 )
303 }
282 }
304}
283}
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 @@
1use crate::{
1use crate::{
2− jsonrpc,
2+ find_root, jsonrpc,
3 transport::{Payload, Transport},
3 transport::{Payload, Transport},
4 Call, Error, OffsetEncoding, Result,
4 Call, Error, OffsetEncoding, Result,
5};
5};
6
6
7−use helix_core::{find_root, ChangeSet, Rope};
7+use helix_core::{ChangeSet, Rope};
8use helix_loader::{self, VERSION_AND_GIT_HASH};
8use helix_loader::{self, VERSION_AND_GIT_HASH};
9use lsp::PositionEncodingKind;
9use lsp::PositionEncodingKind;
10use lsp_types as lsp;
10use lsp_types as lsp;
11use serde::Deserialize;
11use serde::Deserialize;
12use serde_json::Value;
12use serde_json::Value;
13−use std::collections::HashMap;
14use std::future::Future;
13use std::future::Future;
15use std::process::Stdio;
14use std::process::Stdio;
16use std::sync::{
15use std::sync::{
17 atomic::{AtomicU64, Ordering},
16 atomic::{AtomicU64, Ordering},
18 Arc,
17 Arc,
19};
18};
19+use std::{collections::HashMap, path::PathBuf};
20use tokio::{
20use tokio::{
21 io::{BufReader, BufWriter},
21 io::{BufReader, BufWriter},
22 process::{Child, Command},
22 process::{Child, Command},
@@ -49,6 +49,7 @@ impl Client {
49 config: Option<Value>,
49 config: Option<Value>,
50 server_environment: HashMap<String, String>,
50 server_environment: HashMap<String, String>,
51 root_markers: &[String],
51 root_markers: &[String],
52+ manual_roots: &[PathBuf],
52 id: usize,
53 id: usize,
53 req_timeout: u64,
54 req_timeout: u64,
54 doc_path: Option<&std::path::PathBuf>,
55 doc_path: Option<&std::path::PathBuf>,
@@ -77,8 +78,11 @@ impl Client {
77 Transport::start(reader, writer, stderr, id);
78 Transport::start(reader, writer, stderr, id);
78
79
79 let root_path = find_root(
80 let root_path = find_root(
81+ doc_path
80− doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())),
82+ .and_then(|x| x.parent().and_then(|x| x.to_str()))
83+ .unwrap_or("."),
81 root_markers,
84 root_markers,
85+ manual_roots,
82 );
86 );
83
87
84 let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();
88 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};
10pub use lsp_types as lsp;
10pub use lsp_types as lsp;
11
11
12use futures_util::stream::select_all::SelectAll;
12use futures_util::stream::select_all::SelectAll;
13+use helix_core::{
14+ find_workspace,
13−use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration};
15+ syntax::{LanguageConfiguration, LanguageServerConfiguration},
16+};
14use tokio::sync::mpsc::UnboundedReceiver;
17use tokio::sync::mpsc::UnboundedReceiver;
15
18
16use std::{
19use std::{
17 collections::{hash_map::Entry, HashMap},
20 collections::{hash_map::Entry, HashMap},
21+ path::PathBuf,
18 sync::{
22 sync::{
19 atomic::{AtomicUsize, Ordering},
23 atomic::{AtomicUsize, Ordering},
20 Arc,
24 Arc,
@@ -641,6 +645,7 @@ impl Registry {
641 &mut self,
645 &mut self,
642 language_config: &LanguageConfiguration,
646 language_config: &LanguageConfiguration,
643 doc_path: Option<&std::path::PathBuf>,
647 doc_path: Option<&std::path::PathBuf>,
648+ root_dirs: &[PathBuf],
644 ) -> Result<Option<Arc<Client>>> {
649 ) -> Result<Option<Arc<Client>>> {
645 let config = match &language_config.language_server {
650 let config = match &language_config.language_server {
646 Some(config) => config,
651 Some(config) => config,
@@ -656,7 +661,7 @@ impl Registry {
656 let id = self.counter.fetch_add(1, Ordering::Relaxed);
661 let id = self.counter.fetch_add(1, Ordering::Relaxed);
657
662
658 let NewClientResult(client, incoming) =
663 let NewClientResult(client, incoming) =
659− start_client(id, language_config, config, doc_path)?;
664+ start_client(id, language_config, config, doc_path, root_dirs)?;
660 self.incoming.push(UnboundedReceiverStream::new(incoming));
665 self.incoming.push(UnboundedReceiverStream::new(incoming));
661
666
662 let (_, old_client) = entry.insert((id, client.clone()));
667 let (_, old_client) = entry.insert((id, client.clone()));
@@ -684,6 +689,7 @@ impl Registry {
684 &mut self,
689 &mut self,
685 language_config: &LanguageConfiguration,
690 language_config: &LanguageConfiguration,
686 doc_path: Option<&std::path::PathBuf>,
691 doc_path: Option<&std::path::PathBuf>,
692+ root_dirs: &[PathBuf],
687 ) -> Result<Option<Arc<Client>>> {
693 ) -> Result<Option<Arc<Client>>> {
688 let config = match &language_config.language_server {
694 let config = match &language_config.language_server {
689 Some(config) => config,
695 Some(config) => config,
@@ -697,7 +703,7 @@ impl Registry {
697 let id = self.counter.fetch_add(1, Ordering::Relaxed);
703 let id = self.counter.fetch_add(1, Ordering::Relaxed);
698
704
699 let NewClientResult(client, incoming) =
705 let NewClientResult(client, incoming) =
700− start_client(id, language_config, config, doc_path)?;
706+ start_client(id, language_config, config, doc_path, root_dirs)?;
701 self.incoming.push(UnboundedReceiverStream::new(incoming));
707 self.incoming.push(UnboundedReceiverStream::new(incoming));
702
708
703 entry.insert((id, client.clone()));
709 entry.insert((id, client.clone()));
@@ -798,6 +804,7 @@ fn start_client(
798 config: &LanguageConfiguration,
804 config: &LanguageConfiguration,
799 ls_config: &LanguageServerConfiguration,
805 ls_config: &LanguageServerConfiguration,
800 doc_path: Option<&std::path::PathBuf>,
806 doc_path: Option<&std::path::PathBuf>,
807+ root_dirs: &[PathBuf],
801) -> Result<NewClientResult> {
808) -> Result<NewClientResult> {
802 let (client, incoming, initialize_notify) = Client::start(
809 let (client, incoming, initialize_notify) = Client::start(
803 &ls_config.command,
810 &ls_config.command,
@@ -805,6 +812,7 @@ fn start_client(
805 config.config.clone(),
812 config.config.clone(),
806 ls_config.environment.clone(),
813 ls_config.environment.clone(),
807 &config.roots,
814 &config.roots,
815+ config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
808 id,
816 id,
809 ls_config.timeout,
817 ls_config.timeout,
810 doc_path,
818 doc_path,
@@ -842,6 +850,48 @@ fn start_client(
842 Ok(NewClientResult(client, incoming))
850 Ok(NewClientResult(client, incoming))
843}
851}
844
852
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+
845#[cfg(test)]
895#[cfg(test)]
846mod tests {
896mod tests {
847 use super::{lsp, util::*, OffsetEncoding};
897 use super::{lsp, util::*, OffsetEncoding};
helix-term/src/commands.rs+2−4
@@ -12,7 +12,7 @@ pub use typed::*;
12use helix_core::{
12use helix_core::{
13 char_idx_at_visual_offset, comment,
13 char_idx_at_visual_offset, comment,
14 doc_formatter::TextFormat,
14 doc_formatter::TextFormat,
15− encoding, find_first_non_whitespace_char, find_root, graphemes,
15+ encoding, find_first_non_whitespace_char, find_workspace, graphemes,
16 history::UndoKind,
16 history::UndoKind,
17 increment, indent,
17 increment, indent,
18 indent::IndentStyle,
18 indent::IndentStyle,
@@ -2419,9 +2419,7 @@ fn append_mode(cx: &mut Context) {
2419}
2419}
2420
2420
2421fn file_picker(cx: &mut Context) {
2421fn file_picker(cx: &mut Context) {
2422− // We don't specify language markers, root will be the root of the current
2422+ let root = find_workspace();
2423− // git repo or the current dir if we're not in a repo
2424− let root = find_root(None, &[]);
2425 let picker = ui::file_picker(root, &cx.editor.config());
2423 let picker = ui::file_picker(root, &cx.editor.config());
2426 cx.push_layer(Box::new(overlayed(picker)));
2424 cx.push_layer(Box::new(overlayed(picker)));
2427}
2425}
helix-term/src/commands/typed.rs+25−1
@@ -1371,13 +1371,16 @@ fn lsp_restart(
1371 return Ok(());
1371 return Ok(());
1372 }
1372 }
1373
1373
1374+ let editor_config = cx.editor.config.load();
1374 let (_view, doc) = current!(cx.editor);
1375 let (_view, doc) = current!(cx.editor);
1375 let config = doc
1376 let config = doc
1376 .language_config()
1377 .language_config()
1377 .context("LSP not defined for the current document")?;
1378 .context("LSP not defined for the current document")?;
1378
1379
1379 let scope = config.scope.clone();
1380 let scope = config.scope.clone();
1381+ cx.editor
1380− cx.editor.language_servers.restart(config, doc.path())?;
1382+ .language_servers
1383+ .restart(config, doc.path(), &editor_config.workspace_lsp_roots)?;
1381
1384
1382 // This collect is needed because refresh_language_server would need to re-borrow editor.
1385 // This collect is needed because refresh_language_server would need to re-borrow editor.
1383 let document_ids_to_refresh: Vec<DocumentId> = cx
1386 let document_ids_to_refresh: Vec<DocumentId> = cx
@@ -1970,6 +1973,20 @@ fn open_config(
1970 Ok(())
1973 Ok(())
1971}
1974}
1972
1975
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+
1973fn open_log(
1990fn open_log(
1974 cx: &mut compositor::Context,
1991 cx: &mut compositor::Context,
1975 _args: &[Cow<str>],
1992 _args: &[Cow<str>],
@@ -2646,6 +2663,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2646 fun: open_config,
2663 fun: open_config,
2647 signature: CommandSignature::none(),
2664 signature: CommandSignature::none(),
2648 },
2665 },
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+ },
2649 TypableCommand {
2673 TypableCommand {
2650 name: "log-open",
2674 name: "log-open",
2651 aliases: &[],
2675 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;
2use helix_view::document::Mode;
4use helix_view::document::Mode;
3use serde::Deserialize;
5use serde::Deserialize;
4use std::collections::HashMap;
6use std::collections::HashMap;
5use std::fmt::Display;
7use std::fmt::Display;
8+use std::fs;
6use std::io::Error as IOError;
9use std::io::Error as IOError;
7−use std::path::PathBuf;
8use toml::de::Error as TomlError;
10use toml::de::Error as TomlError;
9
11
10−#[derive(Debug, Clone, PartialEq, Deserialize)]
12+#[derive(Debug, Clone, PartialEq)]
11−#[serde(deny_unknown_fields)]
12pub struct Config {
13pub struct Config {
13 pub theme: Option<String>,
14 pub theme: Option<String>,
14− #[serde(default = "default")]
15 pub keys: HashMap<Mode, Keymap>,
15 pub keys: HashMap<Mode, Keymap>,
16− #[serde(default)]
17 pub editor: helix_view::editor::Config,
16 pub editor: helix_view::editor::Config,
18}
17}
19
18
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+
20impl Default for Config {
27impl Default for Config {
21 fn default() -> Config {
28 fn default() -> Config {
22 Config {
29 Config {
23 theme: None,
30 theme: None,
24− keys: default(),
31+ keys: keymap::default(),
25 editor: helix_view::editor::Config::default(),
32 editor: helix_view::editor::Config::default(),
26 }
33 }
27 }
34 }
@@ -33,6 +40,12 @@ pub enum ConfigLoadError {
33 Error(IOError),
40 Error(IOError),
34}
41}
35
42
43+impl Default for ConfigLoadError {
44+ fn default() -> Self {
45+ ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder"))
46+ }
47+}
48+
36impl Display for ConfigLoadError {
49impl Display for ConfigLoadError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
51 match self {
@@ -43,17 +56,72 @@ impl Display for ConfigLoadError {
43}
56}
44
57
45impl Config {
58impl Config {
59+ pub fn load(
60+ global: Result<String, ConfigLoadError>,
61+ local: Result<String, ConfigLoadError>,
46− pub fn load(config_path: PathBuf) -> Result<Config, ConfigLoadError> {
62+ ) -> Result<Config, ConfigLoadError> {
47− match std::fs::read_to_string(config_path) {
63+ let global_config: Result<ConfigRaw, ConfigLoadError> =
48− Ok(config) => toml::from_str(&config)
64+ global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
49− .map(merge_keys)
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()),
50− .map_err(ConfigLoadError::BadConfig),
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
51− Err(err) => Err(ConfigLoadError::Error(err)),
113+ (Err(err), Err(_)) => return Err(err),
52− }
114+ };
115+
116+ Ok(res)
53 }
117 }
54
118
55 pub fn load_default() -> Result<Config, ConfigLoadError> {
119 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)
57 }
125 }
58}
126}
59
127
@@ -61,6 +129,12 @@ impl Config {
61mod tests {
129mod tests {
62 use super::*;
130 use super::*;
63
131
132+ impl Config {
133+ fn load_test(config: &str) -> Config {
134+ Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap()
135+ }
136+ }
137+
64 #[test]
138 #[test]
65 fn parsing_keymaps_config_file() {
139 fn parsing_keymaps_config_file() {
66 use crate::keymap;
140 use crate::keymap;
@@ -77,18 +151,24 @@ mod tests {
77 A-F12 = "move_next_word_end"
151 A-F12 = "move_next_word_end"
78 "#;
152 "#;
79
153
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+
80 assert_eq!(
168 assert_eq!(
81− toml::from_str::<Config>(sample_keymaps).unwrap(),
169+ Config::load_test(sample_keymaps),
82 Config {
170 Config {
83− keys: hashmap! {
171+ keys,
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− },
92 ..Default::default()
172 ..Default::default()
93 }
173 }
94 );
174 );
@@ -97,11 +177,11 @@ mod tests {
97 #[test]
177 #[test]
98 fn keys_resolve_to_correct_defaults() {
178 fn keys_resolve_to_correct_defaults() {
99 // From serde default
179 // From serde default
100− let default_keys = toml::from_str::<Config>("").unwrap().keys;
180+ let default_keys = Config::load_test("").keys;
101− assert_eq!(default_keys, default());
181+ assert_eq!(default_keys, keymap::default());
102
182
103 // From the Default trait
183 // From the Default trait
104 let default_keys = Config::default().keys;
184 let default_keys = Config::default().keys;
105− assert_eq!(default_keys, default());
185+ assert_eq!(default_keys, keymap::default());
106 }
186 }
107}
187}
helix-term/src/keymap.rs+36−43
@@ -2,7 +2,6 @@ pub mod default;
2pub mod macros;
2pub mod macros;
3
3
4pub use crate::commands::MappableCommand;
4pub use crate::commands::MappableCommand;
5−use crate::config::Config;
6use arc_swap::{
5use arc_swap::{
7 access::{DynAccess, DynGuard},
6 access::{DynAccess, DynGuard},
8 ArcSwap,
7 ArcSwap,
@@ -16,7 +15,7 @@ use std::{
16 sync::Arc,
15 sync::Arc,
17};
16};
18
17
19−use default::default;
18+pub use default::default;
20use macros::key;
19use macros::key;
21
20
22#[derive(Debug, Clone)]
21#[derive(Debug, Clone)]
@@ -417,12 +416,10 @@ impl Default for Keymaps {
417}
416}
418
417
419/// Merge default config keys with user overwritten keys for custom user config.
418/// Merge default config keys with user overwritten keys for custom user config.
420−pub fn merge_keys(mut config: Config) -> Config {
419+pub fn merge_keys(dst: &mut HashMap<Mode, Keymap>, mut delta: HashMap<Mode, Keymap>) {
421− let mut delta = std::mem::replace(&mut config.keys, default());
422− for (mode, keys) in &mut config.keys {
420+ for (mode, keys) in dst {
423 keys.merge(delta.remove(mode).unwrap_or_default())
421 keys.merge(delta.remove(mode).unwrap_or_default())
424 }
422 }
425− config
426}
423}
427
424
428#[cfg(test)]
425#[cfg(test)]
@@ -449,26 +446,24 @@ mod tests {
449
446
450 #[test]
447 #[test]
451 fn merge_partial_keys() {
448 fn merge_partial_keys() {
452− let config = Config {
453− keys: hashmap! {
449+ let keymap = hashmap! {
454− Mode::Normal => Keymap::new(
450+ Mode::Normal => Keymap::new(
455− keymap!({ "Normal mode"
451+ keymap!({ "Normal mode"
456− "i" => normal_mode,
452+ "i" => normal_mode,
457− "无" => insert_mode,
453+ "无" => insert_mode,
458− "z" => jump_backward,
454+ "z" => jump_backward,
459− "g" => { "Merge into goto mode"
455+ "g" => { "Merge into goto mode"
460− "$" => goto_line_end,
456+ "$" => goto_line_end,
461− "g" => delete_char_forward,
457+ "g" => delete_char_forward,
462− },
463− })
458+ },
464− )
465− },
459+ })
466− ..Default::default()
460+ )
467 };
461 };
468− let mut merged_config = merge_keys(config.clone());
462+ let mut merged_keyamp = default();
469− assert_ne!(config, merged_config);
463+ merge_keys(&mut merged_keyamp, keymap.clone());
464+ assert_ne!(keymap, merged_keyamp);
470
465
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())));
472 assert_eq!(
467 assert_eq!(
473 keymap.get(Mode::Normal, key!('i')),
468 keymap.get(Mode::Normal, key!('i')),
474 KeymapResult::Matched(MappableCommand::normal_mode),
469 KeymapResult::Matched(MappableCommand::normal_mode),
@@ -486,7 +481,7 @@ mod tests {
486 "Leaf should replace node"
481 "Leaf should replace node"
487 );
482 );
488
483
489− let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
484+ let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
490 // Assumes that `g` is a node in default keymap
485 // Assumes that `g` is a node in default keymap
491 assert_eq!(
486 assert_eq!(
492 keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
487 keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
@@ -506,30 +501,28 @@ mod tests {
506 "Old leaves in subnode should be present in merged node"
501 "Old leaves in subnode should be present in merged node"
507 );
502 );
508
503
509− assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1);
504+ assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1);
510− assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0);
505+ assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0);
511 }
506 }
512
507
513 #[test]
508 #[test]
514 fn order_should_be_set() {
509 fn order_should_be_set() {
515− let config = Config {
516− keys: hashmap! {
510+ let keymap = hashmap! {
517− Mode::Normal => Keymap::new(
511+ Mode::Normal => Keymap::new(
518− keymap!({ "Normal mode"
512+ keymap!({ "Normal mode"
519− "space" => { ""
513+ "space" => { ""
520− "s" => { ""
514+ "s" => { ""
521− "v" => vsplit,
515+ "v" => vsplit,
522− "c" => hsplit,
516+ "c" => hsplit,
523− },
524 },
517 },
525− })
518+ },
526− )
519+ })
527− },
520+ )
528− ..Default::default()
529 };
521 };
530− let mut merged_config = merge_keys(config.clone());
522+ let mut merged_keyamp = default();
531− assert_ne!(config, merged_config);
523+ merge_keys(&mut merged_keyamp, keymap.clone());
524+ assert_ne!(keymap, merged_keyamp);
532− let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
525+ let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
533 // Make sure mapping works
526 // Make sure mapping works
534 assert_eq!(
527 assert_eq!(
535 keymap
528 keymap
helix-term/src/main.rs+14−13
@@ -3,7 +3,7 @@ use crossterm::event::EventStream;
3use helix_loader::VERSION_AND_GIT_HASH;
3use helix_loader::VERSION_AND_GIT_HASH;
4use helix_term::application::Application;
4use helix_term::application::Application;
5use helix_term::args::Args;
5use helix_term::args::Args;
6−use helix_term::config::Config;
6+use helix_term::config::{Config, ConfigLoadError};
7use std::path::PathBuf;
7use std::path::PathBuf;
8
8
9fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
9fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@@ -126,18 +126,19 @@ FLAGS:
126
126
127 helix_loader::initialize_config_file(args.config_file.clone());
127 helix_loader::initialize_config_file(args.config_file.clone());
128
128
129− let config = match std::fs::read_to_string(helix_loader::config_file()) {
129+ let config = match Config::load_default() {
130− Ok(config) => toml::from_str(&config)
130+ Ok(config) => config,
131− .map(helix_term::keymap::merge_keys)
131+ Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
132− .unwrap_or_else(|err| {
132+ Config::default()
133+ }
134+ Err(ConfigLoadError::Error(err)) => return Err(Error::new(err)),
135+ Err(ConfigLoadError::BadConfig(err)) => {
133− eprintln!("Bad config: {}", err);
136+ eprintln!("Bad config: {}", err);
134− eprintln!("Press <ENTER> to continue with default config");
137+ eprintln!("Press <ENTER> to continue with default config");
135− use std::io::Read;
138+ use std::io::Read;
136− let _ = std::io::stdin().read(&mut []);
139+ let _ = std::io::stdin().read(&mut []);
137− Config::default()
138− }),
139− Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
140+ Config::default()
140− Err(err) => return Err(Error::new(err)),
141+ }
141 };
142 };
142
143
143 let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {
144 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 @@
1use std::{
1use std::{
2 fs::File,
2 fs::File,
3 io::{Read, Write},
3 io::{Read, Write},
4+ mem::replace,
4 path::PathBuf,
5 path::PathBuf,
5 time::Duration,
6 time::Duration,
6};
7};
@@ -222,10 +223,11 @@ pub fn temp_file_with_contents<S: AsRef<str>>(
222
223
223/// Generates a config with defaults more suitable for integration tests
224/// Generates a config with defaults more suitable for integration tests
224pub fn test_config() -> Config {
225pub fn test_config() -> Config {
225− merge_keys(Config {
226+ Config {
226 editor: test_editor_config(),
227 editor: test_editor_config(),
228+ keys: helix_term::keymap::default(),
227 ..Default::default()
229 ..Default::default()
228− })
230+ }
229}
231}
230
232
231pub fn test_editor_config() -> helix_view::editor::Config {
233pub fn test_editor_config() -> helix_view::editor::Config {
@@ -300,8 +302,10 @@ impl AppBuilder {
300
302
301 // Remove this attribute once `with_config` is used in a test:
303 // Remove this attribute once `with_config` is used in a test:
302 #[allow(dead_code)]
304 #[allow(dead_code)]
303− pub fn with_config(mut self, config: Config) -> Self {
305+ pub fn with_config(mut self, mut config: Config) -> Self {
306+ let keys = replace(&mut config.keys, helix_term::keymap::default());
304− self.config = helix_term::keymap::merge_keys(config);
307+ merge_keys(&mut config.keys, keys);
308+ self.config = config;
305 self
309 self
306 }
310 }
307
311
helix-view/src/editor.rs+7−5
@@ -282,6 +282,8 @@ pub struct Config {
282 /// Whether to color modes with different colors. Defaults to `false`.
282 /// Whether to color modes with different colors. Defaults to `false`.
283 pub color_modes: bool,
283 pub color_modes: bool,
284 pub soft_wrap: SoftWrap,
284 pub soft_wrap: SoftWrap,
285+ /// Workspace specific lsp ceiling dirs
286+ pub workspace_lsp_roots: Vec<PathBuf>,
285}
287}
286
288
287#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
289#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -747,6 +749,7 @@ impl Default for Config {
747 soft_wrap: SoftWrap::default(),
749 soft_wrap: SoftWrap::default(),
748 text_width: 80,
750 text_width: 80,
749 completion_replace: false,
751 completion_replace: false,
752+ workspace_lsp_roots: Vec::new(),
750 }
753 }
751 }
754 }
752}
755}
@@ -1087,15 +1090,14 @@ impl Editor {
1087 }
1090 }
1088
1091
1089 // if doc doesn't have a URL it's a scratch buffer, ignore it
1092 // 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)?;
1093+ let doc = self.document(doc_id)?;
1092− (doc.language.clone(), doc.path().cloned())
1094+ let (lang, path) = (doc.language.clone(), doc.path().cloned());
1093− };
1095+ let root_dirs = &doc.config.load().workspace_lsp_roots;
1094
1096
1095 // try to find a language server based on the language name
1097 // try to find a language server based on the language name
1096 let language_server = lang.as_ref().and_then(|language| {
1098 let language_server = lang.as_ref().and_then(|language| {
1097 self.language_servers
1099 self.language_servers
1098− .get(language, path.as_ref())
1100+ .get(language, path.as_ref(), root_dirs)
1099 .map_err(|e| {
1101 .map_err(|e| {
1100 log::error!(
1102 log::error!(
1101 "Failed to initialize the LSP for `{}` {{ {} }}",
1103 "Failed to initialize the LSP for `{}` {{ {} }}",