Compare commits

...

46 Commits

Author SHA1 Message Date
Trivernis b01774deef Merge pull request 'feature/icons' (#11) from feature/icons into main
Reviewed-on: #11
1 year ago
trivernis 8c87021a53
Properly add icon support into tree rendering
The icon to render is passed as an additonal field
and rendered directly to the surface so that the
style can be rendered as well
1 year ago
trivernis 7ccbea2a31
Add rudimentary nerdfonts support 1 year ago
trivernis 82adbb35ab
Merge remote-tracking branch 'lazytanuki/icons' into feature/icons 1 year ago
LazyTanuki cd8a7de454 doc: icons configuration 1 year ago
LazyTanuki d9e342796e feat: handle icons in statusline widget, bufferline and gutter 1 year ago
LazyTanuki 18945587ff feat: add icons to pickers 1 year ago
LazyTanuki cfcf2ff4ff feat: add icons launch and runtime loading 1 year ago
LazyTanuki 63051a7163 wip: add the `icons` module as well as default and nerdfonts flavors 1 year ago
LazyTanuki 55de407681 wip: documented and moved `theme::Loader::read_names` to `helix_loader::read_toml_names` 1 year ago
LazyTanuki 7d6b2cbbf6 wip: moved `load_inheritable_toml`, `path` (→ `get_toml_path`), and `load_toml` from theme.rs to the `helix-loader` module 1 year ago
LazyTanuki d4c3609c43 wip: generalised `load_theme` into `load_inheritable_toml` 1 year ago
Blaž Hrastnik 58e457a4e1
Revert "Fix #6605: Remove soft-wrap.enable option wrapping. (#6656)"
This caused a bug that would ignore the global config.

This reverts commit af88a3c15c.
1 year ago
Jan Scheer 25858ec2e3
themes: add inlay-hint to nightfox (#6655) 1 year ago
gibbz00 af88a3c15c
Fix #6605: Remove soft-wrap.enable option wrapping. (#6656)
Co-authored-by: gibbz00 <gabrielhansson@gmail.com>
1 year ago
Daniel Sedlak e856906f76
Fix typos (#6643) 1 year ago
karei 1148ce1fd9
Add support for Robot Framework files (#6611)
* Add support for Robot Framework files

* Run docgen
1 year ago
Michael b663b89529
xml: highlight .xsd as XML files (#6631)
xsd or "XML Schema Definition" files are in XML format and should therefore be
highlighted as such
1 year ago
Danillo Melo 3dd715a115
Update Ruby Highlights (#6587)
* update ruby highlights

* Updated SQL injection.scm

* Move private, public, protected to builtin methods
1 year ago
Clara Hobbs 4b32b544fc
Add textobject queries for Julia (#6588)
* Add textobjects queries for Julia

* Update docs for Julia textobject queries
1 year ago
Casper Rogild Storm 7ce52e5b2c
Added `ferra` theme (#6619)
* Added ferra theme

* Updated with author information

* Conform to themelint
1 year ago
Erasin Wang c22ebfe62e
Add Hurl Support (#6450)
* Add http Support

It's like [vscode-restclient](https://github.com/Huachao/vscode-restclient)

- https://github.com/erasin/tree-sitter-http/tree/main/tests

* Add Hurl Support
1 year ago
Gyeongwan Koh 951e8686e8
Colorize inlay hints in the boo_berry theme (#6625) 1 year ago
Michael Davis fc4ca96c29
Update tree-sitter to v0.20.10 (#6608)
We used a git dependency to take advantage of the latest fixes in
master but a new release is now available:
https://crates.io/crates/tree-sitter/0.20.10
1 year ago
Ivan Ermakov b6909bc41a
Add gdformat support (#6614) 1 year ago
Anton Romanov 531b745c54
[theme][zenburn] set inlay hint to comment style (#6593) 1 year ago
Constantin Angheloiu 01b70762fd
Dim pane divider color in base16_transparent theme (#6534) 1 year ago
Jack Wolfard 577aded04a
Recognize CUDA files as C++ (#6521) 1 year ago
Bertrand Bousquet 2f4b9a47f3
Update Varua theme for inlay hints (#6589) 1 year ago
Rohit K Viswanath 480784d2cf
Update inlay-hint color for mellow & rasmus themes (#6583) 1 year ago
Slug dbafe756fa
Update base16_transparent and dark_high_contrast themes (#6577)
* Update inlay-hint and wrap for base16_transparent
* Update inlay-hint and wrap for dark_high_contrast
* Tune dark_high_contrast cursor match theming
1 year ago
dependabot[bot] 2bdb58fba4
build(deps): bump futures-util from 0.3.27 to 0.3.28 (#6575)
Bumps [futures-util](https://github.com/rust-lang/futures-rs) from 0.3.27 to 0.3.28.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.27...0.3.28)

---
updated-dependencies:
- dependency-name: futures-util
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 9d88387305
build(deps): bump futures-executor from 0.3.27 to 0.3.28 (#6576)
Bumps [futures-executor](https://github.com/rust-lang/futures-rs) from 0.3.27 to 0.3.28.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.27...0.3.28)

---
updated-dependencies:
- dependency-name: futures-executor
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
Rowan Shi 789833c995
minor: R lang config update --slave to --no-echo (#6570) 1 year ago
Yevgnen 43072f7876
Update colors for inlay hints for emacs theme (#6569) 1 year ago
Bertrand Bousquet d0c9f38b68
Update Varua theme for soft wrap (#6568) 1 year ago
Dmitry Ulyanov dd6e0cce3b
Fix line number display for LSP goto pickers (#6559)
Line numbers are 0-indexed in the LSP spec but 1-indexed for display
and jumping purposes in Helix.
1 year ago
Jack Allison 1fcfef12be
Update OneDark theme to use light-gray for inlay hints. (#6503)
Co-authored-by: Jack Allison <jacallis@cisco.com>
1 year ago
Sebastian Zivota d63c717b82
dracula theme: style inlay hints as comments (#6515) 1 year ago
Casper Rogild Storm 9420ba7484
Let..else refactor (#6562) 1 year ago
Pascal Kuthe 1073dd6329
robustly handle invalid LSP ranges (#6512) 1 year ago
Pascal Kuthe bfe8d267fe
normalize LSP workspaces (#6517) 1 year ago
Michael Davis 38b9bdf871 Recursive create the pkgname directory when creating a release tarball
This step without the '-p' works fine for regular releases but it can
fail if the CI is running when this file changes or on a branch
matching 'patch/ci-release-*'.
1 year ago
Michael Davis 6bfc309741 Remove the rust-toolchain.toml file before building the release
The 'dtolnay/rust-toolchain' action ignores the rust-toolchain.toml
file, but the installed 'cargo' respects it. This can create a version
mismatch if the MSRV is different from the stable rust version. Any
additional targets installed by rustup like aarch64-darwin might not
be installed for the correct version. To fix this, we remove the
rust-toolchain.toml file before calling 'cargo'.
1 year ago
Michael Davis fc5e515b30 Enable aarch64-macos releases 1 year ago
Yusuf Bera Ertan c3c87741d9
build(nix): update flake dependencies, remove deprecated code from flake 1 year ago

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

@ -1,10 +1,11 @@
use super::{Prompt, TreeOp, TreeView, TreeViewItem};
use super::{tree::TreeIcons, Prompt, TreeOp, TreeView, TreeViewItem};
use crate::{
compositor::{Component, Context, EventResult},
ctrl, key, shift, ui,
};
use anyhow::{bail, ensure, Result};
use helix_core::Position;
use helix_view::{
editor::{Action, ExplorerPosition},
graphics::{CursorKind, Rect},
@ -13,9 +14,9 @@ use helix_view::{
theme::Modifier,
Editor,
};
use std::cmp::Ordering;
use std::path::{Path, PathBuf};
use std::{borrow::Cow, fs::DirEntry};
use std::{cmp::Ordering, sync::Arc};
use tui::{
buffer::Buffer as Surface,
widgets::{Block, Borders, Widget},
@ -172,7 +173,7 @@ impl Explorer {
.unwrap_or_else(|_| "./".into())
.canonicalize()?;
Ok(Self {
tree: Self::new_tree_view(current_root.clone())?,
tree: Self::new_tree_view(current_root.clone(), Self::get_tree_icons(cx))?,
history: vec![],
show_help: false,
state: State::new(true, current_root),
@ -182,10 +183,44 @@ impl Explorer {
})
}
fn get_tree_icons(cx: &Context) -> TreeIcons {
let icons = cx.editor.icons.clone();
let defaults = TreeIcons::default();
let file_icon = icons
.ui
.as_ref()
.and_then(|s| s.get("file").cloned())
.unwrap_or(defaults.item);
let item = file_icon.clone();
let tree_closed = icons
.ui
.as_ref()
.and_then(|s| s.get("folder").cloned())
.unwrap_or(defaults.tree_closed);
let tree_opened = icons
.ui
.as_ref()
.and_then(|s| s.get("folder_opened").cloned())
.unwrap_or(defaults.tree_opened);
TreeIcons {
tree_closed,
tree_opened,
item,
icon_fn: Some(Arc::new(move |item| {
icons
.icon_from_path(Some(&PathBuf::from(item)))
.cloned()
.unwrap_or_else(|| file_icon.to_owned())
})),
}
}
#[cfg(test)]
fn from_path(root: PathBuf, column_width: u16) -> Result<Self> {
Ok(Self {
tree: Self::new_tree_view(root.clone())?,
tree: Self::new_tree_view(root.clone(), TreeIcons::default())?,
history: vec![],
show_help: false,
state: State::new(true, root),
@ -195,9 +230,11 @@ impl Explorer {
})
}
fn new_tree_view(root: PathBuf) -> Result<TreeView<FileInfo>> {
fn new_tree_view(root: PathBuf, icons: TreeIcons) -> Result<TreeView<FileInfo>> {
let root = FileInfo::root(root);
Ok(TreeView::build_tree(root)?.with_enter_fn(Self::toggle_current))
Ok(TreeView::build_tree(root)?
.with_enter_fn(Self::toggle_current)
.icons(icons))
}
fn push_history(&mut self, tree_view: TreeView<FileInfo>, current_root: PathBuf) {
@ -213,7 +250,7 @@ impl Explorer {
if self.state.current_root.eq(&root) {
return Ok(());
}
let tree = Self::new_tree_view(root.clone())?;
let tree = Self::new_tree_view(root.clone(), self.tree.icons.clone())?;
let old_tree = std::mem::replace(&mut self.tree, tree);
self.push_history(old_tree, self.state.current_root.clone());
self.state.current_root = root;

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

@ -1,7 +1,7 @@
use std::cmp::Ordering;
use std::{cmp::Ordering, sync::Arc};
use anyhow::Result;
use helix_view::theme::Modifier;
use helix_view::{icons::Icon, theme::Modifier};
use crate::{
compositor::{Component, Context, EventResult},
@ -290,6 +290,7 @@ pub struct TreeView<T: TreeViewItem> {
max_len: usize,
count: usize,
tree_symbol_style: String,
pub icons: TreeIcons,
#[allow(clippy::type_complexity)]
pre_render: Option<Box<dyn Fn(&mut Self, Rect) + 'static>>,
@ -325,6 +326,7 @@ impl<T: TreeViewItem> TreeView<T> {
on_next_key: None,
search_prompt: None,
search_str: "".into(),
icons: TreeIcons::default(),
})
}
@ -349,6 +351,11 @@ impl<T: TreeViewItem> TreeView<T> {
self
}
pub fn icons(mut self, icons: TreeIcons) -> Self {
self.icons = icons;
self
}
/// Reveal item in the tree based on the given `segments`.
///
/// The name of the root should be excluded.
@ -775,12 +782,33 @@ struct RenderedLine {
content: String,
selected: bool,
is_ancestor_of_current_item: bool,
icon: Icon,
}
struct RenderTreeParams<'a, T> {
tree: &'a Tree<T>,
prefix: &'a String,
level: usize,
selected: usize,
icons: &'a TreeIcons,
}
#[derive(Clone)]
pub struct TreeIcons {
pub tree_closed: Icon,
pub tree_opened: Icon,
pub item: Icon,
pub icon_fn: Option<Arc<dyn Fn(&str) -> Icon>>,
}
impl Default for TreeIcons {
fn default() -> Self {
Self {
tree_closed: Icon::unstyled('⏵'),
tree_opened: Icon::unstyled('⏷'),
item: Icon::unstyled(' '),
icon_fn: None,
}
}
}
fn render_tree<T: TreeViewItem>(
@ -789,28 +817,36 @@ fn render_tree<T: TreeViewItem>(
prefix,
level,
selected,
icons,
}: RenderTreeParams<T>,
) -> Vec<RenderedLine> {
let indent = if level > 0 {
let indicator = if tree.item().is_parent() {
if tree.is_opened {
"⏷"
} else {
"⏵"
}
let name = tree.item.name();
let icon = if tree.item().is_parent() {
if tree.is_opened {
icons.tree_opened.to_owned()
} else {
" "
};
format!("{}{} ", prefix, indicator)
icons.tree_closed.to_owned()
}
} else {
"".to_string()
if let Some(icon_fn) = icons.icon_fn.as_ref() {
icon_fn(&name)
} else {
icons.item.to_owned()
}
};
let indent = if level > 0 {
format!("{} ", prefix)
} else {
String::new()
};
let name = tree.item.name();
let head = RenderedLine {
indent,
selected: selected == tree.index,
is_ancestor_of_current_item: selected != tree.index && tree.get(selected).is_some(),
content: name,
icon,
};
let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " });
vec![head]
@ -821,6 +857,7 @@ fn render_tree<T: TreeViewItem>(
prefix: &prefix,
level: level + 1,
selected,
icons,
})
}))
.collect()
@ -861,12 +898,20 @@ impl<T: TreeViewItem + Clone> TreeView<T> {
style,
);
let x = area.x.saturating_add(indent_len);
surface.set_stringn(
x,
area.y,
line.icon.icon_char.to_string(),
2,
line.icon.style.map(|s| s.into()).unwrap_or(style),
);
let style = if line.selected {
style.add_modifier(Modifier::REVERSED)
} else {
style
};
let x = area.x.saturating_add(indent_len);
let x = x.saturating_add(2);
surface.set_stringn(
x,
area.y,
@ -915,6 +960,7 @@ impl<T: TreeViewItem + Clone> TreeView<T> {
prefix: &"".to_string(),
level: 0,
selected: self.selected,
icons: &self.icons,
};
let lines = render_tree(params);

@ -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,304 @@
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));
}
}
pub fn unstyled(icon_char: char) -> Self {
Self {
icon_char,
style: None,
}
}
}
#[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