mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 08:23:27 +02:00
Compare commits
5 Commits
inlay-hint
...
config_ref
Author | SHA1 | Date | |
---|---|---|---|
|
87266b0a07 | ||
|
dcbe8496b9 | ||
|
fb13130701 | ||
|
7ba8674466 | ||
|
5e74d3c821 |
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -62,9 +62,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca87830a3e3fb156dc96cfbd31cb620265dd053be734723f22b760d6cc3c3051"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@@ -1047,11 +1047,29 @@ dependencies = [
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "helix-config"
|
||||
version = "23.10.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"globset",
|
||||
"hashbrown 0.14.3",
|
||||
"indexmap",
|
||||
"parking_lot",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "helix-core"
|
||||
version = "23.10.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"bitflags 2.4.1",
|
||||
"chrono",
|
||||
@@ -1059,8 +1077,10 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"etcetera",
|
||||
"hashbrown 0.14.3",
|
||||
"helix-config",
|
||||
"helix-loader",
|
||||
"imara-diff",
|
||||
"indexmap",
|
||||
"indoc",
|
||||
"log",
|
||||
"nucleo",
|
||||
@@ -1088,6 +1108,7 @@ version = "23.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"fern",
|
||||
"helix-config",
|
||||
"helix-core",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -1128,13 +1149,16 @@ dependencies = [
|
||||
name = "helix-lsp"
|
||||
version = "23.10.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"globset",
|
||||
"helix-config",
|
||||
"helix-core",
|
||||
"helix-loader",
|
||||
"helix-parsec",
|
||||
"indexmap",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"parking_lot",
|
||||
@@ -1235,6 +1259,7 @@ dependencies = [
|
||||
"clipboard-win",
|
||||
"crossterm",
|
||||
"futures-util",
|
||||
"helix-config",
|
||||
"helix-core",
|
||||
"helix-dap",
|
||||
"helix-event",
|
||||
@@ -1337,12 +1362,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@@ -2,6 +2,7 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"helix-core",
|
||||
"helix-config",
|
||||
"helix-view",
|
||||
"helix-term",
|
||||
"helix-tui",
|
||||
|
28
helix-config/Cargo.toml
Normal file
28
helix-config/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "helix-config"
|
||||
description = "Helix editor core editing primitives"
|
||||
include = ["src/**/*", "README.md"]
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
categories.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ahash = "0.8.6"
|
||||
hashbrown = { version = "0.14.3", features = ["raw"] }
|
||||
parking_lot = "0.12"
|
||||
anyhow = "1.0.79"
|
||||
indexmap = { version = "2.1.0", features = ["serde"] }
|
||||
serde = { version = "1.0" }
|
||||
serde_json = "1.0"
|
||||
globset = "0.4.14"
|
||||
regex = "1.10.2"
|
||||
regex-syntax = "0.8.2"
|
||||
which = "5.0.0"
|
||||
|
||||
regex-syntax = "0.8.2"
|
||||
which = "5.0.0"
|
76
helix-config/src/any.rs
Normal file
76
helix-config/src/any.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
/// this is a reimplementation of dynamic dispatch that only stores the
|
||||
/// information we need and stores everythin inline. Values that are smaller or
|
||||
/// the same size as a slice (2 usize) are also stored inline. This avoids
|
||||
/// significant overallocation when setting lots of simple config
|
||||
/// options (integers, strings, lists, enums)
|
||||
use std::any::{Any, TypeId};
|
||||
use std::mem::{align_of, size_of, MaybeUninit};
|
||||
|
||||
pub struct ConfigData {
|
||||
data: MaybeUninit<[usize; 2]>,
|
||||
ty: TypeId,
|
||||
drop_fn: unsafe fn(MaybeUninit<[usize; 2]>),
|
||||
}
|
||||
|
||||
const fn store_inline<T>() -> bool {
|
||||
size_of::<T>() <= size_of::<[usize; 2]>() && align_of::<T>() <= align_of::<[usize; 2]>()
|
||||
}
|
||||
|
||||
impl ConfigData {
|
||||
unsafe fn drop_impl<T: Any>(mut data: MaybeUninit<[usize; 2]>) {
|
||||
if store_inline::<T>() {
|
||||
data.as_mut_ptr().cast::<T>().drop_in_place();
|
||||
} else {
|
||||
let ptr = data.as_mut_ptr().cast::<*mut T>().read();
|
||||
drop(Box::from_raw(ptr));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<T: Any>(&self) -> &T {
|
||||
assert_eq!(TypeId::of::<T>(), self.ty);
|
||||
unsafe {
|
||||
if store_inline::<T>() {
|
||||
return &*self.data.as_ptr().cast();
|
||||
}
|
||||
let data: *const T = self.data.as_ptr().cast::<*const T>().read();
|
||||
&*data
|
||||
}
|
||||
}
|
||||
pub fn new<T: Any>(val: T) -> Self {
|
||||
let mut data = MaybeUninit::uninit();
|
||||
if store_inline::<T>() {
|
||||
let data: *mut T = data.as_mut_ptr() as _;
|
||||
unsafe {
|
||||
data.write(val);
|
||||
}
|
||||
} else {
|
||||
assert!(store_inline::<*const T>());
|
||||
let data: *mut *const T = data.as_mut_ptr() as _;
|
||||
unsafe {
|
||||
data.write(Box::into_raw(Box::new(val)));
|
||||
}
|
||||
};
|
||||
Self {
|
||||
data,
|
||||
ty: TypeId::of::<T>(),
|
||||
drop_fn: ConfigData::drop_impl::<T>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigData {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
(self.drop_fn)(self.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ConfigData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ConfigData").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for ConfigData {}
|
||||
unsafe impl Sync for ConfigData {}
|
42
helix-config/src/convert.rs
Normal file
42
helix-config/src/convert.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::any::ConfigData;
|
||||
use crate::validator::Ty;
|
||||
use crate::Value;
|
||||
|
||||
pub trait IntoTy: Clone {
|
||||
type Ty: Ty;
|
||||
fn into_ty(self) -> Self::Ty;
|
||||
}
|
||||
|
||||
impl<T: Ty> IntoTy for T {
|
||||
type Ty = Self;
|
||||
|
||||
fn into_ty(self) -> Self::Ty {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<T: IntoTy> IntoTy for &[T] {
|
||||
type Ty = Box<[T::Ty]>;
|
||||
|
||||
fn into_ty(self) -> Self::Ty {
|
||||
self.iter().cloned().map(T::into_ty).collect()
|
||||
}
|
||||
}
|
||||
impl<T: IntoTy, const N: usize> IntoTy for &[T; N] {
|
||||
type Ty = Box<[T::Ty]>;
|
||||
|
||||
fn into_ty(self) -> Self::Ty {
|
||||
self.iter().cloned().map(T::into_ty).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTy for &str {
|
||||
type Ty = Box<str>;
|
||||
|
||||
fn into_ty(self) -> Self::Ty {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn ty_into_value<T: Ty>(val: &ConfigData) -> Value {
|
||||
T::to_value(val.get())
|
||||
}
|
113
helix-config/src/definition.rs
Normal file
113
helix-config/src/definition.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::*;
|
||||
|
||||
mod language;
|
||||
mod lsp;
|
||||
mod ui;
|
||||
|
||||
pub use lsp::init_language_server_config;
|
||||
|
||||
options! {
|
||||
use ui::*;
|
||||
use lsp::*;
|
||||
use language::*;
|
||||
|
||||
struct WrapConfig {
|
||||
/// Soft wrap lines that exceed viewport width.
|
||||
enable: bool = false,
|
||||
/// Maximum free space left at the end of the line.
|
||||
/// Automatically limited to a quarter of the viewport.
|
||||
max_wrap: u16 = 20,
|
||||
/// Maximum indentation to carry over when soft wrapping a line.
|
||||
/// Automatically limited to a quarter of the viewport.
|
||||
max_indent_retain: u16 = 40,
|
||||
/// Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap`.
|
||||
wrap_indicator: String = "↪",
|
||||
/// Soft wrap at `text-width` instead of using the full viewport size.
|
||||
wrap_at_text_width: bool = false,
|
||||
/// Maximum line length. Used for the `:reflow` command and
|
||||
/// soft-wrapping if `soft-wrap.wrap-at-text-width` is set
|
||||
text_width: usize = 80,
|
||||
}
|
||||
|
||||
struct MouseConfig {
|
||||
/// Enable mouse mode
|
||||
#[read = copy]
|
||||
mouse: bool = true,
|
||||
/// Number of lines to scroll per scroll wheel step.
|
||||
#[read = copy]
|
||||
scroll_lines: usize = 3,
|
||||
/// Middle click paste support
|
||||
#[read = copy]
|
||||
middle_click_paste: bool = true,
|
||||
}
|
||||
struct SmartTabConfig {
|
||||
/// If set to true, then when the cursor is in a position with
|
||||
/// non-whitespace to its left, instead of inserting a tab, it will run
|
||||
/// `move_parent_node_end`. If there is only whitespace to the left,
|
||||
/// then it inserts a tab as normal. With the default bindings, to
|
||||
/// explicitly insert a tab character, press Shift-tab.
|
||||
#[name = "smart-tab.enable"]
|
||||
#[read = copy]
|
||||
enable: bool = true,
|
||||
/// Normally, when a menu is on screen, such as when auto complete
|
||||
/// is triggered, the tab key is bound to cycling through the items.
|
||||
/// This means when menus are on screen, one cannot use the tab key
|
||||
/// to trigger the `smart-tab` command. If this option is set to true,
|
||||
/// the `smart-tab` command always takes precedence, which means one
|
||||
/// cannot use the tab key to cycle through menu items. One of the other
|
||||
/// bindings must be used instead, such as arrow keys or `C-n`/`C-p`.
|
||||
#[name = "smart-tab.supersede-menu"]
|
||||
#[read = copy]
|
||||
supersede_menu: bool = false,
|
||||
}
|
||||
|
||||
struct SearchConfig {
|
||||
/// Enable smart case regex searching (case-insensitive unless pattern
|
||||
/// contains upper case characters)
|
||||
#[name = "search.smart-case"]
|
||||
#[read = copy]
|
||||
smart_case: bool = true,
|
||||
/// Whether the search should wrap after depleting the matches
|
||||
#[name = "search.wrap-round"]
|
||||
#[read = copy]
|
||||
wrap_round: bool = true,
|
||||
}
|
||||
|
||||
struct MiscConfig {
|
||||
/// Number of lines of padding around the edge of the screen when scrolling.
|
||||
#[read = copy]
|
||||
scrolloff: usize = 5,
|
||||
/// Shell to use when running external commands
|
||||
#[read = deref]
|
||||
shell: List<String> = if cfg!(windows) {
|
||||
&["cmd", "/C"]
|
||||
} else {
|
||||
&["sh", "-c"]
|
||||
},
|
||||
/// Enable automatic saving on the focus moving away from Helix.
|
||||
/// Requires [focus event support](https://github.com/helix-editor/
|
||||
/// helix/wiki/Terminal-Support) from your terminal
|
||||
#[read = copy]
|
||||
auto_save: bool = false,
|
||||
/// Whether to automatically insert a trailing line-ending on write
|
||||
/// if missing
|
||||
#[read = copy]
|
||||
insert_final_newline: bool = true,
|
||||
/// Time in milliseconds since last keypress before idle timers trigger.
|
||||
/// Used for autocompletion, set to 0 for instant
|
||||
#[read = copy]
|
||||
idle_timeout: Duration = Duration::from_millis(250),
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for Duration {
|
||||
fn from_value(val: Value) -> anyhow::Result<Self> {
|
||||
let val: usize = val.typed()?;
|
||||
Ok(Duration::from_millis(val as _))
|
||||
}
|
||||
fn to_value(&self) -> Value {
|
||||
Value::Int(self.as_millis().try_into().unwrap())
|
||||
}
|
||||
}
|
27
helix-config/src/definition/language.rs
Normal file
27
helix-config/src/definition/language.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use crate::*;
|
||||
|
||||
options! {
|
||||
struct LanguageConfig {
|
||||
/// regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site.
|
||||
#[validator = regex_str_validator()]
|
||||
injection_regex: Option<String> = None,
|
||||
/// The interpreters from the shebang line, for example `["sh", "bash"]`
|
||||
#[read = deref]
|
||||
shebangs: List<String> = List::default(),
|
||||
/// The token to use as a comment-token
|
||||
#[read = deref]
|
||||
comment_token: String = "//",
|
||||
/// The tree-sitter grammar to use (defaults to the language name)
|
||||
grammar: Option<String> = None,
|
||||
}
|
||||
|
||||
struct FormatterConfiguration {
|
||||
#[read = copy]
|
||||
auto_format: bool = true,
|
||||
#[name = "formatter.command"]
|
||||
formatter_command: Option<String> = None,
|
||||
#[name = "formatter.args"]
|
||||
#[read = deref]
|
||||
formatter_args: List<String> = List::default(),
|
||||
}
|
||||
}
|
266
helix-config/src/definition/lsp.rs
Normal file
266
helix-config/src/definition/lsp.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Describes the severity level of a [`Diagnostic`].
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
|
||||
pub enum Severity {
|
||||
Hint,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Ty for Severity {
|
||||
fn from_value(val: Value) -> anyhow::Result<Self> {
|
||||
let val: String = val.typed()?;
|
||||
match &*val {
|
||||
"hint" => Ok(Severity::Hint),
|
||||
"info" => Ok(Severity::Info),
|
||||
"warning" => Ok(Severity::Warning),
|
||||
"error" => Ok(Severity::Error),
|
||||
_ => bail!("expected one of 'hint', 'info', 'warning' or 'error' (got {val:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
match self {
|
||||
Severity::Hint => "hint".into(),
|
||||
Severity::Info => "info".into(),
|
||||
Severity::Warning => "warning".into(),
|
||||
Severity::Error => "error".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to stdx
|
||||
/// Helper macro that automatically generates an array
|
||||
/// that contains all variants of an enum
|
||||
macro_rules! variant_list {
|
||||
(
|
||||
$(#[$outer:meta])*
|
||||
$vis: vis enum $name: ident {
|
||||
$($(#[$inner: meta])* $variant: ident $(= $_: literal)?),*$(,)?
|
||||
}
|
||||
) => {
|
||||
$(#[$outer])*
|
||||
$vis enum $name {
|
||||
$($(#[$inner])* $variant),*
|
||||
}
|
||||
impl $name {
|
||||
$vis const ALL: &[$name] = &[$(Self::$variant),*];
|
||||
}
|
||||
}
|
||||
}
|
||||
variant_list! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum LanguageServerFeature {
|
||||
Format,
|
||||
GotoDeclaration,
|
||||
GotoDefinition,
|
||||
GotoTypeDefinition,
|
||||
GotoReference,
|
||||
GotoImplementation,
|
||||
// Goto, use bitflags, combining previous Goto members?
|
||||
SignatureHelp,
|
||||
Hover,
|
||||
DocumentHighlight,
|
||||
Completion,
|
||||
CodeAction,
|
||||
WorkspaceCommand,
|
||||
DocumentSymbols,
|
||||
WorkspaceSymbols,
|
||||
// Symbols, use bitflags, see above?
|
||||
Diagnostics,
|
||||
RenameSymbol,
|
||||
InlayHints,
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageServerFeature {
|
||||
fn to_str(self) -> &'static str {
|
||||
use LanguageServerFeature::*;
|
||||
|
||||
match self {
|
||||
Format => "format",
|
||||
GotoDeclaration => "goto-declaration",
|
||||
GotoDefinition => "goto-definition",
|
||||
GotoTypeDefinition => "goto-type-definition",
|
||||
GotoReference => "goto-reference",
|
||||
GotoImplementation => "goto-implementation",
|
||||
SignatureHelp => "signature-help",
|
||||
Hover => "hover",
|
||||
DocumentHighlight => "document-highlight",
|
||||
Completion => "completion",
|
||||
CodeAction => "code-action",
|
||||
WorkspaceCommand => "workspace-command",
|
||||
DocumentSymbols => "document-symbols",
|
||||
WorkspaceSymbols => "workspace-symbols",
|
||||
Diagnostics => "diagnostics",
|
||||
RenameSymbol => "rename-symbol",
|
||||
InlayHints => "inlay-hints",
|
||||
}
|
||||
}
|
||||
fn description(self) -> &'static str {
|
||||
use LanguageServerFeature::*;
|
||||
|
||||
match self {
|
||||
Format => "Use this language server for autoformatting.",
|
||||
GotoDeclaration => "Use this language server for the goto_declaration command.",
|
||||
GotoDefinition => "Use this language server for the goto_definition command.",
|
||||
GotoTypeDefinition => "Use this language server for the goto_type_definition command.",
|
||||
GotoReference => "Use this language server for the goto_reference command.",
|
||||
GotoImplementation => "Use this language server for the goto_implementation command.",
|
||||
SignatureHelp => "Use this language server to display signature help.",
|
||||
Hover => "Use this language server to display hover information.",
|
||||
DocumentHighlight => {
|
||||
"Use this language server for the select_references_to_symbol_under_cursor command."
|
||||
}
|
||||
Completion => "Request completion items from this language server.",
|
||||
CodeAction => "Use this language server for the code_action command.",
|
||||
WorkspaceCommand => "Use this language server for :lsp-workspace-command.",
|
||||
DocumentSymbols => "Use this language server for the symbol_picker command.",
|
||||
WorkspaceSymbols => "Use this language server for the workspace_symbol_picker command.",
|
||||
Diagnostics => "Display diagnostics emitted by this language server.",
|
||||
RenameSymbol => "Use this language server for the rename_symbol command.",
|
||||
InlayHints => "Display inlay hints form this language server.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LanguageServerFeature {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let feature = self.to_str();
|
||||
write!(f, "{feature}",)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for LanguageServerFeature {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{self}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for LanguageServerFeature {
|
||||
fn from_value(val: Value) -> anyhow::Result<Self> {
|
||||
let val: String = val.typed()?;
|
||||
use LanguageServerFeature::*;
|
||||
|
||||
match &*val {
|
||||
"format" => Ok(Format),
|
||||
"goto-declaration" => Ok(GotoDeclaration),
|
||||
"goto-definition" => Ok(GotoDefinition),
|
||||
"goto-type-definition" => Ok(GotoTypeDefinition),
|
||||
"goto-reference" => Ok(GotoReference),
|
||||
"goto-implementation" => Ok(GotoImplementation),
|
||||
"signature-help" => Ok(SignatureHelp),
|
||||
"hover" => Ok(Hover),
|
||||
"document-highlight" => Ok(DocumentHighlight),
|
||||
"completion" => Ok(Completion),
|
||||
"code-action" => Ok(CodeAction),
|
||||
"workspace-command" => Ok(WorkspaceCommand),
|
||||
"document-symbols" => Ok(DocumentSymbols),
|
||||
"workspace-symbols" => Ok(WorkspaceSymbols),
|
||||
"diagnostics" => Ok(Diagnostics),
|
||||
"rename-symbol" => Ok(RenameSymbol),
|
||||
"inlay-hints" => Ok(InlayHints),
|
||||
_ => bail!("invalid language server feature {val}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
Value::String(self.to_str().into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_language_server_config(registry: &mut OptionRegistry, languag_server: &str) {
|
||||
registry.register(
|
||||
&format!("language-servers.{languag_server}.active"),
|
||||
"Wether this language servers is used for a buffer",
|
||||
false,
|
||||
);
|
||||
for &feature in LanguageServerFeature::ALL {
|
||||
registry.register(
|
||||
&format!("language-servers.{languag_server}.{feature}"),
|
||||
feature.description(),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
options! {
|
||||
struct LspConfig {
|
||||
/// Enables LSP integration. Setting to false will completely disable language servers.
|
||||
#[name = "lsp.enable"]
|
||||
#[read = copy]
|
||||
enable: bool = true,
|
||||
/// Enables LSP integration. Setting to false will completely disable language servers.
|
||||
#[name = "lsp.display-messages"]
|
||||
#[read = copy]
|
||||
display_messages: bool = false,
|
||||
/// Enable automatic popup of signature help (parameter hints)
|
||||
#[name = "lsp.auto-signature-help"]
|
||||
#[read = copy]
|
||||
auto_signature_help: bool = true,
|
||||
/// Enable automatic popup of signature help (parameter hints)
|
||||
#[name = "lsp.display-inlay-hints"]
|
||||
#[read = copy]
|
||||
display_inlay_hints: bool = false,
|
||||
/// Display docs under signature help popup
|
||||
#[name = "lsp.display-signature-help-docs"]
|
||||
#[read = copy]
|
||||
display_signature_help_docs: bool = true,
|
||||
/// Enables snippet completions. Requires a server restart
|
||||
/// (`:lsp-restart`) to take effect after `:config-reload`/`:set`.
|
||||
#[name = "lsp.snippets"]
|
||||
#[read = copy]
|
||||
snippets: bool = true,
|
||||
/// Include declaration in the goto references popup.
|
||||
#[name = "lsp.goto-reference-include-declaration"]
|
||||
#[read = copy]
|
||||
goto_reference_include_declaration: bool = true,
|
||||
// TODO(breaing): prefix all options below with `lsp.`
|
||||
/// The language-id for language servers, checkout the
|
||||
/// table at [TextDocumentItem](https://microsoft.github.io/
|
||||
/// language-server-protocol/specifications/lsp/3.17/specification/
|
||||
/// #textDocumentItem) for the right id
|
||||
#[name = "languague-id"]
|
||||
language_server_id: Option<String> = None,
|
||||
// TODO(breaking): rename to root-markers to differentiate from workspace-roots
|
||||
// TODO: also makes this setteble on the language server
|
||||
/// A set of marker files to look for when trying to find the workspace
|
||||
/// root. For example `Cargo.lock`, `yarn.lock`
|
||||
roots: List<String> = List::default(),
|
||||
// TODO: also makes this setteble on the language server
|
||||
/// Directories relative to the workspace root that are treated as LSP
|
||||
/// roots. The search for root markers (starting at the path of the
|
||||
/// file) will stop at these paths.
|
||||
#[name = "workspace-lsp-roots"]
|
||||
workspace_roots: List<String> = List::default(),
|
||||
/// An array of LSP diagnostic sources assumed unchanged when the
|
||||
/// language server resends the same set of diagnostics. Helix can track
|
||||
/// the position for these diagnostics internally instead. Useful for
|
||||
/// diagnostics that are recomputed on save.
|
||||
persistent_diagnostic_sources: List<String> = List::default(),
|
||||
/// Minimal severity of diagnostic for it to be displayed. (Allowed
|
||||
/// values: `error`, `warning`, `info`, `hint`)
|
||||
diagnostic_severity: Severity = Severity::Hint,
|
||||
}
|
||||
|
||||
struct CompletionConfig {
|
||||
/// Automatic auto-completion, automatically pop up without user trigger.
|
||||
#[read = copy]
|
||||
auto_completion: bool = true,
|
||||
/// Whether to apply completion item instantly when selected
|
||||
#[read = copy]
|
||||
preview_completion_insert: bool = true,
|
||||
/// Whether to apply completion item instantly when selected
|
||||
#[read = copy]
|
||||
completion_replace: bool = false,
|
||||
/// Whether to apply completion item instantly when selected
|
||||
#[read = copy]
|
||||
completion_trigger_len: u8 = 2,
|
||||
}
|
||||
}
|
291
helix-config/src/definition/ui.rs
Normal file
291
helix-config/src/definition/ui.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum StatusLineElement {
|
||||
/// The editor mode (Normal, Insert, Visual/Selection)
|
||||
Mode,
|
||||
/// The LSP activity spinner
|
||||
Spinner,
|
||||
/// The file basename (the leaf of the open file's path)
|
||||
FileBaseName,
|
||||
/// The relative file path
|
||||
FileName,
|
||||
// The file modification indicator
|
||||
FileModificationIndicator,
|
||||
/// An indicator that shows `"[readonly]"` when a file cannot be written
|
||||
ReadOnlyIndicator,
|
||||
/// The file encoding
|
||||
FileEncoding,
|
||||
/// The file line endings (CRLF or LF)
|
||||
FileLineEnding,
|
||||
/// The file type (language ID or "text")
|
||||
FileType,
|
||||
/// A summary of the number of errors and warnings
|
||||
Diagnostics,
|
||||
/// A summary of the number of errors and warnings on file and workspace
|
||||
WorkspaceDiagnostics,
|
||||
/// The number of selections (cursors)
|
||||
Selections,
|
||||
/// The number of characters currently in primary selection
|
||||
PrimarySelectionLength,
|
||||
/// The cursor position
|
||||
Position,
|
||||
/// The separator string
|
||||
Separator,
|
||||
/// The cursor position as a percent of the total file
|
||||
PositionPercentage,
|
||||
/// The total line numbers of the current file
|
||||
TotalLineNumbers,
|
||||
/// A single space
|
||||
Spacer,
|
||||
/// Current version control information
|
||||
VersionControl,
|
||||
/// Indicator for selected register
|
||||
Register,
|
||||
}
|
||||
|
||||
config_serde_adapter!(StatusLineElement);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
/// UNSTABLE
|
||||
pub enum CursorKind {
|
||||
/// █
|
||||
Block,
|
||||
/// |
|
||||
Bar,
|
||||
/// _
|
||||
Underline,
|
||||
/// Hidden cursor, can set cursor position with this to let IME have correct cursor position.
|
||||
Hidden,
|
||||
}
|
||||
|
||||
impl Default for CursorKind {
|
||||
fn default() -> Self {
|
||||
Self::Block
|
||||
}
|
||||
}
|
||||
|
||||
config_serde_adapter!(CursorKind);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum WhitespaceRenderValue {
|
||||
None,
|
||||
// TODO
|
||||
// Selection,
|
||||
All,
|
||||
}
|
||||
|
||||
config_serde_adapter!(WhitespaceRenderValue);
|
||||
|
||||
/// bufferline render modes
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BufferLine {
|
||||
/// Don't render bufferline
|
||||
Never,
|
||||
/// Always render
|
||||
Always,
|
||||
/// Only if multiple buffers are open
|
||||
Multiple,
|
||||
}
|
||||
|
||||
config_serde_adapter!(BufferLine);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PopupBorderConfig {
|
||||
None,
|
||||
All,
|
||||
Popup,
|
||||
Menu,
|
||||
}
|
||||
|
||||
config_serde_adapter!(PopupBorderConfig);
|
||||
|
||||
options! {
|
||||
struct UiConfig {
|
||||
/// Whether to display info boxes
|
||||
#[read = copy]
|
||||
auto_info: bool = true,
|
||||
/// 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)
|
||||
#[read = copy]
|
||||
bufferline: BufferLine = BufferLine::Never,
|
||||
/// Highlight all lines with a cursor
|
||||
#[read = copy]
|
||||
cursorline: bool = false,
|
||||
/// Highlight all columns with a cursor
|
||||
#[read = copy]
|
||||
cursorcolumn: bool = false,
|
||||
/// List of column positions at which to display the rulers.
|
||||
#[read = deref]
|
||||
rulers: List<u16> = List::default(),
|
||||
/// Whether to color the mode indicator with different colors depending on the mode itself
|
||||
#[read = copy]
|
||||
popup_border: bool = false,
|
||||
/// Whether to color the mode indicator with different colors depending on the mode itself
|
||||
#[read = copy]
|
||||
color_modes: bool = false,
|
||||
}
|
||||
|
||||
struct WhiteSpaceRenderConfig {
|
||||
#[name = "whitespace.characters.space"]
|
||||
#[read = copy]
|
||||
space_char: char = '·', // U+00B7
|
||||
#[name = "whitespace.characters.nbsp"]
|
||||
#[read = copy]
|
||||
nbsp_char: char = '⍽', // U+237D
|
||||
#[name = "whitespace.characters.tab"]
|
||||
#[read = copy]
|
||||
tab_char: char = '→', // U+2192
|
||||
#[name = "whitespace.characters.tabpad"]
|
||||
#[read = copy]
|
||||
tabpad_char: char = '⏎', // U+23CE
|
||||
#[name = "whitespace.characters.newline"]
|
||||
#[read = copy]
|
||||
newline_char: char = ' ',
|
||||
#[name = "whitespace.render.default"]
|
||||
#[read = copy]
|
||||
render: WhitespaceRenderValue = WhitespaceRenderValue::None,
|
||||
#[name = "whitespace.render.space"]
|
||||
#[read = copy]
|
||||
render_space: Option<WhitespaceRenderValue> = None,
|
||||
#[name = "whitespace.render.nbsp"]
|
||||
#[read = copy]
|
||||
render_nbsp: Option<WhitespaceRenderValue> = None,
|
||||
#[name = "whitespace.render.tab"]
|
||||
#[read = copy]
|
||||
render_tab: Option<WhitespaceRenderValue> = None,
|
||||
#[name = "whitespace.render.newline"]
|
||||
#[read = copy]
|
||||
render_newline: Option<WhitespaceRenderValue> = None,
|
||||
}
|
||||
|
||||
struct TerminfoConfig {
|
||||
/// Set to `true` to override automatic detection of terminal truecolor
|
||||
/// support in the event of a false negative
|
||||
#[name = "true-color"]
|
||||
#[read = copy]
|
||||
force_true_color: bool = false,
|
||||
/// Set to `true` to override automatic detection of terminal undercurl
|
||||
/// support in the event of a false negative
|
||||
#[name = "undercurl"]
|
||||
#[read = copy]
|
||||
force_undercurl: bool = false,
|
||||
}
|
||||
|
||||
struct IndentGuidesConfig {
|
||||
/// Whether to render indent guides
|
||||
#[read = copy]
|
||||
render: bool = false,
|
||||
/// Character to use for rendering indent guides
|
||||
#[read = copy]
|
||||
character: char = '│',
|
||||
/// Number of indent levels to skip
|
||||
#[read = copy]
|
||||
skip_levels: u8 = 0,
|
||||
}
|
||||
|
||||
struct CursorShapeConfig {
|
||||
/// Cursor shape in normal mode
|
||||
#[name = "cursor-shape.normal"]
|
||||
#[read = copy]
|
||||
normal_mode_cursor: CursorKind = CursorKind::Block,
|
||||
/// Cursor shape in select mode
|
||||
#[name = "cursor-shape.select"]
|
||||
#[read = copy]
|
||||
select_mode_cursor: CursorKind = CursorKind::Block,
|
||||
/// Cursor shape in insert mode
|
||||
#[name = "cursor-shape.insert"]
|
||||
#[read = copy]
|
||||
insert_mode_cursor: CursorKind = CursorKind::Block,
|
||||
}
|
||||
|
||||
struct FilePickerConfig {
|
||||
/// Whether to exclude hidden files from any file pickers.
|
||||
#[name = "file-picker.hidden"]
|
||||
#[read = copy]
|
||||
hidden: bool = true,
|
||||
/// Follow symlinks instead of ignoring them
|
||||
#[name = "file-picker.follow-symlinks"]
|
||||
#[read = copy]
|
||||
follow_symlinks: bool = true,
|
||||
/// Ignore symlinks that point at files already shown in the picker
|
||||
#[name = "file-picker.deduplicate-links"]
|
||||
#[read = copy]
|
||||
deduplicate_links: bool = true,
|
||||
/// Enables reading ignore files from parent directories.
|
||||
#[name = "file-picker.parents"]
|
||||
#[read = copy]
|
||||
parents: bool = true,
|
||||
/// Enables reading `.ignore` files.
|
||||
#[name = "file-picker.ignore"]
|
||||
#[read = copy]
|
||||
ignore: bool = true,
|
||||
/// Enables reading `.gitignore` files.
|
||||
#[name = "file-picker.git-ignore"]
|
||||
#[read = copy]
|
||||
git_ignore: bool = true,
|
||||
/// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
|
||||
#[name = "file-picker.git-global"]
|
||||
#[read = copy]
|
||||
git_global: bool = true,
|
||||
/// Enables reading `.git/info/exclude` files.
|
||||
#[name = "file-picker.git-exclude"]
|
||||
#[read = copy]
|
||||
git_exclude: bool = true,
|
||||
/// Maximum Depth to recurse directories in file picker and global search.
|
||||
#[name = "file-picker.max-depth"]
|
||||
#[read = copy]
|
||||
max_depth: Option<usize> = None,
|
||||
}
|
||||
|
||||
struct StatusLineConfig{
|
||||
/// A list of elements aligned to the left of the statusline
|
||||
#[name = "statusline.left"]
|
||||
#[read = deref]
|
||||
left: List<StatusLineElement> = &[
|
||||
StatusLineElement::Mode,
|
||||
StatusLineElement::Spinner,
|
||||
StatusLineElement::FileName,
|
||||
StatusLineElement::ReadOnlyIndicator,
|
||||
StatusLineElement::FileModificationIndicator,
|
||||
],
|
||||
/// A list of elements aligned to the middle of the statusline
|
||||
#[name = "statusline.center"]
|
||||
#[read = deref]
|
||||
center: List<StatusLineElement> = List::default(),
|
||||
/// A list of elements aligned to the right of the statusline
|
||||
#[name = "statusline.right"]
|
||||
#[read = deref]
|
||||
right: List<StatusLineElement> = &[
|
||||
StatusLineElement::Diagnostics,
|
||||
StatusLineElement::Selections,
|
||||
StatusLineElement::Register,
|
||||
StatusLineElement::Position,
|
||||
StatusLineElement::FileEncoding,
|
||||
],
|
||||
/// The character used to separate elements in the statusline
|
||||
#[name = "statusline.seperator"]
|
||||
#[read = deref]
|
||||
seperator: String = "│",
|
||||
/// The text shown in the `mode` element for normal mode
|
||||
#[name = "statusline.mode.normal"]
|
||||
#[read = deref]
|
||||
mode_indicator_normal: String = "NOR",
|
||||
/// The text shown in the `mode` element for insert mode
|
||||
#[name = "statusline.mode.insert"]
|
||||
#[read = deref]
|
||||
mode_indicator_insert: String = "INS",
|
||||
/// The text shown in the `mode` element for select mode
|
||||
#[name = "statusline.mode.select"]
|
||||
#[read = deref]
|
||||
mode_indicator_select: String = "SEL",
|
||||
}
|
||||
}
|
10
helix-config/src/env.rs
Normal file
10
helix-config/src/env.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// TOOD: move to stdx
|
||||
|
||||
pub fn binary_exists(binary_name: &str) -> bool {
|
||||
which::which(binary_name).is_ok()
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn env_var_is_set(env_var_name: &str) -> bool {
|
||||
std::env::var_os(env_var_name).is_some()
|
||||
}
|
246
helix-config/src/lib.rs
Normal file
246
helix-config/src/lib.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use hashbrown::hash_map::Entry;
|
||||
use hashbrown::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
|
||||
|
||||
use any::ConfigData;
|
||||
use convert::ty_into_value;
|
||||
pub use convert::IntoTy;
|
||||
pub use definition::{init_config, init_language_server_config};
|
||||
pub use toml::read_toml_config;
|
||||
use validator::StaticValidator;
|
||||
pub use validator::{regex_str_validator, ty_validator, IntegerRangeValidator, Ty, Validator};
|
||||
pub use value::{from_value, to_value, Value};
|
||||
|
||||
mod any;
|
||||
mod convert;
|
||||
mod definition;
|
||||
pub mod env;
|
||||
mod macros;
|
||||
mod toml;
|
||||
mod validator;
|
||||
mod value;
|
||||
|
||||
pub type Guard<'a, T> = MappedRwLockReadGuard<'a, T>;
|
||||
pub type Map<T> = IndexMap<Box<str>, T, ahash::RandomState>;
|
||||
pub type String = Box<str>;
|
||||
pub type List<T> = Box<[T]>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OptionInfo {
|
||||
pub name: Arc<str>,
|
||||
pub description: Box<str>,
|
||||
pub validator: Box<dyn Validator>,
|
||||
pub into_value: fn(&ConfigData) -> Value,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OptionManager {
|
||||
vals: RwLock<HashMap<Arc<str>, ConfigData>>,
|
||||
parent: Option<Arc<OptionManager>>,
|
||||
}
|
||||
|
||||
impl OptionManager {
|
||||
pub fn get<T: Any>(&self, option: &str) -> Guard<'_, T> {
|
||||
Guard::map(self.get_data(option), ConfigData::get)
|
||||
}
|
||||
|
||||
pub fn get_data(&self, option: &str) -> Guard<'_, ConfigData> {
|
||||
let mut current_scope = self;
|
||||
loop {
|
||||
let lock = current_scope.vals.read();
|
||||
if let Ok(res) = RwLockReadGuard::try_map(lock, |options| options.get(option)) {
|
||||
return res;
|
||||
}
|
||||
let Some(new_scope) = current_scope.parent.as_deref() else{
|
||||
unreachable!("option must be atleast defined in the global scope")
|
||||
};
|
||||
current_scope = new_scope;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_deref<T: Deref + Any>(&self, option: &str) -> Guard<'_, T::Target> {
|
||||
Guard::map(self.get::<T>(option), T::deref)
|
||||
}
|
||||
|
||||
pub fn get_folded<T: Any, R>(
|
||||
&self,
|
||||
option: &str,
|
||||
init: R,
|
||||
mut fold: impl FnMut(&T, R) -> R,
|
||||
) -> R {
|
||||
let mut res = init;
|
||||
let mut current_scope = self;
|
||||
loop {
|
||||
let options = current_scope.vals.read();
|
||||
if let Some(option) = options.get(option).map(|val| val.get()) {
|
||||
res = fold(option, res);
|
||||
}
|
||||
let Some(new_scope) = current_scope.parent.as_deref() else{
|
||||
break
|
||||
};
|
||||
current_scope = new_scope;
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub fn get_value(
|
||||
&self,
|
||||
option: impl Into<Arc<str>>,
|
||||
registry: &OptionRegistry,
|
||||
) -> anyhow::Result<Value> {
|
||||
let option: Arc<str> = option.into();
|
||||
let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") };
|
||||
let data = self.get_data(&option);
|
||||
let val = (opt.into_value)(&data);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
pub fn create_scope(self: &Arc<OptionManager>) -> OptionManager {
|
||||
OptionManager {
|
||||
vals: RwLock::default(),
|
||||
parent: Some(self.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_parent_scope(&mut self, parent: Arc<OptionManager>) {
|
||||
self.parent = Some(parent)
|
||||
}
|
||||
|
||||
pub fn set_unchecked(&self, option: Arc<str>, val: ConfigData) {
|
||||
self.vals.write().insert(option, val);
|
||||
}
|
||||
|
||||
pub fn append(
|
||||
&self,
|
||||
option: impl Into<Arc<str>>,
|
||||
val: impl Into<Value>,
|
||||
registry: &OptionRegistry,
|
||||
max_depth: usize,
|
||||
) -> anyhow::Result<()> {
|
||||
let val = val.into();
|
||||
let option: Arc<str> = option.into();
|
||||
let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") };
|
||||
let old_data = self.get_data(&option);
|
||||
let mut old = (opt.into_value)(&old_data);
|
||||
old.append(val, max_depth);
|
||||
let val = opt.validator.validate(old)?;
|
||||
self.set_unchecked(option, val);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the value of a config option. Returns an error if this config
|
||||
/// option doesn't exist or the provided value is not valid.
|
||||
pub fn set(
|
||||
&self,
|
||||
option: impl Into<Arc<str>>,
|
||||
val: impl Into<Value>,
|
||||
registry: &OptionRegistry,
|
||||
) -> anyhow::Result<()> {
|
||||
let option: Arc<str> = option.into();
|
||||
let val = val.into();
|
||||
let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") };
|
||||
let val = opt.validator.validate(val)?;
|
||||
self.set_unchecked(option, val);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// unsets an options so that its value will be read from
|
||||
/// the parent scope instead
|
||||
pub fn unset(&self, option: &str) {
|
||||
self.vals.write().remove(option);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OptionRegistry {
|
||||
options: HashMap<Arc<str>, OptionInfo>,
|
||||
defaults: Arc<OptionManager>,
|
||||
}
|
||||
|
||||
impl OptionRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
options: HashMap::with_capacity(1024),
|
||||
defaults: Arc::new(OptionManager {
|
||||
vals: RwLock::new(HashMap::with_capacity(1024)),
|
||||
parent: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<T: IntoTy>(&mut self, name: &str, description: &str, default: T) {
|
||||
self.register_with_validator(
|
||||
name,
|
||||
description,
|
||||
default,
|
||||
StaticValidator::<T::Ty> { ty: PhantomData },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn register_with_validator<T: IntoTy>(
|
||||
&mut self,
|
||||
name: &str,
|
||||
description: &str,
|
||||
default: T,
|
||||
validator: impl Validator,
|
||||
) {
|
||||
let mut name: Arc<str> = name.into();
|
||||
// convert from snake case to kebab case in place without an additional
|
||||
// allocation this is save since we only replace ascii with ascii in
|
||||
// place std really ougth to have a function for this :/
|
||||
// TODO: move to stdx as extension trait
|
||||
for byte in unsafe { Arc::get_mut(&mut name).unwrap().as_bytes_mut() } {
|
||||
if *byte == b'-' {
|
||||
*byte = b'_';
|
||||
}
|
||||
}
|
||||
let default = default.into_ty();
|
||||
match self.options.entry(name.clone()) {
|
||||
Entry::Vacant(e) => {
|
||||
// make sure the validator is correct
|
||||
if cfg!(debug_assertions) {
|
||||
validator.validate(T::Ty::to_value(&default)).unwrap();
|
||||
}
|
||||
let opt = OptionInfo {
|
||||
name: name.clone(),
|
||||
description: description.into(),
|
||||
validator: Box::new(validator),
|
||||
into_value: ty_into_value::<T::Ty>,
|
||||
};
|
||||
e.insert(opt);
|
||||
}
|
||||
Entry::Occupied(ent) => {
|
||||
ent.get()
|
||||
.validator
|
||||
.validate(T::Ty::to_value(&default))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
self.defaults.set_unchecked(name, ConfigData::new(default));
|
||||
}
|
||||
|
||||
pub fn global_scope(&self) -> Arc<OptionManager> {
|
||||
self.defaults.clone()
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&OptionInfo> {
|
||||
self.options.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OptionRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
130
helix-config/src/macros.rs
Normal file
130
helix-config/src/macros.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
/// This macro allows specifiying a trait of related config
|
||||
/// options with a struct like syntax. From that information
|
||||
/// two things are generated:
|
||||
///
|
||||
/// * A `init_config` function that registers the config options with the
|
||||
/// `OptionRegistry` registry.
|
||||
/// * A **trait** definition with an accessor for every config option that is
|
||||
/// implemented for `OptionManager`.
|
||||
///
|
||||
/// The accessors on the trait allow convenient statically typed access to
|
||||
/// config fields. The accessors return `Guard<T>` (which allows derferecning to
|
||||
/// &T). Any type that implements copy can be returned as a copy instead by
|
||||
/// specifying `#[read = copy]`. Collections like `List<T>` and `String` are not
|
||||
/// copy However, they usually implement deref (to &[T] and &str respectively).
|
||||
/// Working with the dereferneced &str/&[T] is more convenient then &String and &List<T>. The
|
||||
/// accessor will return these if `#[read = deref]` is specified.
|
||||
///
|
||||
/// The doc comments will be retained for the accessors and also stored in the
|
||||
/// option registrry for dispaly in the UI and documentation.
|
||||
///
|
||||
/// The name of a config option can be changed with #[name = "<name>"],
|
||||
/// otherwise the name of the field is used directly. The OptionRegistry
|
||||
/// automatically converts all names to kebab-case so a name attribute is only
|
||||
/// required if the name is supposed to be significantly altered.
|
||||
///
|
||||
/// In some cases more complex validation may be necssary. In that case the
|
||||
/// valiidtator can be provided with an exprission that implements the `Validator`
|
||||
/// trait: `#[validator = create_validator()]`.
|
||||
#[macro_export]
|
||||
macro_rules! options {
|
||||
(
|
||||
$(use $use: ident::*;)*
|
||||
$($(#[$($meta: tt)*])* struct $ident: ident {
|
||||
$(
|
||||
$(#[doc = $option_desc: literal])*
|
||||
$(#[name = $option_name: literal])?
|
||||
$(#[validator = $option_validator: expr])?
|
||||
$(#[read = $($extra: tt)*])?
|
||||
$option: ident: $ty: ty = $default: expr
|
||||
),+$(,)?
|
||||
})+
|
||||
) => {
|
||||
$(pub use $use::*;)*
|
||||
$($(#[$($meta)*])* pub trait $ident {
|
||||
$(
|
||||
$(#[doc = $option_desc])*
|
||||
fn $option(&self) -> $crate::options!(@ret_ty $($($extra)*)? $ty);
|
||||
)+
|
||||
})+
|
||||
pub fn init_config(registry: &mut $crate::OptionRegistry) {
|
||||
$($use::init_config(registry);)*
|
||||
$($(
|
||||
let name = $crate::options!(@name $option $($option_name)?);
|
||||
let docs = concat!("" $(,$option_desc,)" "*);
|
||||
$crate::options!(@register registry name docs $default, $ty $(,$option_validator)?);
|
||||
)+)+
|
||||
}
|
||||
$(impl $ident for $crate::OptionManager {
|
||||
$(
|
||||
$(#[doc = $option_desc])*
|
||||
fn $option(&self) -> $crate::options!(@ret_ty $($($extra)*)? $ty) {
|
||||
let name = $crate::options!(@name $option $($option_name)?);
|
||||
$crate::options!(@get $($($extra)*)? self, $ty, name)
|
||||
}
|
||||
)+
|
||||
})+
|
||||
};
|
||||
(@register $registry: ident $name: ident $desc: ident $default: expr, $ty:ty) => {{
|
||||
use $crate::IntoTy;
|
||||
let val: $ty = $default.into_ty();
|
||||
$registry.register($name, $desc, val);
|
||||
}};
|
||||
(@register $registry: ident $name: ident $desc: ident $default: expr, $ty:ty, $validator: expr) => {{
|
||||
use $crate::IntoTy;
|
||||
let val: $ty = $default.into_ty();
|
||||
$registry.register_with_validator($name, $desc, val, $validator);
|
||||
}};
|
||||
(@name $ident: ident) => {
|
||||
::std::stringify!($ident)
|
||||
};
|
||||
(@name $ident: ident $name: literal) => {
|
||||
$name
|
||||
};
|
||||
(@ret_ty copy $ty: ty) => {
|
||||
$ty
|
||||
};
|
||||
(@ret_ty map($fn: expr, $ret_ty: ty) $ty: ty) => {
|
||||
$ret_ty
|
||||
};
|
||||
(@ret_ty fold($init: expr, $fn: expr, $ret_ty: ty) $ty: ty) => {
|
||||
$ret_ty
|
||||
};
|
||||
(@ret_ty deref $ty: ty) => {
|
||||
$crate::Guard<'_, <$ty as ::std::ops::Deref>::Target>
|
||||
};
|
||||
(@ret_ty $ty: ty) => {
|
||||
$crate::Guard<'_, $ty>
|
||||
};
|
||||
(@get map($fn: expr, $ret_ty: ty) $config: ident, $ty: ty, $name: ident) => {
|
||||
let val = $config.get::<$ty>($name);
|
||||
$fn(val)
|
||||
};
|
||||
(@get fold($init: expr, $fn: expr, $ret_ty: ty) $config: ident, $ty: ty, $name: ident) => {
|
||||
$config.get_folded::<$ty, $ret_ty>($name, $init, $fn)
|
||||
};
|
||||
(@get copy $config: ident, $ty: ty, $name: ident) => {
|
||||
*$config.get::<$ty>($name)
|
||||
};
|
||||
(@get deref $config: ident, $ty: ty, $name: ident) => {
|
||||
$config.get_deref::<$ty>($name)
|
||||
};
|
||||
(@get $config: ident, $ty: ty, $name: ident) => {
|
||||
$config.get::<$ty>($name)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! config_serde_adapter {
|
||||
($ty: ident) => {
|
||||
impl $crate::Ty for $ty {
|
||||
fn to_value(&self) -> $crate::Value {
|
||||
$crate::to_value(self).unwrap()
|
||||
}
|
||||
fn from_value(val: $crate::Value) -> ::anyhow::Result<Self> {
|
||||
let val = $crate::from_value(val)?;
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
80
helix-config/src/tests.rs
Normal file
80
helix-config/src/tests.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config_serde_adapter;
|
||||
use crate::OptionRegistry;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LineNumber {
|
||||
/// Show absolute line number
|
||||
#[serde(alias = "abs")]
|
||||
Absolute,
|
||||
/// If focused and in normal/select mode, show relative line number to the primary cursor.
|
||||
/// If unfocused or in insert mode, show absolute line number.
|
||||
#[serde(alias = "rel")]
|
||||
Relative,
|
||||
}
|
||||
|
||||
config_serde_adapter!(LineNumber);
|
||||
|
||||
fn setup_registry() -> OptionRegistry {
|
||||
let mut registry = OptionRegistry::new();
|
||||
registry.register(
|
||||
"scrolloff",
|
||||
"Number of lines of padding around the edge of the screen when scrolling",
|
||||
5usize,
|
||||
);
|
||||
registry.register(
|
||||
"shell",
|
||||
"Shell to use when running external commands",
|
||||
&["sh", "-c"],
|
||||
);
|
||||
registry.register("mouse", "Enable mouse mode", true);
|
||||
registry.register(
|
||||
"line-number",
|
||||
"Line number display: `absolute` simply shows each line's number, while \
|
||||
`relative` shows the distance from the current line. When unfocused or in \
|
||||
insert mode, `relative` will still show absolute line numbers",
|
||||
LineNumber::Absolute,
|
||||
);
|
||||
registry
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_values() {
|
||||
let registry = setup_registry();
|
||||
let global_scope = registry.global_scope();
|
||||
let scrolloff: usize = *global_scope.get("scrolloff");
|
||||
let shell_ = global_scope.get_deref::<Box<[_]>>("shell");
|
||||
let shell: &[Box<str>] = &shell_;
|
||||
let mouse: bool = *global_scope.get("mouse");
|
||||
let line_number: LineNumber = *global_scope.get("line-number");
|
||||
assert_eq!(scrolloff, 5);
|
||||
assert!(shell.iter().map(Box::deref).eq(["sh", "-c"]));
|
||||
assert!(mouse);
|
||||
assert_eq!(line_number, LineNumber::Absolute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scope_overwrite() {
|
||||
let registry = setup_registry();
|
||||
let global_scope = registry.global_scope();
|
||||
let scope_1 = Arc::new(global_scope.create_scope());
|
||||
let scope_2 = Arc::new(global_scope.create_scope());
|
||||
let mut scope_3 = scope_1.create_scope();
|
||||
scope_1.set("line-number", "rel", ®istry).unwrap();
|
||||
let line_number: LineNumber = *scope_3.get("line-number");
|
||||
assert_eq!(line_number, LineNumber::Relative);
|
||||
scope_3.set_parent_scope(scope_2.clone());
|
||||
let line_number: LineNumber = *scope_3.get("line-number");
|
||||
assert_eq!(line_number, LineNumber::Absolute);
|
||||
scope_2.set("line-number", "rel", ®istry).unwrap();
|
||||
let line_number: LineNumber = *scope_3.get("line-number");
|
||||
assert_eq!(line_number, LineNumber::Relative);
|
||||
scope_2.set("line-number", "abs", ®istry).unwrap();
|
||||
let line_number: LineNumber = *scope_3.get("line-number");
|
||||
assert_eq!(line_number, LineNumber::Absolute);
|
||||
}
|
69
helix-config/src/toml.rs
Normal file
69
helix-config/src/toml.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::{Map, OptionManager, OptionRegistry, Value};
|
||||
|
||||
/// Inserts the config declaration from a map deserialized from toml into
|
||||
/// options manager. Returns an error if any of theu config options are
|
||||
/// invalid The convresion may not be exactly one-to-one to retain backwards
|
||||
/// compatibility
|
||||
pub fn read_toml_config(
|
||||
config_entries: Map<Value>,
|
||||
options: &OptionManager,
|
||||
registry: &OptionRegistry,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut buf = String::new();
|
||||
for (key, val) in config_entries {
|
||||
if matches!(val, Value::Map(_)) {
|
||||
buf.push_str(&key);
|
||||
visit(&mut buf, val, options, registry)?;
|
||||
buf.clear();
|
||||
} else {
|
||||
visit(&mut key.to_string(), val, options, registry)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit(
|
||||
path: &mut String,
|
||||
val: Value,
|
||||
options: &OptionManager,
|
||||
registry: &OptionRegistry,
|
||||
) -> anyhow::Result<()> {
|
||||
match &**path {
|
||||
// don't descend
|
||||
"auto-format" => {
|
||||
// treat as unset
|
||||
if Value::Bool(true) == val {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
"auto-pairs" => return options.set("auto-pairs", val, registry),
|
||||
"enviorment" => return options.set("enviorment", val, registry),
|
||||
"config" => return options.set("config", val, registry),
|
||||
"gutters" if matches!(val, Value::List(_)) => {
|
||||
return options.set("gutters.layout", val, registry);
|
||||
}
|
||||
"gutters" if matches!(val, Value::List(_)) => {
|
||||
return options.set("gutters.layout", val, registry);
|
||||
}
|
||||
"whitespace.render" if matches!(val, Value::String(_)) => {
|
||||
return options.set("whitespace.render.default", val, registry);
|
||||
}
|
||||
"language-servers" => {
|
||||
// merge list/map of language servers but if "only" and "except" are specified overwrite
|
||||
return options.append("language-servers", val, registry, 0);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
if let Value::Map(val) = val {
|
||||
let old_path_len = path.len();
|
||||
for (key, val) in val.into_iter() {
|
||||
path.push('.');
|
||||
path.push_str(&key);
|
||||
visit(path, val, options, registry)?;
|
||||
path.truncate(old_path_len);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
options.set(&**path, val, registry)
|
||||
}
|
||||
}
|
296
helix-config/src/validator.rs
Normal file
296
helix-config/src/validator.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use std::any::{type_name, Any};
|
||||
use std::error::Error;
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
|
||||
use crate::any::ConfigData;
|
||||
use crate::Value;
|
||||
|
||||
pub trait Validator: 'static + Debug {
|
||||
fn validate(&self, val: Value) -> Result<ConfigData>;
|
||||
}
|
||||
|
||||
pub trait Ty: Sized + Clone + 'static {
|
||||
fn from_value(val: Value) -> Result<Self>;
|
||||
fn to_value(&self) -> Value;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct IntegerRangeValidator<T> {
|
||||
pub min: isize,
|
||||
pub max: isize,
|
||||
ty: PhantomData<T>,
|
||||
}
|
||||
impl<E, T> IntegerRangeValidator<T>
|
||||
where
|
||||
E: Debug,
|
||||
T: TryInto<isize, Error = E>,
|
||||
{
|
||||
pub fn new(min: T, max: T) -> Self {
|
||||
Self {
|
||||
min: min.try_into().unwrap(),
|
||||
max: max.try_into().unwrap(),
|
||||
ty: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Debug for IntegerRangeValidator<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("IntegerRangeValidator")
|
||||
.field("min", &self.min)
|
||||
.field("max", &self.max)
|
||||
.field("ty", &type_name::<T>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, T> IntegerRangeValidator<T>
|
||||
where
|
||||
E: Error + Sync + Send + 'static,
|
||||
T: Any + TryFrom<isize, Error = E>,
|
||||
{
|
||||
pub fn validate(&self, val: Value) -> Result<T> {
|
||||
let IntegerRangeValidator { min, max, .. } = *self;
|
||||
let Value::Int(val) = val else {
|
||||
bail!("expected an integer")
|
||||
};
|
||||
ensure!(
|
||||
min <= val && val <= max,
|
||||
"expected an integer between {min} and {max} (got {val})",
|
||||
);
|
||||
Ok(T::try_from(val)?)
|
||||
}
|
||||
}
|
||||
impl<E, T> Validator for IntegerRangeValidator<T>
|
||||
where
|
||||
E: Error + Sync + Send + 'static,
|
||||
T: Any + TryFrom<isize, Error = E>,
|
||||
{
|
||||
fn validate(&self, val: Value) -> Result<ConfigData> {
|
||||
Ok(ConfigData::new(self.validate(val)))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! integer_tys {
|
||||
($($ty: ident),*) => {
|
||||
$(
|
||||
impl Ty for $ty {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::Int((*self).try_into().unwrap())
|
||||
}
|
||||
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
IntegerRangeValidator::new($ty::MIN, $ty::MAX).validate(val)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
integer_tys! {
|
||||
i8, i16, i32, isize,
|
||||
u8, u16, u32
|
||||
}
|
||||
|
||||
impl Ty for usize {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::Int((*self).try_into().unwrap())
|
||||
}
|
||||
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
IntegerRangeValidator::new(0usize, isize::MAX as usize).validate(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for u64 {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::Int((*self).try_into().unwrap())
|
||||
}
|
||||
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
IntegerRangeValidator::new(0u64, isize::MAX as u64).validate(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for bool {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::Bool(*self)
|
||||
}
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
let Value::Bool(val) = val else {
|
||||
bail!("expected a boolean")
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for Box<str> {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::String(self.clone().into_string())
|
||||
}
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
let Value::String(val) = val else {
|
||||
bail!("expected a string")
|
||||
};
|
||||
Ok(val.into_boxed_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for char {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::String(self.to_string())
|
||||
}
|
||||
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
let Value::String(val) = val else {
|
||||
bail!("expected a string")
|
||||
};
|
||||
ensure!(
|
||||
val.chars().count() == 1,
|
||||
"expecet a single character (got {val:?})"
|
||||
);
|
||||
Ok(val.chars().next().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for std::string::String {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::String(self.clone())
|
||||
}
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
let Value::String(val) = val else {
|
||||
bail!("expected a string")
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ty> Ty for Option<T> {
|
||||
fn to_value(&self) -> Value {
|
||||
match self {
|
||||
Some(_) => todo!(),
|
||||
None => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
if val == Value::Null {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(T::from_value(val)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ty> Ty for Box<T> {
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
Ok(Box::new(T::from_value(val)?))
|
||||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
T::to_value(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ty> Ty for indexmap::IndexMap<Box<str>, T, ahash::RandomState> {
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
let Value::Map(map) = val else {
|
||||
bail!("expected a map");
|
||||
};
|
||||
map.into_iter()
|
||||
.map(|(k, v)| Ok((k, T::from_value(v)?)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
let map = self
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.to_value()))
|
||||
.collect();
|
||||
Value::Map(Box::new(map))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ty> Ty for Box<[T]> {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::List(self.iter().map(T::to_value).collect())
|
||||
}
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
let Value::List(val) = val else {
|
||||
bail!("expected a list")
|
||||
};
|
||||
val.iter().cloned().map(T::from_value).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for serde_json::Value {
|
||||
fn from_value(val: Value) -> Result<Self> {
|
||||
Ok(val.into())
|
||||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct StaticValidator<T: Ty> {
|
||||
pub(super) ty: PhantomData<fn(&T)>,
|
||||
}
|
||||
|
||||
impl<T: Ty> Debug for StaticValidator<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("StaticValidator")
|
||||
.field("ty", &type_name::<T>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ty> Validator for StaticValidator<T> {
|
||||
fn validate(&self, val: Value) -> Result<ConfigData> {
|
||||
let val = <T as Ty>::from_value(val)?;
|
||||
Ok(ConfigData::new(val))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TyValidator<F, T: Ty> {
|
||||
pub(super) ty: PhantomData<fn(&T)>,
|
||||
f: F,
|
||||
}
|
||||
|
||||
impl<T: Ty, F> Debug for TyValidator<F, T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("TyValidator")
|
||||
.field("ty", &type_name::<T>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F> Validator for TyValidator<F, T>
|
||||
where
|
||||
T: Ty,
|
||||
F: Fn(&T) -> anyhow::Result<()> + 'static,
|
||||
{
|
||||
fn validate(&self, val: Value) -> Result<ConfigData> {
|
||||
let val = <T as Ty>::from_value(val)?;
|
||||
(self.f)(&val)?;
|
||||
Ok(ConfigData::new(val))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ty_validator<T, F>(f: F) -> impl Validator
|
||||
where
|
||||
T: Ty,
|
||||
F: Fn(&T) -> anyhow::Result<()> + 'static,
|
||||
{
|
||||
TyValidator { ty: PhantomData, f }
|
||||
}
|
||||
|
||||
pub fn regex_str_validator() -> impl Validator {
|
||||
ty_validator(|val: &crate::String| {
|
||||
regex_syntax::parse(val)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
448
helix-config/src/value.rs
Normal file
448
helix-config/src/value.rs
Normal file
@@ -0,0 +1,448 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::ser::{Error as _, Impossible};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Error, Result};
|
||||
|
||||
use crate::Ty;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Value {
|
||||
List(Vec<Value>),
|
||||
Map(Box<IndexMap<Box<str>, Value, ahash::RandomState>>),
|
||||
Int(isize),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
String(String),
|
||||
Null,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn typed<T: Ty>(self) -> anyhow::Result<T> {
|
||||
T::from_value(self)
|
||||
}
|
||||
|
||||
pub fn append(&mut self, val: Value, depth: usize) {
|
||||
match (self, val) {
|
||||
(Value::List(dst), Value::List(ref mut val)) => dst.append(val),
|
||||
(Value::Map(dst), Value::Map(val)) if depth == 0 || dst.is_empty() => {
|
||||
dst.extend(val.into_iter())
|
||||
}
|
||||
(Value::Map(dst), Value::Map(val)) => {
|
||||
dst.reserve(val.len());
|
||||
for (k, v) in val.into_iter() {
|
||||
// we don't use the entry api because we want
|
||||
// to maintain thhe ordering
|
||||
let merged = match dst.shift_remove(&k) {
|
||||
Some(mut old) => {
|
||||
old.append(v, depth - 1);
|
||||
old
|
||||
}
|
||||
None => v,
|
||||
};
|
||||
dst.insert(k, merged);
|
||||
}
|
||||
}
|
||||
(dst, val) => *dst = val,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(value: &str) -> Self {
|
||||
Value::String(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! from_int {
|
||||
($($ty: ident),*) => {
|
||||
$(
|
||||
impl From<$ty> for Value {
|
||||
fn from(value: $ty) -> Self {
|
||||
Value::Int(value.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl From<serde_json::Value> for Value {
|
||||
fn from(value: serde_json::Value) -> Self {
|
||||
to_value(value).unwrap()
|
||||
}
|
||||
}
|
||||
impl From<&serde_json::Value> for Value {
|
||||
fn from(value: &serde_json::Value) -> Self {
|
||||
to_value(value).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value> for serde_json::Value {
|
||||
fn from(value: Value) -> Self {
|
||||
serde_json::to_value(value).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
from_int!(isize, usize, u32, i32, i16, u16, i8, u8);
|
||||
|
||||
pub fn to_value<T>(value: T) -> Result<Value>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
value.serialize(Serializer)
|
||||
}
|
||||
|
||||
pub fn from_value<T>(value: Value) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
// roundtripping trough json is very inefficient *and incorrect* (captures
|
||||
// json semantics that don't apply to us)
|
||||
// TODO: implement a custom deserializer just like serde_json does
|
||||
serde_json::from_value(value.into())
|
||||
}
|
||||
|
||||
// We only use our own error type; no need for From conversions provided by the
|
||||
// standard library's try! macro. This reduces lines of LLVM IR by 4%.
|
||||
macro_rules! tri {
|
||||
($e:expr $(,)?) => {
|
||||
match $e {
|
||||
core::result::Result::Ok(val) => val,
|
||||
core::result::Result::Err(err) => return core::result::Result::Err(err),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Serializer whose output is a `Value`.
|
||||
///
|
||||
/// This is the serializer that backs [`serde_json::to_value`][crate::to_value].
|
||||
/// Unlike the main serde_json serializer which goes from some serializable
|
||||
/// value of type `T` to JSON text, this one goes from `T` to
|
||||
/// `serde_json::Value`.
|
||||
///
|
||||
/// The `to_value` function is implementable as:
|
||||
///
|
||||
/// ```
|
||||
/// use serde::Serialize;
|
||||
/// use serde_json::{Error, Value};
|
||||
///
|
||||
/// pub fn to_value<T>(input: T) -> Result<Value, Error>
|
||||
/// where
|
||||
/// T: Serialize,
|
||||
/// {
|
||||
/// input.serialize(serde_json::value::Serializer)
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Serializer;
|
||||
|
||||
impl serde::Serializer for Serializer {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
type SerializeSeq = SerializeVec;
|
||||
type SerializeTuple = SerializeVec;
|
||||
type SerializeTupleStruct = SerializeVec;
|
||||
type SerializeTupleVariant = Impossible<Value, Error>;
|
||||
type SerializeMap = SerializeMap;
|
||||
type SerializeStruct = SerializeMap;
|
||||
type SerializeStructVariant = Impossible<Value, Error>;
|
||||
|
||||
#[inline]
|
||||
fn serialize_bool(self, value: bool) -> Result<Value> {
|
||||
Ok(Value::Bool(value))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_i8(self, value: i8) -> Result<Value> {
|
||||
self.serialize_i64(value as i64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_i16(self, value: i16) -> Result<Value> {
|
||||
self.serialize_i64(value as i64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_i32(self, value: i32) -> Result<Value> {
|
||||
self.serialize_i64(value as i64)
|
||||
}
|
||||
|
||||
fn serialize_i64(self, value: i64) -> Result<Value> {
|
||||
Ok(Value::Int(value.try_into().unwrap()))
|
||||
}
|
||||
|
||||
fn serialize_i128(self, _value: i128) -> Result<Value> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_u8(self, value: u8) -> Result<Value> {
|
||||
self.serialize_u64(value as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_u16(self, value: u16) -> Result<Value> {
|
||||
self.serialize_u64(value as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_u32(self, value: u32) -> Result<Value> {
|
||||
self.serialize_u64(value as u64)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_u64(self, value: u64) -> Result<Value> {
|
||||
Ok(Value::Int(value.try_into().unwrap()))
|
||||
}
|
||||
|
||||
fn serialize_u128(self, _value: u128) -> Result<Value> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_f32(self, float: f32) -> Result<Value> {
|
||||
Ok(Value::Float(float as f64))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_f64(self, float: f64) -> Result<Value> {
|
||||
Ok(Value::Float(float))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_char(self, value: char) -> Result<Value> {
|
||||
let mut s = String::new();
|
||||
s.push(value);
|
||||
Ok(Value::String(s))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_str(self, value: &str) -> Result<Value> {
|
||||
Ok(Value::String(value.into()))
|
||||
}
|
||||
|
||||
fn serialize_bytes(self, value: &[u8]) -> Result<Value> {
|
||||
let vec = value.iter().map(|&b| Value::Int(b.into())).collect();
|
||||
Ok(Value::List(vec))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_unit(self) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_unit_struct(self, _name: &'static str) -> Result<Value> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
variant: &'static str,
|
||||
) -> Result<Value> {
|
||||
self.serialize_str(variant)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_newtype_struct<T>(self, _name: &'static str, _value: &T) -> Result<Value>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_newtype_variant<T>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_value: &T,
|
||||
) -> Result<Value>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_none(self) -> Result<Value> {
|
||||
self.serialize_unit()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_some<T>(self, value: &T) -> Result<Value>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
|
||||
Ok(SerializeVec {
|
||||
vec: Vec::with_capacity(len.unwrap_or(0)),
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
|
||||
self.serialize_seq(Some(len))
|
||||
}
|
||||
|
||||
fn serialize_tuple_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
len: usize,
|
||||
) -> Result<Self::SerializeTupleStruct> {
|
||||
self.serialize_seq(Some(len))
|
||||
}
|
||||
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeTupleVariant> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
|
||||
Ok(SerializeMap {
|
||||
map: IndexMap::default(),
|
||||
next_key: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStructVariant> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn collect_str<T>(self, value: &T) -> Result<Value>
|
||||
where
|
||||
T: ?Sized + Display,
|
||||
{
|
||||
Ok(Value::String(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SerializeVec {
|
||||
vec: Vec<Value>,
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeSeq for SerializeVec {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_element<T>(&mut self, value: &T) -> Result<()>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
self.vec.push(tri!(to_value(value)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
Ok(Value::List(self.vec))
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeTuple for SerializeVec {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_element<T>(&mut self, value: &T) -> Result<()>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
serde::ser::SerializeSeq::serialize_element(self, value)
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
serde::ser::SerializeSeq::end(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeTupleStruct for SerializeVec {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T>(&mut self, value: &T) -> Result<()>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
serde::ser::SerializeSeq::serialize_element(self, value)
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
serde::ser::SerializeSeq::end(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SerializeMap {
|
||||
map: IndexMap<Box<str>, Value, ahash::RandomState>,
|
||||
next_key: Option<Box<str>>,
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeMap for SerializeMap {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_key<T>(&mut self, key: &T) -> Result<()>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let key = to_value(key)?;
|
||||
let Value::String(val) = key else {
|
||||
return Err(Error::custom("only string keys are supported"));
|
||||
};
|
||||
self.next_key = Some(val.into_boxed_str());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_value<T>(&mut self, value: &T) -> Result<()>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let key = self.next_key.take();
|
||||
// Panic because this indicates a bug in the program rather than an
|
||||
// expected failure.
|
||||
let key = key.expect("serialize_value called before serialize_key");
|
||||
self.map.insert(key, tri!(to_value(value)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
Ok(Value::Map(Box::new(self.map)))
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStruct for SerializeMap {
|
||||
type Ok = Value;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
serde::ser::SerializeMap::serialize_entry(self, key, value)
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Value> {
|
||||
serde::ser::SerializeMap::end(self)
|
||||
}
|
||||
}
|
@@ -17,6 +17,7 @@ integration = []
|
||||
|
||||
[dependencies]
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
helix-config = { path = "../helix-config" }
|
||||
|
||||
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
|
||||
smallvec = "1.11"
|
||||
@@ -51,6 +52,8 @@ textwrap = "0.16.0"
|
||||
|
||||
nucleo.workspace = true
|
||||
parking_lot = "0.12"
|
||||
anyhow = "1.0.79"
|
||||
indexmap = { version = "2.1.0", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
quickcheck = { version = "1", default-features = false }
|
||||
|
@@ -2,8 +2,10 @@
|
||||
//! this module provides the functionality to insert the paired closing character.
|
||||
|
||||
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use helix_config::options;
|
||||
use indexmap::IndexMap;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
// Heavily based on https://github.com/codemirror/closebrackets/
|
||||
@@ -19,7 +21,7 @@ pub const DEFAULT_PAIRS: &[(char, char)] = &[
|
||||
/// The type that represents the collection of auto pairs,
|
||||
/// keyed by both opener and closer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AutoPairs(HashMap<char, Pair>);
|
||||
pub struct AutoPairs(IndexMap<char, Pair, ahash::RandomState>);
|
||||
|
||||
/// Represents the config for a particular pairing.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -75,15 +77,15 @@ impl From<(&char, &char)> for Pair {
|
||||
|
||||
impl AutoPairs {
|
||||
/// Make a new AutoPairs set with the given pairs and default conditions.
|
||||
pub fn new<'a, V: 'a, A>(pairs: V) -> Self
|
||||
pub fn new<'a, V: 'a, A>(pairs: V) -> anyhow::Result<Self>
|
||||
where
|
||||
V: IntoIterator<Item = A>,
|
||||
V: IntoIterator<Item = anyhow::Result<A>>,
|
||||
A: Into<Pair>,
|
||||
{
|
||||
let mut auto_pairs = HashMap::new();
|
||||
let mut auto_pairs = IndexMap::default();
|
||||
|
||||
for pair in pairs.into_iter() {
|
||||
let auto_pair = pair.into();
|
||||
let auto_pair = pair?.into();
|
||||
|
||||
auto_pairs.insert(auto_pair.open, auto_pair);
|
||||
|
||||
@@ -92,7 +94,7 @@ impl AutoPairs {
|
||||
}
|
||||
}
|
||||
|
||||
Self(auto_pairs)
|
||||
Ok(Self(auto_pairs))
|
||||
}
|
||||
|
||||
pub fn get(&self, ch: char) -> Option<&Pair> {
|
||||
@@ -102,7 +104,7 @@ impl AutoPairs {
|
||||
|
||||
impl Default for AutoPairs {
|
||||
fn default() -> Self {
|
||||
AutoPairs::new(DEFAULT_PAIRS.iter())
|
||||
AutoPairs::new(DEFAULT_PAIRS.iter().map(Ok)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,3 +373,43 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
|
||||
log::debug!("auto pair transaction: {:#?}", t);
|
||||
t
|
||||
}
|
||||
|
||||
options! {
|
||||
struct AutopairConfig {
|
||||
/// Mapping of character pairs like `{ '(' = ')', '`' = '`' }` that are
|
||||
/// automatically closed by the editor when typed.
|
||||
auto_pairs: AutoPairs = AutoPairs::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_config::Ty for AutoPairs {
|
||||
fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
|
||||
let map = match val {
|
||||
helix_config::Value::Map(map) => map,
|
||||
helix_config::Value::Bool(false) => return Ok(Self(IndexMap::default())),
|
||||
_ => bail!("expect 'false' or a map of pairs"),
|
||||
};
|
||||
let pairs = map.into_iter().map(|(open, close)| {
|
||||
let open = helix_config::Value::String(open.into_string());
|
||||
Ok(Pair {
|
||||
open: open.typed()?,
|
||||
close: close.typed()?,
|
||||
})
|
||||
});
|
||||
AutoPairs::new(pairs)
|
||||
}
|
||||
|
||||
fn to_value(&self) -> helix_config::Value {
|
||||
let map = self
|
||||
.0
|
||||
.values()
|
||||
.map(|pair| {
|
||||
(
|
||||
pair.open.to_string().into(),
|
||||
helix_config::Value::String(pair.close.into()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
helix_config::Value::Map(Box::new(map))
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +0,0 @@
|
||||
/// Syntax configuration loader based on built-in languages.toml.
|
||||
pub fn default_syntax_loader() -> crate::syntax::Configuration {
|
||||
helix_loader::config::default_lang_config()
|
||||
.try_into()
|
||||
.expect("Could not serialize built-in languages.toml")
|
||||
}
|
||||
/// Syntax configuration loader based on user configured languages.toml.
|
||||
pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
|
||||
helix_loader::config::user_lang_config()?.try_into()
|
||||
}
|
@@ -1,16 +1,36 @@
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use helix_config::{config_serde_adapter, options, IntegerRangeValidator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
|
||||
|
||||
use crate::{
|
||||
chars::{char_is_line_ending, char_is_whitespace},
|
||||
find_first_non_whitespace_char,
|
||||
graphemes::{grapheme_width, tab_width_at},
|
||||
syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
|
||||
syntax::{LanguageConfiguration, RopeProvider, Syntax},
|
||||
tree_sitter::Node,
|
||||
Position, Rope, RopeGraphemes, RopeSlice,
|
||||
};
|
||||
|
||||
/// How the indentation for a newly inserted line should be determined.
|
||||
/// If the selected heuristic is not available (e.g. because the current
|
||||
/// language has no tree-sitter indent queries), a simpler one will be used.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum IndentationHeuristic {
|
||||
/// Just copy the indentation of the line that the cursor is currently on.
|
||||
Simple,
|
||||
/// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line.
|
||||
TreeSitter,
|
||||
/// Use tree-sitter indent queries to compute the expected difference in indentation between the new line
|
||||
/// and the line before. Add this to the actual indentation level of the line before.
|
||||
#[default]
|
||||
Hybrid,
|
||||
}
|
||||
config_serde_adapter!(IndentationHeuristic);
|
||||
|
||||
/// Enum representing indentation style.
|
||||
///
|
||||
/// Only values 1-8 are valid for the `Spaces` variant.
|
||||
@@ -20,6 +40,50 @@ pub enum IndentStyle {
|
||||
Spaces(u8),
|
||||
}
|
||||
|
||||
options! {
|
||||
struct IndentationConfig {
|
||||
/// The number columns that a tabs are aligned to.
|
||||
#[name = "ident.tab_width"]
|
||||
#[read = copy]
|
||||
tab_width: usize = 4,
|
||||
/// Indentation inserted/removed into the document when indenting/dedenting.
|
||||
/// This can be set to an integer representing N spaces or "tab" for tabs.
|
||||
#[name = "ident.unit"]
|
||||
#[read = copy]
|
||||
indent_style: IndentStyle = IndentStyle::Tabs,
|
||||
/// How the indentation for a newly inserted line is computed:
|
||||
/// `simple` just copies the indentation level from the previous line,
|
||||
/// `tree-sitter` computes the indentation based on the syntax tree and
|
||||
/// `hybrid` combines both approaches.
|
||||
/// If the chosen heuristic is not available, a different one will
|
||||
/// be used as a fallback (the fallback order being `hybrid` ->
|
||||
/// `tree-sitter` -> `simple`).
|
||||
#[read = copy]
|
||||
indent_heuristic: IndentationHeuristic = IndentationHeuristic::Hybrid
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_config::Ty for IndentStyle {
|
||||
fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
|
||||
match val {
|
||||
helix_config::Value::String(s) if s == "t" || s == "tab" => Ok(IndentStyle::Tabs),
|
||||
helix_config::Value::Int(_) => {
|
||||
let spaces = IntegerRangeValidator::new(0, MAX_INDENT)
|
||||
.validate(val)
|
||||
.map_err(|err| anyhow!("invalid number of spaces! {err}"))?;
|
||||
Ok(IndentStyle::Spaces(spaces))
|
||||
}
|
||||
_ => bail!("expected an integer (spaces) or 'tab'"),
|
||||
}
|
||||
}
|
||||
fn to_value(&self) -> helix_config::Value {
|
||||
match *self {
|
||||
IndentStyle::Tabs => helix_config::Value::String("tab".into()),
|
||||
IndentStyle::Spaces(spaces) => helix_config::Value::Int(spaces as _),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 16 spaces
|
||||
const INDENTS: &str = " ";
|
||||
pub const MAX_INDENT: u8 = 16;
|
||||
|
@@ -3,7 +3,6 @@ pub use encoding_rs as encoding;
|
||||
pub mod auto_pairs;
|
||||
pub mod chars;
|
||||
pub mod comment;
|
||||
pub mod config;
|
||||
pub mod diagnostic;
|
||||
pub mod diff;
|
||||
pub mod doc_formatter;
|
||||
@@ -36,6 +35,7 @@ pub mod unicode {
|
||||
pub use unicode_width as width;
|
||||
}
|
||||
|
||||
use helix_config::OptionRegistry;
|
||||
pub use helix_loader::find_workspace;
|
||||
|
||||
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
|
||||
@@ -70,3 +70,9 @@ pub use diagnostic::Diagnostic;
|
||||
|
||||
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
|
||||
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};
|
||||
|
||||
pub fn init_config(registry: &mut OptionRegistry) {
|
||||
line_ending::init_config(registry);
|
||||
auto_pairs::init_config(registry);
|
||||
indent::init_config(registry);
|
||||
}
|
||||
|
@@ -1,3 +1,6 @@
|
||||
use anyhow::bail;
|
||||
use helix_config::{options, Ty};
|
||||
|
||||
use crate::{Rope, RopeSlice};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -5,6 +8,61 @@ pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::Crlf;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::LF;
|
||||
|
||||
options! {
|
||||
struct LineEndingConfig {
|
||||
/// The line ending to use for new documents. Can be `lf` or `crlf`. If
|
||||
/// helix was compiled with the `unicode-lines` feature then `vt`, `ff`,
|
||||
/// `cr`, `nel`, `ls` or `ps` are also allowed.
|
||||
#[read = copy]
|
||||
default_line_ending: LineEnding = NATIVE_LINE_ENDING,
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty for LineEnding {
|
||||
fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
|
||||
let val: String = val.typed()?;
|
||||
match &*val {
|
||||
"crlf" => Ok(LineEnding::Crlf),
|
||||
"lf" => Ok(LineEnding::LF),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
"vt" => Ok(LineEnding::VT),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
"ff" => Ok(LineEnding::FF),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
"cr" => Ok(LineEnding::CR),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
"nel" => Ok(LineEnding::Nel),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
"ls" => Ok(LineEnding::LS),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
"ps" => Ok(LineEnding::PS),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
_ => bail!("expecte one of 'lf', 'crlf', 'vt', 'ff', 'cr', 'nel', 'ls' or 'ps'"),
|
||||
#[cfg(not(feature = "unicode-lines"))]
|
||||
_ => bail!("expecte one of 'lf' or 'crlf'"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_value(&self) -> helix_config::Value {
|
||||
match self {
|
||||
LineEnding::Crlf => "crlf".into(),
|
||||
LineEnding::LF => "lf".into(),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
VT => "vt".into(),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
FF => "ff".into(),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
CR => "cr".into(),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
Nel => "nel".into(),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
LS => "ls".into(),
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
PS => "ps".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents one of the valid Unicode line endings.
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum LineEnding {
|
||||
|
@@ -84,422 +84,8 @@ pub struct Configuration {
|
||||
|
||||
impl Default for Configuration {
|
||||
fn default() -> Self {
|
||||
crate::config::default_syntax_loader()
|
||||
}
|
||||
}
|
||||
|
||||
// largely based on tree-sitter/cli/src/loader.rs
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct LanguageConfiguration {
|
||||
#[serde(rename = "name")]
|
||||
pub language_id: String, // c-sharp, rust, tsx
|
||||
#[serde(rename = "language-id")]
|
||||
// see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
|
||||
pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
|
||||
pub scope: String, // source.rust
|
||||
pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
|
||||
#[serde(default)]
|
||||
pub shebangs: Vec<String>, // interpreter(s) associated with language
|
||||
#[serde(default)]
|
||||
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
|
||||
pub comment_token: Option<String>,
|
||||
pub text_width: Option<usize>,
|
||||
pub soft_wrap: Option<SoftWrap>,
|
||||
|
||||
#[serde(default)]
|
||||
pub auto_format: bool,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub formatter: Option<FormatterConfiguration>,
|
||||
|
||||
#[serde(default)]
|
||||
pub diagnostic_severity: Severity,
|
||||
|
||||
pub grammar: Option<String>, // tree-sitter grammar name, defaults to language_id
|
||||
|
||||
// content_regex
|
||||
#[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
|
||||
pub injection_regex: Option<Regex>,
|
||||
// first_line_regex
|
||||
//
|
||||
#[serde(skip)]
|
||||
pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
|
||||
// tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Vec::is_empty",
|
||||
serialize_with = "serialize_lang_features",
|
||||
deserialize_with = "deserialize_lang_features"
|
||||
)]
|
||||
pub language_servers: Vec<LanguageServerFeatures>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub indent: Option<IndentationConfiguration>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub(crate) indent_query: OnceCell<Option<Query>>,
|
||||
#[serde(skip)]
|
||||
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub debugger: Option<DebugAdapterConfig>,
|
||||
|
||||
/// Automatic insertion of pairs to parentheses, brackets,
|
||||
/// etc. Defaults to true. Optionally, this can be a list of 2-tuples
|
||||
/// to specify a list of characters to pair. This overrides the
|
||||
/// global setting.
|
||||
#[serde(default, skip_serializing, deserialize_with = "deserialize_auto_pairs")]
|
||||
pub auto_pairs: Option<AutoPairs>,
|
||||
|
||||
pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
|
||||
|
||||
/// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
|
||||
/// Falling back to the current working directory if none are configured.
|
||||
pub workspace_lsp_roots: Option<Vec<PathBuf>>,
|
||||
#[serde(default)]
|
||||
pub persistent_diagnostic_sources: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum FileType {
|
||||
/// The extension of the file, either the `Path::extension` or the full
|
||||
/// filename if the file does not have an extension.
|
||||
Extension(String),
|
||||
/// The suffix of a file. This is compared to a given file's absolute
|
||||
/// path, so it can be used to detect files based on their directories.
|
||||
Suffix(String),
|
||||
}
|
||||
|
||||
impl Serialize for FileType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeMap;
|
||||
|
||||
match self {
|
||||
FileType::Extension(extension) => serializer.serialize_str(extension),
|
||||
FileType::Suffix(suffix) => {
|
||||
let mut map = serializer.serialize_map(Some(1))?;
|
||||
map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FileType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
struct FileTypeVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for FileTypeVisitor {
|
||||
type Value = FileType;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("string or table")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(FileType::Extension(value.to_string()))
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: serde::de::MapAccess<'de>,
|
||||
{
|
||||
match map.next_entry::<String, String>()? {
|
||||
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
|
||||
suffix.replace('/', std::path::MAIN_SEPARATOR_STR)
|
||||
})),
|
||||
Some((key, _value)) => Err(serde::de::Error::custom(format!(
|
||||
"unknown key in `file-types` list: {}",
|
||||
key
|
||||
))),
|
||||
None => Err(serde::de::Error::custom(
|
||||
"expected a `suffix` key in the `file-types` entry",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(FileTypeVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LanguageServerFeature {
|
||||
Format,
|
||||
GotoDeclaration,
|
||||
GotoDefinition,
|
||||
GotoTypeDefinition,
|
||||
GotoReference,
|
||||
GotoImplementation,
|
||||
// Goto, use bitflags, combining previous Goto members?
|
||||
SignatureHelp,
|
||||
Hover,
|
||||
DocumentHighlight,
|
||||
Completion,
|
||||
CodeAction,
|
||||
WorkspaceCommand,
|
||||
DocumentSymbols,
|
||||
WorkspaceSymbols,
|
||||
// Symbols, use bitflags, see above?
|
||||
Diagnostics,
|
||||
RenameSymbol,
|
||||
InlayHints,
|
||||
}
|
||||
|
||||
impl Display for LanguageServerFeature {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use LanguageServerFeature::*;
|
||||
let feature = match self {
|
||||
Format => "format",
|
||||
GotoDeclaration => "goto-declaration",
|
||||
GotoDefinition => "goto-definition",
|
||||
GotoTypeDefinition => "goto-type-definition",
|
||||
GotoReference => "goto-type-definition",
|
||||
GotoImplementation => "goto-implementation",
|
||||
SignatureHelp => "signature-help",
|
||||
Hover => "hover",
|
||||
DocumentHighlight => "document-highlight",
|
||||
Completion => "completion",
|
||||
CodeAction => "code-action",
|
||||
WorkspaceCommand => "workspace-command",
|
||||
DocumentSymbols => "document-symbols",
|
||||
WorkspaceSymbols => "workspace-symbols",
|
||||
Diagnostics => "diagnostics",
|
||||
RenameSymbol => "rename-symbol",
|
||||
InlayHints => "inlay-hints",
|
||||
};
|
||||
write!(f, "{feature}",)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
enum LanguageServerFeatureConfiguration {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
Features {
|
||||
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
|
||||
only_features: HashSet<LanguageServerFeature>,
|
||||
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
|
||||
except_features: HashSet<LanguageServerFeature>,
|
||||
name: String,
|
||||
},
|
||||
Simple(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LanguageServerFeatures {
|
||||
pub name: String,
|
||||
pub only: HashSet<LanguageServerFeature>,
|
||||
pub excluded: HashSet<LanguageServerFeature>,
|
||||
}
|
||||
|
||||
impl LanguageServerFeatures {
|
||||
pub fn has_feature(&self, feature: LanguageServerFeature) -> bool {
|
||||
(self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_lang_features<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<LanguageServerFeatures>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw: Vec<LanguageServerFeatureConfiguration> = Deserialize::deserialize(deserializer)?;
|
||||
let res = raw
|
||||
.into_iter()
|
||||
.map(|config| match config {
|
||||
LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures {
|
||||
name,
|
||||
..Default::default()
|
||||
},
|
||||
LanguageServerFeatureConfiguration::Features {
|
||||
only_features,
|
||||
except_features,
|
||||
name,
|
||||
} => LanguageServerFeatures {
|
||||
name,
|
||||
only: only_features,
|
||||
excluded: except_features,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
fn serialize_lang_features<S>(
|
||||
map: &Vec<LanguageServerFeatures>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut serializer = serializer.serialize_seq(Some(map.len()))?;
|
||||
for features in map {
|
||||
let features = if features.only.is_empty() && features.excluded.is_empty() {
|
||||
LanguageServerFeatureConfiguration::Simple(features.name.to_owned())
|
||||
} else {
|
||||
LanguageServerFeatureConfiguration::Features {
|
||||
only_features: features.only.clone(),
|
||||
except_features: features.excluded.clone(),
|
||||
name: features.name.to_owned(),
|
||||
}
|
||||
};
|
||||
serializer.serialize_element(&features)?;
|
||||
}
|
||||
serializer.end()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageServerConfiguration {
|
||||
pub command: String,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub args: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub environment: HashMap<String, String>,
|
||||
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
|
||||
pub config: Option<serde_json::Value>,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct FormatterConfiguration {
|
||||
pub command: String,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct AdvancedCompletion {
|
||||
pub name: Option<String>,
|
||||
pub completion: Option<String>,
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum DebugConfigCompletion {
|
||||
Named(String),
|
||||
Advanced(AdvancedCompletion),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum DebugArgumentValue {
|
||||
String(String),
|
||||
Array(Vec<String>),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DebugTemplate {
|
||||
pub name: String,
|
||||
pub request: String,
|
||||
pub completion: Vec<DebugConfigCompletion>,
|
||||
pub args: HashMap<String, DebugArgumentValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DebugAdapterConfig {
|
||||
pub name: String,
|
||||
pub transport: String,
|
||||
#[serde(default)]
|
||||
pub command: String,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
pub port_arg: Option<String>,
|
||||
pub templates: Vec<DebugTemplate>,
|
||||
#[serde(default)]
|
||||
pub quirks: DebuggerQuirks,
|
||||
}
|
||||
|
||||
// Different workarounds for adapters' differences
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct DebuggerQuirks {
|
||||
#[serde(default)]
|
||||
pub absolute_paths: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IndentationConfiguration {
|
||||
#[serde(deserialize_with = "deserialize_tab_width")]
|
||||
pub tab_width: usize,
|
||||
pub unit: String,
|
||||
}
|
||||
|
||||
/// How the indentation for a newly inserted line should be determined.
|
||||
/// If the selected heuristic is not available (e.g. because the current
|
||||
/// language has no tree-sitter indent queries), a simpler one will be used.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum IndentationHeuristic {
|
||||
/// Just copy the indentation of the line that the cursor is currently on.
|
||||
Simple,
|
||||
/// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line.
|
||||
TreeSitter,
|
||||
/// Use tree-sitter indent queries to compute the expected difference in indentation between the new line
|
||||
/// and the line before. Add this to the actual indentation level of the line before.
|
||||
#[default]
|
||||
Hybrid,
|
||||
}
|
||||
|
||||
/// Configuration for auto pairs
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
|
||||
pub enum AutoPairConfig {
|
||||
/// Enables or disables auto pairing. False means disabled. True means to use the default pairs.
|
||||
Enable(bool),
|
||||
|
||||
/// The mappings of pairs.
|
||||
Pairs(HashMap<char, char>),
|
||||
}
|
||||
|
||||
impl Default for AutoPairConfig {
|
||||
fn default() -> Self {
|
||||
AutoPairConfig::Enable(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AutoPairConfig> for Option<AutoPairs> {
|
||||
fn from(auto_pair_config: &AutoPairConfig) -> Self {
|
||||
match auto_pair_config {
|
||||
AutoPairConfig::Enable(false) => None,
|
||||
AutoPairConfig::Enable(true) => Some(AutoPairs::default()),
|
||||
AutoPairConfig::Pairs(pairs) => Some(AutoPairs::new(pairs.iter())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AutoPairConfig> for Option<AutoPairs> {
|
||||
fn from(auto_pairs_config: AutoPairConfig) -> Self {
|
||||
(&auto_pairs_config).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AutoPairConfig {
|
||||
type Err = std::str::ParseBoolError;
|
||||
|
||||
// only do bool parsing for runtime setting
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let enable: bool = s.parse()?;
|
||||
Ok(AutoPairConfig::Enable(enable))
|
||||
todo!()
|
||||
// crate::config::default_syntax_loader()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,10 +286,6 @@ impl LanguageConfiguration {
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> &str {
|
||||
&self.scope
|
||||
}
|
||||
|
||||
fn load_query(&self, kind: &str) -> Option<Query> {
|
||||
let query_text = read_query(&self.language_id, kind);
|
||||
if query_text.is_empty() {
|
||||
@@ -722,6 +304,7 @@ impl LanguageConfiguration {
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SoftWrap {
|
||||
@@ -848,13 +431,6 @@ impl Loader {
|
||||
configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
|
||||
}
|
||||
|
||||
pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> {
|
||||
self.language_configs
|
||||
.iter()
|
||||
.find(|config| config.scope == scope)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn language_config_for_language_id(&self, id: &str) -> Option<Arc<LanguageConfiguration>> {
|
||||
self.language_configs
|
||||
.iter()
|
||||
|
@@ -14,6 +14,7 @@ homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-config = { path = "../helix-config" }
|
||||
|
||||
anyhow = "1.0"
|
||||
log = "0.4"
|
||||
|
146
helix-dap/src/config.rs
Normal file
146
helix-dap/src/config.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use anyhow::bail;
|
||||
use helix_config::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
options! {
|
||||
struct DebugAdapterConfig {
|
||||
#[name = "debugger.name"]
|
||||
name: Option<String> = None,
|
||||
#[name = "debugger.transport"]
|
||||
#[read = copy]
|
||||
transport: Transport = Transport::Stdio,
|
||||
#[name = "debugger.command"]
|
||||
#[read = deref]
|
||||
command: String = "",
|
||||
#[name = "debugger.args"]
|
||||
#[read = deref]
|
||||
args: List<String> = List::default(),
|
||||
#[name = "debugger.port-arg"]
|
||||
#[read = deref]
|
||||
port_arg: String = "",
|
||||
#[name = "debugger.templates"]
|
||||
#[read = deref]
|
||||
templates: List<DebugTemplate> = List::default(),
|
||||
#[name = "debugger.quirks.absolut-path"]
|
||||
#[read = copy]
|
||||
absolut_path: bool = false,
|
||||
#[name = "terminal.command"]
|
||||
terminal_command: Option<String> = get_terminal_provider().map(|term| term.command),
|
||||
#[name = "terminal.args"]
|
||||
#[read = deref]
|
||||
terminal_args: List<String> = get_terminal_provider().map(|term| term.args.into_boxed_slice()).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Transport {
|
||||
Stdio,
|
||||
Tcp,
|
||||
}
|
||||
|
||||
impl Ty for Transport {
|
||||
fn from_value(val: Value) -> anyhow::Result<Self> {
|
||||
match &*String::from_value(val)? {
|
||||
"stdio" => Ok(Transport::Stdio),
|
||||
"tcp" => Ok(Transport::Tcp),
|
||||
val => bail!("expected 'stdio' or 'tcp' (got {val:?})"),
|
||||
}
|
||||
}
|
||||
fn to_value(&self) -> Value {
|
||||
match self {
|
||||
Transport::Stdio => "stdio".into(),
|
||||
Transport::Tcp => "tcp".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum DebugArgumentValue {
|
||||
String(String),
|
||||
Array(Vec<String>),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct AdvancedCompletion {
|
||||
pub name: Option<String>,
|
||||
pub completion: Option<String>,
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
pub enum DebugConfigCompletion {
|
||||
Named(String),
|
||||
Advanced(AdvancedCompletion),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DebugTemplate {
|
||||
pub name: String,
|
||||
pub request: String,
|
||||
pub completion: Vec<DebugConfigCompletion>,
|
||||
pub args: Map<DebugArgumentValue>,
|
||||
}
|
||||
|
||||
// TODO: integrate this better with the new config system (less nesting)
|
||||
// the best way to do that is probably a rewrite. I think these templates
|
||||
// are probably overkill here. This may be easier to solve by moving the logic
|
||||
// to scheme
|
||||
config_serde_adapter!(DebugTemplate);
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct TerminalConfig {
|
||||
pub command: String,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
use helix_config::env::binary_exists;
|
||||
|
||||
if binary_exists("wt") {
|
||||
return Some(TerminalConfig {
|
||||
command: "wt".into(),
|
||||
args: vec![
|
||||
"new-tab".into(),
|
||||
"--title".into(),
|
||||
"DEBUG".into(),
|
||||
"cmd".into(),
|
||||
"/C".into(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
Some(TerminalConfig {
|
||||
command: "conhost".into(),
|
||||
args: vec!["cmd".into(), "/C".into()],
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "wasm32")))]
|
||||
fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
use helix_config::env::{binary_exists, env_var_is_set};
|
||||
|
||||
if env_var_is_set("TMUX") && binary_exists("tmux") {
|
||||
return Some(TerminalConfig {
|
||||
command: "tmux".into(),
|
||||
args: vec!["split-window".into()],
|
||||
});
|
||||
}
|
||||
|
||||
if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") {
|
||||
return Some(TerminalConfig {
|
||||
command: "wezterm".into(),
|
||||
args: vec!["cli".into(), "split-pane".into()],
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
mod client;
|
||||
mod config;
|
||||
mod transport;
|
||||
mod types;
|
||||
|
||||
|
@@ -14,6 +14,7 @@ homepage.workspace = true
|
||||
|
||||
[dependencies]
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-config = { path = "../helix-config" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
helix-parsec = { path = "../helix-parsec" }
|
||||
|
||||
@@ -30,3 +31,5 @@ tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-
|
||||
tokio-stream = "0.1.14"
|
||||
which = "5.0.0"
|
||||
parking_lot = "0.12.1"
|
||||
ahash = "0.8.6"
|
||||
indexmap = { version = "2.1.0", features = ["serde"] }
|
||||
|
@@ -1,9 +1,12 @@
|
||||
use crate::{
|
||||
config::LanguageServerConfig,
|
||||
find_lsp_workspace, jsonrpc,
|
||||
transport::{Payload, Transport},
|
||||
Call, Error, OffsetEncoding, Result,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use helix_config::{self as config, OptionManager};
|
||||
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
|
||||
use helix_loader::{self, VERSION_AND_GIT_HASH};
|
||||
use lsp::{
|
||||
@@ -13,15 +16,14 @@ use lsp::{
|
||||
};
|
||||
use lsp_types as lsp;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use tokio::{
|
||||
io::{BufReader, BufWriter},
|
||||
process::{Child, Command},
|
||||
@@ -50,13 +52,11 @@ pub struct Client {
|
||||
server_tx: UnboundedSender<Payload>,
|
||||
request_counter: AtomicU64,
|
||||
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
|
||||
config: Option<Value>,
|
||||
root_path: std::path::PathBuf,
|
||||
root_uri: Option<lsp::Url>,
|
||||
workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
|
||||
initialize_notify: Arc<Notify>,
|
||||
/// workspace folders added while the server is still initializing
|
||||
req_timeout: u64,
|
||||
config: Arc<OptionManager>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -170,23 +170,20 @@ impl Client {
|
||||
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
pub fn start(
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
config: Option<Value>,
|
||||
server_environment: HashMap<String, String>,
|
||||
config: Arc<OptionManager>,
|
||||
root_markers: &[String],
|
||||
manual_roots: &[PathBuf],
|
||||
id: usize,
|
||||
name: String,
|
||||
req_timeout: u64,
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
|
||||
// Resolve path to the binary
|
||||
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
|
||||
let cmd = which::which(config.command().as_deref().context("no command defined")?)
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
|
||||
let process = Command::new(cmd)
|
||||
.envs(server_environment)
|
||||
.args(args)
|
||||
.envs(config.enviorment().iter().map(|(k, v)| (&**k, &**v)))
|
||||
.args(config.args().iter().map(|v| &**v))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -233,7 +230,6 @@ impl Client {
|
||||
request_counter: AtomicU64::new(0),
|
||||
capabilities: OnceCell::new(),
|
||||
config,
|
||||
req_timeout,
|
||||
root_path,
|
||||
root_uri,
|
||||
workspace_folders: Mutex::new(workspace_folders),
|
||||
@@ -374,8 +370,8 @@ impl Client {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Option<&Value> {
|
||||
self.config.as_ref()
|
||||
pub fn config(&self) -> config::Guard<Option<Box<Value>>> {
|
||||
self.config.server_config()
|
||||
}
|
||||
|
||||
pub async fn workspace_folders(
|
||||
@@ -404,7 +400,7 @@ impl Client {
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
self.call_with_timeout::<R>(params, self.req_timeout)
|
||||
self.call_with_timeout::<R>(params, self.config.timeout())
|
||||
}
|
||||
|
||||
fn call_with_timeout<R: lsp::request::Request>(
|
||||
@@ -512,7 +508,7 @@ impl Client {
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result<lsp::InitializeResult> {
|
||||
if let Some(config) = &self.config {
|
||||
if let Some(config) = &*self.config() {
|
||||
log::info!("Using custom LSP config: {}", config);
|
||||
}
|
||||
|
||||
@@ -524,7 +520,7 @@ impl Client {
|
||||
// clients will prefer _uri if possible
|
||||
root_path: self.root_path.to_str().map(|path| path.to_owned()),
|
||||
root_uri: self.root_uri.clone(),
|
||||
initialization_options: self.config.clone(),
|
||||
initialization_options: self.config().as_deref().cloned(),
|
||||
capabilities: lsp::ClientCapabilities {
|
||||
workspace: Some(lsp::WorkspaceClientCapabilities {
|
||||
configuration: Some(true),
|
||||
@@ -1152,17 +1148,12 @@ impl Client {
|
||||
};
|
||||
|
||||
// merge FormattingOptions with 'config.format'
|
||||
let config_format = self
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.get("format"))
|
||||
.and_then(|fmt| HashMap::<String, lsp::FormattingProperty>::deserialize(fmt).ok());
|
||||
|
||||
let options = if let Some(mut properties) = config_format {
|
||||
let mut config_format = self.config.format();
|
||||
let options = if !config_format.is_empty() {
|
||||
// passed in options take precedence over 'config.format'
|
||||
properties.extend(options.properties);
|
||||
config_format.extend(options.properties);
|
||||
lsp::FormattingOptions {
|
||||
properties,
|
||||
properties: config_format,
|
||||
..options
|
||||
}
|
||||
} else {
|
||||
|
67
helix-lsp/src/config.rs
Normal file
67
helix-lsp/src/config.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::bail;
|
||||
use helix_config::{options, List, Map, String, Ty, Value};
|
||||
|
||||
use crate::lsp;
|
||||
|
||||
// TODO: differentiating between Some(null) and None is not really practical
|
||||
// since the distinction is lost on a roundtrip trough config::Value.
|
||||
// Porbably better to change our code to treat null the way we currently
|
||||
// treat None
|
||||
options! {
|
||||
struct LanguageServerConfig {
|
||||
/// The name or path of the language server binary to execute. Binaries must be in `$PATH`
|
||||
command: Option<String> = None,
|
||||
/// A list of arguments to pass to the language server binary
|
||||
#[read = deref]
|
||||
args: List<String> = List::default(),
|
||||
/// Any environment variables that will be used when starting the language server
|
||||
enviorment: Map<String> = Map::default(),
|
||||
/// LSP initialization options
|
||||
#[name = "config"]
|
||||
server_config: Option<Box<serde_json::Value>> = None,
|
||||
/// LSP initialization options
|
||||
#[read = copy]
|
||||
timeout: u64 = 20,
|
||||
// TODO: merge
|
||||
/// LSP formatting options
|
||||
#[name = "config.format"]
|
||||
#[read = fold(HashMap::new(), fold_format_config, FormatConfig)]
|
||||
format: Map<FormattingProperty> = Map::default()
|
||||
}
|
||||
}
|
||||
|
||||
type FormatConfig = HashMap<std::string::String, lsp::FormattingProperty>;
|
||||
|
||||
fn fold_format_config(config: &Map<FormattingProperty>, mut res: FormatConfig) -> FormatConfig {
|
||||
for (k, v) in config.iter() {
|
||||
res.entry(k.to_string()).or_insert_with(|| v.0.clone());
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
// damm orphan rules :/
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
struct FormattingProperty(lsp::FormattingProperty);
|
||||
|
||||
impl Ty for FormattingProperty {
|
||||
fn from_value(val: Value) -> anyhow::Result<Self> {
|
||||
match val {
|
||||
Value::Int(_) => Ok(FormattingProperty(lsp::FormattingProperty::Number(
|
||||
i32::from_value(val)?,
|
||||
))),
|
||||
Value::Bool(val) => Ok(FormattingProperty(lsp::FormattingProperty::Bool(val))),
|
||||
Value::String(val) => Ok(FormattingProperty(lsp::FormattingProperty::String(val))),
|
||||
_ => bail!("expected a string, boolean or integer"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
match self.0 {
|
||||
lsp::FormattingProperty::Bool(val) => Value::Bool(val),
|
||||
lsp::FormattingProperty::Number(val) => Value::Int(val as _),
|
||||
lsp::FormattingProperty::String(ref val) => Value::String(val.clone()),
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
mod client;
|
||||
mod config;
|
||||
pub mod file_event;
|
||||
pub mod jsonrpc;
|
||||
pub mod snippet;
|
||||
@@ -11,6 +12,7 @@ pub use lsp::{Position, Url};
|
||||
pub use lsp_types as lsp;
|
||||
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
use helix_config::OptionRegistry;
|
||||
use helix_core::{
|
||||
path,
|
||||
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
|
||||
@@ -26,6 +28,8 @@ use std::{
|
||||
use thiserror::Error;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use crate::config::init_config;
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
pub type LanguageServerName = String;
|
||||
|
||||
@@ -636,17 +640,25 @@ pub struct Registry {
|
||||
counter: usize,
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
|
||||
pub file_event_handler: file_event::Handler,
|
||||
pub config: OptionRegistry,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
|
||||
Self {
|
||||
let mut res = Self {
|
||||
inner: HashMap::new(),
|
||||
syn_loader,
|
||||
counter: 0,
|
||||
incoming: SelectAll::new(),
|
||||
file_event_handler: file_event::Handler::new(),
|
||||
}
|
||||
config: OptionRegistry::new(),
|
||||
};
|
||||
res.reset_config();
|
||||
res
|
||||
}
|
||||
|
||||
pub fn reset_config(&mut self) {
|
||||
init_config(&mut self.config);
|
||||
}
|
||||
|
||||
pub fn get_by_id(&self, id: usize) -> Option<&Client> {
|
||||
@@ -882,15 +894,11 @@ fn start_client(
|
||||
enable_snippets: bool,
|
||||
) -> Result<NewClient> {
|
||||
let (client, incoming, initialize_notify) = Client::start(
|
||||
&ls_config.command,
|
||||
&ls_config.args,
|
||||
ls_config.config.clone(),
|
||||
ls_config.environment.clone(),
|
||||
todo!(),
|
||||
&config.roots,
|
||||
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
|
||||
id,
|
||||
name,
|
||||
ls_config.timeout,
|
||||
doc_path,
|
||||
)?;
|
||||
|
||||
|
@@ -699,7 +699,7 @@ impl Application {
|
||||
// Trigger a workspace/didChangeConfiguration notification after initialization.
|
||||
// This might not be required by the spec but Neovim does this as well, so it's
|
||||
// probably a good idea for compatibility.
|
||||
if let Some(config) = language_server.config() {
|
||||
if let Some(config) = language_server.config().as_deref() {
|
||||
tokio::spawn(language_server.did_change_configuration(config.clone()));
|
||||
}
|
||||
|
||||
@@ -1023,7 +1023,8 @@ impl Application {
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let mut config = language_server.config()?;
|
||||
let config = language_server.config();
|
||||
let mut config = config.as_deref()?;
|
||||
if let Some(section) = item.section.as_ref() {
|
||||
// for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
|
||||
if !section.is_empty() {
|
||||
@@ -1032,7 +1033,7 @@ impl Application {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(config)
|
||||
Some(config.to_owned())
|
||||
})
|
||||
.collect();
|
||||
Ok(json!(result))
|
||||
|
@@ -16,6 +16,7 @@ term = ["crossterm"]
|
||||
|
||||
[dependencies]
|
||||
helix-core = { path = "../helix-core" }
|
||||
helix-config = { path = "../helix-config" }
|
||||
helix-event = { path = "../helix-event" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
helix-lsp = { path = "../helix-lsp" }
|
||||
|
@@ -1518,13 +1518,6 @@ impl Document {
|
||||
current_revision
|
||||
}
|
||||
|
||||
/// Corresponding language scope name. Usually `source.<lang>`.
|
||||
pub fn language_scope(&self) -> Option<&str> {
|
||||
self.language
|
||||
.as_ref()
|
||||
.map(|language| language.scope.as_str())
|
||||
}
|
||||
|
||||
/// Language name for the document. Corresponds to the `name` key in
|
||||
/// `languages.toml` configuration.
|
||||
pub fn language_name(&self) -> Option<&str> {
|
||||
|
@@ -41,833 +41,13 @@ use anyhow::{anyhow, bail, Error};
|
||||
|
||||
pub use helix_core::diagnostic::Severity;
|
||||
use helix_core::{
|
||||
auto_pairs::AutoPairs,
|
||||
syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
|
||||
Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING,
|
||||
syntax::{self, LanguageServerFeature},
|
||||
Change, Position, Selection,
|
||||
};
|
||||
use helix_dap as dap;
|
||||
use helix_lsp::lsp;
|
||||
|
||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use arc_swap::access::{DynAccess, DynGuard};
|
||||
|
||||
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let millis = u64::deserialize(deserializer)?;
|
||||
Ok(Duration::from_millis(millis))
|
||||
}
|
||||
|
||||
fn serialize_duration_millis<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_u64(
|
||||
duration
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.map_err(|_| serde::ser::Error::custom("duration value overflowed u64"))?,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||
pub struct GutterConfig {
|
||||
/// Gutter Layout
|
||||
pub layout: Vec<GutterType>,
|
||||
/// Options specific to the "line-numbers" gutter
|
||||
pub line_numbers: GutterLineNumbersConfig,
|
||||
}
|
||||
|
||||
impl Default for GutterConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
layout: vec![
|
||||
GutterType::Diagnostics,
|
||||
GutterType::Spacer,
|
||||
GutterType::LineNumbers,
|
||||
GutterType::Spacer,
|
||||
GutterType::Diff,
|
||||
],
|
||||
line_numbers: GutterLineNumbersConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<GutterType>> for GutterConfig {
|
||||
fn from(x: Vec<GutterType>) -> Self {
|
||||
GutterConfig {
|
||||
layout: x,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_gutter_seq_or_struct<'de, D>(deserializer: D) -> Result<GutterConfig, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct GutterVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for GutterVisitor {
|
||||
type Value = GutterConfig;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"an array of gutter names or a detailed gutter configuration"
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut gutters = Vec::new();
|
||||
while let Some(gutter) = seq.next_element::<String>()? {
|
||||
gutters.push(
|
||||
gutter
|
||||
.parse::<GutterType>()
|
||||
.map_err(serde::de::Error::custom)?,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(gutters.into())
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let deserializer = serde::de::value::MapAccessDeserializer::new(map);
|
||||
Deserialize::deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(GutterVisitor)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||
pub struct GutterLineNumbersConfig {
|
||||
/// Minimum number of characters to use for line number gutter. Defaults to 3.
|
||||
pub min_width: usize,
|
||||
}
|
||||
|
||||
impl Default for GutterLineNumbersConfig {
|
||||
fn default() -> Self {
|
||||
Self { min_width: 3 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||
pub struct FilePickerConfig {
|
||||
/// IgnoreOptions
|
||||
/// Enables ignoring hidden files.
|
||||
/// Whether to hide hidden files in file picker and global search results. Defaults to true.
|
||||
pub hidden: bool,
|
||||
/// Enables following symlinks.
|
||||
/// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true.
|
||||
pub follow_symlinks: bool,
|
||||
/// Hides symlinks that point into the current directory. Defaults to true.
|
||||
pub deduplicate_links: bool,
|
||||
/// Enables reading ignore files from parent directories. Defaults to true.
|
||||
pub parents: bool,
|
||||
/// Enables reading `.ignore` files.
|
||||
/// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
|
||||
pub ignore: bool,
|
||||
/// Enables reading `.gitignore` files.
|
||||
/// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
|
||||
pub git_ignore: bool,
|
||||
/// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
|
||||
/// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
|
||||
pub git_global: bool,
|
||||
/// Enables reading `.git/info/exclude` files.
|
||||
/// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
|
||||
pub git_exclude: bool,
|
||||
/// WalkBuilder options
|
||||
/// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`.
|
||||
pub max_depth: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for FilePickerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hidden: true,
|
||||
follow_symlinks: true,
|
||||
deduplicate_links: true,
|
||||
parents: true,
|
||||
ignore: true,
|
||||
git_ignore: true,
|
||||
git_global: true,
|
||||
git_exclude: true,
|
||||
max_depth: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
|
||||
pub scrolloff: usize,
|
||||
/// Number of lines to scroll at once. Defaults to 3
|
||||
pub scroll_lines: isize,
|
||||
/// Mouse support. Defaults to true.
|
||||
pub mouse: bool,
|
||||
/// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
|
||||
pub shell: Vec<String>,
|
||||
/// Line number mode.
|
||||
pub line_number: LineNumber,
|
||||
/// Highlight the lines cursors are currently on. Defaults to false.
|
||||
pub cursorline: bool,
|
||||
/// Highlight the columns cursors are currently on. Defaults to false.
|
||||
pub cursorcolumn: bool,
|
||||
#[serde(deserialize_with = "deserialize_gutter_seq_or_struct")]
|
||||
pub gutters: GutterConfig,
|
||||
/// Middle click paste support. Defaults to true.
|
||||
pub middle_click_paste: bool,
|
||||
/// Automatic insertion of pairs to parentheses, brackets,
|
||||
/// etc. Optionally, this can be a list of 2-tuples to specify a
|
||||
/// global list of characters to pair. Defaults to true.
|
||||
pub auto_pairs: AutoPairConfig,
|
||||
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
|
||||
pub auto_completion: bool,
|
||||
/// Automatic formatting on save. Defaults to true.
|
||||
pub auto_format: bool,
|
||||
/// Automatic save on focus lost. Defaults to false.
|
||||
pub auto_save: bool,
|
||||
/// Set a global text_width
|
||||
pub text_width: usize,
|
||||
/// Time in milliseconds since last keypress before idle timers trigger.
|
||||
/// Used for autocompletion, set to 0 for instant. Defaults to 250ms.
|
||||
#[serde(
|
||||
serialize_with = "serialize_duration_millis",
|
||||
deserialize_with = "deserialize_duration_millis"
|
||||
)]
|
||||
pub idle_timeout: Duration,
|
||||
/// Whether to insert the completion suggestion on hover. Defaults to true.
|
||||
pub preview_completion_insert: bool,
|
||||
pub completion_trigger_len: u8,
|
||||
/// Whether to instruct the LSP to replace the entire word when applying a completion
|
||||
/// or to only insert new text
|
||||
pub completion_replace: bool,
|
||||
/// Whether to display infoboxes. Defaults to true.
|
||||
pub auto_info: bool,
|
||||
pub file_picker: FilePickerConfig,
|
||||
/// Configuration of the statusline elements
|
||||
pub statusline: StatusLineConfig,
|
||||
/// Shape for cursor in each mode
|
||||
pub cursor_shape: CursorShapeConfig,
|
||||
/// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
|
||||
pub true_color: bool,
|
||||
/// Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative. Defaults to `false`.
|
||||
pub undercurl: bool,
|
||||
/// Search configuration.
|
||||
#[serde(default)]
|
||||
pub search: SearchConfig,
|
||||
pub lsp: LspConfig,
|
||||
pub terminal: Option<TerminalConfig>,
|
||||
/// Column numbers at which to draw the rulers. Defaults to `[]`, meaning no rulers.
|
||||
pub rulers: Vec<u16>,
|
||||
#[serde(default)]
|
||||
pub whitespace: WhitespaceConfig,
|
||||
/// Persistently display open buffers along the top
|
||||
pub bufferline: BufferLine,
|
||||
/// Vertical indent width guides.
|
||||
pub indent_guides: IndentGuidesConfig,
|
||||
/// Whether to color modes with different colors. Defaults to `false`.
|
||||
pub color_modes: bool,
|
||||
pub soft_wrap: SoftWrap,
|
||||
/// Workspace specific lsp ceiling dirs
|
||||
pub workspace_lsp_roots: Vec<PathBuf>,
|
||||
/// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
|
||||
pub default_line_ending: LineEndingConfig,
|
||||
/// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`.
|
||||
pub insert_final_newline: bool,
|
||||
/// Enables smart tab
|
||||
pub smart_tab: Option<SmartTabConfig>,
|
||||
/// Draw border around popups.
|
||||
pub popup_border: PopupBorderConfig,
|
||||
/// Which indent heuristic to use when a new line is inserted
|
||||
#[serde(default)]
|
||||
pub indent_heuristic: IndentationHeuristic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "kebab-case", default)]
|
||||
pub struct SmartTabConfig {
|
||||
pub enable: bool,
|
||||
pub supersede_menu: bool,
|
||||
}
|
||||
|
||||
impl Default for SmartTabConfig {
|
||||
fn default() -> Self {
|
||||
SmartTabConfig {
|
||||
enable: true,
|
||||
supersede_menu: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct TerminalConfig {
|
||||
pub command: String,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
use crate::env::binary_exists;
|
||||
|
||||
if binary_exists("wt") {
|
||||
return Some(TerminalConfig {
|
||||
command: "wt".to_string(),
|
||||
args: vec![
|
||||
"new-tab".to_string(),
|
||||
"--title".to_string(),
|
||||
"DEBUG".to_string(),
|
||||
"cmd".to_string(),
|
||||
"/C".to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
Some(TerminalConfig {
|
||||
command: "conhost".to_string(),
|
||||
args: vec!["cmd".to_string(), "/C".to_string()],
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "wasm32")))]
|
||||
pub fn get_terminal_provider() -> Option<TerminalConfig> {
|
||||
use crate::env::{binary_exists, env_var_is_set};
|
||||
|
||||
if env_var_is_set("TMUX") && binary_exists("tmux") {
|
||||
return Some(TerminalConfig {
|
||||
command: "tmux".to_string(),
|
||||
args: vec!["split-window".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") {
|
||||
return Some(TerminalConfig {
|
||||
command: "wezterm".to_string(),
|
||||
args: vec!["cli".to_string(), "split-pane".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct LspConfig {
|
||||
/// Enables LSP
|
||||
pub enable: bool,
|
||||
/// Display LSP progress messages below statusline
|
||||
pub display_messages: bool,
|
||||
/// Enable automatic pop up of signature help (parameter hints)
|
||||
pub auto_signature_help: bool,
|
||||
/// Display docs under signature help popup
|
||||
pub display_signature_help_docs: bool,
|
||||
/// Display inlay hints
|
||||
pub display_inlay_hints: bool,
|
||||
/// Whether to enable snippet support
|
||||
pub snippets: bool,
|
||||
/// Whether to include declaration in the goto reference query
|
||||
pub goto_reference_include_declaration: bool,
|
||||
}
|
||||
|
||||
impl Default for LspConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable: true,
|
||||
display_messages: false,
|
||||
auto_signature_help: true,
|
||||
display_signature_help_docs: true,
|
||||
display_inlay_hints: false,
|
||||
snippets: true,
|
||||
goto_reference_include_declaration: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||
pub struct SearchConfig {
|
||||
/// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
|
||||
pub smart_case: bool,
|
||||
/// Whether the search should wrap after depleting the matches. Default to true.
|
||||
pub wrap_around: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||
pub struct StatusLineConfig {
|
||||
pub left: Vec<StatusLineElement>,
|
||||
pub center: Vec<StatusLineElement>,
|
||||
pub right: Vec<StatusLineElement>,
|
||||
pub separator: String,
|
||||
pub mode: ModeConfig,
|
||||
}
|
||||
|
||||
impl Default for StatusLineConfig {
|
||||
fn default() -> Self {
|
||||
use StatusLineElement as E;
|
||||
|
||||
Self {
|
||||
left: vec![
|
||||
E::Mode,
|
||||
E::Spinner,
|
||||
E::FileName,
|
||||
E::ReadOnlyIndicator,
|
||||
E::FileModificationIndicator,
|
||||
],
|
||||
center: vec![],
|
||||
right: vec![
|
||||
E::Diagnostics,
|
||||
E::Selections,
|
||||
E::Register,
|
||||
E::Position,
|
||||
E::FileEncoding,
|
||||
],
|
||||
separator: String::from("│"),
|
||||
mode: ModeConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||
pub struct ModeConfig {
|
||||
pub normal: String,
|
||||
pub insert: String,
|
||||
pub select: String,
|
||||
}
|
||||
|
||||
impl Default for ModeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
normal: String::from("NOR"),
|
||||
insert: String::from("INS"),
|
||||
select: String::from("SEL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum StatusLineElement {
|
||||
/// The editor mode (Normal, Insert, Visual/Selection)
|
||||
Mode,
|
||||
|
||||
/// The LSP activity spinner
|
||||
Spinner,
|
||||
|
||||
/// The file basename (the leaf of the open file's path)
|
||||
FileBaseName,
|
||||
|
||||
/// The relative file path
|
||||
FileName,
|
||||
|
||||
// The file modification indicator
|
||||
FileModificationIndicator,
|
||||
|
||||
/// An indicator that shows `"[readonly]"` when a file cannot be written
|
||||
ReadOnlyIndicator,
|
||||
|
||||
/// The file encoding
|
||||
FileEncoding,
|
||||
|
||||
/// The file line endings (CRLF or LF)
|
||||
FileLineEnding,
|
||||
|
||||
/// The file type (language ID or "text")
|
||||
FileType,
|
||||
|
||||
/// A summary of the number of errors and warnings
|
||||
Diagnostics,
|
||||
|
||||
/// A summary of the number of errors and warnings on file and workspace
|
||||
WorkspaceDiagnostics,
|
||||
|
||||
/// The number of selections (cursors)
|
||||
Selections,
|
||||
|
||||
/// The number of characters currently in primary selection
|
||||
PrimarySelectionLength,
|
||||
|
||||
/// The cursor position
|
||||
Position,
|
||||
|
||||
/// The separator string
|
||||
Separator,
|
||||
|
||||
/// The cursor position as a percent of the total file
|
||||
PositionPercentage,
|
||||
|
||||
/// The total line numbers of the current file
|
||||
TotalLineNumbers,
|
||||
|
||||
/// A single space
|
||||
Spacer,
|
||||
|
||||
/// Current version control information
|
||||
VersionControl,
|
||||
|
||||
/// Indicator for selected register
|
||||
Register,
|
||||
}
|
||||
|
||||
// Cursor shape is read and used on every rendered frame and so needs
|
||||
// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CursorShapeConfig([CursorKind; 3]);
|
||||
|
||||
impl CursorShapeConfig {
|
||||
pub fn from_mode(&self, mode: Mode) -> CursorKind {
|
||||
self.get(mode as usize).copied().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CursorShapeConfig {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
|
||||
let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
|
||||
Ok(CursorShapeConfig([
|
||||
into_cursor(Mode::Normal),
|
||||
into_cursor(Mode::Select),
|
||||
into_cursor(Mode::Insert),
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for CursorShapeConfig {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(self.len()))?;
|
||||
let modes = [Mode::Normal, Mode::Select, Mode::Insert];
|
||||
for mode in modes {
|
||||
map.serialize_entry(&mode, &self.from_mode(mode))?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for CursorShapeConfig {
|
||||
type Target = [CursorKind; 3];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CursorShapeConfig {
|
||||
fn default() -> Self {
|
||||
Self([CursorKind::Block; 3])
|
||||
}
|
||||
}
|
||||
|
||||
/// bufferline render modes
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BufferLine {
|
||||
/// Don't render bufferline
|
||||
#[default]
|
||||
Never,
|
||||
/// Always render
|
||||
Always,
|
||||
/// Only if multiple buffers are open
|
||||
Multiple,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LineNumber {
|
||||
/// Show absolute line number
|
||||
Absolute,
|
||||
|
||||
/// If focused and in normal/select mode, show relative line number to the primary cursor.
|
||||
/// If unfocused or in insert mode, show absolute line number.
|
||||
Relative,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for LineNumber {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"absolute" | "abs" => Ok(Self::Absolute),
|
||||
"relative" | "rel" => Ok(Self::Relative),
|
||||
_ => anyhow::bail!("Line number can only be `absolute` or `relative`."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum GutterType {
|
||||
/// Show diagnostics and other features like breakpoints
|
||||
Diagnostics,
|
||||
/// Show line numbers
|
||||
LineNumbers,
|
||||
/// Show one blank space
|
||||
Spacer,
|
||||
/// Highlight local changes
|
||||
Diff,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for GutterType {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"diagnostics" => Ok(Self::Diagnostics),
|
||||
"spacer" => Ok(Self::Spacer),
|
||||
"line-numbers" => Ok(Self::LineNumbers),
|
||||
"diff" => Ok(Self::Diff),
|
||||
_ => anyhow::bail!(
|
||||
"Gutter type can only be `diagnostics`, `spacer`, `line-numbers` or `diff`."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct WhitespaceConfig {
|
||||
pub render: WhitespaceRender,
|
||||
pub characters: WhitespaceCharacters,
|
||||
}
|
||||
|
||||
impl Default for WhitespaceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
render: WhitespaceRender::Basic(WhitespaceRenderValue::None),
|
||||
characters: WhitespaceCharacters::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged, rename_all = "kebab-case")]
|
||||
pub enum WhitespaceRender {
|
||||
Basic(WhitespaceRenderValue),
|
||||
Specific {
|
||||
default: Option<WhitespaceRenderValue>,
|
||||
space: Option<WhitespaceRenderValue>,
|
||||
nbsp: Option<WhitespaceRenderValue>,
|
||||
tab: Option<WhitespaceRenderValue>,
|
||||
newline: Option<WhitespaceRenderValue>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum WhitespaceRenderValue {
|
||||
None,
|
||||
// TODO
|
||||
// Selection,
|
||||
All,
|
||||
}
|
||||
|
||||
impl WhitespaceRender {
|
||||
pub fn space(&self) -> WhitespaceRenderValue {
|
||||
match *self {
|
||||
Self::Basic(val) => val,
|
||||
Self::Specific { default, space, .. } => {
|
||||
space.or(default).unwrap_or(WhitespaceRenderValue::None)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn nbsp(&self) -> WhitespaceRenderValue {
|
||||
match *self {
|
||||
Self::Basic(val) => val,
|
||||
Self::Specific { default, nbsp, .. } => {
|
||||
nbsp.or(default).unwrap_or(WhitespaceRenderValue::None)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn tab(&self) -> WhitespaceRenderValue {
|
||||
match *self {
|
||||
Self::Basic(val) => val,
|
||||
Self::Specific { default, tab, .. } => {
|
||||
tab.or(default).unwrap_or(WhitespaceRenderValue::None)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn newline(&self) -> WhitespaceRenderValue {
|
||||
match *self {
|
||||
Self::Basic(val) => val,
|
||||
Self::Specific {
|
||||
default, newline, ..
|
||||
} => newline.or(default).unwrap_or(WhitespaceRenderValue::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct WhitespaceCharacters {
|
||||
pub space: char,
|
||||
pub nbsp: char,
|
||||
pub tab: char,
|
||||
pub tabpad: char,
|
||||
pub newline: char,
|
||||
}
|
||||
|
||||
impl Default for WhitespaceCharacters {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
space: '·', // U+00B7
|
||||
nbsp: '⍽', // U+237D
|
||||
tab: '→', // U+2192
|
||||
newline: '⏎', // U+23CE
|
||||
tabpad: ' ',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct IndentGuidesConfig {
|
||||
pub render: bool,
|
||||
pub character: char,
|
||||
pub skip_levels: u8,
|
||||
}
|
||||
|
||||
impl Default for IndentGuidesConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
skip_levels: 0,
|
||||
render: false,
|
||||
character: '│',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Line ending configuration.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LineEndingConfig {
|
||||
/// The platform's native line ending.
|
||||
///
|
||||
/// `crlf` on Windows, otherwise `lf`.
|
||||
#[default]
|
||||
Native,
|
||||
/// Line feed.
|
||||
LF,
|
||||
/// Carriage return followed by line feed.
|
||||
Crlf,
|
||||
/// Form feed.
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
FF,
|
||||
/// Carriage return.
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
CR,
|
||||
/// Next line.
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
Nel,
|
||||
}
|
||||
|
||||
impl From<LineEndingConfig> for LineEnding {
|
||||
fn from(line_ending: LineEndingConfig) -> Self {
|
||||
match line_ending {
|
||||
LineEndingConfig::Native => NATIVE_LINE_ENDING,
|
||||
LineEndingConfig::LF => LineEnding::LF,
|
||||
LineEndingConfig::Crlf => LineEnding::Crlf,
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
LineEndingConfig::FF => LineEnding::FF,
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
LineEndingConfig::CR => LineEnding::CR,
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
LineEndingConfig::Nel => LineEnding::Nel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PopupBorderConfig {
|
||||
None,
|
||||
All,
|
||||
Popup,
|
||||
Menu,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scrolloff: 5,
|
||||
scroll_lines: 3,
|
||||
mouse: true,
|
||||
shell: if cfg!(windows) {
|
||||
vec!["cmd".to_owned(), "/C".to_owned()]
|
||||
} else {
|
||||
vec!["sh".to_owned(), "-c".to_owned()]
|
||||
},
|
||||
line_number: LineNumber::Absolute,
|
||||
cursorline: false,
|
||||
cursorcolumn: false,
|
||||
gutters: GutterConfig::default(),
|
||||
middle_click_paste: true,
|
||||
auto_pairs: AutoPairConfig::default(),
|
||||
auto_completion: true,
|
||||
auto_format: true,
|
||||
auto_save: false,
|
||||
idle_timeout: Duration::from_millis(250),
|
||||
preview_completion_insert: true,
|
||||
completion_trigger_len: 2,
|
||||
auto_info: true,
|
||||
file_picker: FilePickerConfig::default(),
|
||||
statusline: StatusLineConfig::default(),
|
||||
cursor_shape: CursorShapeConfig::default(),
|
||||
true_color: false,
|
||||
undercurl: false,
|
||||
search: SearchConfig::default(),
|
||||
lsp: LspConfig::default(),
|
||||
terminal: get_terminal_provider(),
|
||||
rulers: Vec::new(),
|
||||
whitespace: WhitespaceConfig::default(),
|
||||
bufferline: BufferLine::default(),
|
||||
indent_guides: IndentGuidesConfig::default(),
|
||||
color_modes: false,
|
||||
soft_wrap: SoftWrap {
|
||||
enable: Some(false),
|
||||
..SoftWrap::default()
|
||||
},
|
||||
text_width: 80,
|
||||
completion_replace: false,
|
||||
workspace_lsp_roots: Vec::new(),
|
||||
default_line_ending: LineEndingConfig::default(),
|
||||
insert_final_newline: true,
|
||||
smart_tab: Some(SmartTabConfig::default()),
|
||||
popup_border: PopupBorderConfig::None,
|
||||
indent_heuristic: IndentationHeuristic::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SearchConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
wrap_around: true,
|
||||
smart_case: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
use arc_swap::access::DynGuard;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Breakpoint {
|
||||
@@ -1236,8 +416,7 @@ impl Editor {
|
||||
Ok(client) => Some((lang, client)),
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
|
||||
language.scope(),
|
||||
"Failed to initialize the language servers for `{}` {{ {} }}",
|
||||
lang,
|
||||
err
|
||||
);
|
||||
|
@@ -1,13 +1,96 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use helix_config::{config_serde_adapter, options, List};
|
||||
use helix_core::syntax::LanguageServerFeature;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
editor::GutterType,
|
||||
graphics::{Style, UnderlineStyle},
|
||||
Document, Editor, Theme, View,
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LineNumber {
|
||||
/// Show absolute line number
|
||||
#[serde(alias = "abs")]
|
||||
Absolute,
|
||||
/// If focused and in normal/select mode, show relative line number to the primary cursor.
|
||||
/// If unfocused or in insert mode, show absolute line number.
|
||||
#[serde(alias = "rel")]
|
||||
Relative,
|
||||
}
|
||||
|
||||
config_serde_adapter!(LineNumber);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum GutterType {
|
||||
/// Show diagnostics and other features like breakpoints
|
||||
Diagnostics,
|
||||
/// Show line numbers
|
||||
LineNumbers,
|
||||
/// Show one blank space
|
||||
Spacer,
|
||||
/// Highlight local changes
|
||||
Diff,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for GutterType {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"diagnostics" => Ok(Self::Diagnostics),
|
||||
"spacer" => Ok(Self::Spacer),
|
||||
"line-numbers" => Ok(Self::LineNumbers),
|
||||
"diff" => Ok(Self::Diff),
|
||||
_ => anyhow::bail!(
|
||||
"expected one of `diagnostics`, `spacer`, `line-numbers` or `diff` (found {s:?})"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl helix_config::Ty for GutterType {
|
||||
fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
|
||||
let val: String = val.typed()?;
|
||||
val.parse()
|
||||
}
|
||||
|
||||
fn to_value(&self) -> helix_config::Value {
|
||||
match self {
|
||||
GutterType::Diagnostics => "diagnostics".into(),
|
||||
GutterType::LineNumbers => "lineNumbers".into(),
|
||||
GutterType::Spacer => "spacer".into(),
|
||||
GutterType::Diff => "diff".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options! {
|
||||
struct GutterConfig {
|
||||
/// A list of gutters to display
|
||||
#[name = "gutters.layout"]
|
||||
layout: List<GutterType> = &[
|
||||
GutterType::Diagnostics,
|
||||
GutterType::Spacer,
|
||||
GutterType::LineNumbers,
|
||||
GutterType::Spacer,
|
||||
GutterType::Diff,
|
||||
],
|
||||
/// The minimum number of characters the line number gutter should take up.
|
||||
#[name = "gutters.line-numbers.min-width"]
|
||||
line_number_min_width: usize = 3,
|
||||
/// Line number display: `absolute` simply shows each line's number,
|
||||
/// while `relative` shows the distance from the current line. When
|
||||
/// unfocused or in insert mode, `relative` will still show absolute
|
||||
/// line numbers
|
||||
#[name = "line-number"]
|
||||
line_number_mode: LineNumber = LineNumber::Absolute,
|
||||
}
|
||||
}
|
||||
|
||||
fn count_digits(n: usize) -> usize {
|
||||
(usize::checked_ilog10(n).unwrap_or(0) + 1) as usize
|
||||
}
|
||||
|
Reference in New Issue
Block a user