Skip to content

Commit 9319c32

Browse files
committed
Continue loading the config on error
1 parent ec21de0 commit 9319c32

File tree

9 files changed

+188
-23
lines changed

9 files changed

+188
-23
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

helix-core/src/syntax.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ pub struct IndentationConfiguration {
179179

180180
/// Configuration for auto pairs
181181
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182-
#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
182+
#[serde(rename_all = "kebab-case", untagged)]
183183
pub enum AutoPairConfig {
184184
/// Enables or disables auto pairing. False means disabled. True means to use the default pairs.
185185
Enable(bool),

helix-term/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ toml = "0.5"
6161

6262
serde_json = "1.0"
6363
serde = { version = "1.0", features = ["derive"] }
64+
serde_ignored = "0.1.2"
6465

6566
# ripgrep for global search
6667
grep-regex = "0.1.9"

helix-term/src/application.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::{
1919

2020
use log::{error, warn};
2121
use std::{
22+
collections::BTreeSet,
2223
io::{stdin, stdout, Write},
2324
sync::Arc,
2425
time::{Duration, Instant},
@@ -271,10 +272,11 @@ impl Application {
271272
}
272273

273274
fn refresh_config(&mut self) {
274-
let config = Config::load(helix_loader::config_file()).unwrap_or_else(|err| {
275-
self.editor.set_error(err.to_string());
276-
Config::default()
277-
});
275+
let config = Config::load(helix_loader::config_file(), &mut BTreeSet::new())
276+
.unwrap_or_else(|err| {
277+
self.editor.set_error(err.to_string());
278+
Config::default()
279+
});
278280

279281
// Refresh theme
280282
if let Some(theme) = config.theme.clone() {

helix-term/src/config.rs

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
use crate::keymap::{default::default, merge_keys, Keymap};
22
use helix_view::document::Mode;
33
use serde::Deserialize;
4-
use std::collections::HashMap;
4+
use std::collections::{BTreeSet, HashMap};
55
use std::fmt::Display;
66
use std::io::Error as IOError;
77
use std::path::PathBuf;
88
use toml::de::Error as TomlError;
99

10+
use helix_view::editor::ok_or_default;
11+
12+
// NOTE: The fields in this struct use the deserializer ok_or_default to continue parsing when
13+
// there is an error. In that case, it will use the default value.
1014
#[derive(Debug, Clone, PartialEq, Deserialize)]
11-
#[serde(deny_unknown_fields)]
1215
pub struct Config {
16+
#[serde(default, deserialize_with = "ok_or_default")]
1317
pub theme: Option<String>,
14-
#[serde(default = "default")]
18+
#[serde(default = "default", deserialize_with = "ok_or_default")]
1519
pub keys: HashMap<Mode, Keymap>,
16-
#[serde(default)]
20+
#[serde(default, deserialize_with = "ok_or_default")]
1721
pub editor: helix_view::editor::Config,
1822
}
1923

@@ -43,17 +47,24 @@ impl Display for ConfigLoadError {
4347
}
4448

4549
impl Config {
46-
pub fn load(config_path: PathBuf) -> Result<Config, ConfigLoadError> {
50+
pub fn load(
51+
config_path: PathBuf,
52+
ignored_keys: &mut BTreeSet<String>,
53+
) -> Result<Config, ConfigLoadError> {
4754
match std::fs::read_to_string(config_path) {
48-
Ok(config) => toml::from_str(&config)
55+
Ok(config) => {
56+
serde_ignored::deserialize(&mut toml::Deserializer::new(&config), |path| {
57+
ignored_keys.insert(path.to_string());
58+
})
4959
.map(merge_keys)
50-
.map_err(ConfigLoadError::BadConfig),
60+
.map_err(ConfigLoadError::BadConfig)
61+
}
5162
Err(err) => Err(ConfigLoadError::Error(err)),
5263
}
5364
}
5465

55-
pub fn load_default() -> Result<Config, ConfigLoadError> {
56-
Config::load(helix_loader::config_file())
66+
pub fn load_default(ignored_keys: &mut BTreeSet<String>) -> Result<Config, ConfigLoadError> {
67+
Config::load(helix_loader::config_file(), ignored_keys)
5768
}
5869
}
5970

@@ -104,4 +115,51 @@ mod tests {
104115
let default_keys = Config::default().keys;
105116
assert_eq!(default_keys, default());
106117
}
118+
119+
#[test]
120+
fn partial_config_parsing() {
121+
use crate::keymap;
122+
use crate::keymap::Keymap;
123+
use helix_core::hashmap;
124+
use helix_view::document::Mode;
125+
126+
let sample_keymaps = r#"
127+
theme = false
128+
129+
[editor]
130+
line-number = false
131+
mous = "false"
132+
scrolloff = 7
133+
134+
[editor.search]
135+
smart-case = false
136+
137+
[keys.insert]
138+
y = "move_line_down"
139+
SC-a = "delete_selection"
140+
141+
[keys.normal]
142+
A-F12 = "move_next_word_end"
143+
"#;
144+
145+
let mut editor = helix_view::editor::Config::default();
146+
editor.search.smart_case = false;
147+
editor.scrolloff = 7;
148+
149+
assert_eq!(
150+
toml::from_str::<Config>(sample_keymaps).unwrap(),
151+
Config {
152+
keys: hashmap! {
153+
Mode::Insert => Keymap::new(keymap!({ "Insert mode"
154+
"y" => move_line_down,
155+
})),
156+
Mode::Normal => Keymap::new(keymap!({ "Normal mode"
157+
"A-F12" => move_next_word_end,
158+
})),
159+
},
160+
editor,
161+
..Default::default()
162+
}
163+
);
164+
}
107165
}

helix-term/src/keymap.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ impl<'de> Deserialize<'de> for KeyTrieNode {
3434
D: serde::Deserializer<'de>,
3535
{
3636
let map = HashMap::<KeyEvent, KeyTrie>::deserialize(deserializer)?;
37+
let map: HashMap<_, _> = map
38+
.into_iter()
39+
.filter_map(|(key, value)| {
40+
// Filter the KeyEvents that has an invalid value because those come from a
41+
// parsing error and we should just ignore them.
42+
if key == KeyEvent::invalid() {
43+
None
44+
} else {
45+
Some((key, value))
46+
}
47+
})
48+
.collect();
3749
let order = map.keys().copied().collect::<Vec<_>>(); // NOTE: map.keys() has arbitrary order
3850
Ok(Self {
3951
map,

helix-term/src/main.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use anyhow::{Context, Error, Result};
22
use helix_term::application::Application;
33
use helix_term::args::Args;
44
use helix_term::config::{Config, ConfigLoadError};
5+
use helix_view::input::{get_config_error, set_config_error};
6+
use std::collections::BTreeSet;
57
use std::path::PathBuf;
68

79
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@@ -114,7 +116,8 @@ FLAGS:
114116
std::fs::create_dir_all(&conf_dir).ok();
115117
}
116118

117-
let config = match Config::load_default() {
119+
let mut ignored_keys = BTreeSet::new();
120+
let config = match Config::load_default(&mut ignored_keys) {
118121
Ok(config) => config,
119122
Err(err) => {
120123
match err {
@@ -134,6 +137,19 @@ FLAGS:
134137
}
135138
};
136139

140+
if !ignored_keys.is_empty() {
141+
let keys = ignored_keys.into_iter().collect::<Vec<_>>().join(", ");
142+
eprintln!("Ignored keys in config: {}", keys);
143+
set_config_error();
144+
}
145+
146+
if get_config_error() {
147+
eprintln!("Press <ENTER> to continue");
148+
use std::io::Read;
149+
// This waits for an enter press.
150+
let _ = std::io::stdin().read(&mut []);
151+
}
152+
137153
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
138154

139155
// TODO: use the thread local executor to spawn the application task separately from the work pool

helix-view/src/editor.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
document::{Mode, SCRATCH_BUFFER_NAME},
44
graphics::{CursorKind, Rect},
55
info::Info,
6-
input::KeyEvent,
6+
input::{set_config_error, KeyEvent},
77
theme::{self, Theme},
88
tree::{self, Tree},
99
Document, DocumentId, View, ViewId,
@@ -44,6 +44,20 @@ use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}
4444

4545
use arc_swap::access::{DynAccess, DynGuard};
4646

47+
pub fn ok_or_default<'a, T, D>(deserializer: D) -> Result<T, D::Error>
48+
where
49+
T: Deserialize<'a> + Default,
50+
D: Deserializer<'a>,
51+
{
52+
let result = T::deserialize(deserializer);
53+
if let Err(ref error) = result {
54+
// FIXME: the error message does not contain the key or the position.
55+
eprintln!("Bad config for value: {}", error);
56+
set_config_error();
57+
}
58+
Ok(result.unwrap_or_default())
59+
}
60+
4761
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
4862
where
4963
D: serde::Deserializer<'de>,
@@ -65,7 +79,7 @@ where
6579
}
6680

6781
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68-
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
82+
#[serde(rename_all = "kebab-case", default)]
6983
pub struct FilePickerConfig {
7084
/// IgnoreOptions
7185
/// Enables ignoring hidden files.
@@ -104,26 +118,36 @@ impl Default for FilePickerConfig {
104118
}
105119
}
106120

121+
// NOTE: The fields in this struct use the deserializer ok_or_default to continue parsing when
122+
// there is an error. In that case, it will use the default value.
107123
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108-
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
124+
#[serde(rename_all = "kebab-case", default)]
109125
pub struct Config {
110126
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
127+
#[serde(deserialize_with = "ok_or_default")]
111128
pub scrolloff: usize,
112129
/// Number of lines to scroll at once. Defaults to 3
130+
#[serde(deserialize_with = "ok_or_default")]
113131
pub scroll_lines: isize,
114132
/// Mouse support. Defaults to true.
133+
#[serde(deserialize_with = "ok_or_default")]
115134
pub mouse: bool,
116135
/// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
136+
#[serde(deserialize_with = "ok_or_default")]
117137
pub shell: Vec<String>,
118138
/// Line number mode.
139+
#[serde(deserialize_with = "ok_or_default")]
119140
pub line_number: LineNumber,
120141
/// Middle click paste support. Defaults to true.
142+
#[serde(deserialize_with = "ok_or_default")]
121143
pub middle_click_paste: bool,
122144
/// Automatic insertion of pairs to parentheses, brackets,
123145
/// etc. Optionally, this can be a list of 2-tuples to specify a
124146
/// global list of characters to pair. Defaults to true.
147+
#[serde(deserialize_with = "ok_or_default")]
125148
pub auto_pairs: AutoPairConfig,
126149
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
150+
#[serde(deserialize_with = "ok_or_default")]
127151
pub auto_completion: bool,
128152
/// Time in milliseconds since last keypress before idle timers trigger.
129153
/// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
@@ -132,28 +156,35 @@ pub struct Config {
132156
deserialize_with = "deserialize_duration_millis"
133157
)]
134158
pub idle_timeout: Duration,
159+
#[serde(deserialize_with = "ok_or_default")]
135160
pub completion_trigger_len: u8,
136161
/// Whether to display infoboxes. Defaults to true.
162+
#[serde(deserialize_with = "ok_or_default")]
137163
pub auto_info: bool,
164+
#[serde(deserialize_with = "ok_or_default")]
138165
pub file_picker: FilePickerConfig,
139166
/// Shape for cursor in each mode
167+
#[serde(deserialize_with = "ok_or_default")]
140168
pub cursor_shape: CursorShapeConfig,
141169
/// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
170+
#[serde(deserialize_with = "ok_or_default")]
142171
pub true_color: bool,
143172
/// Search configuration.
144-
#[serde(default)]
173+
#[serde(default, deserialize_with = "ok_or_default")]
145174
pub search: SearchConfig,
175+
#[serde(default, deserialize_with = "ok_or_default")]
146176
pub lsp: LspConfig,
147177
}
148178

149179
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
150-
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
180+
#[serde(rename_all = "kebab-case")]
151181
pub struct LspConfig {
182+
#[serde(deserialize_with = "ok_or_default")]
152183
pub display_messages: bool,
153184
}
154185

155186
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156-
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
187+
#[serde(rename_all = "kebab-case", default)]
157188
pub struct SearchConfig {
158189
/// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
159190
pub smart_case: bool,
@@ -226,6 +257,12 @@ pub enum LineNumber {
226257
Relative,
227258
}
228259

260+
impl Default for LineNumber {
261+
fn default() -> Self {
262+
Self::Absolute
263+
}
264+
}
265+
229266
impl std::str::FromStr for LineNumber {
230267
type Err = anyhow::Error;
231268

0 commit comments

Comments
 (0)