Merge remote-tracking branch 'lazytanuki/icons' into feature/icons

pull/11/head
trivernis 1 year ago
commit 82adbb35ab
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG Key ID: DFFFCC2C7A02DB45

@ -86,12 +86,12 @@ jobs:
target: x86_64-pc-windows-msvc
cross: false
# 23.03: build issues
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
# cross: false
# skip_tests: true # x86_64 host can't run aarch64 code
- build: aarch64-macos
os: macos-latest
rust: stable
target: aarch64-apple-darwin
cross: false
skip_tests: true # x86_64 host can't run aarch64 code
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu
@ -114,6 +114,12 @@ jobs:
mkdir -p runtime/grammars/sources
tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources
# The rust-toolchain action ignores rust-toolchain.toml files.
# Removing this before building with cargo ensures that the rust-toolchain
# is considered the same between installation and usage.
- name: Remove the rust-toolchain.toml file
run: rm rust-toolchain.toml
- name: Install ${{ matrix.rust }} toolchain
uses: dtolnay/rust-toolchain@master
with:
@ -249,7 +255,7 @@ jobs:
exe=".exe"
fi
pkgname=helix-$GITHUB_REF_NAME-$platform
mkdir $pkgname
mkdir -p $pkgname
cp $source/LICENSE $source/README.md $pkgname
mkdir $pkgname/contrib
cp -r $source/contrib/completion $pkgname/contrib

21
Cargo.lock generated

