Skip to content

Commit 605ce52

Browse files
committed
Continue loading the config on error
1 parent 0902ede commit 605ce52

File tree

8 files changed

+181
-22
lines changed

8 files changed

+181
-22
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
@@ -59,6 +59,7 @@ toml = "0.5"
5959

6060
serde_json = "1.0"
6161
serde = { version = "1.0", features = ["derive"] }
62+
serde_ignored = "0.1.2"
6263

6364
# ripgrep for global search
6465
grep-regex = "0.1.9"

helix-term/src/config.rs

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
use serde::Deserialize;
22

3+
use helix_view::editor::ok_or_default;
4+
35
use crate::keymap::Keymaps;
46

7+
// NOTE: The fields in this struct use the deserializer ok_or_default to continue parsing when
8+
// there is an error. In that case, it will use the default value.
59
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
6-
#[serde(deny_unknown_fields)]
710
pub struct Config {
11+
#[serde(default, deserialize_with = "ok_or_default")]
812
pub theme: Option<String>,
9-
#[serde(default)]
13+
#[serde(default, deserialize_with = "ok_or_default")]
1014
pub lsp: LspConfig,
11-
#[serde(default)]
15+
#[serde(default, deserialize_with = "ok_or_default")]
1216
pub keys: Keymaps,
13-
#[serde(default)]
17+
#[serde(default, deserialize_with = "ok_or_default")]
1418
pub editor: helix_view::editor::Config,
1519
}
1620

1721
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
18-
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
22+
#[serde(rename_all = "kebab-case")]
1923
pub struct LspConfig {
24+
#[serde(deserialize_with = "ok_or_default")]
2025
pub display_messages: bool,
2126
}
2227

@@ -56,4 +61,51 @@ mod tests {
5661
}
5762
);
5863
}
64+
65+
#[test]
66+
fn partial_config_parsing() {
67+
use crate::keymap;
68+
use crate::keymap::Keymap;
69+
use helix_core::hashmap;
70+
use helix_view::document::Mode;
71+
72+
let sample_keymaps = r#"
73+
theme = false
74+
75+
[editor]
76+
line-number = false
77+
mous = "false"
78+
scrolloff = 7
79+
80+
[editor.search]
81+
smart-case = false
82+
83+
[keys.insert]
84+
y = "move_line_down"
85+
SC-a = "delete_selection"
86+
87+
[keys.normal]
88+
A-F12 = "move_next_word_end"
89+
"#;
90+
91+
let mut editor = helix_view::editor::Config::default();
92+
editor.search.smart_case = false;
93+
editor.scrolloff = 7;
94+
95+
assert_eq!(
96+
toml::from_str::<Config>(sample_keymaps).unwrap(),
97+
Config {
98+
keys: Keymaps(hashmap! {
99+
Mode::Insert => Keymap::new(keymap!({ "Insert mode"
100+
"y" => move_line_down,
101+
})),
102+
Mode::Normal => Keymap::new(keymap!({ "Normal mode"
103+
"A-F12" => move_next_word_end,
104+
})),
105+
}),
106+
editor,
107+
..Default::default()
108+
}
109+
);
110+
}
59111
}

