Compare commits

...

131 Commits

Author SHA1 Message Date
trivernis cd76c9fd2e
Change helix logo for dark theme 2 years ago
trivernis 41b9e81f61
Change logo and update README 2 years ago
Trivernis b01774deef Merge pull request 'feature/icons' (#11) from feature/icons into main
Reviewed-on: #11
2 years 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
2 years ago
trivernis 7ccbea2a31
Add rudimentary nerdfonts support 2 years ago
trivernis 82adbb35ab
Merge remote-tracking branch 'lazytanuki/icons' into feature/icons 2 years ago
LazyTanuki cd8a7de454 doc: icons configuration 2 years ago
LazyTanuki d9e342796e feat: handle icons in statusline widget, bufferline and gutter 2 years ago
LazyTanuki 18945587ff feat: add icons to pickers 2 years ago
LazyTanuki cfcf2ff4ff feat: add icons launch and runtime loading 2 years ago
LazyTanuki 63051a7163 wip: add the `icons` module as well as default and nerdfonts flavors 2 years ago
LazyTanuki 55de407681 wip: documented and moved `theme::Loader::read_names` to `helix_loader::read_toml_names` 2 years ago
LazyTanuki 7d6b2cbbf6 wip: moved `load_inheritable_toml`, `path` (→ `get_toml_path`), and `load_toml` from theme.rs to the `helix-loader` module 2 years ago
LazyTanuki d4c3609c43 wip: generalised `load_theme` into `load_inheritable_toml` 2 years ago
Trivernis 27e965ef2f Merge pull request 'File Explorer Patches' (#10) from feature/file-explorer into main
Reviewed-on: #10
2 years ago
trivernis bd32cb3114
Add --show-explorer cli arg 2 years ago
trivernis 432522e724
Add update of selected file in explorer when switching buffers 2 years ago
Trivernis bfb2ce8a7a Merge pull request 'feature/file-explorer' (#9) from feature/file-explorer into main
Reviewed-on: #9
2 years ago
Trivernis fd8c4eda69 Merge pull request 'Add dracula-purple theme' (#8) from feature/dracula-purple-theme into main
Reviewed-on: #8
2 years ago
trivernis 1d86a9bdcc
Add dracula-purple theme 2 years ago
Trivernis a40aa917c6 Merge pull request 'Add `rm` command to delete the current file' (#7) from feature/delete-command into main
Reviewed-on: #7
2 years ago
trivernis 2f169b172f
Add `rm` command to delete the current file 2 years ago
wongjiahau cf9669f276 fix(ci): clippy error 2 years ago
wongjiahau 88ac941407 Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau f37c795c96 chore(ui/prompt): use &str instead of Cow<str>
- Resolve https://github.com/helix-editor/helix/pull/5768/files#r1140994104
2 years ago
wongjiahau eebff622de chore(doc/configuration/explorer/position): remove `overlay` option 2 years ago
wongjiahau a331e52971 chore(keymap): remove "<space>E"
- Personally, I never uses this shortcut
- Secondly, we are running out of keys for mappings, so I would like to
  reserve "<space>E" for other more useful mappings
2 years ago
wongjiahau f5aec54fe2 chore(commands): revert accidental typo
- Resolve https://github.com/helix-editor/helix/pull/5768/files#r1143859919
2 years ago
wongjiahau 33542e9ddb refactor: remove unnecessary dev-dependencies
- Resolve https://github.com/helix-editor/helix/pull/5768/files#r1126720143
2 years ago
wongjiahau 898c1670d1 fix(integration-test/test_goto_file_impl): failing due to untested changes 2 years ago
wongjiahau 404f950b09 fix(tests/explorer/new_folder): failing on Windows
Co-authored-by: LEI <github@lei.sh>
Reference: https://github.com/helix-editor/helix/pull/5768#discussion_r1143991188
2 years ago
wongjiahau ee34720a31 style(explorer): move title to statusline
- so that the UI is more consistent with other component of the editor
- also it may improve the focus indication
2 years ago
wongjiahau 1be2ac286b fix(ui/explorer): tree search cursor not rendered 2 years ago
wongjiahau e5dfde2a9b refactor(explorer): remove overlay option 2 years ago
wongjiahau afda68a11d chore: cargo fmt 2 years ago
wongjiahau f5af209f09 refactor(explorer): remove preview
- Also moved Tree search prompt to bottom
2 years ago
wongjiahau 52be2e0c43 refactor(ui/tree): remove filter 2 years ago
wongjiahau 41ebc30ea6 fix(ui/tree/clone): `is_openend` should not be false
Resolve https://github.com/helix-editor/helix/pull/5768#discussion_r1133066209
2 years ago
wongjiahau 8b561e2e88 fix: type error 2 years ago
wongjiahau 9a1aff25bd refactor(ui/explorer/close_documents): concise code
Resolve https://github.com/helix-editor/helix/pull/5768#discussion_r1133065427
2 years ago
wongjiahau 178086767f refactor(ui/explorer/handle_prompt_event): remove unnecessary function
Resolve https://github.com/helix-editor/helix/pull/5768#discussion_r1133064955
2 years ago
wongjiahau c4c3e8075e style(explorer/delete): capitalize default choice
Resolve https://github.com/helix-editor/helix/pull/5768#discussion_r1133064678
2 years ago
wongjiahau 54b16936db Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau 20241fb256 Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau eb9287d816 fix(ci): cargo fmt and windows test 2 years ago
WJH 1108c883c4
Merge branch 'master' into tree_explore 2 years ago
wongjiahau d043ea4db4 Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau 10032eb156 fix(ci): cargo fmt 2 years ago
wongjiahau 7ccee10297 chore: correction of e991ed9 2 years ago
wongjiahau 9726ae7dbb fix(ci/test): failing on Windows 2 years ago
wongjiahau e991ed9b17 refactor(runtime/themes): revert changes to theme files
- This is because explorer specific styling has been abandoned for
  simplicity
2 years ago
wongjiahau d1e6a21016 Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau d62b487321 feat(ui/tree): undo breaking changes
- bind tree-based movements to other keys, namely J,K,H,L
2 years ago
wongjiahau 80a2f8642c Merge branch 'tree_explore' of github.com:pinelang/helix-tree-explorer into tree_explore 2 years ago
wongjiahau aa6780e149 feat(ui/tree): tree-based movements 2 years ago
wongjiahau bc62b7615d fix(ci): failing windows test & clippy 2 years ago
wongjiahau 31c0e84461 fix(ci): failing windows test & clippy 2 years ago
wongjiahau d3db1b6204 style(tree): improve ancestor contrast 2 years ago
wongjiahau 8ef95ee56a Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau a4943a7226 fix(explorer/overlay): prompt overflow
- Previously the prompt appears within the float, which has very limited
  space
- Now, the prompt will be rendered at the editor command area
2 years ago
wongjiahau c2e2f050da feat(explorer/delete): no need to press Enter, just press y
Reference: https://github.com/helix-editor/helix/pull/5768#issuecomment-1449536275
2 years ago
wongjiahau 43b226a2ab feat(explorer/keymap): combine 'a' with 'A'
Reference: https://github.com/helix-editor/helix/pull/5768#issuecomment-1449536275
2 years ago
wongjiahau a2cb28d1d1 chore(keymap): merge with the correct version 2 years ago
wongjiahau 19d436ee56 Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau b18a9746e9 fix(explorer): go to previous root does not update state.current_root 2 years ago
wongjiahau 8379669742 Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau fae4990444 test(tree): search prompt and filter prompt 2 years ago
wongjiahau 7e4feb02ef fix(explore): search using previous search word after filter does not work
- Also implemented restore_saved_view for filter and search
2 years ago
wongjiahau 4a0c620b77 fix(explorer/filter): not working for newly opened folder 2 years ago
wongjiahau c0073edebf Merge branch 'tree_explore' of github.com:pinelang/helix-tree-explorer into tree_explore 2 years ago
WJH c3b8be978e
fix(ci): clippy + failure on Windows 2 years ago
wongjiahau d578f8af61 chore: fix clippy warning 2 years ago
wongjiahau 5d600fef0f doc(helix-term/.gitignore): document purpose of test-explorer 2 years ago
wongjiahau ef1850295b chore: remove temp file 2 years ago
wongjiahau 601f2c4e5f chore(ui/tree): remove useless comments 2 years ago
wongjiahau ba00a80037 fix(tree): shouldn't use patched font 2 years ago
wongjiahau 72b845da15 Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore 2 years ago
wongjiahau 24b50bb525 feat(explorer): toggle preview 2 years ago
wongjiahau 38ef079099 feat(tree): jump forward 2 years ago
wongjiahau b5d92aca45 chore: fix clippy warnings 2 years ago
wongjiahau 36769cb3f6 fix(explorer/keymap): change 'b' to 'B'
- to not clash with Tree 'zb'
2 years ago
wongjiahau dffbc15067 refactor(explorer,tree): remove unwrap to avoid panics 2 years ago
wongjiahau cf9b60a3d1 feat(tree): sticky ancestors 2 years ago
wongjiahau 9205117505 fix: failing tests 2 years ago
wongjiahau 6af9a06e74 feat(explorer): bind "="/"_" to "Zoom in"/"Zoom out" 2 years ago
wongjiahau 899491ba25 feat(tree): add C-n/C-p keybinding 2 years ago
wongjiahau f9ff01dd9c chore(ui/tree): bind 'o' to Toggle 2 years ago
wongjiahau 7b63fda7d2 test(explorer): add integration tests 2 years ago
wongjiahau 6321dc9ade chore: rename explore to explorer 2 years ago
wongjiahau 78bb29732a Merge branch 'master' of https://github.com/helix-editor/helix into add-integration-test 2 years ago
wongjiahau bcb1672378 fix(explore):
- preview panics when term height becomes too small
- preview content not sorted
2 years ago
wongjiahau a259c205c0 fix(explore): help overflow
- render with Info
2 years ago
wongjiahau 2e7709e505 MULTI
- refactor(explore):Move filter to Tree
- feat(explore): Implement mkdir -p (but not tested yet)
- feat(ui/tree): Implement jump backward
- test(ui/tree): Refresh
2 years ago
wongjiahau 2e654a0775 refactor(explore): move search function to Tree 2 years ago
wongjiahau 2a60662e8b feat(explore): add focus indicator 2 years ago
wongjiahau 64059fba47 feat(tree): move left/right 2 years ago
wongjiahau c88164f2fa feat(tree-view): add unit tests 2 years ago
wongjiahau 4dfa8696bd style(tree): increase indentation 2 years ago
wongjiahau f0a4b109ad Merge branch 'refactor-tree-explorer' of github.com:pinelang/helix-tree-explorer into refactor-tree-explorer 2 years ago
wongjiahau 70984fd148 Merge branch 'master' of https://github.com/helix-editor/helix into refactor-tree-explorer 2 years ago
wongjiahau 0f8e0a51ba fix(tree): deleting last file causes panic 2 years ago
wongjiahau ef73559a8e fix(explore): cannot focus explorer if no opened document 2 years ago
wongjiahau 30bac647ef Revert "style(explore): make Right the default position"
This reverts commit 374b8ddd4e.
2 years ago
wongjiahau c8578ba3cc fix: warnings 2 years ago
wongjiahau 374b8ddd4e style(explore): make Right the default position
Refer https://twitter.com/JustinWGrote/status/1346575528560455682
2 years ago
wongjiahau 94e2c2989b fix(command): space e does not focus explorer when no files are opened 2 years ago
wongjiahau 72495363f1 fix(explore): 'h' does not realign preview properly 2 years ago
wongjiahau 9bd534bb6f fix(explore): filter 2 years ago
wongjiahau 85fa1c56b7 feat(explore):
- filter
- close document if the file is deleted or renamed
2 years ago
wongjiahau a079477a23 fix(compile): warnings 2 years ago
wongjiahau 56056e8556 fix(explore): increase size will cause panic 2 years ago
wongjiahau b38a941955 feat(explore): close without clearing previous state 2 years ago
wongjiahau 790192dfd4 doc(explorer): up to date 2 years ago
wongjiahau 2c221f0af1 fix(explore): help page overflow 2 years ago
wongjiahau 35ffc6036d feat(explore): increase/decrease explorer size 2 years ago
wongjiahau 2bafac0c4e feat(explore): go to previous root 2 years ago
wongjiahau ddb7564809 feat(explore): add help 2 years ago
wongjiahau ec2059bf93 style(ui/tree): highlight ancestor 2 years ago
wongjiahau 52a26ff72c feat(explore): refresh 2 years ago
wongjiahau 5a5a1de4b8 fix(explore/rename): should regenarate index 2 years ago
wongjiahau 44b46dda6a feat(explore): rename file/folder 2 years ago
wongjiahau 2af8b41007 feat(explore): remove files/folder 2 years ago
wongjiahau 458fa1ca58 feat(explore): add folder/file 2 years ago
wongjiahau 0f8b641a5d feat(tree): filter 2 years ago
wongjiahau 82fe4a309d test(ui/tree): find 2 years ago
wongjiahau bdab93e856 feat(explore): search 2 years ago
wongjiahau aa397ef801 feat(explore): reveal current file 2 years ago
wongjiahau d04a1ce214 refactor(tree): change internal implementation
Previous: Vec+Tree hybrid, hard to debug and understand
Now: Pure Tree structure, easy to understand and test
2 years ago
wongjiahau c446c39645 feat(explorer/position): right
According to https://github.com/helix-editor/helix/pull/5768#issuecomment-1413162928
2 years ago
wongjiahau d9d4daa87d feat(ui/explore): implement "focus current file" 2 years ago
cossonleo b652f96449 tree helper and file explorer 2 years ago

@ -4,7 +4,7 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="logo_dark.svg">
<source media="(prefers-color-scheme: light)" srcset="logo_light.svg">
<img alt="Helix" height="128" src="logo_light.svg">
<img alt="Helix" height="128" src="logo_dark.svg">
</picture>
</h1>
@ -18,7 +18,7 @@
![Screenshot](./screenshot.png)
A Kakoune / Neovim inspired editor, written in Rust.
This is a fork of helix, a Kakoune / Neovim inspired editor, written in Rust.
The editing model is very heavily based on Kakoune; during development I found
myself agreeing with most of Kakoune's design decisions.
@ -43,6 +43,16 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.scm`.
# Additional Features of helix-plus
- [File Explorer](https://github.com/helix-editor/helix/pull/5768)
- Added automatic update of the current file in the tree view
- Added icon support
- Added `--show-explorer` CLI flag to show the explorer when opening helix
- [Icons](https://github.com/helix-editor/helix/pull/2869)
- `rm` command to delete the file associated with the current buffer
- `dracula-purple` theme which is a combination of `dracula` and `boo_berry`
# Installation
[Installation documentation](https://docs.helix-editor.com/install.html).

@ -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 |
@ -185,6 +187,8 @@ auto-pairs = false # defaults to `true`
The default pairs are <code>(){}[]''""``</code>, but these can be customized by
setting `auto-pairs` to a TOML table:
Example
```toml
[editor.auto-pairs]
'(' = ')'
@ -322,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:
@ -343,3 +359,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it
```
### `[editor.explorer]` Section
Sets explorer side width and style.
| Key | Description | Default |
| --- | ----------- | ------- |
| `column-width` | explorer side width | 30 |
| `position` | explorer widget position, `left` or `right` | `left` |

@ -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 = "#…" }
```

@ -291,6 +291,8 @@ This layer is a kludge of mappings, mostly pickers.
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
| `?` | Open command palette | `command_palette` |
| `e` | Reveal current file in explorer | `reveal_current_file` |
> 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file.
@ -442,3 +444,7 @@ Keys to use within prompt, Remapping currently not supported.
| `Tab` | Select next completion item |
| `BackTab` | Select previous completion item |
| `Enter` | Open selected |
# File explorer
Press `?` to see keymaps. Remapping currently not supported.

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

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

@ -1 +1,4 @@
/target
# This folder is used by `test_explorer` to create temporary folders needed for testing
test_explorer

@ -11,7 +11,7 @@ use helix_view::{
document::DocumentSavedEventResult,
editor::{ConfigEvent, EditorEvent},
graphics::Rect,
theme,
icons, theme,
tree::Layout,
Align, Editor,
};
@ -21,11 +21,11 @@ use tui::backend::Backend;
use crate::{
args::Args,
commands::apply_workspace_edit,
compositor::{Compositor, Event},
compositor::{self, Compositor, Event},
config::Config,
job::Jobs,
keymap::Keymaps,
ui::{self, overlay::overlaid},
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,16 +162,34 @@ 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
}));
let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
let mut editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
let mut jobs = Jobs::new();
if args.show_explorer {
let mut context = compositor::Context {
editor: &mut editor,
scroll: None,
jobs: &mut jobs,
};
let mut explorer = Explorer::new(&mut context)?;
explorer.unfocus();
editor_view.explorer = Some(explorer);
}
compositor.push(editor_view);
if args.load_tutor {
@ -168,8 +202,8 @@ 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);
compositor.push(Box::new(overlaid(picker)));
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();
for (i, (file, pos)) in args.files.into_iter().enumerate() {
@ -225,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))]
@ -241,10 +273,11 @@ impl Application {
config,
theme_loader,
icons_loader,
syn_loader,
signals,
jobs: Jobs::new(),
jobs,
lsp_progress: LspProgressMap::new(),
last_render: Instant::now(),
};
@ -413,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(())

@ -10,6 +10,7 @@ pub struct Args {
pub health: bool,
pub health_arg: Option<String>,
pub load_tutor: bool,
pub show_explorer: bool,
pub fetch_grammars: bool,
pub build_grammars: bool,
pub split: Option<Layout>,
@ -32,6 +33,7 @@ impl Args {
"--version" => args.display_version = true,
"--help" => args.display_help = true,
"--tutor" => args.load_tutor = true,
"--show-explorer" => args.show_explorer = true,
"--vsplit" => match args.split {
Some(_) => anyhow::bail!("can only set a split once of a specific type"),
None => args.split = Some(Layout::Vertical),

@ -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,
@ -472,6 +473,8 @@ impl MappableCommand {
record_macro, "Record macro",
replay_macro, "Replay macro",
command_palette, "Open command palette",
open_or_focus_explorer, "Open or focus explorer",
reveal_current_file, "Reveal current file in explorer",
);
}
@ -1989,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)
@ -2001,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()
}
}
}
@ -2117,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(_) => {}
@ -2420,7 +2431,7 @@ 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());
let picker = ui::file_picker(root, &cx.editor.config(), &cx.editor.icons);
cx.push_layer(Box::new(overlaid(picker)));
}
@ -2437,15 +2448,58 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
}
};
let picker = ui::file_picker(path, &cx.editor.config());
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());
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) {
cx.callback = Some(Box::new(
|compositor: &mut Compositor, cx: &mut compositor::Context| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
match editor.explorer.as_mut() {
Some(explore) => explore.focus(),
None => match ui::Explorer::new(cx) {
Ok(explore) => editor.explorer = Some(explore),
Err(err) => cx.editor.set_error(format!("{}", err)),
},
}
}
},
));
}
fn reveal_file(cx: &mut Context, path: Option<PathBuf>) {
cx.callback = Some(Box::new(
|compositor: &mut Compositor, cx: &mut compositor::Context| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
(|| match editor.explorer.as_mut() {
Some(explorer) => match path {
Some(path) => explorer.reveal_file(path),
None => explorer.reveal_current_file(cx),
},
None => {
editor.explorer = Some(ui::Explorer::new(cx)?);
if let Some(explorer) = editor.explorer.as_mut() {
explorer.reveal_current_file(cx)?;
}
Ok(())
}
})()
.unwrap_or_else(|err| cx.editor.set_error(err.to_string()))
}
},
));
}
fn reveal_current_file(cx: &mut Context) {
reveal_file(cx, None)
}
fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc;
@ -2459,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()
@ -2469,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('+');
@ -2477,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()])
}
}
}
@ -2495,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);
},
@ -2523,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()
@ -2543,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()
}
}
}
@ -2577,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();
@ -2596,7 +2674,7 @@ fn jumplist_picker(cx: &mut Context) {
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() {
@ -2638,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),

@ -8,7 +8,7 @@ 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)?;
@ -273,6 +274,7 @@ pub fn dap_launch(cx: &mut Context) {
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,
};
@ -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
@ -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,7 +433,7 @@ pub fn symbol_picker(cx: &mut Context) {
}
};
let picker = sym_picker(symbols, current_url, offset_encoding);
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() {
@ -476,7 +538,7 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) {
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(),
@ -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,6 +1012,7 @@ fn goto_impl(
let picker = FilePicker::new(
locations,
cwdir,
None,
move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
},

@ -115,7 +115,7 @@ 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());
let picker = ui::file_picker(path, &editor.config(), &editor.icons);
compositor.push(Box::new(overlaid(picker)));
},
));
@ -298,6 +298,30 @@ fn force_buffer_close_all(
buffer_close_by_ids_impl(cx, &document_ids, true)
}
fn delete(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
cx.block_try_flush_writes()?;
let doc = doc_mut!(cx.editor);
if doc.path().is_none() {
bail!("cannot delete a buffer with no associated file on the disk");
}
let doc_id = view!(cx.editor).doc;
let future = doc.delete();
cx.jobs.add(Job::new(future));
buffer_close_by_ids_impl(cx, &[doc_id], true)
}
fn buffer_next(
cx: &mut compositor::Context,
_args: &[Cow<str>],
@ -853,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>],
@ -1332,7 +1380,7 @@ 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(overlaid(picker)))
@ -2229,6 +2277,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: force_buffer_close_all,
signature: CommandSignature::none(),
},
TypableCommand {
name: "delete",
aliases: &["remove", "rm", "del"],
doc: "Deletes the file associated with the current buffer",
fun: delete,
signature: CommandSignature::none(),
},
TypableCommand {
name: "buffer-next",
aliases: &["bn", "bnext"],
@ -2374,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: &[],

@ -34,6 +34,48 @@ impl<'a> Context<'a> {
tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?;
Ok(())
}
/// Purpose: to test `handle_event` without escalating the test case to integration test
/// Usage:
/// ```
/// let mut editor = Context::dummy_editor();
/// let mut jobs = Context::dummy_jobs();
/// let mut cx = Context::dummy(&mut jobs, &mut editor);
/// ```
#[cfg(test)]
pub fn dummy(jobs: &'a mut Jobs, editor: &'a mut helix_view::Editor) -> Context<'a> {
Context {
jobs,
scroll: None,
editor,
}
}
#[cfg(test)]
pub fn dummy_jobs() -> Jobs {
Jobs::new()
}
#[cfg(test)]
pub fn dummy_editor() -> Editor {
use crate::config::Config;
use arc_swap::{access::Map, ArcSwap};
use helix_core::syntax::{self, Configuration};
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),
|config: &Config| &config.editor,
))),
)
}
}
pub trait Component: Any + AnyComponent {
@ -72,6 +114,21 @@ pub trait Component: Any + AnyComponent {
fn id(&self) -> Option<&'static str> {
None
}
#[cfg(test)]
/// Utility method for testing `handle_event` without using integration test.
/// Especially useful for testing helper components such as `Prompt`, `TreeView` etc
fn handle_events(&mut self, events: &str) -> anyhow::Result<()> {
use helix_view::input::parse_macro;
let mut editor = Context::dummy_editor();
let mut jobs = Context::dummy_jobs();
let mut cx = Context::dummy(&mut jobs, &mut editor);
for event in parse_macro(events)? {
self.handle_event(&Event::Key(event), &mut cx);
}
Ok(())
}
}
pub struct Compositor {

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

@ -274,6 +274,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"r" => rename_symbol,
"h" => select_references_to_symbol_under_cursor,
"?" => command_palette,
"e" => reveal_current_file,
},
"z" => { "View"
"z" | "c" => align_view_center,

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

@ -6,7 +6,7 @@ use crate::{
keymap::{KeymapResult, Keymaps},
ui::{
document::{render_document, LinePos, TextRenderer, TranslatedPosition},
Completion, ProgressSpinners,
Completion, Explorer, ProgressSpinners,
},
};
@ -23,7 +23,7 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
editor::{CompleteAction, CursorShapeConfig, ExplorerPosition},
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers},
@ -43,6 +43,7 @@ pub struct EditorView {
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
pub(crate) explorer: Option<Explorer>,
}
#[derive(Debug, Clone)]
@ -68,6 +69,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
explorer: None,
}
}
@ -531,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)
@ -551,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;
@ -1205,6 +1239,11 @@ impl Component for EditorView {
event: &Event,
context: &mut crate::compositor::Context,
) -> EventResult {
if let Some(explore) = self.explorer.as_mut() {
if let EventResult::Consumed(callback) = explore.handle_event(event, context) {
return EventResult::Consumed(callback);
}
}
let mut cx = commands::Context {
editor: context.editor,
count: None,
@ -1361,6 +1400,8 @@ impl Component for EditorView {
surface.set_style(area, cx.editor.theme.get("ui.background"));
let config = cx.editor.config();
let editor_area = area.clip_bottom(1);
// check if bufferline should be rendered
use helix_view::editor::BufferLine;
let use_bufferline = match config.bufferline {
@ -1369,15 +1410,43 @@ impl Component for EditorView {
_ => false,
};
// -1 for commandline and -1 for bufferline
let mut editor_area = area.clip_bottom(1);
if use_bufferline {
editor_area = editor_area.clip_top(1);
}
let editor_area = if use_bufferline {
editor_area.clip_top(1)
} else {
editor_area
};
let editor_area = if let Some(explorer) = &self.explorer {
let explorer_column_width = if explorer.is_opened() {
explorer.column_width().saturating_add(2)
} else {
0
};
// For future developer:
// We should have a Dock trait that allows a component to dock to the top/left/bottom/right
// of another component.
match config.explorer.position {
ExplorerPosition::Left => editor_area.clip_left(explorer_column_width),
ExplorerPosition::Right => editor_area.clip_right(explorer_column_width),
}
} else {
editor_area
};
// if the terminal size suddenly changed, we need to trigger a resize
cx.editor.resize(editor_area);
if let Some(explorer) = self.explorer.as_mut() {
if !explorer.is_focus() {
let area = if use_bufferline {
area.clip_top(1)
} else {
area
};
explorer.render(area, surface, cx);
}
}
if use_bufferline {
Self::render_bufferline(cx.editor, area.with_height(1), surface);
}
@ -1456,9 +1525,47 @@ impl Component for EditorView {
if let Some(completion) = self.completion.as_mut() {
completion.render(area, surface, cx);
}
if let Some(explore) = self.explorer.as_mut() {
let needs_update = explore.is_focus() || {
if let Some(current_document_path) = doc!(cx.editor).path().cloned() {
if let Some(current_explore_path) = explore.current_file() {
if *current_explore_path != current_document_path {
let _ = explore.reveal_file(current_document_path);
true
} else {
false
}
} else {
let _ = explore.reveal_file(current_document_path);
true
}
} else {
false
}
};
if needs_update {
let area = if use_bufferline {
area.clip_top(1)
} else {
area
};
explore.render(area, surface, cx);
}
}
}
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
if let Some(explore) = &self.explorer {
if explore.is_focus() {
let cursor = explore.cursor(_area, editor);
if cursor.0.is_some() {
return cursor;
}
}
}
match editor.cursor() {
// All block cursors are drawn manually
(pos, CursorKind::Block) => (pos, CursorKind::Hidden),

File diff suppressed because it is too large Load Diff

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

@ -1,6 +1,7 @@
mod completion;
mod document;
pub(crate) mod editor;
mod explorer;
mod fuzzy_match;
mod info;
pub mod lsp;
@ -13,12 +14,14 @@ mod prompt;
mod spinner;
mod statusline;
mod text;
mod tree;
use crate::compositor::{Component, Compositor};
use crate::filter_picker_entry;
use crate::job::{self, Callback};
pub use completion::Completion;
pub use editor::EditorView;
use helix_view::icons::Icons;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
@ -26,7 +29,9 @@ pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
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;
@ -158,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;
@ -220,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() {
@ -239,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;
@ -280,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());
@ -311,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() {

@ -19,26 +19,7 @@ pub struct Overlay<T> {
pub fn overlaid<T>(content: T) -> Overlay<T> {
Overlay {
content,
calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)),
}
}
fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect {
fn mul_and_cast(size: u16, factor: u8) -> u16 {
((size as u32) * (factor as u32) / 100).try_into().unwrap()
}
let inner_w = mul_and_cast(rect.width, percent_horizontal);
let inner_h = mul_and_cast(rect.height, percent_vertical);
let offset_x = rect.width.saturating_sub(inner_w) / 2;
let offset_y = rect.height.saturating_sub(inner_h) / 2;
Rect {
x: rect.x + offset_x,
y: rect.y + offset_y,
width: inner_w,
height: inner_h,
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 = " ";
@ -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)

@ -94,6 +94,10 @@ impl Prompt {
self
}
pub fn prompt(&self) -> &str {
self.prompt.as_ref()
}
pub fn line(&self) -> &String {
&self.line
}

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

File diff suppressed because it is too large Load Diff

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

@ -544,6 +544,21 @@ impl Document {
}
}
/// Deletes the file associated with this document
pub fn delete(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
let path = self
.path()
.expect("Cannot delete with no path set!")
.clone();
async move {
use tokio::fs;
fs::remove_file(path).await?;
Ok(())
}
}
/// If supported, returns the changes that should be applied to this document in order
/// to format it nicely.
// We can't use anyhow::Result here since the output of the future has to be

@ -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},
@ -211,6 +212,51 @@ impl Default for FilePickerConfig {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct ExplorerConfig {
pub position: ExplorerPosition,
/// explorer column width
pub column_width: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExplorerPosition {
Left,
Right,
}
impl Default for ExplorerConfig {
fn default() -> Self {
Self {
position: ExplorerPosition::Left,
column_width: 36,
}
}
}
#[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 {
@ -281,9 +327,13 @@ pub struct Config {
pub indent_guides: IndentGuidesConfig,
/// Whether to color modes with different colors. Defaults to `false`.
pub color_modes: bool,
/// explore config
pub explorer: ExplorerConfig,
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)]
@ -452,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,
@ -749,10 +802,12 @@ impl Default for Config {
bufferline: BufferLine::default(),
indent_guides: IndentGuidesConfig::default(),
color_modes: false,
explorer: ExplorerConfig::default(),
soft_wrap: SoftWrap::default(),
text_width: 80,
completion_replace: false,
workspace_lsp_roots: Vec::new(),
icons: IconsConfig::default(),
}
}
}
@ -829,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
@ -851,7 +908,7 @@ pub struct Editor {
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
/// Allows asynchronous tasks to control the rendering
/// The `Notify` allows asynchronous tasks to request the editor to perform a redraw
/// The `RwLock` blocks the editor from performing the render until an exclusive lock can be acquired
/// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired
pub redraw_handle: RedrawHandle,
pub needs_redraw: bool,
/// Cached position of the cursor calculated during rendering.
@ -923,15 +980,30 @@ pub enum CloseError {
SaveError(anyhow::Error),
}
impl From<CloseError> for anyhow::Error {
fn from(error: CloseError) -> Self {
match error {
CloseError::DoesNotExist => anyhow::anyhow!("Document doesn't exist"),
CloseError::BufferModified(error) => {
anyhow::anyhow!(format!("Buffer modified: '{error}'"))
}
CloseError::SaveError(error) => anyhow::anyhow!(format!("Save error: {error}")),
}
}
}
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;
@ -974,6 +1046,8 @@ impl Editor {
needs_redraw: false,
cursor_cache: Cell::new(None),
completion_request_handle: None,
icons,
icons_loader,
}
}
@ -1074,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;
}
}
@ -1081,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)