@ -450,15 +450,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.27"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-executor"
version = "0.3.27"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
dependencies = [
"futures-core",
"futures-task",
@ -467,15 +467,15 @@ dependencies = [
[[package]]
name = "futures-task"
version = "0.3.27"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
[[package]]
name = "futures-util"
version = "0.3.27"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-core",
"futures-task",
@ -2192,8 +2192,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.9"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d"
dependencies = [
"cc",
"regex",

@ -32,6 +32,3 @@ inherits = "test"
package.helix-core.opt-level = 2
package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

@ -10,6 +10,7 @@
- [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md)
- [Themes](./themes.md)
- [Icons](./icons.md)
- [Key remapping](./remapping.md)
- [Languages](./languages.md)
- [Guides](./guides/README.md)

@ -11,6 +11,7 @@ Example config:
```toml
theme = "onedark"
icons = "nerdfonts"
[editor]
line-number = "relative"
@ -108,6 +109,7 @@ The following statusline elements can be configured:
| `file-line-ending` | The file line endings (CRLF or LF) |
| `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file |
| `file-type-icon` | The icon representing the language of the open file, or else its file type (see `[editor.icons]` section) |
| `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections |
@ -324,6 +326,18 @@ Currently unused
Currently unused
### `[editor.icons]` Section
Option for displaying icons within the editor.
> Warning: some symbols (such as file-type and symbol-kind icons that you would see in the picker) are not available in the "default" icon set. They usually require a patched font such as [NerdFonts](https://www.nerdfonts.com/) to be installed and configured in your terminal emulator, and the corresponding icon set to be configured in the editor (for example, using `icons = "nerdfonts"` in your configuration file).
| Key | Description | Default |
| --- | --- | --- |
| `picker` | Whether icons in pickers are enabled. | `true` |
| `bufferline` | Whether icons in the buffer line are enabled. | `true` |
| `statusline` | Whether icons in the status line are enabled. | `true` |
### `[editor.soft-wrap]` Section
Options for soft wrapping lines that exceed the view width:

@ -59,6 +59,7 @@
| heex | ✓ | ✓ | | `elixir-ls` |
| hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server` |
| hurl | ✓ | | ✓ | |
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
@ -68,7 +69,7 @@
| json | ✓ | | ✓ | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` |
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| julia | ✓ | | ✓ | `julia` |
| julia | ✓ | | ✓ | `julia` |
| kdl | ✓ | | | |
| kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | ✓ | | `texlab` |
@ -116,6 +117,7 @@
| rego | ✓ | | | `regols` |
| rescript | ✓ | ✓ | | `rescript-language-server` |
| rmarkdown | ✓ | | ✓ | `R` |
| robot | ✓ | | | `robotframework_ls` |
| ron | ✓ | | ✓ | |
| rst | ✓ | | | |
| ruby | ✓ | ✓ | ✓ | `solargraph` |

@ -29,6 +29,7 @@
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme (show current theme if no name specified). |
| `:icons` | Change the editor icon flavor (show current flavor if no name specified). |
| `:clipboard-yank` | Yank main selection into system clipboard. |
| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |

@ -0,0 +1,140 @@
# Icons
## Requirements
File-type and symbol-kind icons require a patched font such as [NerdFonts](https://www.nerdfonts.com/) to be installed and configured in your terminal emulator. These types of fonts are called *patched* fonts because they define arbitrary symbols for a range of Unicode values, which may vary from one font to another. Therefore, you need to use an icon flavor adapted to your configured terminal font, otherwise you may end up with undefined characters and mismatched icons.
To enable file-type and symbol-kind icons within the editor, see the `[editor.icons]` section of the [configuration file](./configuration.md).
To use an icon flavor add `icons = "<name>"` to your [`config.toml`](./configuration.md) at the very top of the file before the first section or select it during runtime using `:icons <name>`.
## Creating an icon flavor
Create a file with the name of your icon flavor as file name (i.e `myicons.toml`) and place it in your `icons` directory (i.e `~/.config/helix/icons`). The directory might have to be created beforehand.
The name "default" is reserved for the builtin icons and cannot be overridden by user defined icons.
The name of the icon flavor must be set using the `name` key.
The default icons.toml can be found [here](https://github.com/helix-editor/helix/blob/master/icons.toml), and user submitted icon flavors [here](https://github.com/helix-editor/helix/blob/master/runtime/icons).
Icons flavors have five sections:
- Diagnostics
- Breakpoints
- Diff
- Symbol kinds
- Mime types
Each line in these sections is specified as below:
```toml
key = { icon = "…", color = "#ff0000" }
```
where `key` represents what you want to style, `icon` specifies the character to show as the icon, and `color` specifies the foreground color of the icon. `color` can be omitted to defer to the defaults.
### Diagnostic icons
The `[diagnostic]` section defines four **required** diagnostic icons:
- `error`
- `warning`
- `info`
- `hint`
These icons appear in the gutter, in the diagnostic pickers as well as in the status line diagnostic component.
By default, they have the foreground color defined in the current theme's corresponding keys.
> An icon flavor TOML file must define all of these icons.
### Diff icons
The `[diff]` section defines three **required** diffing icons:
- `added`
- `deleted`
- `modified`
These icons appear in the gutter.
By default, they have the foreground color defined in the current theme's corresponding keys.
> An icon flavor TOML file must define all of these icons.
### Breakpoint icons
The `[breakpoint]` section defines two **required** breakpoint icons:
- `verified`
- `unverified`
These icons appear in the gutter while using the Debug Adapter Protocol (DAP). Their color depends on the breakpoint's condition and log message, it cannot be overridden by the `color` key.
> An icon flavor TOML file must define all of these icons.
### Symbol kinds icons
The `[symbol-kind]` section defines **optional** icons for the following required LSP-defined symbol kinds:
- `file` (this icon is also used on files for which the mime type has not been defined in the next section, as a "generic file" icon)
- `module`
- `namespace`
- `package`
- `class`
- `method`
- `property`
- `field`
- `constructor`
- `enumeration`
- `interface`
- `variable`
- `function`
- `constant`
- `string`
- `number`
- `boolean`
- `array`
- `object`
- `key`
- `null`
- `enum-member`
- `structure`
- `event`
- `operator`
- `type-parameter`
By default, these icons have the same style as the loaded theme's `keyword` key. Their style can be customized using the `symbolkind` key in the theme configuration file, or it can individually be overridden by their `color` key.
> An icon flavor TOML file must define either none or all of these icons.
### Mime types icons
The `[mime-type]` section defines **optional** icons for mime types or filename, such as:
```toml
[mime-type]
".bashrc" = { icon = "…", color = "#…" }
"LICENSE" = { icon = "…", color = "#…" }
"rs" = { icon = "…", color = "#…" }
```
These icons appear in the file picker, in the statusline `file-type-icon` component, and in the bufferline (when enabled).
> An icon flavor TOML file can define none, some or all of these icons.
### Inheritance
Extend upon other icon flavors by setting the `inherits` property to an existing theme.
```toml
inherits = "nerdfonts"
name = "custom_nerdfonts"
# Override the icon for generic files:
[symbol-kind]
file = {icon = "…"}
# Override the icon for Rust files
[mime-type]
"rs" = { icon = "…", color = "#…" }
```

@ -316,14 +316,15 @@ These scopes are used for theming the editor interface:
| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `warning` | Diagnostics warning icon (gutter, statusline, and diagnostic pickers) |
| `error` | Diagnostics error icon (gutter, statusline, and diagnostic pickers) |
| `info` | Diagnostics info icon (gutter, statusline, and diagnostic pickers) |
| `hint` | Diagnostics hint icon (gutter, statusline, and diagnostic pickers) |
| `diagnostic` | Diagnostics fallback style (editing area) |
| `diagnostic.hint` | Diagnostics hint (editing area) |
| `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
| `symbolkind` | Symbol kind icons (symbol picker) |
[editor-section]: ./configuration.md#editor-section

@ -18,9 +18,6 @@
},
"dream2nix": {
"inputs": {
"alejandra": [
"nci"
],
"all-cabal-json": [
"nci"
],
@ -28,6 +25,8 @@
"devshell": [
"nci"
],
"drv-parts": "drv-parts",
"flake-compat": "flake-compat",
"flake-parts": [
"nci",
"parts"
@ -51,6 +50,7 @@
"nci",
"nixpkgs"
],
"nixpkgsV1": "nixpkgsV1",
"poetry2nix": [
"nci"
],
@ -62,11 +62,11 @@
]
},
"locked": {
"lastModified": 1677289985,
"narHash": "sha256-lUp06cTTlWubeBGMZqPl9jODM99LpWMcwxRiscFAUJg=",
"lastModified": 1680258209,
"narHash": "sha256-lEo50RXI/17/a9aCIun8Hz62ZJ5JM5RGeTgclIP+Lgc=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "28b973a8d4c30cc1cbb3377ea2023a76bc3fb889",
"rev": "6f512b5a220fdb26bd3c659f7b55e4f052ec8b35",
"type": "github"
},
"original": {
@ -75,6 +75,54 @@
"type": "github"
}
},
"drv-parts": {
"inputs": {
"flake-compat": [
"nci",
"dream2nix",
"flake-compat"
],
"flake-parts": [
"nci",
"dream2nix",
"flake-parts"
],
"nixpkgs": [
"nci",
"dream2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1680172861,
"narHash": "sha256-QMyI338xRxaHFDlCXdLCtgelGQX2PdlagZALky4ZXJ8=",
"owner": "davhau",
"repo": "drv-parts",
"rev": "ced8a52f62b0a94244713df2225c05c85b416110",
"type": "github"
},
"original": {
"owner": "davhau",
"repo": "drv-parts",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1659877975,
@ -119,11 +167,11 @@
]
},
"locked": {
"lastModified": 1677297103,
"narHash": "sha256-ArlJIbp9NGV9yvhZdV0SOUFfRlI/kHeKoCk30NbSiLc=",
"lastModified": 1680329418,
"narHash": "sha256-+KN0eQLSZvL1J0kDO8/fxv0UCHTyZCADLmpIfeeiSGo=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "a79272a2cb0942392bb3a5bf9a3ec6bc568795b2",
"rev": "98c1d2ff5155f0fee5d290f6b982cb990839d540",
"type": "github"
},
"original": {
@ -134,11 +182,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1677063315,
"narHash": "sha256-qiB4ajTeAOVnVSAwCNEEkoybrAlA+cpeiBxLobHndE8=",
"lastModified": 1680213900,
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "988cc958c57ce4350ec248d2d53087777f9e1949",
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2",
"type": "github"
},
"original": {
@ -151,11 +199,11 @@
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1675183161,
"narHash": "sha256-Zq8sNgAxDckpn7tJo7V1afRSk2eoVbu3OjI1QklGLNg=",
"lastModified": 1678375444,
"narHash": "sha256-XIgHfGvjFvZQ8hrkfocanCDxMefc/77rXeHvYdzBMc8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e1e1b192c1a5aab2960bf0a0bd53a2e8124fa18e",
"rev": "130fa0baaa2b93ec45523fdcde942f6844ee9f6e",
"type": "github"
},
"original": {
@ -166,6 +214,21 @@
"type": "github"
}
},
"nixpkgsV1": {
"locked": {
"lastModified": 1678500271,
"narHash": "sha256-tRBLElf6f02HJGG0ZR7znMNFv/Uf7b2fFInpTHiHaSE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5eb98948b66de29f899c7fe27ae112a47964baf8",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.11",
"type": "indirect"
}
},
"parts": {
"inputs": {
"nixpkgs-lib": [
@ -174,11 +237,11 @@
]
},
"locked": {
"lastModified": 1675933616,
"narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=",
"lastModified": 1679737941,
"narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "47478a4a003e745402acf63be7f9a092d51b83d7",
"rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c",
"type": "github"
},
"original": {
@ -192,11 +255,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1675933616,
"narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=",
"lastModified": 1679737941,
"narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "47478a4a003e745402acf63be7f9a092d51b83d7",
"rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c",
"type": "github"
},
"original": {
@ -221,11 +284,11 @@
]
},
"locked": {
"lastModified": 1677292251,
"narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=",
"lastModified": 1680315536,
"narHash": "sha256-0AsBuKssJMbcRcw4HJQwJsUHhZxR5+gaf6xPQayhR44=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc",
"rev": "5c8c151bdd639074a0051325c16df1a64ee23497",
"type": "github"
},
"original": {

@ -123,8 +123,6 @@
then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
else "$RUSTFLAGS";
in {
# by default NCI adds rust-analyzer component, but helix toolchain doesn't have rust-analyzer
nci.toolchains.shell.components = ["rust-src" "rustfmt" "clippy"];
nci.projects."helix-project".relPath = "";
nci.crates."helix-term" = {
overrides = {

@ -109,7 +109,7 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
/// softwrapping positions are estimated with an O(1) algorithm
/// to ensure consistent performance for large lines (currently unimplemented)
///
/// Usualy you want to use `visual_offset_from_anchor` instead but this function
/// Usually you want to use `visual_offset_from_anchor` instead but this function
/// can be useful (and faster) if
/// * You already know the visual position of the block
/// * You only care about the horizontal offset (column) and not the vertical offset (row)
@ -291,7 +291,7 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize)
///
/// If no (text) grapheme starts at exactly at the specified column the
/// start of the grapheme to the left is returned. If there is no grapheme
/// to the left (for example if the line starts with virtual text) then the positiong
/// to the left (for example if the line starts with virtual text) then the positioning
/// of the next grapheme to the right is returned.
///
/// If the `line` coordinate is beyond the end of the file, the EOF

@ -38,7 +38,7 @@ use std::borrow::Cow;
/// Ranges are considered to be inclusive on the left and
/// exclusive on the right, regardless of anchor-head ordering.
/// This means, for example, that non-zero-width ranges that
/// are directly adjecent, sharing an edge, do not overlap.
/// are directly adjacent, sharing an edge, do not overlap.
/// However, a zero-width range will overlap with the shared
/// left-edge of another range.
///

@ -294,14 +294,14 @@ mod test {
#[test]
fn test_lists() {
let input =
r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#;
r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "quotes"]'"#;
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":set"),
Cow::from("statusline.center"),
Cow::from(r#"["file-type","file-encoding"]"#),
Cow::from(r#"["list", "in", "qoutes"]"#),
Cow::from(r#"["list", "in", "quotes"]"#),
];
assert_eq!(expected, result);
}

@ -555,6 +555,8 @@ impl LanguageConfiguration {
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct SoftWrap {
/// Soft wrap lines that exceed viewport width. Default to off
// NOTE: Option on purpose because the struct is shared between language config and global config.
// By default the option is None so that the language config falls back to the global config unless explicitly set.
pub enable: Option<bool>,
/// Maximum space left free at the end of the line.
/// This space is used to wrap text at word boundaries. If that is not possible within this limit

@ -172,7 +172,7 @@ impl TextAnnotations {
for char_idx in char_range {
if let Some((_, Some(highlight))) = self.overlay_at(char_idx) {
// we don't know the number of chars the original grapheme takes
// however it doesn't matter as highlight bounderies are automatically
// however it doesn't matter as highlight boundaries are automatically
// aligned to grapheme boundaries in the rendering code
highlights.push((highlight.0, char_idx..char_idx + 1))
}
@ -203,7 +203,7 @@ impl TextAnnotations {
/// Add new grapheme overlays.
///
/// The overlayed grapheme will be rendered with `highlight`
/// The overlaid grapheme will be rendered with `highlight`
/// patched on top of `ui.text`.
///
/// The overlays **must be sorted** by their `char_idx`.

@ -1,8 +1,14 @@
pub mod config;
pub mod grammar;
use anyhow::{anyhow, Result};
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::{Path, PathBuf};
use once_cell::sync::Lazy;
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
};
use toml::Value;
pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
@ -154,8 +160,6 @@ pub fn log_file() -> PathBuf {
/// where one usually wants to override or add to the array instead of
/// replacing it altogether.
pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value {
use toml::Value;
fn get_name(v: &Value) -> Option<&str> {
v.get("name").and_then(Value::as_str)
}
@ -209,6 +213,115 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
}
}
/// Recursively load a TOML document, merging with any inherited parent files.
///
/// The paths that have been visited in the inheritance hierarchy are tracked
/// to detect and avoid cycling.
///
/// It is possible for one file to inherit from another file with the same name
/// so long as the second file is in a search directory with lower priority.
/// However, it is not recommended that users do this as it will make tracing
/// errors more difficult.
pub fn load_inheritable_toml(
name: &str,
search_directories: &[PathBuf],
visited_paths: &mut HashSet<PathBuf>,
default_toml_data: &HashMap<&str, &Lazy<Value>>,
merge_toml_docs: fn(Value, Value) -> Value,
) -> Result<Value> {
let path = get_toml_path(name, search_directories, visited_paths)?;
let toml_doc = load_toml(&path)?;
let inherits = toml_doc.get("inherits");
let toml_doc = if let Some(parent_toml_name) = inherits {
let parent_toml_name = parent_toml_name.as_str().ok_or_else(|| {
anyhow!(
"{:?}: expected 'inherits' to be a string: {}",
path,
parent_toml_name
)
})?;
let parent_toml_doc = match default_toml_data.get(parent_toml_name) {
Some(p) => (**p).clone(),
None => load_inheritable_toml(
parent_toml_name,
search_directories,
visited_paths,
default_toml_data,
merge_toml_docs,
)?,
};
merge_toml_docs(parent_toml_doc, toml_doc)
} else {
toml_doc
};
Ok(toml_doc)
}
/// Returns the path to the TOML document with the given name
///
/// Ignores paths already visited and follows directory priority order.
fn get_toml_path(
name: &str,
search_directories: &[PathBuf],
visited_paths: &mut HashSet<PathBuf>,
) -> Result<PathBuf> {
let filename = format!("{}.toml", name);
let mut cycle_found = false; // track if there was a path, but it was in a cycle
search_directories
.iter()
.find_map(|dir| {
let path = dir.join(&filename);
if !path.exists() {
None
} else if visited_paths.contains(&path) {
// Avoiding cycle, continuing to look in lower priority directories
cycle_found = true;
None
} else {
visited_paths.insert(path.clone());
Some(path)
}
})
.ok_or_else(|| {
if cycle_found {
anyhow!("Toml: cycle found in inheriting: {}", name)
} else {
anyhow!("Toml: file not found for: {}", name)
}
})
}
// Loads the TOML data as `toml::Value`
fn load_toml(path: &Path) -> Result<Value> {
let data = std::fs::read_to_string(path)?;
let value = toml::from_str(&data)?;
Ok(value)
}
/// Returns the names of the TOML documents within a directory
pub fn read_toml_names(path: &Path) -> Vec<String> {
std::fs::read_dir(path)
.map(|entries| {
entries
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
(path.extension()? == "toml")
.then(|| path.file_stem().unwrap().to_string_lossy().into_owned())
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod merge_toml_tests {
use std::str;

@ -4,7 +4,7 @@ use crate::{
Call, Error, OffsetEncoding, Result,
};
use helix_core::{find_workspace, ChangeSet, Rope};
use helix_core::{find_workspace, path, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
@ -52,8 +52,8 @@ pub struct Client {
root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>,
workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
initalize_notify: Arc<Notify>,
/// workspace folders added while the server is still initalizing
initialize_notify: Arc<Notify>,
/// workspace folders added while the server is still initializing
req_timeout: u64,
}
@ -66,6 +66,7 @@ impl Client {
may_support_workspace: bool,
) -> bool {
let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::get_normalized_path(&workspace);
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
@ -91,14 +92,14 @@ impl Client {
return true;
}
// this server definitly doesn't support multiple workspace, no need to check capabilities
// this server definitely doesn't support multiple workspace, no need to check capabilities
if !may_support_workspace {
return false;
}
let Some(capabilities) = self.capabilities.get() else {
let client = Arc::clone(self);
// initalization hasn't finished yet, deal with this new root later
// initialization hasn't finished yet, deal with this new root later
// TODO: In the edgecase that a **new root** is added
// for an LSP that **doesn't support workspace_folders** before initaliation is finished
// the new roots are ignored.
@ -107,7 +108,7 @@ impl Client {
// documents LSP client handle. It's doable but a pretty weird edgecase so let's
// wait and see if anyone ever runs into it.
tokio::spawn(async move {
client.initalize_notify.notified().await;
client.initialize_notify.notified().await;
if let Some(workspace_folders_caps) = client
.capabilities()
.workspace
@ -201,6 +202,7 @@ impl Client {
let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id);
let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::get_normalized_path(&workspace);
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
@ -232,7 +234,7 @@ impl Client {
root_path,
root_uri,
workspace_folders: Mutex::new(workspace_folders),
initalize_notify: initialize_notify.clone(),
initialize_notify: initialize_notify.clone(),
};
Ok((client, server_rx, initialize_notify))
@ -277,7 +279,7 @@ impl Client {
"utf-16" => Some(OffsetEncoding::Utf16),
"utf-32" => Some(OffsetEncoding::Utf32),
encoding => {
log::error!("Server provided invalid position encording {encoding}, defaulting to utf-16");
log::error!("Server provided invalid position encoding {encoding}, defaulting to utf-16");
None
},
})

@ -10,7 +10,10 @@ pub use lsp::{Position, Url};
pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration};
use helix_core::{
path,
syntax::{LanguageConfiguration, LanguageServerConfiguration},
};
use tokio::sync::mpsc::UnboundedReceiver;
use std::{
@ -129,7 +132,11 @@ pub mod util {
) -> Option<usize> {
let pos_line = pos.line as usize;
if pos_line > doc.len_lines() - 1 {
return None;
// If it extends past the end, truncate it to the end. This is because the
// way the LSP describes the range including the last newline is by
// specifying a line number after what we would call the last line.
log::warn!("LSP position {pos:?} out of range assuming EOF");
return Some(doc.len_chars());
}
// We need to be careful here to fully comply ith the LSP spec.
@ -145,10 +152,10 @@ pub mod util {
// > \n, \r\n and \r. Positions are line end character agnostic.
// > So you can not specify a position that denotes \r|\n or \n| where | represents the character offset.
//
// This means that while the line must be in bounds the `charater`
// This means that while the line must be in bounds the `character`
// must be capped to the end of the line.
// Note that the end of the line here is **before** the line terminator
// so we must use `line_end_char_index` istead of `doc.line_to_char(pos_line + 1)`
// so we must use `line_end_char_index` instead of `doc.line_to_char(pos_line + 1)`
//
// FIXME: Helix does not fully comply with the LSP spec for line terminators.
// The LSP standard requires that line terminators are ['\n', '\r\n', '\r'].
@ -239,9 +246,20 @@ pub mod util {
pub fn lsp_range_to_range(
doc: &Rope,
range: lsp::Range,
mut range: lsp::Range,
offset_encoding: OffsetEncoding,
) -> Option<Range> {
// This is sort of an edgecase. It's not clear from the spec how to deal with
// ranges where end < start. They don't make much sense but vscode simply caps start to end
// and because it's not specified quite a few LS rely on this as a result (for example the TS server)
if range.start > range.end {
log::error!(
"Invalid LSP range start {:?} > end {:?}, using an empty range at the end instead",
range.start,
range.end
);
range.start = range.end;
}
let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?;
let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?;
@ -875,7 +893,7 @@ fn start_client(
/// * if the file is outside `workspace` return `None`
/// * start at `file` and search the file tree upward
/// * stop the search at the first `root_dirs` entry that contains `file`
/// * if no `root_dirs` matchs `file` stop at workspace
/// * if no `root_dirs` matches `file` stop at workspace
/// * Returns the top most directory that contains a `root_marker`
/// * If no root marker and we stopped at a `root_dirs` entry, return the directory we stopped at
/// * If we stopped at `workspace` instead and `workspace_is_cwd == false` return `None`
@ -888,12 +906,13 @@ pub fn find_lsp_workspace(
workspace_is_cwd: bool,
) -> Option<PathBuf> {
let file = std::path::Path::new(file);
let file = if file.is_absolute() {
let mut file = if file.is_absolute() {
file.to_path_buf()
} else {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
current_dir.join(file)
};
file = path::get_normalized_path(&file);
if !file.starts_with(workspace) {
return None;
@ -910,7 +929,7 @@ pub fn find_lsp_workspace(
if root_dirs
.iter()
.any(|root_dir| root_dir == ancestor.strip_prefix(workspace).unwrap())
.any(|root_dir| path::get_normalized_path(&workspace.join(root_dir)) == ancestor)
{
// if the worskapce is the cwd do not search any higher for workspaces
// but specify
@ -947,16 +966,16 @@ mod tests {
test_case!("", (0, 0) => Some(0));
test_case!("", (0, 1) => Some(0));
test_case!("", (1, 0) => None);
test_case!("", (1, 0) => Some(0));
test_case!("\n\n", (0, 0) => Some(0));
test_case!("\n\n", (1, 0) => Some(1));
test_case!("\n\n", (1, 1) => Some(1));
test_case!("\n\n", (2, 0) => Some(2));
test_case!("\n\n", (3, 0) => None);
test_case!("\n\n", (3, 0) => Some(2));
test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
test_case!("test\n\n\n\ncase", (4, 5) => Some(12));
test_case!("", (u32::MAX, u32::MAX) => None);
test_case!("", (u32::MAX, u32::MAX) => Some(0));
}
#[test]

@ -61,7 +61,7 @@ fn render_elements(
offset: &mut usize,
tabstops: &mut Vec<(usize, (usize, usize))>,
newline_with_offset: &str,
include_placeholer: bool,
include_placeholder: bool,
) {
use SnippetElement::*;
@ -89,7 +89,7 @@ fn render_elements(
offset,
tabstops,
newline_with_offset,
include_placeholer,
include_placeholder,
);
}
&Tabstop { tabstop } => {
@ -100,14 +100,14 @@ fn render_elements(
value: inner_snippet_elements,
} => {
let start_offset = *offset;
if include_placeholer {
if include_placeholder {
render_elements(
inner_snippet_elements,
insert,
offset,
tabstops,
newline_with_offset,
include_placeholer,
include_placeholder,
);
}
tabstops.push((*tabstop, (start_offset, *offset)));
@ -127,7 +127,7 @@ fn render_elements(
pub fn render(
snippet: &Snippet<'_>,
newline_with_offset: &str,
include_placeholer: bool,
include_placeholder: bool,
) -> (Tendril, Vec<SmallVec<[(usize, usize); 1]>>) {
let mut insert = Tendril::new();
let mut tabstops = Vec::new();
@ -139,7 +139,7 @@ pub fn render(
&mut offset,
&mut tabstops,
newline_with_offset,
include_placeholer,
include_placeholder,
);
// sort in ascending order (except for 0, which should always be the last one (per lsp doc))

@ -11,7 +11,7 @@ use helix_view::{
document::DocumentSavedEventResult,
editor::{ConfigEvent, EditorEvent},
graphics::Rect,
theme,
icons, theme,
tree::Layout,
Align, Editor,
};
@ -25,7 +25,7 @@ use crate::{
config::Config,
job::Jobs,
keymap::Keymaps,
ui::{self, overlay::overlayed, Explorer},
ui::{self, overlay::overlaid as overlayed, Explorer},
};
use log::{debug, error, warn};
@ -69,6 +69,7 @@ pub struct Application {
#[allow(dead_code)]
theme_loader: Arc<theme::Loader>,
icons_loader: Arc<icons::Loader>,
#[allow(dead_code)]
syn_loader: Arc<syntax::Loader>,
@ -111,9 +112,9 @@ impl Application {
use helix_view::editor::Action;
let mut theme_parent_dirs = vec![helix_loader::config_dir()];
theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs));
let mut theme_and_icons_parent_dirs = vec![helix_loader::config_dir()];
theme_and_icons_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_and_icons_parent_dirs));
let true_color = config.editor.true_color || crate::true_color();
let theme = config
@ -131,6 +132,21 @@ impl Application {
})
.unwrap_or_else(|| theme_loader.default_theme(true_color));
let icons_loader = std::sync::Arc::new(icons::Loader::new(&theme_and_icons_parent_dirs));
let icons = config
.icons
.as_ref()
.and_then(|icons| {
icons_loader
.load(icons, &theme, true_color)
.map_err(|e| {
log::warn!("failed to load icons `{}` - {}", icons, e);
e
})
.ok()
})
.unwrap_or_else(|| icons_loader.default(&theme));
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
#[cfg(not(feature = "integration"))]
@ -146,12 +162,16 @@ impl Application {
let mut editor = Editor::new(
area,
theme_loader.clone(),
icons_loader.clone(),
syn_loader.clone(),
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.editor
})),
);
editor.set_theme(theme);
editor.set_icons(icons);
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.keys
}));
@ -182,7 +202,7 @@ impl Application {
if first.is_dir() {
std::env::set_current_dir(first).context("set current dir")?;
editor.new_file(Action::VerticalSplit);
let picker = ui::file_picker(".".into(), &config.load().editor);
let picker = ui::file_picker(".".into(), &config.load().editor, &editor.icons);
compositor.push(Box::new(overlayed(picker)));
} else {
let nr_of_files = args.files.len();
@ -239,8 +259,6 @@ impl Application {
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
}
editor.set_theme(theme);
#[cfg(windows)]
let signals = futures_util::stream::empty();
#[cfg(not(windows))]
@ -255,6 +273,7 @@ impl Application {
config,
theme_loader,
icons_loader,
syn_loader,
signals,
@ -427,12 +446,27 @@ impl Application {
Ok(())
}
/// Refresh icons after config change
fn refresh_icons(&mut self, config: &Config) -> Result<(), Error> {
if let Some(icons) = config.icons.clone() {
let true_color = config.editor.true_color || crate::true_color();
let icons = self
.icons_loader
.load(&icons, &self.editor.theme, true_color)
.map_err(|err| anyhow::anyhow!("Failed to load icons `{}`: {}", icons, err))?;
self.editor.set_icons(icons);
}
Ok(())
}
fn refresh_config(&mut self) {
let mut refresh_config = || -> Result<(), Error> {
let default_config = Config::load_default()
.map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?;
self.refresh_language_config()?;
self.refresh_theme(&default_config)?;
self.refresh_icons(&default_config)?;
// Store new config
self.config.store(Arc::new(default_config));
Ok(())

@ -6,7 +6,7 @@ pub use dap::*;
use helix_vcs::Hunk;
pub use lsp::*;
use tokio::sync::oneshot;
use tui::widgets::Row;
use tui::{text::Span, widgets::Row};
pub use typed::*;
use helix_core::{
@ -34,6 +34,7 @@ use helix_view::{
clipboard::ClipboardType,
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion},
icons::Icons,
info::Info,
input::KeyEvent,
keyboard::KeyCode,
@ -54,7 +55,7 @@ use crate::{
job::Callback,
keymap::ReverseKeymap,
ui::{
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlayed, FilePicker, Picker,
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker,
Popup, Prompt, PromptEvent,
},
};
@ -1563,7 +1564,7 @@ fn half_page_down(cx: &mut Context) {
}
#[allow(deprecated)]
// currently uses the deprected `visual_coords_at_pos`/`pos_at_visual_coords` functions
// currently uses the deprecated `visual_coords_at_pos`/`pos_at_visual_coords` functions
// as this function ignores softwrapping (and virtual text) and instead only cares
// about "text visual position"
//
@ -1991,11 +1992,12 @@ fn global_search(cx: &mut Context) {
impl ui::menu::Item for FileResult {
type Data = Option<PathBuf>;
fn format(&self, current_path: &Self::Data) -> Row {
fn format<'a>(&self, current_path: &Self::Data, icons: Option<&'a Icons>) -> Row {
let icon = icons.and_then(|icons| icons.icon_from_path(Some(&self.path)));
let relative_path = helix_core::path::get_relative_path(&self.path)
.to_string_lossy()
.into_owned();
if current_path
let path_span: Span = if current_path
.as_ref()
.map(|p| p == &self.path)
.unwrap_or(false)
@ -2003,6 +2005,12 @@ fn global_search(cx: &mut Context) {
format!("{} (*)", relative_path).into()
} else {
relative_path.into()
};
if let Some(icon) = icon {
Row::new([icon.into(), path_span])
} else {
path_span.into()
}
}
}
@ -2119,6 +2127,7 @@ fn global_search(cx: &mut Context) {
let picker = FilePicker::new(
all_matches,
current_path,
editor.config().icons.picker.then_some(&editor.icons),
move |cx, FileResult { path, line_num }, action| {
match cx.editor.open(path, action) {
Ok(_) => {}
@ -2149,7 +2158,7 @@ fn global_search(cx: &mut Context) {
Some((path.clone().into(), Some((*line_num, *line_num))))
},
);
compositor.push(Box::new(overlayed(picker)));
compositor.push(Box::new(overlaid(picker)));
},
));
Ok(call)
@ -2422,8 +2431,8 @@ fn append_mode(cx: &mut Context) {
fn file_picker(cx: &mut Context) {
let root = find_workspace().0;
let picker = ui::file_picker(root, &cx.editor.config());
cx.push_layer(Box::new(overlayed(picker)));
let picker = ui::file_picker(root, &cx.editor.config(), &cx.editor.icons);
cx.push_layer(Box::new(overlaid(picker)));
}
fn file_picker_in_current_buffer_directory(cx: &mut Context) {
@ -2439,13 +2448,13 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
}
};
let picker = ui::file_picker(path, &cx.editor.config());
cx.push_layer(Box::new(overlayed(picker)));
let picker = ui::file_picker(path, &cx.editor.config(), &cx.editor.icons);
cx.push_layer(Box::new(overlaid(picker)));
}
fn file_picker_in_current_directory(cx: &mut Context) {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./"));
let picker = ui::file_picker(cwd, &cx.editor.config());
cx.push_layer(Box::new(overlayed(picker)));
let picker = ui::file_picker(cwd, &cx.editor.config(), &cx.editor.icons);
cx.push_layer(Box::new(overlaid(picker)));
}
fn open_or_focus_explorer(cx: &mut Context) {
@ -2504,7 +2513,7 @@ fn buffer_picker(cx: &mut Context) {
impl ui::menu::Item for BufferMeta {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row {
let path = self
.path
.as_deref()
@ -2514,6 +2523,9 @@ fn buffer_picker(cx: &mut Context) {
None => SCRATCH_BUFFER_NAME,
};
// Get the filetype icon, or a "file" icon for scratch buffers
let icon = icons.and_then(|icons| icons.icon_from_path(self.path.as_ref()));
let mut flags = String::new();
if self.is_modified {
flags.push('+');
@ -2522,7 +2534,17 @@ fn buffer_picker(cx: &mut Context) {
flags.push('*');
}
Row::new([self.id.to_string(), flags, path.to_string()])
if let Some(icon) = icon {
let icon_span = Span::from(icon);
Row::new(vec![
icon_span,
self.id.to_string().into(),
flags.into(),
path.to_string().into(),
])
} else {
Row::new([self.id.to_string(), flags, path.to_string()])
}
}
}
@ -2540,6 +2562,7 @@ fn buffer_picker(cx: &mut Context) {
.map(|doc| new_meta(doc))
.collect(),
(),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
|cx, meta, action| {
cx.editor.switch(meta.id, action);
},
@ -2553,7 +2576,7 @@ fn buffer_picker(cx: &mut Context) {
Some((meta.id.into(), Some((line, line))))
},
);
cx.push_layer(Box::new(overlayed(picker)));
cx.push_layer(Box::new(overlaid(picker)));
}
fn jumplist_picker(cx: &mut Context) {
@ -2568,7 +2591,10 @@ fn jumplist_picker(cx: &mut Context) {
impl ui::menu::Item for JumpMeta {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row {
// Get the filetype icon, or a "file" icon for scratch buffers
let icon = icons.and_then(|icons| icons.icon_from_path(self.path.as_ref()));
let path = self
.path
.as_deref()
@ -2588,7 +2614,13 @@ fn jumplist_picker(cx: &mut Context) {
} else {
format!(" ({})", flags.join(""))
};
format!("{} {}{} {}", self.id, path, flag, self.text).into()
let path_span: Span = format!("{} {}{} {}", self.id, path, flag, self.text).into();
if let Some(icon) = icon {
Row::new(vec![icon.into(), path_span])
} else {
path_span.into()
}
}
}
@ -2622,6 +2654,7 @@ fn jumplist_picker(cx: &mut Context) {
})
.collect(),
(),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
|cx, meta, action| {
cx.editor.switch(meta.id, action);
let config = cx.editor.config();
@ -2635,13 +2668,13 @@ fn jumplist_picker(cx: &mut Context) {
Some((meta.path.clone()?.into(), Some((line, line))))
},
);
cx.push_layer(Box::new(overlayed(picker)));
cx.push_layer(Box::new(overlaid(picker)));
}
impl ui::menu::Item for MappableCommand {
type Data = ReverseKeymap;
fn format(&self, keymap: &Self::Data) -> Row {
fn format<'a>(&self, keymap: &Self::Data, _icons: Option<&'a Icons>) -> Row {
let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
bindings.iter().fold(String::new(), |mut acc, bind| {
if !acc.is_empty() {
@ -2683,7 +2716,7 @@ pub fn command_palette(cx: &mut Context) {
}
}));
let picker = Picker::new(commands, keymap, move |cx, command, _action| {
let picker = Picker::new(commands, keymap, None, move |cx, command, _action| {
let mut ctx = Context {
register: None,
count: std::num::NonZeroUsize::new(1),
@ -2709,7 +2742,7 @@ pub fn command_palette(cx: &mut Context) {
}
}
});
compositor.push(Box::new(overlayed(picker)));
compositor.push(Box::new(overlaid(picker)));
},
));
}
@ -4230,7 +4263,7 @@ pub fn completion(cx: &mut Context) {
None => return,
};
// setup a chanel that allows the request to be canceled
// setup a channel that allows the request to be canceled
let (tx, rx) = oneshot::channel();
// set completion_request so that this request can be canceled
// by setting completion_request, the old channel stored there is dropped
@ -4283,7 +4316,7 @@ pub fn completion(cx: &mut Context) {
let (view, doc) = current_ref!(editor);
// check if the completion request is stale.
//
// Completions are completed asynchrounsly and therefore the user could
// Completions are completed asynchronously and therefore the user could
//switch document/view or leave insert mode. In all of thoise cases the
// completion should be discarded
if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc {

@ -2,13 +2,13 @@ use super::{Context, Editor};
use crate::{
compositor::{self, Compositor},
job::{Callback, Jobs},
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
ui::{self, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
};
use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
use helix_dap::{self as dap, Client};
use helix_lsp::block_on;
use helix_view::editor::Breakpoint;
use helix_view::{editor::Breakpoint, icons::Icons};
use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream;
@ -25,7 +25,7 @@ use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select
impl ui::menu::Item for StackFrame {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
self.name.as_str().into() // TODO: include thread_states in the label
}
}
@ -33,7 +33,7 @@ impl ui::menu::Item for StackFrame {
impl ui::menu::Item for DebugTemplate {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
self.name.as_str().into()
}
}
@ -41,7 +41,7 @@ impl ui::menu::Item for DebugTemplate {
impl ui::menu::Item for Thread {
type Data = ThreadStates;
fn format(&self, thread_states: &Self::Data) -> Row {
fn format<'a>(&self, thread_states: &Self::Data, _icons: Option<&'a Icons>) -> Row {
format!(
"{} ({})",
self.name,
@ -76,6 +76,7 @@ fn thread_picker(
let picker = FilePicker::new(
threads,
thread_states,
None,
move |cx, thread, _action| callback_fn(cx.editor, thread),
move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
@ -270,9 +271,10 @@ pub fn dap_launch(cx: &mut Context) {
let templates = config.templates.clone();
cx.push_layer(Box::new(overlayed(Picker::new(
cx.push_layer(Box::new(overlaid(Picker::new(
templates,
(),
None,
|cx, template, _action| {
let completions = template.completion.clone();
let name = template.name.clone();
@ -731,6 +733,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let picker = FilePicker::new(
frames,
(),
None,
move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find

@ -3,15 +3,12 @@ use helix_lsp::{
block_on,
lsp::{
self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity,
NumberOrString,
NumberOrString, SymbolKind,
},
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding,
};
use tui::{
text::{Span, Spans},
widgets::Row,
};
use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor, Open};
@ -19,6 +16,7 @@ use helix_core::{path, text_annotations::InlineAnnotation, Selection};
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
editor::Action,
icons::{self, Icon, Icons},
theme::Style,
Document, View,
};
@ -26,7 +24,7 @@ use helix_view::{
use crate::{
compositor::{self, Compositor},
ui::{
self, lsp::SignatureHelp, overlay::overlayed, DynamicPicker, FileLocation, FilePicker,
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
Popup, PromptEvent,
},
};
@ -57,7 +55,7 @@ impl ui::menu::Item for lsp::Location {
/// Current working directory.
type Data = PathBuf;
fn format(&self, cwdir: &Self::Data) -> Row {
fn format<'a>(&self, cwdir: &Self::Data, _icons: Option<&'a Icons>) -> Row {
// The preallocation here will overallocate a few characters since it will account for the
// URL's scheme, which is not used most of the time since that scheme will be "file://".
// Those extra chars will be used to avoid allocating when writing the line number (in the
@ -81,7 +79,7 @@ impl ui::menu::Item for lsp::Location {
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
// is a simple `/` and not `C:\` (with whatever drive letter)
write!(&mut res, ":{}", self.range.start.line)
write!(&mut res, ":{}", self.range.start.line + 1)
.expect("Will only failed if allocating fail");
res.into()
}
@ -91,16 +89,58 @@ impl ui::menu::Item for lsp::SymbolInformation {
/// Path to currently focussed document
type Data = Option<lsp::Url>;
fn format(&self, current_doc_path: &Self::Data) -> Row {
fn format<'a>(&self, current_doc_path: &Self::Data, icons: Option<&'a Icons>) -> Row {
let icon =
icons
.and_then(|icons| icons.symbol_kind.as_ref())
.and_then(|symbol_kind_icons| match self.kind {
SymbolKind::FILE => symbol_kind_icons.get("file"),
SymbolKind::MODULE => symbol_kind_icons.get("module"),
SymbolKind::NAMESPACE => symbol_kind_icons.get("namespace"),
SymbolKind::PACKAGE => symbol_kind_icons.get("package"),
SymbolKind::CLASS => symbol_kind_icons.get("class"),
SymbolKind::METHOD => symbol_kind_icons.get("method"),
SymbolKind::PROPERTY => symbol_kind_icons.get("property"),
SymbolKind::FIELD => symbol_kind_icons.get("field"),
SymbolKind::CONSTRUCTOR => symbol_kind_icons.get("constructor"),
SymbolKind::ENUM => symbol_kind_icons.get("enumeration"),
SymbolKind::INTERFACE => symbol_kind_icons.get("interface"),
SymbolKind::FUNCTION => symbol_kind_icons.get("function"),
SymbolKind::VARIABLE => symbol_kind_icons.get("variable"),
SymbolKind::CONSTANT => symbol_kind_icons.get("constant"),
SymbolKind::STRING => symbol_kind_icons.get("string"),
SymbolKind::NUMBER => symbol_kind_icons.get("number"),
SymbolKind::BOOLEAN => symbol_kind_icons.get("boolean"),
SymbolKind::ARRAY => symbol_kind_icons.get("array"),
SymbolKind::OBJECT => symbol_kind_icons.get("object"),
SymbolKind::KEY => symbol_kind_icons.get("key"),
SymbolKind::NULL => symbol_kind_icons.get("null"),
SymbolKind::ENUM_MEMBER => symbol_kind_icons.get("enum-member"),
SymbolKind::STRUCT => symbol_kind_icons.get("structure"),
SymbolKind::EVENT => symbol_kind_icons.get("event"),
SymbolKind::OPERATOR => symbol_kind_icons.get("operator"),
SymbolKind::TYPE_PARAMETER => symbol_kind_icons.get("type-parameter"),
_ => Some(&icons::BLANK_ICON),
});
if current_doc_path.as_ref() == Some(&self.location.uri) {
self.name.as_str().into()
if let Some(icon) = icon {
Row::new([Span::from(icon), self.name.as_str().into()])
} else {
self.name.as_str().into()
}
} else {
match self.location.uri.to_file_path() {
let symbol_span: Span = match self.location.uri.to_file_path() {
Ok(path) => {
let get_relative_path = path::get_relative_path(path.as_path());
format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
}
Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
};
if let Some(icon) = icon {
Row::new([Span::from(icon), symbol_span])
} else {
Row::from(symbol_span)
}
}
}
@ -121,7 +161,18 @@ struct PickerDiagnostic {
impl ui::menu::Item for PickerDiagnostic {
type Data = (DiagnosticStyles, DiagnosticsFormat);
fn format(&self, (styles, format): &Self::Data) -> Row {
fn format<'a>(&self, (styles, format): &Self::Data, icons: Option<&'a Icons>) -> Row {
let icon: Option<&'a Icon> =
icons
.zip(self.diag.severity)
.map(|(icons, severity)| match severity {
DiagnosticSeverity::ERROR => &icons.diagnostic.error,
DiagnosticSeverity::WARNING => &icons.diagnostic.warning,
DiagnosticSeverity::HINT => &icons.diagnostic.hint,
DiagnosticSeverity::INFORMATION => &icons.diagnostic.info,
_ => &icons::BLANK_ICON,
});
let mut style = self
.diag
.severity
@ -152,12 +203,20 @@ impl ui::menu::Item for PickerDiagnostic {
}
};
Spans::from(vec![
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
.into()
if let Some(icon) = icon {
Row::new(vec![
icon.into(),
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
} else {
Row::new(vec![
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
}
}
}
@ -213,11 +272,13 @@ fn sym_picker(
symbols: Vec<lsp::SymbolInformation>,
current_path: Option<lsp::Url>,
offset_encoding: OffsetEncoding,
editor: &Editor,
) -> FilePicker<lsp::SymbolInformation> {
// TODO: drop current_path comparison and instead use workspace: bool flag?
FilePicker::new(
symbols,
current_path.clone(),
editor.config().icons.picker.then_some(&editor.icons),
move |cx, symbol, action| {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
@ -293,6 +354,7 @@ fn diag_picker(
FilePicker::new(
flat_diag,
(styles, format),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
move |cx, PickerDiagnostic { url, diag }, action| {
if current_path.as_ref() == Some(url) {
let (view, doc) = current!(cx.editor);
@ -371,8 +433,8 @@ pub fn symbol_picker(cx: &mut Context) {
}
};
let picker = sym_picker(symbols, current_url, offset_encoding);
compositor.push(Box::new(overlayed(picker)))
let picker = sym_picker(symbols, current_url, offset_encoding, editor);
compositor.push(Box::new(overlaid(picker)))
}
},
)
@ -394,9 +456,9 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
cx.callback(
future,
move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
move |editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
let symbols = response.unwrap_or_default();
let picker = sym_picker(symbols, current_url, offset_encoding);
let picker = sym_picker(symbols, current_url, offset_encoding, editor);
let get_symbols = |query: String, editor: &mut Editor| {
let doc = doc!(editor);
let language_server = match doc.language_server() {
@ -431,7 +493,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
future.boxed()
};
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
compositor.push(Box::new(overlayed(dyn_picker)))
compositor.push(Box::new(overlaid(dyn_picker)))
},
)
}
@ -454,7 +516,7 @@ pub fn diagnostics_picker(cx: &mut Context) {
DiagnosticsFormat::HideSourcePath,
offset_encoding,
);
cx.push_layer(Box::new(overlayed(picker)));
cx.push_layer(Box::new(overlaid(picker)));
}
}
@ -471,12 +533,12 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) {
DiagnosticsFormat::ShowSourcePath,
offset_encoding,
);
cx.push_layer(Box::new(overlayed(picker)));
cx.push_layer(Box::new(overlaid(picker)));
}
impl ui::menu::Item for lsp::CodeActionOrCommand {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
match self {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
@ -491,7 +553,7 @@ impl ui::menu::Item for lsp::CodeActionOrCommand {
///
/// While the `kind` field is defined as open ended in the LSP spec (any value may be used)
/// in practice a closed set of common values (mostly suggested in the LSP spec) are used.
/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker)
/// VSCode displays each of these categories separately (separated by a heading in the codeactions picker)
/// to make them easier to navigate. Helix does not display these headings to the user.
/// However it does sort code actions by their categories to achieve the same order as the VScode picker,
/// just without the headings.
@ -521,7 +583,7 @@ fn action_category(action: &CodeActionOrCommand) -> u32 {
}
}
fn action_prefered(action: &CodeActionOrCommand) -> bool {
fn action_preferred(action: &CodeActionOrCommand) -> bool {
matches!(
action,
CodeActionOrCommand::CodeAction(CodeAction {
@ -600,12 +662,12 @@ pub fn code_action(cx: &mut Context) {
}
// Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
// Many details are modeled after vscode because langauge servers are usually tested against it.
// Many details are modeled after vscode because language servers are usually tested against it.
// VScode sorts the codeaction two times:
//
// First the codeactions that fix some diagnostics are moved to the front.
// If both codeactions fix some diagnostics (or both fix none) the codeaction
// that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate
// that is marked with `is_preferred` is shown first. The codeactions are then shown in separate
// submenus that only contain a certain category (see `action_category`) of actions.
//
// Below this done in in a single sorting step
@ -627,10 +689,10 @@ pub fn code_action(cx: &mut Context) {
return order;
}
// if one of the codeactions is marked as prefered show it first
// if one of the codeactions is marked as preferred show it first
// otherwise keep the original LSP sorting
action_prefered(action1)
.cmp(&action_prefered(action2))
action_preferred(action1)
.cmp(&action_preferred(action2))
.reverse()
});
@ -672,7 +734,7 @@ pub fn code_action(cx: &mut Context) {
impl ui::menu::Item for lsp::Command {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
self.title.as_str().into()
}
}
@ -950,12 +1012,13 @@ fn goto_impl(
let picker = FilePicker::new(
locations,
cwdir,
None,
move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
},
move |_editor, location| Some(location_to_file_location(location)),
);
compositor.push(Box::new(overlayed(picker)));
compositor.push(Box::new(overlaid(picker)));
}
}
}

@ -115,8 +115,8 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path, &editor.config());
compositor.push(Box::new(overlayed(picker)));
let picker = ui::file_picker(path, &editor.config(), &editor.icons);
compositor.push(Box::new(overlaid(picker)));
},
));
Ok(call)
@ -877,6 +877,30 @@ fn theme(
Ok(())
}
fn icons(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
let true_color = cx.editor.config.load().true_color || crate::true_color();
if let PromptEvent::Validate = event {
if let Some(flavor_name) = args.first() {
let icons = cx
.editor
.icons_loader
.load(flavor_name, &cx.editor.theme, true_color)
.map_err(|err| anyhow!("Could not load icon flavor: {}", err))?;
cx.editor.set_icons(icons);
} else {
let name = cx.editor.icons.name().to_string();
cx.editor.set_status(name);
}
};
Ok(())
}
fn yank_main_selection_to_clipboard(
cx: &mut compositor::Context,
_args: &[Cow<str>],
@ -1356,10 +1380,10 @@ fn lsp_workspace_command(
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), |cx, command, _action| {
let picker = ui::Picker::new(commands, (), None, |cx, command, _action| {
execute_lsp_command(cx.editor, command.clone());
});
compositor.push(Box::new(overlayed(picker)))
compositor.push(Box::new(overlaid(picker)))
},
));
Ok(call)
@ -2149,20 +2173,16 @@ fn reset_diff_change(
let scrolloff = editor.config().scrolloff;
let (view, doc) = current!(editor);
// TODO refactor to use let..else once MSRV is raised to 1.65
let handle = match doc.diff_handle() {
Some(handle) => handle,
None => bail!("Diff is not available in the current buffer"),
let Some(handle) = doc.diff_handle() else {
bail!("Diff is not available in the current buffer")
};
let diff = handle.load();
let doc_text = doc.text().slice(..);
let line = doc.selection(view.id).primary().cursor_line(doc_text);
// TODO refactor to use let..else once MSRV is raised to 1.65
let hunk_idx = match diff.hunk_at(line as u32, true) {
Some(hunk_idx) => hunk_idx,
None => bail!("There is no change at the cursor"),
let Some(hunk_idx) = diff.hunk_at(line as u32, true) else {
bail!("There is no change at the cursor")
};
let hunk = diff.nth_hunk(hunk_idx);
let diff_base = diff.diff_base();
@ -2409,6 +2429,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: theme,
signature: CommandSignature::positional(&[completers::theme]),
},
TypableCommand {
name: "icons",
aliases: &[],
doc: "Change the editor icon flavor (show current flavor if no name specified).",
fun: icons,
signature: CommandSignature::positional(&[completers::icons]),
},
TypableCommand {
name: "clipboard-yank",
aliases: &[],

@ -61,13 +61,14 @@ impl<'a> Context<'a> {
use crate::config::Config;
use arc_swap::{access::Map, ArcSwap};
use helix_core::syntax::{self, Configuration};
use helix_view::theme;
use helix_view::{icons, theme};
use std::sync::Arc;
let config = Arc::new(ArcSwap::from_pointee(Config::default()));
Editor::new(
Rect::new(0, 0, 60, 120),
Arc::new(theme::Loader::new(&[])),
Arc::new(icons::Loader::new(&[])),
Arc::new(syntax::Loader::new(Configuration { language: vec![] })),
Arc::new(Arc::new(Map::new(
Arc::clone(&config),

@ -12,6 +12,7 @@ use toml::de::Error as TomlError;
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub theme: Option<String>,
pub icons: Option<String>,
pub keys: HashMap<Mode, Keymap>,
pub editor: helix_view::editor::Config,
}
@ -20,6 +21,7 @@ pub struct Config {
#[serde(deny_unknown_fields)]
pub struct ConfigRaw {
pub theme: Option<String>,
pub icons: Option<String>,
pub keys: Option<HashMap<Mode, Keymap>>,
pub editor: Option<toml::Value>,
}
@ -28,6 +30,7 @@ impl Default for Config {
fn default() -> Config {
Config {
theme: None,
icons: None,
keys: keymap::default(),
editor: helix_view::editor::Config::default(),
}
@ -86,6 +89,7 @@ impl Config {
Config {
theme: local.theme.or(global.theme),
icons: local.icons.or(global.icons),
keys,
editor,
}
@ -102,6 +106,7 @@ impl Config {
}
Config {
theme: config.theme,
icons: config.icons,
keys,
editor: config.editor.map_or_else(
|| Ok(helix_view::editor::Config::default()),

@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::{
document::SavePoint,
editor::CompleteAction,
icons::Icons,
theme::{Modifier, Style},
ViewId,
};
@ -33,7 +34,8 @@ impl menu::Item for CompletionItem {
.into()
}
fn format(&self, _data: &Self::Data) -> menu::Row {
// Before implementing icons for the `CompletionItemKind`s, something must be done to `Menu::required_size` and `Menu::recalculate_size` in order to have correct sizes even with icons.
fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> menu::Row {
let deprecated = self.deprecated.unwrap_or_default()
|| self.tags.as_ref().map_or(false, |tags| {
tags.contains(&lsp::CompletionItemTag::DEPRECATED)
@ -141,16 +143,12 @@ impl Completion {
}
};
let start_offset =
match util::lsp_pos_to_pos(doc.text(), edit.range.start, offset_encoding) {
Some(start) => start as i128 - primary_cursor as i128,
None => return Transaction::new(doc.text()),
};
let end_offset =
match util::lsp_pos_to_pos(doc.text(), edit.range.end, offset_encoding) {
Some(end) => end as i128 - primary_cursor as i128,
None => return Transaction::new(doc.text()),
};
let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
return Transaction::new(doc.text());
};
let start_offset = range.anchor as i128 - primary_cursor as i128;
let end_offset = range.head as i128 - primary_cursor as i128;
(Some((start_offset, end_offset)), edit.new_text)
} else {

@ -118,7 +118,7 @@ pub fn render_document(
fn translate_positions(
char_pos: usize,
first_visisble_char_idx: usize,
first_visible_char_idx: usize,
translated_positions: &mut [TranslatedPosition],
text_fmt: &TextFormat,
renderer: &mut TextRenderer,
@ -126,7 +126,7 @@ fn translate_positions(
) {
// check if any positions translated on the fly (like cursor) has been reached
for (char_idx, callback) in &mut *translated_positions {
if *char_idx < char_pos && *char_idx >= first_visisble_char_idx {
if *char_idx < char_pos && *char_idx >= first_visible_char_idx {
// by replacing the char_index with usize::MAX large number we ensure
// that the same position is only translated once
// text will never reach usize::MAX as rust memory allocations are limited
@ -259,7 +259,7 @@ pub fn render_text<'t>(
}
}
// aquire the correct grapheme style
// acquire the correct grapheme style
if char_pos >= style_span.1 {
style_span = styles.next().unwrap_or((Style::default(), usize::MAX));
}
@ -404,7 +404,7 @@ impl<'a> TextRenderer<'a> {
let cut_off_start = self.col_offset.saturating_sub(position.col);
let is_whitespace = grapheme.is_whitespace();
// TODO is it correct to apply the whitspace style to all unicode white spaces?
// TODO is it correct to apply the whitespace style to all unicode white spaces?
if is_whitespace {
style = style.patch(self.whitespace_style);
}

@ -533,8 +533,24 @@ impl EditorView {
let mut x = viewport.x;
let current_doc = view!(editor).doc;
let config = editor.config();
let icons_enabled = config.icons.bufferline;
for doc in editor.documents() {
let filetype_icon = doc
.language_config()
.and_then(|config| {
config
.file_types
.iter()
.map(|filetype| match filetype {
helix_core::syntax::FileType::Extension(s) => s,
helix_core::syntax::FileType::Suffix(s) => s,
})
.find_map(|filetype| editor.icons.icon_from_filetype(filetype))
})
.or_else(|| editor.icons.icon_from_path(doc.path()));
let fname = doc
.path()
.unwrap_or(&scratch)
@ -553,6 +569,22 @@ impl EditorView {
let used_width = viewport.x.saturating_sub(x);
let rem_width = surface.area.width.saturating_sub(used_width);
if icons_enabled {
if let Some(icon) = filetype_icon {
x = surface
.set_stringn(
x,
viewport.y,
format!(" {}", icon.icon_char),
rem_width as usize,
match icon.style {
Some(s) => style.patch(s.into()),
None => style,
},
)
.0;
}
}
x = surface
.set_stringn(x, viewport.y, text, rem_width as usize, style)
.0;

@ -54,7 +54,7 @@ impl QueryAtom {
}
fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec<usize>) -> bool {
// for inverse there are no indicies to return
// for inverse there are no indices to return
// just return whether we matched
if self.inverse {
return self.matches(matcher, item);
@ -120,7 +120,7 @@ enum QueryAtomKind {
///
/// Usage: `foo`
Fuzzy,
/// Item contains query atom as a continous substring
/// Item contains query atom as a continuous substring
///
/// Usage `'foo`
Substring,
@ -213,7 +213,7 @@ impl FuzzyQuery {
Some(score)
}
pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else(
|| Some((0, Vec::new())),
|atom| matcher.fuzzy_indices(item, atom),

@ -7,8 +7,8 @@ fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
items
.iter()
.filter_map(|item| {
let (_, indicies) = query.fuzzy_indicies(item, &matcher)?;
let matched_string = indicies
let (_, indices) = query.fuzzy_indices(item, &matcher)?;
let matched_string = indices
.iter()
.map(|&pos| item.chars().nth(pos).unwrap())
.collect();

@ -4,29 +4,29 @@ use crate::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
};
use tui::{buffer::Buffer as Surface, widgets::Table};
use tui::{buffer::Buffer as Surface, text::Span, widgets::Table};
pub use tui::widgets::{Cell, Row};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_view::{graphics::Rect, Editor};
use helix_view::{graphics::Rect, icons::Icons, Editor};
use tui::layout::Constraint;
pub trait Item {
/// Additional editor state that is used for label calculation.
type Data;
fn format(&self, data: &Self::Data) -> Row;
fn format<'a>(&self, data: &Self::Data, icons: Option<&'a Icons>) -> Row;
fn sort_text(&self, data: &Self::Data) -> Cow<str> {
let label: String = self.format(data).cell_text().collect();
let label: String = self.format(data, None).cell_text().collect();
label.into()
}
fn filter_text(&self, data: &Self::Data) -> Cow<str> {
let label: String = self.format(data).cell_text().collect();
let label: String = self.format(data, None).cell_text().collect();
label.into()
}
}
@ -35,11 +35,15 @@ impl Item for PathBuf {
/// Root prefix to strip.
type Data = PathBuf;
fn format(&self, root_path: &Self::Data) -> Row {
self.strip_prefix(root_path)
fn format<'a>(&self, root_path: &Self::Data, icons: Option<&'a Icons>) -> Row {
let path_str = self
.strip_prefix(root_path)
.unwrap_or(self)
.to_string_lossy()
.into()
.to_string_lossy();
match icons.and_then(|icons| icons.icon_from_path(Some(self))) {
Some(icon) => Row::new([icon.into(), Span::raw(path_str)]),
None => path_str.into(),
}
}
}
@ -142,10 +146,10 @@ impl<T: Item> Menu<T> {
let n = self
.options
.first()
.map(|option| option.format(&self.editor_data).cells.len())
.map(|option| option.format(&self.editor_data, None).cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.format(&self.editor_data);
let row = option.format(&self.editor_data, None);
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
@ -331,7 +335,7 @@ impl<T: Item + 'static> Component for Menu<T> {
let rows = options
.iter()
.map(|option| option.format(&self.editor_data));
.map(|option| option.format(&self.editor_data, None));
let table = Table::new(rows)
.style(style)
.highlight_style(selected)

@ -21,7 +21,7 @@ use crate::filter_picker_entry;
use crate::job::{self, Callback};
pub use completion::Completion;
pub use editor::EditorView;
pub use explorer::Explorer;
use helix_view::icons::Icons;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
@ -31,6 +31,7 @@ pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
pub use tree::{TreeOp, TreeView, TreeViewItem};
pub use explorer::Explorer;
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;
use helix_view::Editor;
@ -162,7 +163,11 @@ pub fn regex_prompt(
cx.push_layer(Box::new(prompt));
}
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
pub fn file_picker(
root: PathBuf,
config: &helix_view::editor::Config,
icons: &Icons,
) -> FilePicker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant;
@ -224,6 +229,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
FilePicker::new(
files,
root,
config.icons.picker.then_some(icons),
move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
@ -243,7 +249,7 @@ pub mod completers {
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme;
use helix_view::{editor::Config, Editor};
use once_cell::sync::Lazy;
use std::borrow::Cow;
@ -284,9 +290,9 @@ pub mod completers {
}
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes"));
let mut names = helix_loader::read_toml_names(&helix_loader::config_dir().join("themes"));
for rt_dir in helix_loader::runtime_dirs() {
names.extend(theme::Loader::read_names(&rt_dir.join("themes")));
names.extend(helix_loader::read_toml_names(&rt_dir.join("themes")));
}
names.push("default".into());
names.push("base16_default".into());
@ -315,6 +321,37 @@ pub mod completers {
names
}
pub fn icons(_editor: &Editor, input: &str) -> Vec<Completion> {
let mut names = helix_loader::read_toml_names(&helix_loader::config_dir().join("icons"));
for rt_dir in helix_loader::runtime_dirs() {
names.extend(helix_loader::read_toml_names(&rt_dir.join("icons")));
}
names.push("default".into());
names.sort();
names.dedup();
let mut names: Vec<_> = names
.into_iter()
.map(|name| ((0..), Cow::from(name)))
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
.into_iter()
.filter_map(|(_range, name)| {
matcher.fuzzy_match(&name, input).map(|score| (name, score))
})
.collect();
matches.sort_unstable_by(|(name1, score1), (name2, score2)| {
(Reverse(*score1), name1).cmp(&(Reverse(*score2), name2))
});
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names
}
/// Recursive function to get all keys from this value and add them to vec
fn get_keys(value: &serde_json::Value, vec: &mut Vec<String>, scope: Option<&str>) {
if let Some(map) = value.as_object() {

@ -16,7 +16,7 @@ pub struct Overlay<T> {
}
/// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom
pub fn overlayed<T>(content: T) -> Overlay<T> {
pub fn overlaid<T>(content: T) -> Overlay<T> {
Overlay {
content,
calc_child_size: Box::new(|rect: Rect| rect.overlayed()),

@ -31,6 +31,7 @@ use helix_core::{
use helix_view::{
editor::Action,
graphics::{CursorKind, Margin, Modifier, Rect},
icons::Icons,
theme::Style,
view::ViewPosition,
Document, DocumentId, Editor,
@ -126,11 +127,12 @@ impl<T: Item> FilePicker<T> {
pub fn new(
options: Vec<T>,
editor_data: T::Data,
icons: Option<&'_ Icons>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
let truncate_start = true;
let mut picker = Picker::new(options, editor_data, callback_fn);
let mut picker = Picker::new(options, editor_data, icons, callback_fn);
picker.truncate_start = truncate_start;
Self {
@ -424,12 +426,14 @@ pub struct Picker<T: Item> {
widths: Vec<Constraint>,
callback_fn: PickerCallback<T>,
has_icons: bool,
}
impl<T: Item> Picker<T> {
pub fn new(
options: Vec<T>,
editor_data: T::Data,
icons: Option<&'_ Icons>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
let prompt = Prompt::new(
@ -452,9 +456,10 @@ impl<T: Item> Picker<T> {
callback_fn: Box::new(callback_fn),
completion_height: 0,
widths: Vec::new(),
has_icons: icons.is_some(),
};
picker.calculate_column_widths();
picker.calculate_column_widths(icons);
// scoring on empty input
// TODO: just reuse score()
@ -472,23 +477,23 @@ impl<T: Item> Picker<T> {
picker
}
pub fn set_options(&mut self, new_options: Vec<T>) {
pub fn set_options(&mut self, new_options: Vec<T>, icons: &'_ Icons) {
self.options = new_options;
self.cursor = 0;
self.force_score();
self.calculate_column_widths();
self.calculate_column_widths(self.has_icons.then_some(icons));
}
/// Calculate the width constraints using the maximum widths of each column
/// for the current options.
fn calculate_column_widths(&mut self) {
fn calculate_column_widths(&mut self, icons: Option<&'_ Icons>) {
let n = self
.options
.first()
.map(|option| option.format(&self.editor_data).cells.len())
.map(|option| option.format(&self.editor_data, icons).cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.format(&self.editor_data);
let row = option.format(&self.editor_data, icons);
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
@ -779,7 +784,12 @@ impl<T: Item + 'static> Component for Picker<T> {
.skip(offset)
.take(rows as usize)
.map(|pmatch| &self.options[pmatch.index])
.map(|option| option.format(&self.editor_data))
.map(|option| {
option.format(
&self.editor_data,
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
)
})
.map(|mut row| {
const TEMP_CELL_SEP: &str = " ";
@ -794,7 +804,7 @@ impl<T: Item + 'static> Component for Picker<T> {
// might be inconsistencies. This is the best we can do since only the
// text in Row is displayed to the end user.
let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
.fuzzy_indicies(&line, &self.matcher)
.fuzzy_indices(&line, &self.matcher)
.unwrap_or_default();
let highlight_byte_ranges: Vec<_> = line
@ -953,7 +963,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
Some(overlay) => &mut overlay.content.file_picker.picker,
None => return,
};
picker.set_options(new_options);
picker.set_options(new_options, &editor.icons);
editor.reset_idle_timer();
}));
anyhow::Ok(callback)

@ -4,6 +4,7 @@ use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect,
icons::Icon,
theme::Style,
Document, Editor, View,
};
@ -21,6 +22,7 @@ pub struct RenderContext<'a> {
pub focused: bool,
pub spinners: &'a ProgressSpinners,
pub parts: RenderBuffer<'a>,
pub icons: RenderContextIcons<'a>,
}
impl<'a> RenderContext<'a> {
@ -31,6 +33,25 @@ impl<'a> RenderContext<'a> {
focused: bool,
spinners: &'a ProgressSpinners,
) -> Self {
// Determine icon based on language name if possible
let mut filetype_icon = None;
if let Some(language_config) = doc.language_config() {
for filetype in &language_config.file_types {
let filetype_str = match filetype {
helix_core::syntax::FileType::Extension(s) => s,
helix_core::syntax::FileType::Suffix(s) => s,
};
filetype_icon = editor.icons.icon_from_filetype(filetype_str);
if filetype_icon.is_some() {
break;
}
}
}
// Otherwise based on filetype
if filetype_icon.is_none() {
filetype_icon = editor.icons.icon_from_path(doc.path())
}
RenderContext {
editor,
doc,
@ -38,10 +59,21 @@ impl<'a> RenderContext<'a> {
focused,
spinners,
parts: RenderBuffer::default(),
icons: RenderContextIcons {
enabled: editor.config().icons.statusline,
filetype_icon,
vcs_icon: editor.icons.ui.as_ref().and_then(|ui| ui.get("vcs_branch")),
},
}
}
}
pub struct RenderContextIcons<'a> {
pub enabled: bool,
pub filetype_icon: Option<&'a Icon>,
pub vcs_icon: Option<&'a Icon>,
}
#[derive(Default)]
pub struct RenderBuffer<'a> {
pub left: Spans<'a>,
@ -148,6 +180,7 @@ where
helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type,
helix_view::editor::StatusLineElement::FileTypeIcon => render_file_type_icon,
helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics,
helix_view::editor::StatusLineElement::WorkspaceDiagnostics => render_workspace_diagnostics,
helix_view::editor::StatusLineElement::Selections => render_selections,
@ -240,7 +273,13 @@ where
if warnings > 0 {
write(
context,
"●".to_string(),
context
.editor
.icons
.diagnostic
.warning
.icon_char
.to_string(),
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
@ -249,7 +288,7 @@ where
if errors > 0 {
write(
context,
"●".to_string(),
context.editor.icons.diagnostic.error.icon_char.to_string(),
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
@ -282,7 +321,13 @@ where
if warnings > 0 {
write(
context,
"●".to_string(),
context
.editor
.icons
.diagnostic
.warning
.icon_char
.to_string(),
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
@ -291,7 +336,7 @@ where
if errors > 0 {
write(
context,
"●".to_string(),
context.editor.icons.diagnostic.error.icon_char.to_string(),
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
@ -412,6 +457,21 @@ where
write(context, format!(" {} ", file_type), None);
}
fn render_file_type_icon<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
if context.icons.enabled {
if let Some(icon) = context.icons.filetype_icon {
write(
context,
format!("{}", icon.icon_char),
icon.style.map(|icons_style| icons_style.into()),
)
}
}
}
fn render_file_name<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
@ -482,11 +542,12 @@ fn render_version_control<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let head = context
.doc
.version_control_head()
.unwrap_or_default()
.to_string();
let head = context.doc.version_control_head().unwrap_or_default();
write(context, head, None);
if !head.is_empty() && context.icons.enabled {
if let Some(vcs_icon) = context.icons.vcs_icon {
return write(context, format!("{} {head}", vcs_icon.icon_char), None);
}
}
write(context, head.to_string(), None);
}

@ -391,7 +391,7 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")]
async fn cursor_position_append_eof() -> anyhow::Result<()> {
// Selection is fowards
// Selection is forwards
test((
"#[foo|]#",
"abar<esc>",

@ -344,9 +344,9 @@ impl ModifierDiff {
}
}
/// Crossterm uses semicolon as a seperator for colors
/// this is actually not spec compliant (altough commonly supported)
/// However the correct approach is to use colons as a seperator.
/// Crossterm uses semicolon as a separator for colors
/// this is actually not spec compliant (although commonly supported)
/// However the correct approach is to use colons as a separator.
/// This usually doesn't make a difference for emulators that do support colored underlines.
/// However terminals that do not support colored underlines will ignore underlines colors with colons
/// while escape sequences with semicolons are always processed which leads to weird visual artifacts.

@ -49,6 +49,7 @@
use helix_core::line_ending::str_is_line_ending;
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::Style;
use helix_view::icons::Icon;
use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
@ -208,6 +209,15 @@ impl<'a> From<Cow<'a, str>> for Span<'a> {
}
}
impl<'a, 'b> From<&'b Icon> for Span<'a> {
fn from(icon: &'b Icon) -> Self {
Span {
content: format!("{}", icon.icon_char).into(),
style: icon.style.unwrap_or_default().into(),
}
}
}
/// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);

@ -3,6 +3,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode},
graphics::{CursorKind, Rect},
icons::{self, Icons},
info::Info,
input::KeyEvent,
theme::{self, Theme},
@ -235,6 +236,27 @@ impl Default for ExplorerConfig {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct IconsConfig {
/// Enables icons in front of buffer names in bufferline. Defaults to `true`
pub bufferline: bool,
/// Enables icons in front of items in the picker. Defaults to `true`
pub picker: bool,
/// Enables icons in front of items in the statusline. Defaults to `true`
pub statusline: bool,
}
impl Default for IconsConfig {
fn default() -> Self {
Self {
bufferline: true,
picker: true,
statusline: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config {
@ -310,6 +332,8 @@ pub struct Config {
pub soft_wrap: SoftWrap,
/// Workspace specific lsp ceiling dirs
pub workspace_lsp_roots: Vec<PathBuf>,
/// Icons configuration
pub icons: IconsConfig,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -478,6 +502,9 @@ pub enum StatusLineElement {
/// The file type (language ID or "text")
FileType,
/// The file type icon (from file path)
FileTypeIcon,
/// A summary of the number of errors and warnings
Diagnostics,
@ -780,6 +807,7 @@ impl Default for Config {
text_width: 80,
completion_replace: false,
workspace_lsp_roots: Vec::new(),
icons: IconsConfig::default(),
}
}
}
@ -856,6 +884,8 @@ pub struct Editor {
/// The currently applied editor theme. While previewing a theme, the previewed theme
/// is set here.
pub theme: Theme,
pub icons: Icons,
pub icons_loader: Arc<icons::Loader>,
/// The primary Selection prior to starting a goto_line_number preview. This is
/// restored when the preview is aborted, or added to the jumplist when it is
@ -966,11 +996,14 @@ impl Editor {
pub fn new(
mut area: Rect,
theme_loader: Arc<theme::Loader>,
icons_loader: Arc<icons::Loader>,
syn_loader: Arc<syntax::Loader>,
config: Arc<dyn DynAccess<Config>>,
) -> Self {
let conf = config.load();
let auto_pairs = (&conf.auto_pairs).into();
let theme = theme_loader.default();
let icons = icons_loader.default(&theme);
// HAXX: offset the render area height by 1 to account for prompt/commandline
area.height -= 1;
@ -1013,6 +1046,8 @@ impl Editor {
needs_redraw: false,
cursor_cache: Cell::new(None),
completion_request_handle: None,
icons,
icons_loader,
}
}
@ -1113,6 +1148,9 @@ impl Editor {
}
ThemeAction::Set => {
self.last_theme = None;
// Reload the icons to apply default colors based on theme
self.icons.set_diagnostic_icons_base_style(&theme);
self.icons.set_symbolkind_icons_base_style(&theme);
self.theme = theme;
}
}
@ -1120,6 +1158,11 @@ impl Editor {
self._refresh();
}
pub fn set_icons(&mut self, icons: Icons) {
self.icons = icons;
self._refresh();
}
/// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
self.launch_language_server(doc_id)

@ -45,7 +45,7 @@ impl GutterType {
}
pub fn diagnostic<'doc>(
_editor: &'doc Editor,
editor: &'doc Editor,
doc: &'doc Document,
_view: &View,
theme: &Theme,
@ -76,7 +76,13 @@ pub fn diagnostic<'doc>(
// This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search.
let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap();
write!(out, "●").unwrap();
let diagnostic_icon = match diagnostic.severity {
Some(Severity::Error) => &editor.icons.diagnostic.error,
Some(Severity::Warning) | None => &editor.icons.diagnostic.warning,
Some(Severity::Info) => &editor.icons.diagnostic.info,
Some(Severity::Hint) => &editor.icons.diagnostic.hint,
};
write!(out, "{}", diagnostic_icon.icon_char).unwrap();
return Some(match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
@ -90,19 +96,20 @@ pub fn diagnostic<'doc>(
}
pub fn diff<'doc>(
_editor: &'doc Editor,
editor: &'doc Editor,
doc: &'doc Document,
_view: &View,
theme: &Theme,
_is_focused: bool,
) -> GutterFn<'doc> {
let added = theme.get("diff.plus");
let deleted = theme.get("diff.minus");
let modified = theme.get("diff.delta");
if let Some(diff_handle) = doc.diff_handle() {
let added = theme.get("diff.plus");
let deleted = theme.get("diff.minus");
let modified = theme.get("diff.delta");
let hunks = diff_handle.load();
let mut hunk_i = 0;
let mut hunk = hunks.nth_hunk(hunk_i);
let icons = &editor.icons;
Box::new(
move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
// truncating the line is fine here because we don't compute diffs
@ -122,18 +129,18 @@ pub fn diff<'doc>(
}
let (icon, style) = if hunk.is_pure_insertion() {
("▍", added)
(&icons.diff.added, added)
} else if hunk.is_pure_removal() {
if !first_visual_line {
return None;
}
("▔", deleted)
(&icons.diff.deleted, deleted)
} else {
("▍", modified)
(&icons.diff.modified, modified)
};
write!(out, "{}", icon).unwrap();
Some(style)
write!(out, "{}", icon.icon_char).unwrap();
icon.style.map(|i| i.into()).or(Some(style))
},
)
} else {
@ -275,7 +282,11 @@ pub fn breakpoints<'doc>(
breakpoint_style
};
let sym = if breakpoint.verified { "●" } else { "◯" };
let sym = if breakpoint.verified {
editor.icons.breakpoint.verified.icon_char
} else {
editor.icons.breakpoint.unverified.icon_char
};
write!(out, "{}", sym).unwrap();
Some(style)
},
@ -310,7 +321,7 @@ fn execution_pause_indicator<'doc>(
return None;
}
let sym = "▶";
let sym = editor.icons.breakpoint.pause_indicator.icon_char;
write!(out, "{}", sym).unwrap();
Some(style)
},

@ -0,0 +1,297 @@
use helix_loader::merge_toml_values;
use log::warn;
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::{path::PathBuf, str};
use toml::Value;
use crate::graphics::{Color, Style};
use crate::Theme;
pub static BLANK_ICON: Icon = Icon {
icon_char: ' ',
style: None,
};
/// The style of an icon can either be defined by the TOML file, or by the theme.
/// We need to remember that in order to reload the icons colors when the theme changes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconStyle {
Custom(Style),
Default(Style),
}
impl Default for IconStyle {
fn default() -> Self {
IconStyle::Default(Style::default())
}
}
impl From<IconStyle> for Style {
fn from(icon_style: IconStyle) -> Self {
match icon_style {
IconStyle::Custom(style) => style,
IconStyle::Default(style) => style,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Icon {
#[serde(rename = "icon")]
pub icon_char: char,
#[serde(default)]
#[serde(deserialize_with = "icon_color_to_style", rename = "color")]
pub style: Option<IconStyle>,
}
impl Icon {
/// Loads a given style if the icon style is undefined or based on a default value
pub fn with_default_style(&mut self, style: Style) {
if self.style.is_none() || matches!(self.style, Some(IconStyle::Default(_))) {
self.style = Some(IconStyle::Default(style));
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Icons {
pub name: String,
pub mime_type: Option<HashMap<String, Icon>>,
pub diagnostic: Diagnostic,
pub symbol_kind: Option<HashMap<String, Icon>>,
pub breakpoint: Breakpoint,
pub diff: Diff,
pub ui: Option<HashMap<String, Icon>>,
}
impl Icons {
pub fn name(&self) -> &str {
&self.name
}
/// Set theme defined styles to diagnostic icons
pub fn set_diagnostic_icons_base_style(&mut self, theme: &Theme) {
self.diagnostic.error.with_default_style(theme.get("error"));
self.diagnostic.info.with_default_style(theme.get("info"));
self.diagnostic.hint.with_default_style(theme.get("hint"));
self.diagnostic
.warning
.with_default_style(theme.get("warning"));
}
/// Set theme defined styles to symbol-kind icons
pub fn set_symbolkind_icons_base_style(&mut self, theme: &Theme) {
let style = theme
.try_get("symbolkind")
.unwrap_or_else(|| theme.get("keyword"));
if let Some(symbol_kind_icons) = &mut self.symbol_kind {
for (_, icon) in symbol_kind_icons.iter_mut() {
icon.with_default_style(style);
}
}
}
/// Set the default style for all icons
pub fn reset_styles(&mut self) {
if let Some(mime_type_icons) = &mut self.mime_type {
for (_, icon) in mime_type_icons.iter_mut() {
icon.style = Some(IconStyle::Default(Style::default()));
}
}
if let Some(symbol_kind_icons) = &mut self.symbol_kind {
for (_, icon) in symbol_kind_icons.iter_mut() {
icon.style = Some(IconStyle::Default(Style::default()));
}
}
if let Some(ui_icons) = &mut self.ui {
for (_, icon) in ui_icons.iter_mut() {
icon.style = Some(IconStyle::Default(Style::default()));
}
}
self.diagnostic.error.style = Some(IconStyle::Default(Style::default()));
self.diagnostic.warning.style = Some(IconStyle::Default(Style::default()));
self.diagnostic.hint.style = Some(IconStyle::Default(Style::default()));
self.diagnostic.info.style = Some(IconStyle::Default(Style::default()));
}
pub fn icon_from_filetype<'a>(&'a self, filetype: &str) -> Option<&'a Icon> {
if let Some(mime_type_icons) = &self.mime_type {
mime_type_icons.get(filetype)
} else {
None
}
}
/// Try to return a reference to an appropriate icon for the specified file path, with a default "file" icon if none is found.
/// If no such "file" icon is available, return `None`.
pub fn icon_from_path<'a>(&'a self, filepath: Option<&PathBuf>) -> Option<&'a Icon> {
self.mime_type
.as_ref()
.and_then(|mime_type_icons| {
filepath?
.extension()
.or(filepath?.file_name())
.map(|extension_or_filename| extension_or_filename.to_str())?
.and_then(|extension_or_filename| mime_type_icons.get(extension_or_filename))
})
.or_else(|| self.ui.as_ref().and_then(|ui_icons| ui_icons.get("file")))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Diagnostic {
pub error: Icon,
pub warning: Icon,
pub info: Icon,
pub hint: Icon,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Breakpoint {
pub verified: Icon,
pub unverified: Icon,
pub pause_indicator: Icon,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Diff {
pub added: Icon,
pub deleted: Icon,
pub modified: Icon,
}
fn icon_color_to_style<'de, D>(deserializer: D) -> Result<Option<IconStyle>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let mut style = Style::default();
if !s.is_empty() {
match hex_string_to_rgb(&s) {
Ok(c) => {
style = style.fg(c);
}
Err(e) => {
log::error!("{}", e);
}
};
Ok(Some(IconStyle::Custom(style)))
} else {
Ok(None)
}
}
pub fn hex_string_to_rgb(s: &str) -> Result<Color, String> {
if s.starts_with('#') && s.len() >= 7 {
if let (Ok(red), Ok(green), Ok(blue)) = (
u8::from_str_radix(&s[1..3], 16),
u8::from_str_radix(&s[3..5], 16),
u8::from_str_radix(&s[5..7], 16),
) {
return Ok(Color::Rgb(red, green, blue));
}
}
Err(format!("Icon color: malformed hexcode: {}", s))
}
pub struct Loader {
/// Icons directories to search from highest to lowest priority
icons_dirs: Vec<PathBuf>,
}
pub static DEFAULT_ICONS_DATA: Lazy<Value> = Lazy::new(|| {
let bytes = include_bytes!("../../icons.toml");
toml::from_str(str::from_utf8(bytes).unwrap()).expect("Failed to parse base 16 default theme")
});
pub static DEFAULT_ICONS: Lazy<Icons> = Lazy::new(|| Icons {
name: "default".into(),
..Icons::from(DEFAULT_ICONS_DATA.clone())
});
impl Loader {
/// Creates a new loader that can load icons flavors from two directories.
pub fn new(dirs: &[PathBuf]) -> Self {
Self {
icons_dirs: dirs.iter().map(|p| p.join("icons")).collect(),
}
}
/// Loads icons flavors first looking in the `user_dir` then in `default_dir`.
/// The `theme` is needed in order to load default styles for diagnostic icons.
pub fn load(
&self,
name: &str,
theme: &Theme,
true_color: bool,
) -> Result<Icons, anyhow::Error> {
if name == "default" {
return Ok(self.default(theme));
}
let mut visited_paths = HashSet::new();
let default_icons = HashMap::from([("default", &DEFAULT_ICONS_DATA)]);
let mut icons = helix_loader::load_inheritable_toml(
name,
&self.icons_dirs,
&mut visited_paths,
&default_icons,
Self::merge_icons,
)
.map(Icons::from)?;
// Remove all styles when there is no truecolor support.
// Not classy, but less cumbersome than trying to pass a parameter to a deserializer.
if !true_color {
icons.reset_styles();
} else {
icons.set_diagnostic_icons_base_style(theme);
icons.set_symbolkind_icons_base_style(theme);
}
Ok(Icons {
name: name.into(),
..icons
})
}
fn merge_icons(parent: Value, child: Value) -> Value {
merge_toml_values(parent, child, 3)
}
/// Returns the default icon flavor.
/// The `theme` is needed in order to load default styles for diagnostic icons.
pub fn default(&self, theme: &Theme) -> Icons {
let mut icons = DEFAULT_ICONS.clone();
icons.set_diagnostic_icons_base_style(theme);
icons.set_symbolkind_icons_base_style(theme);
icons
}
}
impl From<Value> for Icons {
fn from(value: Value) -> Self {
if let Value::Table(mut table) = value {
// remove inherits from value to prevent errors
table.remove("inherits");
let toml_str = table.to_string();
match toml::from_str(&toml_str) {
Ok(icons) => icons,
Err(e) => {
log::error!("Failed to load icons, falling back to default: {}\n", e);
DEFAULT_ICONS.clone()
}
}
} else {
warn!("Expected icons TOML value to be a table, found {:?}", value);
DEFAULT_ICONS.clone()
}
}
}

@ -12,6 +12,7 @@ pub mod handlers {
pub mod lsp;
}
pub mod base64;
pub mod icons;
pub mod info;
pub mod input;
pub mod keyboard;

@ -1,10 +1,10 @@
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
path::PathBuf,
str,
};
use anyhow::{anyhow, Result};
use anyhow::Result;
use helix_core::hashmap;
use helix_loader::merge_toml_values;
use log::warn;
@ -61,7 +61,18 @@ impl Loader {
}
let mut visited_paths = HashSet::new();
let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?;
let default_themes = HashMap::from([
("default", &DEFAULT_THEME_DATA),
("base16_default", &BASE16_DEFAULT_THEME_DATA),
]);
let theme = helix_loader::load_inheritable_toml(
name,
&self.theme_dirs,
&mut visited_paths,
&default_themes,
Self::merge_themes,
)
.map(Theme::from)?;
Ok(Theme {
name: name.into(),
@ -69,66 +80,12 @@ impl Loader {
})
}
/// Recursively load a theme, merging with any inherited parent themes.
///
/// The paths that have been visited in the inheritance hierarchy are tracked
/// to detect and avoid cycling.
///
/// It is possible for one file to inherit from another file with the same name
/// so long as the second file is in a themes directory with lower priority.
/// However, it is not recommended that users do this as it will make tracing
/// errors more difficult.
fn load_theme(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<Value> {
let path = self.path(name, visited_paths)?;
let theme_toml = self.load_toml(path)?;
let inherits = theme_toml.get("inherits");
let theme_toml = if let Some(parent_theme_name) = inherits {
let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| {
anyhow!(
"Theme: expected 'inherits' to be a string: {}",
parent_theme_name
)
})?;
let parent_theme_toml = match parent_theme_name {
// load default themes's toml from const.
"default" => DEFAULT_THEME_DATA.clone(),
"base16_default" => BASE16_DEFAULT_THEME_DATA.clone(),
_ => self.load_theme(parent_theme_name, visited_paths)?,
};
self.merge_themes(parent_theme_toml, theme_toml)
} else {
theme_toml
};
Ok(theme_toml)
}
pub fn read_names(path: &Path) -> Vec<String> {
std::fs::read_dir(path)
.map(|entries| {
entries
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
(path.extension()? == "toml")
.then(|| path.file_stem().unwrap().to_string_lossy().into_owned())
})
.collect()
})
.unwrap_or_default()
}
// merge one theme into the parent theme
fn merge_themes(&self, parent_theme_toml: Value, theme_toml: Value) -> Value {
fn merge_themes(parent_theme_toml: Value, theme_toml: Value) -> Value {
let parent_palette = parent_theme_toml.get("palette");
let palette = theme_toml.get("palette");
// handle the table seperately since it needs a `merge_depth` of 2
// handle the table separately since it needs a `merge_depth` of 2
// this would conflict with the rest of the theme merge strategy
let palette_values = match (parent_palette, palette) {
(Some(parent_palette), Some(palette)) => {
@ -149,45 +106,6 @@ impl Loader {
merge_toml_values(theme, palette.into(), 1)
}
// Loads the theme data as `toml::Value`
fn load_toml(&self, path: PathBuf) -> Result<Value> {
let data = std::fs::read_to_string(path)?;
let value = toml::from_str(&data)?;
Ok(value)
}
/// Returns the path to the theme with the given name
///
/// Ignores paths already visited and follows directory priority order.
fn path(&self, name: &str, visited_paths: &mut HashSet<PathBuf>) -> Result<PathBuf> {
let filename = format!("{}.toml", name);
let mut cycle_found = false; // track if there was a path, but it was in a cycle
self.theme_dirs
.iter()
.find_map(|dir| {
let path = dir.join(&filename);
if !path.exists() {
None
} else if visited_paths.contains(&path) {
// Avoiding cycle, continuing to look in lower priority directories
cycle_found = true;
None
} else {
visited_paths.insert(path.clone());
Some(path)
}
})
.ok_or_else(|| {
if cycle_found {
anyhow!("Theme: cycle found in inheriting: {}", name)
} else {
anyhow!("Theme: file not found for: {}", name)
}
})
}
pub fn default_theme(&self, true_color: bool) -> Theme {
if true_color {
self.default()

@ -0,0 +1,19 @@
name = "default"
# All icons here must be available as [default Unicode characters](https://en.wikipedia.org/wiki/List_of_Unicode_characters)
[diagnostic]
error = {icon = "●"}
warning = {icon = "●"}
info = {icon = "●"}
hint = {icon = "●"}
[breakpoint]
verified = {icon = "●"}
unverified = {icon = "◯"}
pause-indicator = {icon = "▶"}
[diff]
added = {icon = "▍"}
deleted = {icon = "▔"}
modified = {icon = "▍"}

@ -212,7 +212,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd
name = "cpp"
scope = "source.cpp"
injection-regex = "cpp"
file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H"]
file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
roots = []
comment-token = "//"
language-server = { command = "clangd" }
@ -606,7 +606,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "ruby"
source = { git = "https://github.com/tree-sitter/tree-sitter-ruby", rev = "4c600a463d97e36a0ca5ac57e11f3ac8c297a0fa" }
source = { git = "https://github.com/tree-sitter/tree-sitter-ruby", rev = "206c7077164372c596ffa8eaadb9435c28941364" }
[[language]]
name = "bash"
@ -1437,6 +1437,20 @@ comment-token = "//"
indent = { tab-width = 4, unit = " " }
grammar = "rust"
[[language]]
name = "robot"
scope = "source.robot"
injection-regex = "robot"
file-types = ["robot", "resource"]
comment-token = "#"
roots = []
indent = { tab-width = 4, unit = " " }
language-server = { command = "robotframework_ls" }
[[grammar]]
name = "robot"
source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "f1142bfaa6acfce95e25d2c6d18d218f4f533927" }
[[language]]
name = "r"
scope = "source.r"
@ -1446,7 +1460,7 @@ shebangs = ["r", "R"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "R", args = ["--slave", "-e", "languageserver::run()"] }
language-server = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
[[grammar]]
name = "r"
@ -1545,6 +1559,7 @@ file-types = ["gd"]
shebangs = []
roots = ["project.godot"]
auto-format = true
formatter = { command = "gdformat", args = ["-"] }
comment-token = "#"
indent = { tab-width = 4, unit = "\t" }
@ -2068,7 +2083,7 @@ source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d1
name = "xml"
scope = "source.xml"
injection-regex = "xml"
file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg"]
file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg", "xsd"]
indent = { tab-width = 2, unit = " " }
roots = []
@ -2421,3 +2436,17 @@ language-server = { command = "nimlangserver" }
[[grammar]]
name = "nim"
source = { git = "https://github.com/aMOPel/tree-sitter-nim", rev = "240239b232550e431d67de250d1b5856209e7f06" }
[[language]]
name = "hurl"
scope = "source.hurl"
injection-regex = "hurl"
file-types = ["hurl"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "hurl"
source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "264c42064b61ee21abe88d0061f29a0523352e22" }

@ -0,0 +1,285 @@
name = "nerdfonts"
[diagnostic]
error = {icon = ""}
warning = {icon = ""}
info = {icon = ""}
hint = {icon = ""}
[breakpoint]
verified = {icon = "●"}
unverified = {icon = "◯"}
pause-indicator = {icon = "▶"}
[diff]
added = {icon = "▍"}
deleted = {icon = "▔"}
modified = {icon = "▍"}
[symbol-kind]
file = {icon = ""}
module = {icon = ""}
namespace = {icon = ""}
package = {icon = ""}
class = {icon = "ﴯ"}
method = {icon = ""}
property = {icon = ""}
field = {icon = ""}
constructor = {icon = ""}
enumeration = {icon = ""}
interface = {icon = ""}
variable = {icon = ""}
function = {icon = ""}
constant = {icon = ""}
string = {icon = ""}
number = {icon = ""}
boolean = {icon = ""}
array = {icon = ""}
object = {icon = ""}
key = {icon = ""}
null = {icon = "ﳠ"}
enum-member = {icon = ""}
structure = {icon = "פּ"}
event = {icon = ""}
operator = {icon = ""}
type-parameter = {icon = ""}
[ui]
file = {icon = ""}
folder = {icon = ""}
folder_opened = {icon = ""}
vcs_branch = {icon = ""}
[mime-type]
# This is heavily based on https://github.com/nvim-tree/nvim-web-devicons
".babelrc" = { icon = "ﬥ", color = "#cbcb41" }
".bash_profile" = { icon = "", color = "#89e051" }
".bashrc" = { icon = "", color = "#89e051" }
".DS_Store" = { icon = "", color = "#41535b" }
".gitattributes" = { icon = "", color = "#41535b" }
".gitconfig" = { icon = "", color = "#41535b" }
".gitignore" = { icon = "", color = "#41535b" }
".gitlab-ci.yml" = { icon = "", color = "#e24329" }
".gitmodules" = { icon = "", color = "#41535b" }
".gvimrc" = { icon = "", color = "#019833" }
".npmignore" = { icon = "", color = "#E8274B" }
".npmrc" = { icon = "", color = "#E8274B" }
".settings.json" = { icon = "", color = "#854CC7" }
".vimrc" = { icon = "", color = "#019833" }
".zprofile" = { icon = "", color = "#89e051" }
".zshenv" = { icon = "", color = "#89e051" }
".zshrc" = { icon = "", color = "#89e051" }
"Brewfile" = { icon = "", color = "#701516" }
"CMakeLists.txt" = { icon = "", color = "#6d8086" }
"COMMIT_EDITMSG" = { icon = "", color = "#41535b" }
"COPYING" = { icon = "", color = "#cbcb41" }
"COPYING.LESSER" = { icon = "", color = "#cbcb41" }
"Dockerfile" = { icon = "", color = "#384d54" }
"Gemfile$" = { icon = "", color = "#701516" }
"LICENSE" = { icon = "", color = "#d0bf41" }
"R" = { icon = "ﳒ", color = "#358a5b" }
"Rmd" = { icon = "", color = "#519aba" }
"Vagrantfile$" = { icon = "", color = "#1563FF" }
"_gvimrc" = { icon = "", color = "#019833" }
"_vimrc" = { icon = "", color = "#019833" }
"ai" = { icon = "", color = "#cbcb41" }
"awk" = { icon = "", color = "#4d5a5e" }
"bash" = { icon = "", color = "#89e051" }
"bat" = { icon = "", color = "#C1F12E" }
"bmp" = { icon = "", color = "#a074c4" }
"c" = { icon = "", color = "#599eff" }
"c++" = { icon = "", color = "#f34b7d" }
"cbl" = { icon = "⚙", color = "#005ca5" }
"cc" = { icon = "", color = "#f34b7d" }
"cfg" = { icon = "", color = "#ECECEC" }
"clj" = { icon = "", color = "#8dc149" }
"cljc" = { icon = "", color = "#8dc149" }
"cljs" = { icon = "", color = "#519aba" }
"cljd" = { icon = "", color = "#519aba" }
"cmake" = { icon = "", color = "#6d8086" }
"cob" = { icon = "⚙", color = "#005ca5" }
"cobol" = { icon = "⚙", color = "#005ca5" }
"coffee" = { icon = "", color = "#cbcb41" }
"conf" = { icon = "", color = "#6d8086" }
"config.ru" = { icon = "", color = "#701516" }
"cp" = { icon = "", color = "#519aba" }
"cpp" = { icon = "", color = "#519aba" }
"cpy" = { icon = "⚙", color = "#005ca5" }
"cr" = { icon = "" }
"cs" = { icon = "", color = "#596706" }
"csh" = { icon = "", color = "#4d5a5e" }
"cson" = { icon = "", color = "#cbcb41" }
"css" = { icon = "", color = "#42a5f5" }
"csv" = { icon = "", color = "#89e051" }
"cxx" = { icon = "", color = "#519aba" }
"d" = { icon = "", color = "#427819" }
"dart" = { icon = "", color = "#03589C" }
"db" = { icon = "", color = "#dad8d8" }
"desktop" = { icon = "", color = "#563d7c" }
"diff" = { icon = "", color = "#41535b" }
"doc" = { icon = "", color = "#185abd" }
"dockerfile" = { icon = "", color = "#384d54" }
"drl" = { icon = "", color = "#ffafaf" }
"dropbox" = { icon = "", color = "#0061FE" }
"dump" = { icon = "", color = "#dad8d8" }
"edn" = { icon = "", color = "#519aba" }
"eex" = { icon = "", color = "#a074c4" }
"ejs" = { icon = "", color = "#cbcb41" }
"elm" = { icon = "", color = "#519aba" }
"epp" = { icon = "", color = "#FFA61A" }
"erb" = { icon = "", color = "#701516" }
"erl" = { icon = "", color = "#B83998" }
"ex" = { icon = "", color = "#a074c4" }
"exs" = { icon = "", color = "#a074c4" }
"f#" = { icon = "", color = "#519aba" }
"favicon.ico" = { icon = "", color = "#cbcb41" }
"fnl" = { icon = "🌜", color = "#fff3d7" }
"fish" = { icon = "", color = "#4d5a5e" }
"fs" = { icon = "", color = "#519aba" }
"fsi" = { icon = "", color = "#519aba" }
"fsscript" = { icon = "", color = "#519aba" }
"fsx" = { icon = "", color = "#519aba" }
"gd" = { icon = "", color = "#6d8086" }
"gemspec" = { icon = "", color = "#701516" }
"gif" = { icon = "", color = "#a074c4" }
"git" = { icon = "", color = "#F14C28" }
"glb" = { icon = "", color = "#FFB13B" }
"go" = { icon = "", color = "#519aba" }
"godot" = { icon = "", color = "#6d8086" }
"graphql" = { icon = "", color = "#e535ab" }
"gruntfile" = { icon = "", color = "#e37933" }
"gulpfile" = { icon = "", color = "#cc3e44" }
"h" = { icon = "", color = "#a074c4" }
"haml" = { icon = "", color = "#eaeae1" }
"hbs" = { icon = "", color = "#f0772b" }
"heex" = { icon = "", color = "#a074c4" }
"hh" = { icon = "", color = "#a074c4" }
"hpp" = { icon = "", color = "#a074c4" }
"hrl" = { icon = "", color = "#B83998" }
"hs" = { icon = "", color = "#a074c4" }
"htm" = { icon = "", color = "#e34c26" }
"html" = { icon = "", color = "#e44d26" }
"hxx" = { icon = "", color = "#a074c4" }
"ico" = { icon = "", color = "#cbcb41" }
"import" = { icon = "", color = "#ECECEC" }
"ini" = { icon = "", color = "#6d8086" }
"java" = { icon = "", color = "#cc3e44" }
"jl" = { icon = "", color = "#a270ba" }
"jpeg" = { icon = "", color = "#a074c4" }
"jpg" = { icon = "", color = "#a074c4" }
"js" = { icon = "", color = "#cbcb41" }
"json" = { icon = "", color = "#cbcb41" }
"json5" = { icon = "ﬥ", color = "#cbcb41" }
"jsx" = { icon = "", color = "#519aba" }
"ksh" = { icon = "", color = "#4d5a5e" }
"kt" = { icon = "", color = "#F88A02" }
"kts" = { icon = "", color = "#F88A02" }
"leex" = { icon = "", color = "#a074c4" }
"less" = { icon = "", color = "#563d7c" }
"lhs" = { icon = "", color = "#a074c4" }
"license" = { icon = "", color = "#cbcb41" }
"lua" = { icon = "", color = "#51a0cf" }
"luau" = { icon = "", color = "#51a0cf" }
"makefile" = { icon = "", color = "#6d8086" }
"markdown" = { icon = "", color = "#d74c4c" }
"material" = { icon = "", color = "#B83998" }
"md" = { icon = "", color = "#d74c4c" }
"mdx" = { icon = "", color = "#d74c4c" }
"mint" = { icon = "", color = "#87c095" }
"mix.lock" = { icon = "", color = "#a074c4" }
"mjs" = { icon = "", color = "#f1e05a" }
"ml" = { icon = "λ", color = "#e37933" }
"mli" = { icon = "λ", color = "#e37933" }
"mo" = { icon = "∞", color = "#9772FB" }
"mustache" = { icon = "", color = "#e37933" }
"nim" = { icon = "👑", color = "#f3d400" }
"nix" = { icon = "", color = "#7ebae4" }
"node_modules" = { icon = "", color = "#E8274B" }
"opus" = { icon = "", color = "#F88A02" }
"otf" = { icon = "", color = "#ECECEC" }
"package.json" = { icon = "", color = "#e8274b" }
"package-lock.json" = { icon = "", color = "#7a0d21" }
"pck" = { icon = "", color = "#6d8086" }
"pdf" = { icon = "", color = "#b30b00" }
"php" = { icon = "", color = "#a074c4" }
"pl" = { icon = "", color = "#519aba" }
"pm" = { icon = "", color = "#519aba" }
"png" = { icon = "", color = "#a074c4" }
"pp" = { icon = "", color = "#FFA61A" }
"ppt" = { icon = "", color = "#cb4a32" }
"pro" = { icon = "", color = "#e4b854" }
"Procfile" = { icon = "", color = "#a074c4" }
"ps1" = { icon = "", color = "#4d5a5e" }
"psb" = { icon = "", color = "#519aba" }
"psd" = { icon = "", color = "#519aba" }
"py" = { icon = "", color = "#ffbc03" }
"pyc" = { icon = "", color = "#ffe291" }
"pyd" = { icon = "", color = "#ffe291" }
"pyo" = { icon = "", color = "#ffe291" }
"query" = { icon = "", color = "#90a850" }
"r" = { icon = "ﳒ", color = "#358a5b" }
"rake" = { icon = "", color = "#701516" }
"rakefile" = { icon = "", color = "#701516" }
"rb" = { icon = "", color = "#701516" }
"rlib" = { icon = "", color = "#dea584" }
"rmd" = { icon = "", color = "#519aba" }
"rproj" = { icon = "鉶", color = "#358a5b" }
"rs" = { icon = "", color = "#dea584" }
"rss" = { icon = "", color = "#FB9D3B" }
"sass" = { icon = "", color = "#f55385" }
"sbt" = { icon = "", color = "#cc3e44" }
"scala" = { icon = "", color = "#cc3e44" }
"scm" = { icon = "ﬦ" }
"scss" = { icon = "", color = "#f55385" }
"sh" = { icon = "", color = "#4d5a5e" }
"sig" = { icon = "λ", color = "#e37933" }
"slim" = { icon = "", color = "#e34c26" }
"sln" = { icon = "", color = "#854CC7" }
"sml" = { icon = "λ", color = "#e37933" }
"sql" = { icon = "", color = "#dad8d8" }
"sqlite" = { icon = "", color = "#dad8d8" }
"sqlite3" = { icon = "", color = "#dad8d8" }
"styl" = { icon = "", color = "#8dc149" }
"sublime" = { icon = "", color = "#e37933" }
"suo" = { icon = "", color = "#854CC7" }
"sv" = { icon = "", color = "#019833" }
"svelte" = { icon = "", color = "#ff3e00" }
"svh" = { icon = "", color = "#019833" }
"svg" = { icon = "ﰟ", color = "#FFB13B" }
"swift" = { icon = "", color = "#e37933" }
"t" = { icon = "", color = "#519aba" }
"tbc" = { icon = "﯑", color = "#1e5cb3" }
"tcl" = { icon = "﯑", color = "#1e5cb3" }
"terminal" = { icon = "", color = "#31B53E" }
"tex" = { icon = "ﭨ", color = "#3D6117" }
"tf" = { icon = "", color = "#5F43E9" }
"tfvars" = { icon = "", color = "#5F43E9" }
"toml" = { icon = "", color = "#6d8086" }
"tres" = { icon = "", color = "#cbcb41" }
"ts" = { icon = "", color = "#519aba" }
"tscn" = { icon = "", color = "#a074c4" }
"tsx" = { icon = "", color = "#519aba" }
"twig" = { icon = "", color = "#8dc149" }
"txt" = { icon = "", color = "#89e051" }
"v" = { icon = "", color = "#019833" }
"vh" = { icon = "", color = "#019833" }
"vhd" = { icon = "", color = "#019833" }
"vhdl" = { icon = "", color = "#019833" }
"vim" = { icon = "", color = "#019833" }
"vue" = { icon = "﵂", color = "#8dc149" }
"webmanifest" = { icon = "", color = "#f1e05a" }
"webp" = { icon = "", color = "#a074c4" }
"webpack" = { icon = "ﰩ", color = "#519aba" }
"xcplayground" = { icon = "", color = "#e37933" }
"xls" = { icon = "", color = "#207245" }
"xml" = { icon = "謹", color = "#e37933" }
"xul" = { icon = "", color = "#e37933" }
"yaml" = { icon = "", color = "#6d8086" }
"yml" = { icon = "", color = "#6d8086" }
"zig" = { icon = "", color = "#f69a1b" }
"zsh" = { icon = "", color = "#89e051" }
"sol" = { icon = "ﲹ", color = "#519aba" }
".env" = { icon = "", color = "#faf743" }
"prisma" = { icon = "卑" }
"lock" = { icon = "", color = "#bbbbbb" }
"log" = { icon = "" }

@ -0,0 +1,127 @@
[
"[QueryStringParams]"
"[FormParams]"
"[MultipartFormData]"
"[Cookies]"
"[Captures]"
"[Asserts]"
"[Options]"
"[BasicAuth]"
] @attribute
(comment) @comment
[
(key_string)
(json_key_string)
] @variable.other.member
(value_string) @string
(quoted_string) @string
(json_string) @string
(file_value) @string.special.path
(regex) @string.regex
[
"\\"
(regex_escaped_char)
(quoted_string_escaped_char)
(key_string_escaped_char)
(value_string_escaped_char)
(oneline_string_escaped_char)
(multiline_string_escaped_char)
(filename_escaped_char)
(json_string_escaped_char)
] @constant.character.escape
(method) @type.builtin
(multiline_string_type) @type
[
"status"
"url"
"header"
"cookie"
"body"
"xpath"
"jsonpath"
"regex"
"variable"
"duration"
"sha256"
"md5"
"bytes"
] @function.builtin
(filter) @attribute
(version) @string.special
[
"null"
"cacert"
"location"
"insecure"
"max-redirs"
"retry"
"retry-interval"
"retry-max-count"
(variable_option "variable")
"verbose"
"very-verbose"
] @constant.builtin
(boolean) @constant.builtin.boolean
(variable_name) @variable
[
"not"
"equals"
"=="
"notEquals"
"!="
"greaterThan"
">"
"greaterThanOrEquals"
">="
"lessThan"
"<"
"lessThanOrEquals"
"<="
"startsWith"
"endsWith"
"contains"
"matches"
"exists"
"includes"
"isInteger"
"isFloat"
"isBoolean"
"isString"
"isCollection"
] @keyword.operator
(integer) @constant.numeric.integer
(float) @constant.numeric.float
(status) @constant.numeric
(json_number) @constant.numeric.float
[
":"
","
] @punctuation.delimiter
[
"["
"]"
"{"
"}"
"{{"
"}}"
] @punctuation.special
[
"base64,"
"file,"
"hex,"
] @string.special

@ -0,0 +1,11 @@
[
(json_object)
(json_array)
(xml_tag)
] @indent
[
"}"
"]"
(xml_close_tag)
] @outdent

@ -0,0 +1,14 @@
((comment) @injection.content
(#set! injection.language "comment"))
((json_value) @injection.content
(#set! injection.language "json"))
((xml) @injection.content
(#set! injection.language "xml"))
((multiline_string
(multiline_string_type) @injection.language
(multiline_string_content) @injection.content)
(#set! injection.include-children)
(#set! injection.combined))

@ -0,0 +1,46 @@
(function_definition (_)? @function.inside) @function.around
(short_function_definition (_)? @function.inside) @function.around
(macro_definition (_)? @function.inside) @function.around
(struct_definition (_)? @class.inside) @class.around
(abstract_definition (_)? @class.inside) @class.around
(primitive_definition (_)? @class.inside) @class.around
(parameter_list
; Match all children of parameter_list *except* keyword_parameters
([(identifier)
(slurp_parameter)
(optional_parameter)
(typed_parameter)
(tuple_expression)
(interpolation_expression)
(call_expression)]
@parameter.inside . ","? @parameter.around) @parameter.around)
(keyword_parameters
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(argument_list
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(type_parameter_list
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(line_comment) @comment.inside
(line_comment)+ @comment.around
(block_comment) @comment.inside
(block_comment)+ @comment.around
(_expression (macro_identifier
(identifier) @_name
(#match? @_name "^(test|test_throws|test_logs|inferred|test_deprecated|test_warn|test_nowarn|test_broken|test_skip)$")
)
.
(macro_argument_list) @test.inside) @test.around

@ -0,0 +1,21 @@
(comment) @comment
(ellipses) @punctuation.delimiter
(section_header) @keyword
(extra_text) @comment
(setting_statement) @keyword
(variable_definition (variable_name) @variable)
(keyword_definition (name) @function)
(keyword_definition (body (keyword_setting) @keyword))
(test_case_definition (name) @property)
(keyword_invocation (keyword) @function)
(argument (text_chunk) @string)
(argument (scalar_variable) @string.special)
(argument (list_variable) @string.special)
(argument (dictionary_variable) @string.special)

@ -1,44 +1,67 @@
; Keywords
[
"BEGIN"
"END"
"alias"
"and"
"begin"
"break"
"case"
"class"
"def"
"do"
"else"
"elsif"
"end"
"ensure"
"for"
"if"
"in"
"module"
"next"
"or"
"in"
"rescue"
"retry"
"return"
"then"
"unless"
"until"
"ensure"
] @keyword
[
"if"
"else"
"elsif"
"when"
"case"
"unless"
"then"
] @keyword.control.conditional
[
"for"
"while"
"retry"
"until"
"redo"
] @keyword.control.repeat
[
"yield"
] @keyword
"return"
"next"
"break"
] @keyword.control.return
[
"def"
"undef"
] @keyword.function
((identifier) @keyword.control.import
(#match? @keyword.control.import "^(require|require_relative|load|autoload)$"))
[
"or"
"and"
"not"
] @keyword.operator
((identifier) @keyword
(#match? @keyword "^(private|protected|public)$"))
((identifier) @keyword.control.exception
(#match? @keyword.control.exception "^(raise|fail)$"))
; Function calls
((identifier) @function.method.builtin
(#eq? @function.method.builtin "require"))
((identifier) @function.builtin
(#match? @function.builtin "^(attr|attr_accessor|attr_reader|attr_writer|include|prepend|refine|private|protected|public)$"))
"defined?" @function.method.builtin
"defined?" @function.builtin
(call
method: [(identifier) (constant)] @function.method)
@ -58,7 +81,10 @@
] @variable.other.member
((identifier) @constant.builtin
(#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
(#match? @constant.builtin "^(__FILE__|__LINE__|__ENCODING__)$"))
((constant) @constant.builtin
(#match? @constant.builtin "^(ENV|ARGV|ARGF|RUBY_PLATFORM|RUBY_RELEASE_DATE|RUBY_VERSION|STDERR|STDIN|STDOUT|TOPLEVEL_BINDING)$"))
((constant) @constant
(#match? @constant "^[A-Z\\d_]+$"))
@ -66,22 +92,23 @@
(constant) @constructor
(self) @variable.builtin
(super) @variable.builtin
(super) @function.builtin
[(forward_parameter)(forward_argument)] @variable.parameter
(keyword_parameter name:((_)":" @variable.parameter) @variable.parameter)
(optional_parameter name:((_)"=" @operator) @variable.parameter)
(optional_parameter name: (identifier) @variable.parameter)
(splat_parameter name: (identifier) @variable.parameter) @variable.parameter
(hash_splat_parameter name: (identifier) @variable.parameter) @variable.parameter
(method_parameters (identifier) @variable.parameter)
(block_parameter (identifier) @variable.parameter)
(block_parameters (identifier) @variable.parameter)
(destructured_parameter (identifier) @variable.parameter)
(hash_splat_parameter (identifier) @variable.parameter)
(lambda_parameters (identifier) @variable.parameter)
(method_parameters (identifier) @variable.parameter)
(splat_parameter (identifier) @variable.parameter)
(keyword_parameter name: (identifier) @variable.parameter)
(optional_parameter name: (identifier) @variable.parameter)
((identifier) @function.method
(#is-not? local))
(identifier) @variable
[
(identifier)
] @variable
; Literals
@ -96,10 +123,11 @@
[
(simple_symbol)
(delimited_symbol)
(hash_key_symbol)
(bare_symbol)
] @string.special.symbol
(pair key: ((_)":" @string.special.symbol) @string.special.symbol)
(regex) @string.regexp
(escape_sequence) @constant.character.escape
@ -112,7 +140,7 @@
(nil)
(true)
(false)
]@constant.builtin
] @constant.builtin
(interpolation
"#{" @punctuation.special
@ -121,20 +149,36 @@
(comment) @comment
; Operators
[
"="
":"
"?"
"~"
"=>"
"->"
"!"
] @operator
(assignment
"=" @operator)
(operator_assignment
operator: ["+=" "-=" "*=" "**=" "/=" "||=" "|=" "&&=" "&=" "%=" ">>=" "<<=" "^="] @operator)
(binary
operator: ["/" "|" "==" "===" "||" "&&" ">>" "<<" "<" ">" "<=" ">=" "&" "^" "!~" "=~" "<=>" "**" "*" "!=" "%" "-" "+"] @operator)
(range
operator: [".." "..."] @operator)
[
","
";"
"."
"&."
] @punctuation.delimiter
[
"|"
"("
")"
"["

@ -1,2 +1,8 @@
((comment) @injection.content
(#set! injection.language "comment"))
((heredoc_body
(heredoc_content) @injection.content
(heredoc_end) @name
(#set! injection.language "sql"))
(#eq? @name "SQL"))

@ -9,7 +9,7 @@
"ui.linenr" = { fg = "light-gray" }
"ui.linenr.selected" = { fg = "white", modifiers = ["bold"] }
"ui.popup" = { fg = "white" }
"ui.window" = { fg = "white" }
"ui.window" = { fg = "gray" }
"ui.selection" = { bg = "gray" }
"comment" = "light-gray"
"ui.statusline" = { fg = "white" }
@ -25,6 +25,10 @@
"ui.virtual.ruler" = { bg = "gray" }
"ui.virtual.whitespace" = "gray"
"ui.virtual.indent-guide" = "gray"
"ui.virtual.inlay-hint" = { fg = "white", bg = "gray" }
"ui.virtual.inlay-hint.parameter" = { fg = "white", bg = "gray"}
"ui.virtual.inlay-hint.type" = { fg = "white", bg = "gray"}
"ui.virtual.wrap" = "gray"
"variable" = "light-red"
"constant.numeric" = "yellow"

@ -52,6 +52,7 @@
"ui.virtual.whitespace" = { fg = "berry_desaturated" }
"ui.virtual.ruler" = { bg = "berry_dim" }
"ui.virtual.indent-guide" = { fg = "berry_fade" }
"ui.virtual.inlay-hint" = { fg = "berry_desaturated" }
"diff.plus" = { fg = "mint" }
"diff.delta" = { fg = "gold" }

@ -8,9 +8,14 @@
"ui.text" = "white"
"ui.text.focus" = { modifiers = ["reversed"] } # file picker selected
"ui.virtual" = "gray"
"ui.virtual.whitespace" = "gray"
"ui.virtual.ruler" = { fg = "white", bg = "gray" }
"ui.virtual.indent-guide" = "white"
"ui.virtual.inlay-hint" = { fg = "black", bg = "orange" }
"ui.virtual.inlay-hint.parameter" = { fg = "black", bg = "orange" }
"ui.virtual.inlay-hint.type" = { fg = "black", bg = "orange" }
"ui.virtual.wrap" = "gray"
"ui.statusline" = { fg = "white", bg = "deep_blue" }
"ui.statusline.inactive" = { fg = "gray", bg = "deep_blue" }
@ -22,7 +27,7 @@
"ui.cursor" = { fg = "black", bg = "white" }
"ui.cursor.insert" = { fg = "black", bg = "white" }
"ui.cursor.select" = { fg = "black", bg = "white" }
"ui.cursor.match" = { bg = "white", modifiers = ["dim"] }
"ui.cursor.match" = { modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "black", bg = "white", modifiers = ["slow_blink"] }
"ui.cursor.secondary" = "white"
"ui.cursorline.primary" = { bg = "deep_blue", underline = { color = "orange", style = "double_line" } }

@ -45,6 +45,9 @@
"ui.virtual.whitespace" = { fg = "subtle" }
"ui.virtual.wrap" = { fg = "subtle" }
"ui.virtual.ruler" = { bg = "background_dark"}
"ui.virtual.inlay-hint" = { fg = "comment" }
"ui.virtual.inlay-hint.parameter" = { fg = "comment", modifiers = ["italic"] }
"ui.virtual.inlay-hint.type" = { fg = "comment", modifiers = ["italic"] }
"error" = { fg = "red" }
"warning" = { fg = "cyan" }

@ -70,6 +70,7 @@
"ui.selection.primary" = { bg = "lightgoldenrod2" }
"ui.virtual.whitespace" = "highlight"
"ui.virtual.ruler" = { bg = "gray95" }
"ui.virtual.inlay-hint" = { fg = "gray75" }
"ui.cursorline.primary" = { bg = "darkseagreen2" }
"ui.cursorline.secondary" = { bg = "darkseagreen2" }

@ -0,0 +1,84 @@
# Author : Casper Rogild Storm <casper@asynkron.xyz>
"comment" = { fg = "ferra_bark", modifiers = ["italic"] }
"constant" = { fg = "ferra_sage" }
"function" = { fg = "ferra_coral" }
"function.macro" = { fg = "ferra_mist" }
"keyword" = { fg = "ferra_mist" }
"operator" = { fg = "ferra_mist" }
"punctuation" = { fg = "ferra_blush" }
"string" = { fg = "ferra_sage" }
"type" = { fg = "ferra_rose" }
"variable" = { fg = "ferra_blush" }
"variable.builtin" = { fg = "ferra_rose" }
"tag" = { fg = "ferra_sage" }
"label" = { fg = "ferra_sage" }
"attribute" = { fg = "ferra_blush" }
"namespace" = { fg = "ferra_blush" }
"module" = { fg = "ferra_blush" }
"markup.heading" = { fg = "ferra_sage", modifiers = ["bold"] }
"markup.heading.marker" = { fg = "ferra_bark" }
"markup.list" = { fg = "ferra_mist" }
"markup.bold" = { modifiers = ["bold"] }
"markup.italic" = { modifiers = ["italic"] }
"markup.strikethrough" = { modifiers = ["crossed_out"] }
"markup.link.url" = { fg = "ferra_rose", modifiers = ["underlined"] }
"markup.link.text" = { fg = "ferra_rose" }
"markup.quote" = { fg = "ferra_bark" }
"markup.raw" = { fg = "ferra_coral" }
"ui.background" = { bg = "ferra_night" }
"ui.cursor" = { fg = "ferra_night", bg = "ferra_blush" }
"ui.cursor.match" = { fg = "ferra_night", bg = "ferra_bark" }
"ui.cursor.select" = { fg = "ferra_night", bg = "ferra_rose" }
"ui.cursor.insert" = { fg = "ferra_night", bg = "ferra_coral" }
"ui.linenr" = { fg = "ferra_bark" }
"ui.linenr.selected" = { fg = "ferra_blush" }
"ui.cursorline" = { fg = "ferra_blush", bg = "ferra_ash" }
"ui.statusline" = { fg = "ferra_blush", bg = "ferra_ash" }
"ui.statusline.inactive" = { fg = "ferra_bark", bg = "ferra_ash" }
"ui.statusline.normal" = { fg = "ferra_ash", bg = "ferra_blush" }
"ui.statusline.insert" = { fg = "ferra_ash", bg = "ferra_coral" }
"ui.statusline.select" = { fg = "ferra_ash", bg = "ferra_rose" }
"ui.popup" = { fg = "ferra_blush", bg = "ferra_ash" }
"ui.window" = { fg = "ferra_bark", bg = "ferra_night" }
"ui.help" = { fg = "ferra_blush", bg = "ferra_ash" }
"ui.text" = { fg = "ferra_blush" }
"ui.text.focus" = { fg = "ferra_coral" }
"ui.menu" = { fg = "ferra_blush", bg = "ferra_ash" }
"ui.menu.selected" = { fg = "ferra_coral", bg = "ferra_ash" }
"ui.selection" = { bg = "ferra_umber", fg = "ferra_night" }
"ui.selection.primary" = { bg = "ferra_night", fg = "ferra_umber" }
"ui.virtual" = { fg = "ferra_bark" }
"ui.virtual.whitespace" = { fg = "ferra_bark" }
"ui.virtual.ruler" = { fg = "ferra_night", bg = "ferra_ash" }
"ui.virtual.indent-guide" = { fg = "ferra_ash" }
"ui.virtual.inlay-hint" = { fg = "ferra_bark" }
"diff.plus" = { fg = "ferra_sage" }
"diff.delta" = { fg = "ferra_blush" }
"diff.minus" = { fg = "ferra_ember" }
"error" = { fg = "ferra_ember" }
"warning" = { fg = "ferra_honey" }
"info" = { fg = "ferra_blush" }
"hint" = { fg = "ferra_blush" }
"diagnostic.warning" = { underline = { color = "ferra_honey", style = "curl" } }
"diagnostic.error" = { underline = { color = "ferra_ember", style = "curl" } }
"diagnostic.info" = { underline = { color = "ferra_blush", style = "curl" } }
"diagnostic.hint" = { underline = { color = "ferra_blush", style = "curl" } }
[palette]
ferra_night = "#2b292d"
ferra_ash = "#383539"
ferra_umber = "#4d424b"
ferra_bark = "#6F5D63"
ferra_mist = "#D1D1E0"
ferra_sage = "#B1B695"
ferra_blush = "#fecdb2"
ferra_coral = "#ffa07a"
ferra_rose = "#F6B6C9"
ferra_ember = "#e06b75"
ferra_honey = "#F5D76E"

@ -80,6 +80,7 @@
"ui.virtual" = { fg = "gray02" }
"ui.virtual.indent-guide" = { fg = "gray02" }
"ui.virtual.inlay-hint" = { fg = "gray04" }
"ui.selection" = { bg = "gray03" }
"ui.selection.primary" = { bg = "gray03" }

@ -33,6 +33,7 @@
"ui.virtual.ruler" = { bg = "bg3" } # Vertical rulers (colored columns in editing area).
"ui.virtual.whitespace" = { fg = "bg3" } # Whitespace markers in editing area.
"ui.virtual.indent-guide" = { fg = "black" } # Vertical indent width guides
"ui.virtual.inlay-hint" = { fg = "comment", bg = "bg2" } # Default style for inlay hints of all kinds
"ui.statusline" = { fg = "fg2", bg = "bg0" } # Status line.
"ui.statusline.inactive" = { fg = "fg3", bg = "bg0" } # Status line in unfocused windows.

@ -54,6 +54,7 @@
"ui.virtual.indent-guide" = { fg = "faint-gray" }
"ui.virtual.whitespace" = { fg = "light-gray" }
"ui.virtual.ruler" = { bg = "gray" }
"ui.virtual.inlay-hint" = { fg = "light-gray" }
"ui.cursor" = { fg = "white", modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] }

@ -85,6 +85,7 @@
"ui.virtual" = { fg = "gray03" }
"ui.virtual.indent-guide" = { fg = "gray04" }
"ui.virtual.inlay-hint" = { fg = "gray05" }
"ui.selection" = { bg = "gray03" }
"ui.selection.primary" = { bg = "gray03" }

@ -65,6 +65,8 @@
"ui.virtual.whitespace" = "grey0"
"ui.statusline.insert" = { bg = "green", fg = "bg2" }
"ui.statusline.select" = { bg = "blue", fg = "bg2" }
"ui.virtual.wrap" = { fg = "grey0" }
"ui.virtual.inlay-hint" = { fg = "grey1" }
"hint" = "blue"
"info" = "aqua"

@ -8,7 +8,8 @@
"ui.popup" = { bg = "uibg" }
"ui.selection" = { bg = "#304a3d" }
"ui.selection.primary" = { bg = "#2f2f2f" }
"comment" = { fg = "#7f9f7f" }
"comment" = { fg = "comment" }
"ui.virtual.inlay-hint" = { fg = "comment" }
"comment.block.documentation" = { fg = "black", modifiers = ["bold"] }
"ui.statusline" = { bg = "statusbg", fg = "#ccdc90" }
"ui.statusline.inactive" = { fg = '#2e3330', bg = '#88b090' }
@ -50,6 +51,7 @@
"error" = "errorfg"
[palette]
comment = "#7f9f7f"
bg = "#3f3f3f"
uibg = "#2c2e2e"
constant = "#dca3a3"

@ -1,8 +1,6 @@
use crate::path;
use crate::DynError;
use helix_view::theme::Loader;
use helix_view::theme::Modifier;
use helix_view::Theme;
use helix_view::{theme::Modifier, Theme};
struct Rule {
fg: Option<&'static str>,
@ -180,7 +178,7 @@ pub fn lint(file: String) -> Result<(), DynError> {
}
pub fn lint_all() -> Result<(), DynError> {
let files = Loader::read_names(path::themes().as_path());
let files = helix_loader::read_toml_names(path::themes().as_path());
let files_count = files.len();
let ok_files_count = files
.into_iter()

Loading…
Cancel
Save