helix-term/src/keymap.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,18 @@ impl<'de> Deserialize<'de> for KeyTrieNode {
146146
D: serde::Deserializer<'de>,
147147
{
148148
let map = HashMap::<KeyEvent, KeyTrie>::deserialize(deserializer)?;
149+
let map: HashMap<_, _> = map
150+
.into_iter()
151+
.filter_map(|(key, value)| {
152+
// Filter the KeyEvents that has an invalid value because those come from a
153+
// parsing error and we should just ignore them.
154+
if key == KeyEvent::invalid() {
155+
None
156+
} else {
157+
Some((key, value))
158+
}
159+
})
160+
.collect();
149161
let order = map.keys().copied().collect::<Vec<_>>(); // NOTE: map.keys() has arbitrary order
150162
Ok(Self {
151163
map,

helix-term/src/main.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use helix_term::application::Application;
33
use helix_term::args::Args;
44
use helix_term::config::Config;
55
use helix_term::keymap::merge_keys;
6+
use helix_view::input::{get_config_error, set_config_error};
7+
use std::collections::BTreeSet;
68
use std::path::PathBuf;
79

810
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@@ -118,21 +120,38 @@ FLAGS:
118120
std::fs::create_dir_all(&conf_dir).ok();
119121
}
120122

123+
let mut ignored_keys = BTreeSet::new();
124+
121125
let config = match std::fs::read_to_string(helix_loader::config_file()) {
122-
Ok(config) => toml::from_str(&config)
123-
.map(merge_keys)
124-
.unwrap_or_else(|err| {
125-
eprintln!("Bad config: {}", err);
126-
eprintln!("Press <ENTER> to continue with default config");
127-
use std::io::Read;
128-
// This waits for an enter press.
129-
let _ = std::io::stdin().read(&mut []);
130-
Config::default()
131-
}),
126+
Ok(config) => serde_ignored::deserialize(&mut toml::Deserializer::new(&config), |path| {
127+
ignored_keys.insert(path.to_string());
128+
})
129+
.map(merge_keys)
130+
.unwrap_or_else(|err| {
131+
eprintln!("Bad config: {}", err);
132+
eprintln!("Press <ENTER> to continue with default config");
133+
use std::io::Read;
134+
// This waits for an enter press.
135+
let _ = std::io::stdin().read(&mut []);
136+
Config::default()
137+
}),
132138
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
133139
Err(err) => return Err(Error::new(err)),
134140
};
135141

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

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

helix-view/src/editor.rs

Lines changed: 40 additions & 4 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,
@@ -40,6 +40,20 @@ use helix_dap as dap;
4040

4141
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
4242

43+
pub fn ok_or_default<'a, T, D>(deserializer: D) -> Result<T, D::Error>
44+
where
45+
T: Deserialize<'a> + Default,
46+
D: Deserializer<'a>,
47+
{
48+
let result = T::deserialize(deserializer);
49+
if let Err(ref error) = result {
50+
// FIXME: the error message does not contain the key or the position.
51+
eprintln!("Bad config for value: {}", error);
52+
set_config_error();
53+
}
54+
Ok(result.unwrap_or_default())
55+
}
56+
4357
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
4458
where
4559
D: serde::Deserializer<'de>,
@@ -49,7 +63,7 @@ where
4963
}
5064

5165
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52-
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
66+
#[serde(rename_all = "kebab-case", default)]
5367
pub struct FilePickerConfig {
5468
/// IgnoreOptions
5569
/// Enables ignoring hidden files.
@@ -88,45 +102,61 @@ impl Default for FilePickerConfig {
88102
}
89103
}
90104

105+
// NOTE: The fields in this struct use the deserializer ok_or_default to continue parsing when
106+
// there is an error. In that case, it will use the default value.
91107
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92-
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
108+
#[serde(rename_all = "kebab-case", default)]
93109
pub struct Config {
94110
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
111+
#[serde(deserialize_with = "ok_or_default")]
95112
pub scrolloff: usize,
96113
/// Number of lines to scroll at once. Defaults to 3
114+
#[serde(deserialize_with = "ok_or_default")]
97115
pub scroll_lines: isize,
98116
/// Mouse support. Defaults to true.
117+
#[serde(deserialize_with = "ok_or_default")]
99118
pub mouse: bool,
100119
/// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
120+
#[serde(deserialize_with = "ok_or_default")]
101121
pub shell: Vec<String>,
102122
/// Line number mode.
123+
#[serde(deserialize_with = "ok_or_default")]
103124
pub line_number: LineNumber,
104125
/// Middle click paste support. Defaults to true.
126+
#[serde(deserialize_with = "ok_or_default")]
105127
pub middle_click_paste: bool,
106128
/// Automatic insertion of pairs to parentheses, brackets,
107129
/// etc. Optionally, this can be a list of 2-tuples to specify a
108130
/// global list of characters to pair. Defaults to true.
131+
#[serde(deserialize_with = "ok_or_default")]
109132
pub auto_pairs: AutoPairConfig,
110133
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
134+
#[serde(deserialize_with = "ok_or_default")]
111135
pub auto_completion: bool,
112136
/// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
113137
#[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
114138
pub idle_timeout: Duration,
139+
#[serde(deserialize_with = "ok_or_default")]
115140
pub completion_trigger_len: u8,
116141
/// Whether to display infoboxes. Defaults to true.
142+
#[serde(deserialize_with = "ok_or_default")]
117143
pub auto_info: bool,
144+
#[serde(deserialize_with = "ok_or_default")]
118145
pub file_picker: FilePickerConfig,
119146
/// Shape for cursor in each mode
147+
#[serde(deserialize_with = "ok_or_default")]
120148
pub cursor_shape: CursorShapeConfig,
121149
/// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
150+
#[serde(deserialize_with = "ok_or_default")]
122151
pub true_color: bool,
123152
/// Search configuration.
124153
#[serde(default)]
154+
#[serde(deserialize_with = "ok_or_default")]
125155
pub search: SearchConfig,
126156
}
127157

128158
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129-
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
159+
#[serde(rename_all = "kebab-case", default)]
130160
pub struct SearchConfig {
131161
/// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
132162
pub smart_case: bool,
@@ -199,6 +229,12 @@ pub enum LineNumber {
199229
Relative,
200230
}
201231

232+
impl Default for LineNumber {
233+
fn default() -> Self {
234+
Self::Absolute
235+
}
236+
}
237+
202238
impl std::str::FromStr for LineNumber {
203239
type Err = anyhow::Error;
204240

helix-view/src/input.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
//! Input event handling, currently backed by crossterm.
22
use anyhow::{anyhow, Error};
33
use helix_core::unicode::width::UnicodeWidthStr;
4-
use serde::de::{self, Deserialize, Deserializer};
4+
use serde::de::{Deserialize, Deserializer};
55
use std::fmt;
6+
use std::sync::atomic::{AtomicBool, Ordering};
67

78
use crate::keyboard::{KeyCode, KeyModifiers};
89

10+
static HAD_CONFIG_ERROR: AtomicBool = AtomicBool::new(false);
11+
12+
/// To be called by the Config deserializer when there's an error so that the editor knows it
13+
/// should wait the user to press Enter in order for the user to have the time to see the error
14+
/// before the editor shows up.
15+
pub fn set_config_error() {
16+
HAD_CONFIG_ERROR.store(true, Ordering::SeqCst);
17+
}
18+
19+
/// Return true if there was an error during the Config deserialization.
20+
pub fn get_config_error() -> bool {
21+
HAD_CONFIG_ERROR.load(Ordering::SeqCst)
22+
}
23+
924
/// Represents a key event.
1025
// We use a newtype here because we want to customize Deserialize and Display.
1126
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
@@ -22,6 +37,14 @@ impl KeyEvent {
2237
_ => None,
2338
}
2439
}
40+
41+
/// Return an invalid KeyEvent to use for cases where an event key cannot be parsed.
42+
pub fn invalid() -> Self {
43+
Self {
44+
code: KeyCode::Null,
45+
modifiers: KeyModifiers::NONE,
46+
}
47+
}
2548
}
2649

2750
pub(crate) mod keys {
@@ -210,7 +233,13 @@ impl<'de> Deserialize<'de> for KeyEvent {
210233
D: Deserializer<'de>,
211234
{
212235
let s = String::deserialize(deserializer)?;
213-
s.parse().map_err(de::Error::custom)
236+
let key_event = s.parse();
237+
if let Err(ref error) = key_event {
238+
// TODO: show error position.
239+
eprintln!("Bad config for key: {}", error);
240+
set_config_error();
241+
}
242+
Ok(key_event.unwrap_or_else(|_| KeyEvent::invalid()))
214243
}
215244
}
216245

0 commit comments

Comments
 (0)