@ -248,6 +248,34 @@ impl Rect {
&& self.y < other.y + other.height
&& self.y + self.height > other.y
}
/// Returns a smaller `Rect` with a margin of 5% on each side, and an additional 2 rows at the bottom
pub fn overlayed(self) -> Rect {
self.clip_bottom(2).clip_relative(90, 90)
}
/// Returns a smaller `Rect` with width and height clipped to the given `percent_horizontal`
/// and `percent_vertical`.
///
/// Value of `percent_horizontal` and `percent_vertical` is from 0 to 100.
pub fn clip_relative(self, percent_horizontal: u8, percent_vertical: u8) -> Rect {
fn mul_and_cast(size: u16, factor: u8) -> u16 {
((size as u32) * (factor as u32) / 100).try_into().unwrap()
}
let inner_w = mul_and_cast(self.width, percent_horizontal);
let inner_h = mul_and_cast(self.height, percent_vertical);
let offset_x = self.width.saturating_sub(inner_w) / 2;
let offset_y = self.height.saturating_sub(inner_h) / 2;
Rect {
x: self.x + offset_x,
y: self.y + offset_y,
width: inner_w,
height: inner_h,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

@ -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,62 +80,8 @@ 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");
@ -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 = "▍"}

@ -1 +1,66 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" viewBox="663.38 37.57 575.35 903.75"> <g transform="matrix(1,0,0,1,-31352.7,-1817.25)"> <g transform="matrix(1,0,0,1,31062.7,-20.8972)"> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z" style="fill:#706bc8;"></path> </g> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z" style="fill:#55c5e4;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z" style="fill:#84ddea;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z" style="fill:#997bc8;"></path></g></g></g> </svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
viewBox="663.38 37.57 575.35 903.75"
id="svg22"
sodipodi:docname="logo.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs26" /><sodipodi:namedview
id="namedview24"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.58133371"
inkscape:cx="100.63067"
inkscape:cy="442.08687"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g18" /> <g
transform="matrix(1,0,0,1,-31352.7,-1817.25)"
id="g20"> <g
transform="matrix(1,0,0,1,31062.7,-20.8972)"
id="g18"> <g
transform="matrix(1,0,0,1,-130.173,0.00185558)"
id="g4"> <path
d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z"
style="fill:#706bc8;"
id="path2" /> </g> <g
transform="matrix(1,0,0,1,-130.173,0.00185558)"
id="g8"> <path
d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z"
style="fill:#55c5e4;"
id="path6" /> </g> <g
transform="matrix(1,0,0,1,216.062,984.098)"
id="g12"> <path
d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z"
style="fill:#84ddea;"
id="path10" /> </g> <g
transform="matrix(1,0,0,1,216.062,984.098)"
id="g16"> <path
d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z"
style="fill:#997bc8;"
id="path14" /></g><text
xml:space="preserve"
style="font-size:742.268px;clip-rule:evenodd;fill:#8ff8b6;fill-opacity:1;fill-rule:evenodd;stroke:#8ff8b6;stroke-width:20;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal;-inkscape-font-specification:'sans-serif Bold';font-family:sans-serif;font-weight:bold;font-style:normal;font-stretch:normal;font-variant:normal"
x="1086.8125"
y="2811.8589"
id="text186"><tspan
sodipodi:role="line"
id="tspan184"
x="1086.8125"
y="2811.8589"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:742.268px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#8ff8b6;fill-opacity:1;stroke:#8ff8b6;stroke-width:20;stroke-linecap:square;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;paint-order:normal">+</tspan></text></g></g> </svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

@ -8,7 +8,7 @@
sodipodi:docname="logo_dark.svg"
width="2087.0059"
height="903.71997"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@ -53,13 +53,13 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.28409405"
inkscape:cx="1904.2989"
inkscape:cy="633.59299"
inkscape:zoom="0.40176966"
inkscape:cx="801.45425"
inkscape:cy="775.31987"
inkscape:window-width="1908"
inkscape:window-height="2075"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-height="996"
inkscape:window-x="4"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg22" /> <g
transform="translate(-31352.726,-1817.2547)"
@ -112,4 +112,24 @@
xml:space="preserve"
transform="translate(663.38,37.570044)"
id="text14661"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:997.723px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, @wght=700';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 700;white-space:pre;shape-inside:url(#rect14663);display:inline;fill:#2a292f;fill-opacity:1;stroke:#2a292f;stroke-width:6.652;stroke-dasharray:none;stroke-opacity:1" /></svg>
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:997.723px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, @wght=700';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 700;white-space:pre;shape-inside:url(#rect14663);display:inline;fill:#2a292f;fill-opacity:1;stroke:#2a292f;stroke-width:6.652;stroke-dasharray:none;stroke-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:742.268px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';clip-rule:evenodd;fill:#8ff8b6;fill-opacity:1;fill-rule:evenodd;stroke:#8ff8b6;stroke-width:20;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
x="798.0365"
y="973.64172"
id="text186"><tspan
sodipodi:role="line"
id="tspan184"
x="798.0365"
y="973.64172"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:742.268px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#8ff8b6;fill-opacity:1;stroke:#8ff8b6;stroke-width:20;stroke-linecap:square;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;paint-order:normal">+</tspan></text><text
xml:space="preserve"
style="font-weight:bold;font-size:200px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';clip-rule:evenodd;fill:#8ff8b6;fill-opacity:1;fill-rule:evenodd;stroke:#fe92c5;stroke-width:20;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:2"
x="2269.8679"
y="884.69611"
id="text416"><tspan
sodipodi:role="line"
id="tspan414"
x="2269.8679"
y="884.69611"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:200px;font-family:'FiraCode Nerd Font';-inkscape-font-specification:'FiraCode Nerd Font Bold';fill:#8ff8b6;fill-opacity:1;stroke:none">plus</tspan></text></svg>

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

@ -8,7 +8,7 @@
sodipodi:docname="logo_light.svg"
width="2087.0059"
height="903.71997"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@ -53,13 +53,13 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.28409405"
inkscape:cx="1904.2989"
inkscape:cy="633.59299"
inkscape:zoom="0.40176966"
inkscape:cx="752.91897"
inkscape:cy="551.31092"
inkscape:window-width="1908"
inkscape:window-height="2075"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-height="996"
inkscape:window-x="4"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg22" /> <g
transform="translate(-31352.726,-1817.2547)"
@ -112,4 +112,24 @@
xml:space="preserve"
transform="translate(663.38,37.570044)"
id="text14661"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:997.723px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, @wght=700';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 700;white-space:pre;shape-inside:url(#rect14663);display:inline;fill:#2a292f;fill-opacity:1;stroke:#2a292f;stroke-width:6.652;stroke-dasharray:none;stroke-opacity:1" /></svg>
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:997.723px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, @wght=700';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 700;white-space:pre;shape-inside:url(#rect14663);display:inline;fill:#2a292f;fill-opacity:1;stroke:#2a292f;stroke-width:6.652;stroke-dasharray:none;stroke-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:742.268px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';clip-rule:evenodd;fill:#8ff8b6;fill-opacity:1;fill-rule:evenodd;stroke:#8ff8b6;stroke-width:20;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
x="797.79547"
y="973.59271"
id="text186"><tspan
sodipodi:role="line"
id="tspan184"
x="797.79547"
y="973.59271"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:742.268px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#8ff8b6;fill-opacity:1;stroke:#8ff8b6;stroke-width:20;stroke-linecap:square;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;paint-order:normal">+</tspan></text><text
xml:space="preserve"
style="font-weight:bold;font-size:200px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#8ff8b6;fill-opacity:1;stroke:#fe92c5;stroke-width:20;stroke-linecap:square"
x="2269.3535"
y="886.34314"
id="text416"><tspan
sodipodi:role="line"
id="tspan414"
x="2269.3535"
y="886.34314"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:200px;font-family:'FiraCode Nerd Font';-inkscape-font-specification:'FiraCode Nerd Font Bold';fill:#8ff8b6;fill-opacity:1;stroke:none">plus</tspan></text></svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

@ -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,82 @@
# Author : Sam Sartor <me@samsartor.com>, Trivernis <trivernis@pm.me>
# A port of https://github.com/bceskavich/dracula-at-night
"comment" = { fg = "comment" }
"constant" = { fg = "purple" }
"constant.character.escape" = { fg = "pink" }
"function" = { fg = "green" }
"keyword" = { fg = "pink" }
"operator" = { fg = "pink" }
"special" = { fg = "yellow" }
"punctuation" = { fg = "foreground" }
"string" = { fg = "yellow" }
"string.regexp" = { fg = "red" }
"tag" = { fg = "pink" }
"attribute" = { fg = "cyan" }
"type" = { fg = "cyan", modifiers = ["italic"] }
"type.enum.variant" = { fg = "foreground", modifiers = ["italic"] }
"variable" = { fg = "foreground" }
"variable.builtin" = { fg = "cyan", modifiers = ["italic"] }
"variable.parameter" = { fg ="orange", modifiers = ["italic"] }
"diff.plus" = { fg = "green" }
"diff.delta" = { fg = "orange" }
"diff.minus" = { fg = "red" }
"ui.background" = { fg = "foreground", bg = "background" }
"ui.cursor" = { fg = "background", bg = "orange", modifiers = ["dim"] }
"ui.cursor.match" = { fg = "green", modifiers = ["underlined"] }
"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] }
"ui.cursorline" = {bg = "background_dark"}
"ui.help" = { fg = "foreground", bg = "background_dark" }
"ui.linenr" = { fg = "comment" }
"ui.linenr.selected" = { fg = "foreground" }
"ui.menu" = { fg = "foreground", bg = "background_dark" }
"ui.menu.selected" = { fg = "cyan", bg = "background_dark" }
"ui.popup" = { fg = "foreground", bg = "background_dark" }
"ui.selection" = { fg = "background", bg = "purple", modifiers = ["dim"] }
"ui.selection.primary" = { fg = "background", bg = "pink" }
"ui.text" = { fg = "foreground" }
"ui.text.focus" = { fg = "cyan" }
"ui.window" = { fg = "foreground" }
"ui.virtual.ruler" = { bg = "ruler" }
"ui.virtual.indent-guide" = { fg = "ruler" }
"ui.statusline" = { fg = "foreground", bg = "background_dark" }
"ui.statusline.inactive" = { fg = "comment", bg = "background_dark" }
"ui.statusline.normal" = { fg = "background_dark", bg = "purple"}
"ui.statusline.insert" = { fg = "background_dark", bg = "pink"}
"ui.statusline.select" = { fg = "background_dark", bg = "cyan"}
"error" = { fg = "red" }
"warning" = { fg = "cyan" }
"markup.heading" = { fg = "purple", modifiers = ["bold"] }
"markup.list" = "cyan"
"markup.bold" = { fg = "orange", modifiers = ["bold"] }
"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
"markup.link.url" = "cyan"
"markup.link.text" = "pink"
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "foreground" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "cyan" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "comment" }
rainbow = ["#7c5ea3", "#9c5b95", "#9c5e80", "#6b4466"]
[palette]
background = "#3A2A4D"
background_dark = "#2B1C3D"
foreground = "#f8f8f2"
ruler = "#453254"
comment = "#886C9C"
red = "#ff5555"
orange = "#ffb86c"
yellow = "#f1fa8c"
green = "#50fa7b"
purple = "#bd93f9"
cyan = "#8be9fd"
pink = "#ff79c6"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 230 KiB

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