Compare commits

...

5 Commits

Author SHA1 Message Date
Pascal Kuthe
87266b0a07 replace serde structs with helix-config toml adapter 2024-01-16 14:44:53 +01:00
Pascal Kuthe
dcbe8496b9 remove unused languag.scope option 2024-01-16 14:44:52 +01:00
Pascal Kuthe
fb13130701 migrate language server config to new config system 2024-01-16 14:44:52 +01:00
Pascal Kuthe
7ba8674466 define new config options 2024-01-16 14:44:52 +01:00
Pascal Kuthe
5e74d3c821 config system prototype 2024-01-16 14:44:51 +01:00
35 changed files with 2684 additions and 1322 deletions

34
Cargo.lock generated
View File

@@ -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]]

View File

@@ -2,6 +2,7 @@
resolver = "2"
members = [
"helix-core",
"helix-config",
"helix-view",
"helix-term",
"helix-tui",

28
helix-config/Cargo.toml Normal file
View 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
View 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 {}

View 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())
}

View 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())
}
}

View 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(),
}
}

View 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,
}
}

View 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
View 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
View 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
View 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
View 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", &registry).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", &registry).unwrap();
let line_number: LineNumber = *scope_3.get("line-number");
assert_eq!(line_number, LineNumber::Relative);
scope_2.set("line-number", "abs", &registry).unwrap();
let line_number: LineNumber = *scope_3.get("line-number");
assert_eq!(line_number, LineNumber::Absolute);
}

69
helix-config/src/toml.rs Normal file
View 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)
}
}

View 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
View 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)
}
}

View File

@@ -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 }

View File

@@ -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))
}
}

View File

@@ -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()
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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
View 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
}

View File

@@ -1,4 +1,5 @@
mod client;
mod config;
mod transport;
mod types;

View File

@@ -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"] }

View File

@@ -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
View 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()),
}
}
}

View File

@@ -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,
)?;

View File

@@ -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))

View File

@@ -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" }

View File

@@ -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> {

View File

@@ -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
);

View File

@@ -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
}