Add refresh-config and open-config command

pull/1803/head
Joseph Harrison-Lim 2 years ago
parent 6fdf5d0920
commit 03bade90d2
No known key found for this signature in database
GPG Key ID: 5DAB214D5E62987D

2
Cargo.lock generated

@ -435,6 +435,7 @@ name = "helix-term"
version = "0.6.0"
dependencies = [
"anyhow",
"arc-swap",
"chrono",
"content_inspector",
"crossterm",
@ -483,6 +484,7 @@ name = "helix-view"
version = "0.6.0"
dependencies = [
"anyhow",
"arc-swap",
"bitflags",
"chardetng",
"clipboard-win",

@ -40,6 +40,7 @@ crossterm = { version = "0.23", features = ["event-stream"] }
signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
arc-swap = { version = "1.5.0" }
# Logging
fern = "0.6"

@ -1,10 +1,11 @@
use arc_swap::{access::Map, ArcSwap};
use helix_core::{
config::{default_syntax_loader, user_syntax_loader},
pos_at_coords, syntax, Selection,
};
use helix_dap::{self as dap, Payload, Request};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{editor::Breakpoint, theme, Editor};
use helix_view::{editor::{Breakpoint, ConfigEvent}, theme, Editor};
use serde_json::json;
use crate::{
@ -42,8 +43,7 @@ pub struct Application {
compositor: Compositor,
editor: Editor,
// TODO: share an ArcSwap with Editor?
config: Config,
config: Arc<ArcSwap<Config>>,
#[allow(dead_code)]
theme_loader: Arc<theme::Loader>,
@ -56,7 +56,7 @@ pub struct Application {
}
impl Application {
pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
pub fn new(args: Args, config: Config) -> Result<Self, Error> {
use helix_view::editor::Action;
let mut compositor = Compositor::new()?;
let size = compositor.size();
@ -98,14 +98,16 @@ impl Application {
});
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let config = Arc::new(ArcSwap::from_pointee(config));
let mut editor = Editor::new(
size,
theme_loader.clone(),
syn_loader.clone(),
config.editor.clone(),
ArcSwap::from_pointee(config.load().editor.clone())
// config.clone(),
);
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.load().keys.clone())));
compositor.push(editor_view);
if args.load_tutor {
@ -121,7 +123,7 @@ impl Application {
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
let picker = ui::file_picker(".".into(), &config.editor);
let picker = ui::file_picker(".".into(), &config.load().editor);
compositor.push(Box::new(overlayed(picker)));
} else {
let nr_of_files = args.files.len();
@ -228,6 +230,10 @@ impl Application {
Some(payload) = self.editor.debugger_events.next() => {
self.handle_debugger_message(payload).await;
}
Some(ConfigEvent) = self.editor.config_events.next() => {
self.refresh_config();
self.render();
}
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
@ -245,6 +251,39 @@ impl Application {
}
}
pub fn refresh_config(&mut self) {
let config = Config::load(helix_loader::config_file()).unwrap();
// Just an example to start; Some config properties like "theme" are a bit more involved and require a reload
if let Some(theme) = config.theme.clone() {
let true_color = self.true_color();
self.editor.set_theme(
self.theme_loader
.load(&theme)
.map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, e);
e
})
.ok()
.filter(|theme| (true_color || theme.is_16_color()))
.unwrap_or_else(|| {
if true_color {
self.theme_loader.default()
} else {
self.theme_loader.base16_default()
}
})
.clone(),
);
}
self.config.store(Arc::new(config));
// Is it possible to not do this manually? Presumably I've completely butchered using ArcSwap?
self.editor.config.store(Arc::new(self.config.load().editor.clone()));
}
fn true_color(&self) -> bool {
self.config.load().editor.true_color || crate::true_color()
}
#[cfg(windows)]
// no signal handling available on windows
pub async fn handle_signals(&mut self, _signal: ()) {}
@ -700,7 +739,7 @@ impl Application {
self.lsp_progress.update(server_id, token, work);
}
if self.config.lsp.display_messages {
if self.config.load().lsp.display_messages {
self.editor.set_status(status);
}
}
@ -809,7 +848,7 @@ impl Application {
terminal::enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, terminal::EnterAlternateScreen)?;
if self.config.editor.mouse {
if self.config.load().editor.mouse {
execute!(stdout, EnableMouseCapture)?;
}
Ok(())

@ -848,7 +848,7 @@ fn goto_window(cx: &mut Context, align: Align) {
// - 1 so we have at least one gap in the middle.
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth
// as we type
let scrolloff = cx.editor.config.scrolloff.min(height.saturating_sub(1) / 2);
let scrolloff = cx.editor.config.load().scrolloff.min(height.saturating_sub(1) / 2);
let last_line = view.last_line(doc);
@ -1290,7 +1290,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
let height = view.inner_area().height;
let scrolloff = cx.editor.config.scrolloff.min(height as usize / 2);
let scrolloff = cx.editor.config.load().scrolloff.min(height as usize / 2);
view.offset.row = match direction {
Forward => view.offset.row + offset,
@ -1583,8 +1583,8 @@ fn rsearch(cx: &mut Context) {
fn searcher(cx: &mut Context, direction: Direction) {
let reg = cx.register.unwrap_or('/');
let scrolloff = cx.editor.config.scrolloff;
let wrap_around = cx.editor.config.search.wrap_around;
let scrolloff = cx.editor.config.load().scrolloff;
let wrap_around = cx.editor.config.load().search.wrap_around;
let doc = doc!(cx.editor);
@ -1627,13 +1627,13 @@ fn searcher(cx: &mut Context, direction: Direction) {
}
fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
let scrolloff = cx.editor.config.scrolloff;
let scrolloff = cx.editor.config.load().scrolloff;
let (view, doc) = current!(cx.editor);
let registers = &cx.editor.registers;
if let Some(query) = registers.read('/') {
let query = query.last().unwrap();
let contents = doc.text().slice(..).to_string();
let search_config = &cx.editor.config.search;
let search_config = &cx.editor.config.load().search;
let case_insensitive = if search_config.smart_case {
!query.chars().any(char::is_uppercase)
} else {
@ -1693,8 +1693,8 @@ fn search_selection(cx: &mut Context) {
fn global_search(cx: &mut Context) {
let (all_matches_sx, all_matches_rx) =
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
let smart_case = cx.editor.config.search.smart_case;
let file_picker_config = cx.editor.config.file_picker.clone();
let smart_case = cx.editor.config.load().search.smart_case;
let file_picker_config = cx.editor.config.load().file_picker.clone();
let completions = search_completions(cx, None);
let prompt = ui::regex_prompt(
@ -2023,7 +2023,7 @@ fn append_mode(cx: &mut Context) {
fn file_picker(cx: &mut Context) {
// We don't specify language markers, root will be the root of the current git repo
let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
let picker = ui::file_picker(root, &cx.editor.config);
let picker = ui::file_picker(root, &cx.editor.config.load());
cx.push_layer(Box::new(overlayed(picker)));
}
@ -2573,7 +2573,7 @@ pub mod insert {
use helix_core::chars::char_is_word;
let mut iter = text.chars_at(cursor);
iter.reverse();
for _ in 0..cx.editor.config.completion_trigger_len {
for _ in 0..cx.editor.config.load().completion_trigger_len {
match iter.next() {
Some(c) if char_is_word(c) => {}
_ => return,
@ -4136,7 +4136,7 @@ fn shell_keep_pipe(cx: &mut Context) {
Some('|'),
ui::completers::none,
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
let shell = &cx.editor.config.shell;
let shell = &cx.editor.config.load().shell;
if event != PromptEvent::Validate {
return;
}
@ -4232,7 +4232,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
Some('|'),
ui::completers::none,
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
let shell = &cx.editor.config.shell;
let shell = &cx.editor.config.load().shell;
if event != PromptEvent::Validate {
return;
}
@ -4277,7 +4277,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
// after replace cursor may be out of bounds, do this to
// make sure cursor is in view and update scroll as well
view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
view.ensure_cursor_in_view(doc, cx.editor.config.load().scrolloff);
},
);

@ -1,6 +1,8 @@
use std::{borrow::BorrowMut, sync::Arc};
use super::*;
use helix_view::editor::Action;
use helix_view::editor::{Action, ConfigEvent};
use ui::completers::{self, Completer};
#[derive(Clone)]
@ -533,7 +535,7 @@ fn theme(
.theme_loader
.load(theme)
.with_context(|| format!("Failed setting theme {}", theme))?;
let true_color = cx.editor.config.true_color || crate::true_color();
let true_color = cx.editor.config.load().true_color || crate::true_color();
if !(true_color || theme.is_16_color()) {
bail!("Unsupported theme: theme requires true color support");
}
@ -857,28 +859,28 @@ fn setting(
args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let runtime_config = &mut cx.editor.config;
if args.len() != 2 {
anyhow::bail!("Bad arguments. Usage: `:set key field`");
}
let (key, arg) = (&args[0].to_lowercase(), &args[1]);
match key.as_ref() {
"scrolloff" => runtime_config.scrolloff = arg.parse()?,
"scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
"mouse" => runtime_config.mouse = arg.parse()?,
"line-number" => runtime_config.line_number = arg.parse()?,
"middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
"auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
"auto-completion" => runtime_config.auto_completion = arg.parse()?,
"completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
"auto-info" => runtime_config.auto_info = arg.parse()?,
"true-color" => runtime_config.true_color = arg.parse()?,
"search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
"search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
_ => anyhow::bail!("Unknown key `{}`.", args[0]),
if let Ok(runtime_config) = &mut std::sync::Arc::try_unwrap(cx.editor.config.load().clone()) {
match key.as_ref() {
"scrolloff" => runtime_config.scrolloff = arg.parse()?,
"scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
"mouse" => runtime_config.mouse = arg.parse()?,
"line-number" => runtime_config.line_number = arg.parse()?,
"middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
"auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
"auto-completion" => runtime_config.auto_completion = arg.parse()?,
"completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
"auto-info" => runtime_config.auto_info = arg.parse()?,
"true-color" => runtime_config.true_color = arg.parse()?,
"search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
"search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
_ => anyhow::bail!("Unknown key `{}`.", args[0]),
}
cx.editor.config.store(Arc::new(runtime_config.clone()));
}
Ok(())
@ -970,6 +972,24 @@ fn tree_sitter_subtree(
Ok(())
}
fn open_config(
cx: &mut compositor::Context,
_args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor.open(helix_loader::config_file(), Action::Replace)?;
Ok(())
}
fn refresh_config(
cx: &mut compositor::Context,
_args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor.config_events.push(tokio_stream::once(ConfigEvent));
Ok(())
}
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@ -1342,6 +1362,20 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: tree_sitter_subtree,
completer: None,
},
TypableCommand {
name: "refresh-config",
aliases: &[],
doc: "Refreshes helix's config.",
fun: refresh_config,
completer: None,
},
TypableCommand {
name: "open-config",
aliases: &[],
doc: "Open the helix config.toml file.",
fun: open_config,
completer: None,
},
];
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =

@ -1,6 +1,7 @@
use crate::keymap::{merge_keys, Keymaps};
use anyhow::{Error, Result};
use serde::Deserialize;
use crate::keymap::Keymaps;
use std::path::PathBuf;
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
@ -20,6 +21,25 @@ pub struct LspConfig {
pub display_messages: bool,
}
impl Config {
pub fn load(config_path: PathBuf) -> Result<Config, Error> {
match std::fs::read_to_string(config_path) {
Ok(config) => Result::Ok(toml::from_str(&config)
.map(merge_keys)
.unwrap_or_else(|err| {
eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
Config::default()
})),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Result::Ok(Config::default()),
Err(err) => return Err(Error::new(err)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;

@ -2,7 +2,6 @@ use anyhow::{Context, Error, Result};
use helix_term::application::Application;
use helix_term::args::Args;
use helix_term::config::Config;
use helix_term::keymap::merge_keys;
use std::path::PathBuf;
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@ -118,20 +117,7 @@ FLAGS:
std::fs::create_dir_all(&conf_dir).ok();
}
let config = match std::fs::read_to_string(helix_loader::config_file()) {
Ok(config) => toml::from_str(&config)
.map(merge_keys)
.unwrap_or_else(|err| {
eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
Config::default()
}),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
};
let config = Config::load(helix_loader::config_file())?;
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;

@ -118,7 +118,7 @@ impl EditorView {
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge(
highlights,
Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape),
Self::doc_selection_highlights(doc, view, theme, &editor.config.load().cursor_shape),
))
} else {
Box::new(highlights)
@ -846,7 +846,7 @@ impl EditorView {
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
if self.completion.is_some()
|| !cx.editor.config.auto_completion
|| !cx.editor.config.load().auto_completion
|| doc!(cx.editor).mode != Mode::Insert
{
return EventResult::Ignored(None);
@ -872,6 +872,7 @@ impl EditorView {
event: MouseEvent,
cxt: &mut commands::Context,
) -> EventResult {
let config = cxt.editor.config.load();
match event {
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
@ -972,7 +973,7 @@ impl EditorView {
None => return EventResult::Ignored(None),
}
let offset = cxt.editor.config.scroll_lines.abs() as usize;
let offset = config.scroll_lines.abs() as usize;
commands::scroll(cxt, offset, direction);
cxt.editor.tree.focus = current_view;
@ -984,7 +985,7 @@ impl EditorView {
kind: MouseEventKind::Up(MouseButton::Left),
..
} => {
if !cxt.editor.config.middle_click_paste {
if !config.middle_click_paste {
return EventResult::Ignored(None);
}
@ -1040,7 +1041,7 @@ impl EditorView {
..
} => {
let editor = &mut cxt.editor;
if !editor.config.middle_click_paste {
if !config.middle_click_paste {
return EventResult::Ignored(None);
}
@ -1166,7 +1167,7 @@ impl Component for EditorView {
}
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
view.ensure_cursor_in_view(doc, cx.editor.config.load().scrolloff);
// Store a history state if not in insert mode. This also takes care of
// commiting changes when leaving insert mode.
@ -1217,7 +1218,7 @@ impl Component for EditorView {
self.render_view(cx.editor, doc, view, area, surface, is_focused);
}
if cx.editor.config.auto_info {
if cx.editor.config.load().auto_info {
if let Some(mut info) = cx.editor.autoinfo.take() {
info.render(area, surface, cx);
cx.editor.autoinfo = Some(info)

@ -37,6 +37,7 @@ pub fn regex_prompt(
let doc_id = view.doc;
let snapshot = doc.selection(view.id).clone();
let offset_snapshot = view.offset;
let config = cx.editor.config.load();
let mut prompt = Prompt::new(
prompt,
@ -65,7 +66,7 @@ pub fn regex_prompt(
return;
}
let case_insensitive = if cx.editor.config.search.smart_case {
let case_insensitive = if config.search.smart_case {
!input.chars().any(char::is_uppercase)
} else {
false
@ -84,7 +85,7 @@ pub fn regex_prompt(
fun(view, doc, regex, event);
view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
view.ensure_cursor_in_view(doc, config.scrolloff);
}
Err(_err) => (), // TODO: mark command line as error
}

@ -25,6 +25,8 @@ crossterm = { version = "0.23", optional = true }
once_cell = "1.10"
url = "2"
arc-swap = { version = "1.5.0" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }

@ -13,7 +13,6 @@ use futures_util::future;
use futures_util::stream::select_all::SelectAll;
use tokio_stream::wrappers::UnboundedReceiverStream;
use log::debug;
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
@ -40,6 +39,8 @@ use helix_dap as dap;
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
use arc_swap::{access::{DynAccess}, ArcSwap};
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: serde::Deserializer<'de>,
@ -271,6 +272,8 @@ pub struct Breakpoint {
pub log_message: Option<String>,
}
pub trait DynAccessDebug<T>: DynAccess<T> + std::fmt::Debug {}
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
@ -295,7 +298,7 @@ pub struct Editor {
pub status_msg: Option<(Cow<'static, str>, Severity)>,
pub autoinfo: Option<Info>,
pub config: Config,
pub config: ArcSwap<Config>,
pub auto_pairs: Option<AutoPairs>,
pub idle_timer: Pin<Box<Sleep>>,
@ -305,8 +308,13 @@ pub struct Editor {
pub last_completion: Option<CompleteAction>,
pub exit_code: i32,
pub config_events: SelectAll<tokio_stream::Once<ConfigEvent>>,
}
#[derive(Debug)]
pub struct ConfigEvent;
#[derive(Debug, Clone)]
pub struct CompleteAction {
pub trigger_offset: usize,
@ -326,12 +334,11 @@ impl Editor {
mut area: Rect,
theme_loader: Arc<theme::Loader>,
syn_loader: Arc<syntax::Loader>,
config: Config,
config: ArcSwap<Config>,
) -> Self {
let language_servers = helix_lsp::Registry::new();
let auto_pairs = (&config.auto_pairs).into();
debug!("Editor config: {config:#?}");
let conf = config.load();
let auto_pairs = (&conf.auto_pairs).into();
// HAXX: offset the render area height by 1 to account for prompt/commandline
area.height -= 1;
@ -354,13 +361,14 @@ impl Editor {
clipboard_provider: get_clipboard_provider(),
status_msg: None,
autoinfo: None,
idle_timer: Box::pin(sleep(config.idle_timeout)),
idle_timer: Box::pin(sleep(conf.idle_timeout)),
last_motion: None,
last_completion: None,
pseudo_pending: None,
config,
auto_pairs,
exit_code: 0,
config_events: SelectAll::new(),
}
}
@ -374,7 +382,7 @@ impl Editor {
pub fn reset_idle_timer(&mut self) {
self.idle_timer
.as_mut()
.reset(Instant::now() + self.config.idle_timeout);
.reset(Instant::now() + self.config.load().idle_timeout);
}
pub fn clear_status(&mut self) {
@ -452,7 +460,7 @@ impl Editor {
fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() {
let doc = &self.documents[&view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff)
view.ensure_cursor_in_view(doc, self.config.load().scrolloff)
}
}
@ -702,7 +710,7 @@ impl Editor {
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
let view = self.tree.get_mut(id);
let doc = &self.documents[&view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff)
view.ensure_cursor_in_view(doc, self.config.load().scrolloff)
}
#[inline]
@ -745,7 +753,7 @@ impl Editor {
let inner = view.inner_area();
pos.col += inner.x as usize;
pos.row += inner.y as usize;
let cursorkind = self.config.cursor_shape.from_mode(doc.mode());
let cursorkind = self.config.load().cursor_shape.from_mode(doc.mode());
(Some(pos), cursorkind)
} else {
(None, CursorKind::default())

@ -60,7 +60,7 @@ pub fn line_number<'doc>(
.text()
.char_to_line(doc.selection(view.id).primary().cursor(text));
let config = editor.config.line_number;
let config = editor.config.load().line_number;
let mode = doc.mode;
Box::new(move |line: usize, selected: bool, out: &mut String| {

Loading…
Cancel
Save