main #4

Closed
Trivernis wants to merge 215 commits from main into master

@ -85,6 +85,7 @@ jobs:
rust: stable rust: stable
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
cross: false cross: false
# 23.03: build issues
- build: aarch64-macos - build: aarch64-macos
os: macos-latest os: macos-latest
rust: stable rust: stable
@ -113,6 +114,12 @@ jobs:
mkdir -p runtime/grammars/sources mkdir -p runtime/grammars/sources
tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources
# The rust-toolchain action ignores rust-toolchain.toml files.
# Removing this before building with cargo ensures that the rust-toolchain
# is considered the same between installation and usage.
- name: Remove the rust-toolchain.toml file
run: rm rust-toolchain.toml
- name: Install ${{ matrix.rust }} toolchain - name: Install ${{ matrix.rust }} toolchain
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
@ -155,6 +162,10 @@ jobs:
shell: bash shell: bash
if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux' if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux'
run: | run: |
# Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE
sudo add-apt-repository universe
sudo apt install libfuse2
mkdir dist mkdir dist
name=dev name=dev
@ -244,7 +255,7 @@ jobs:
exe=".exe" exe=".exe"
fi fi
pkgname=helix-$GITHUB_REF_NAME-$platform pkgname=helix-$GITHUB_REF_NAME-$platform
mkdir $pkgname mkdir -p $pkgname
cp $source/LICENSE $source/README.md $pkgname cp $source/LICENSE $source/README.md $pkgname
mkdir $pkgname/contrib mkdir $pkgname/contrib
cp -r $source/contrib/completion $pkgname/contrib cp -r $source/contrib/completion $pkgname/contrib

@ -1,3 +1,276 @@
# 23.03 (2023-03-31)
23.03 brings some long-awaited and exciting features. Thank you to everyone involved! This release saw changes from 102 contributors.
For the full log, check out the [git log](https://github.com/helix-editor/helix/compare/22.12..23.03).
Also check out the [release notes](https://helix-editor.com/news/release-23-03-highlights/) for more commentary on larger features.
Breaking changes:
- Select diagnostic range in `goto_*_diag` commands ([#4713](https://github.com/helix-editor/helix/pull/4713), [#5164](https://github.com/helix-editor/helix/pull/5164), [#6193](https://github.com/helix-editor/helix/pull/6193))
- Remove jump behavior from `increment`/`decrement` ([#4123](https://github.com/helix-editor/helix/pull/4123), [#5929](https://github.com/helix-editor/helix/pull/5929))
- Select change range in `goto_*_change` commands ([#5206](https://github.com/helix-editor/helix/pull/5206))
- Split file modification indicator from filename statusline elements ([#4731](https://github.com/helix-editor/helix/pull/4731), [#6036](https://github.com/helix-editor/helix/pull/6036))
- Jump to symbol ranges in LSP goto commands ([#5986](https://github.com/helix-editor/helix/pull/5986))
- Workspace detection now stops at the first `.helix/` directory (merging multiple `.helix/languages.toml` configurations is no longer supported) ([#5748](https://github.com/helix-editor/helix/pull/5748))
Features:
- Dynamic workspace symbol picker ([#5055](https://github.com/helix-editor/helix/pull/5055))
- Soft-wrap ([#5420](https://github.com/helix-editor/helix/pull/5420), [#5786](https://github.com/helix-editor/helix/pull/5786), [#5893](https://github.com/helix-editor/helix/pull/5893), [#6142](https://github.com/helix-editor/helix/pull/6142), [#6440](https://github.com/helix-editor/helix/pull/6440))
- Initial support for LSP snippet completions ([#5864](https://github.com/helix-editor/helix/pull/5864), [b1f7528](https://github.com/helix-editor/helix/commit/b1f7528), [#6263](https://github.com/helix-editor/helix/pull/6263), [bbf4800](https://github.com/helix-editor/helix/commit/bbf4800), [90348b8](https://github.com/helix-editor/helix/commit/90348b8), [f87299f](https://github.com/helix-editor/helix/commit/f87299f), [#6371](https://github.com/helix-editor/helix/pull/6371), [9fe3adc](https://github.com/helix-editor/helix/commit/9fe3adc))
- Add a statusline element for showing the current version control HEAD ([#5682](https://github.com/helix-editor/helix/pull/5682))
- Display LSP type hints ([#5420](https://github.com/helix-editor/helix/pull/5420), [#5934](https://github.com/helix-editor/helix/pull/5934), [#6312](https://github.com/helix-editor/helix/pull/6312))
- Enable the Kitty keyboard protocol on terminals with support ([#4939](https://github.com/helix-editor/helix/pull/4939), [#6170](https://github.com/helix-editor/helix/pull/6170), [#6194](https://github.com/helix-editor/helix/pull/6194), [#6438](https://github.com/helix-editor/helix/pull/6438))
- Add a statusline element for the basename of the current file ([#5318](https://github.com/helix-editor/helix/pull/5318))
- Add substring matching syntax for the picker ([#5658](https://github.com/helix-editor/helix/pull/5658))
- Support LSP `textDocument/prepareRename` ([#6103](https://github.com/helix-editor/helix/pull/6103))
- Allow multiple runtime directories with priorities ([#5411](https://github.com/helix-editor/helix/pull/5411))
- Allow configuring whether to insert or replace completions ([#5728](https://github.com/helix-editor/helix/pull/5728))
- Allow per-workspace config file `.helix/config.toml` ([#5748](https://github.com/helix-editor/helix/pull/5748))
- Add `workspace-lsp-roots` config option to support multiple LSP roots for use with monorepos ([#5748](https://github.com/helix-editor/helix/pull/5748))
Commands:
- `:pipe-to` which pipes selections into a shell command and ignores output ([#4931](https://github.com/helix-editor/helix/pull/4931))
- `merge_consecutive_selections` (`A-_`) combines all consecutive selections ([#5047](https://github.com/helix-editor/helix/pull/5047))
- `rotate_view_reverse` which focuses the previous view ([#5356](https://github.com/helix-editor/helix/pull/5356))
- `goto_declaration` (`gD`, requires LSP) which jumps to a symbol's declaration ([#5646](https://github.com/helix-editor/helix/pull/5646))
- `file_picker_in_current_buffer_directory` ([#4666](https://github.com/helix-editor/helix/pull/4666))
- `:character-info` which shows information about the character under the cursor ([#4000](https://github.com/helix-editor/helix/pull/4000))
- `:toggle-option` for toggling config options at runtime ([#4085](https://github.com/helix-editor/helix/pull/4085))
- `dap_restart` for restarting a debug session in DAP ([#5651](https://github.com/helix-editor/helix/pull/5651))
- `:lsp-stop` to stop the language server of the current buffer ([#5964](https://github.com/helix-editor/helix/pull/5964))
- `:reset-diff-change` for resetting a diff hunk to its original text ([#4974](https://github.com/helix-editor/helix/pull/4974))
- `:config-open-workspace` for opening the config file local to the current workspace ([#5748](https://github.com/helix-editor/helix/pull/5748))
Usability improvements:
- Remove empty detail section in completion menu when LSP doesn't send details ([#4902](https://github.com/helix-editor/helix/pull/4902))
- Pass client information on LSP initialization ([#4904](https://github.com/helix-editor/helix/pull/4904))
- Allow specifying environment variables for language servers in language config ([#4004](https://github.com/helix-editor/helix/pull/4004))
- Allow detached git worktrees to be recognized as root paths ([#5097](https://github.com/helix-editor/helix/pull/5097))
- Improve error message handling for theme loading failures ([#5073](https://github.com/helix-editor/helix/pull/5073))
- Print the names of binaries required for LSP/DAP in health-check ([#5195](https://github.com/helix-editor/helix/pull/5195))
- Improve sorting in the picker in cases of ties ([#5169](https://github.com/helix-editor/helix/pull/5169))
- Add theming for prompt suggestions ([#5104](https://github.com/helix-editor/helix/pull/5104))
- Open a file picker when using `:open` on directories ([#2707](https://github.com/helix-editor/helix/pull/2707), [#5278](https://github.com/helix-editor/helix/pull/5278))
- Reload language config with `:config-reload` ([#5239](https://github.com/helix-editor/helix/pull/5239), [#5381](https://github.com/helix-editor/helix/pull/5381), [#5431](https://github.com/helix-editor/helix/pull/5431))
- Improve indent queries for python when the tree is errored ([#5332](https://github.com/helix-editor/helix/pull/5332))
- Picker: Open files without closing the picker with `A-ret` ([#4435](https://github.com/helix-editor/helix/pull/4435))
- Allow theming cursors by primary/secondary and by mode ([#5130](https://github.com/helix-editor/helix/pull/5130))
- Allow configuration of the minimum width for the line-numbers gutter ([#4724](https://github.com/helix-editor/helix/pull/4724), [#5696](https://github.com/helix-editor/helix/pull/5696))
- Use filename completer for `:run-shell-command` command ([#5729](https://github.com/helix-editor/helix/pull/5729))
- Surround with line-endings with `ms<ret>` ([#4571](https://github.com/helix-editor/helix/pull/4571))
- Hide duplicate symlinks in file pickers ([#5658](https://github.com/helix-editor/helix/pull/5658))
- Tabulate buffer picker contents ([#5777](https://github.com/helix-editor/helix/pull/5777))
- Add an option to disable LSP ([#4425](https://github.com/helix-editor/helix/pull/4425))
- Short-circuit tree-sitter and word object motions ([#5851](https://github.com/helix-editor/helix/pull/5851))
- Add exit code to failed command message ([#5898](https://github.com/helix-editor/helix/pull/5898))
- Make `m` textobject look for pairs enclosing selections ([#3344](https://github.com/helix-editor/helix/pull/3344))
- Negotiate LSP position encoding ([#5894](https://github.com/helix-editor/helix/pull/5894), [a48d1a4](https://github.com/helix-editor/helix/commit/a48d1a4))
- Display deprecated LSP completions with strikethrough ([#5932](https://github.com/helix-editor/helix/pull/5932))
- Add JSONRPC request ID to failed LSP/DAP request log messages ([#6010](https://github.com/helix-editor/helix/pull/6010), [#6018](https://github.com/helix-editor/helix/pull/6018))
- Ignore case when filtering LSP completions ([#6008](https://github.com/helix-editor/helix/pull/6008))
- Show current language when no arguments are passed to `:set-language` ([#5895](https://github.com/helix-editor/helix/pull/5895))
- Refactor and rewrite all book documentation ([#5534](https://github.com/helix-editor/helix/pull/5534))
- Separate diagnostic picker message and code ([#6095](https://github.com/helix-editor/helix/pull/6095))
- Add a config option to bypass undercurl detection ([#6253](https://github.com/helix-editor/helix/pull/6253))
- Only complete appropriate arguments for typed commands ([#5966](https://github.com/helix-editor/helix/pull/5966))
- Discard outdated LSP diagnostics ([3c9d5d0](https://github.com/helix-editor/helix/commit/3c9d5d0))
- Discard outdated LSP workspace edits ([b6a4927](https://github.com/helix-editor/helix/commit/b6a4927))
- Run shell commands asynchronously ([#6373](https://github.com/helix-editor/helix/pull/6373))
- Show diagnostic codes in LSP diagnostic messages ([#6378](https://github.com/helix-editor/helix/pull/6378))
- Highlight the current line in a DAP debug session ([#5957](https://github.com/helix-editor/helix/pull/5957))
- Hide signature help if it overlaps with the completion menu ([#5523](https://github.com/helix-editor/helix/pull/5523), [7a69c40](https://github.com/helix-editor/helix/commit/7a69c40))
Fixes:
- Fix behavior of `auto-completion` flag for completion-on-trigger ([#5042](https://github.com/helix-editor/helix/pull/5042))
- Reset editor mode when changing buffers ([#5072](https://github.com/helix-editor/helix/pull/5072))
- Respect scrolloff settings in mouse movements ([#5255](https://github.com/helix-editor/helix/pull/5255))
- Avoid trailing `s` when only one file is opened ([#5189](https://github.com/helix-editor/helix/pull/5189))
- Fix erroneous indent between closers of auto-pairs ([#5330](https://github.com/helix-editor/helix/pull/5330))
- Expand `~` when parsing file paths in `:open` ([#5329](https://github.com/helix-editor/helix/pull/5329))
- Fix theme inheritance for default themes ([#5218](https://github.com/helix-editor/helix/pull/5218))
- Fix `extend_line` with a count when the current line(s) are selected ([#5288](https://github.com/helix-editor/helix/pull/5288))
- Prompt: Fix autocompletion for paths containing periods ([#5175](https://github.com/helix-editor/helix/pull/5175))
- Skip serializing JSONRPC params if params is null ([#5471](https://github.com/helix-editor/helix/pull/5471))
- Fix interaction with the `xclip` clipboard provider ([#5426](https://github.com/helix-editor/helix/pull/5426))
- Fix undo/redo execution from the command palette ([#5294](https://github.com/helix-editor/helix/pull/5294))
- Fix highlighting of non-block cursors ([#5575](https://github.com/helix-editor/helix/pull/5575))
- Fix panic when nooping in `join_selections` and `join_selections_space` ([#5423](https://github.com/helix-editor/helix/pull/5423))
- Fix selecting a changed file in global search ([#5639](https://github.com/helix-editor/helix/pull/5639))
- Fix initial syntax highlight layer sort order ([#5196](https://github.com/helix-editor/helix/pull/5196))
- Fix UTF-8 length handling for shellwords ([#5738](https://github.com/helix-editor/helix/pull/5738))
- Remove C-j and C-k bindings from the completion menu ([#5070](https://github.com/helix-editor/helix/pull/5070))
- Always commit to history when pasting ([#5790](https://github.com/helix-editor/helix/pull/5790))
- Properly handle LSP position encoding ([#5711](https://github.com/helix-editor/helix/pull/5711))
- Fix infinite loop in `copy_selection_on_prev_line` ([#5888](https://github.com/helix-editor/helix/pull/5888))
- Fix completion popup positioning ([#5842](https://github.com/helix-editor/helix/pull/5842))
- Fix a panic when uncommenting a line with only a comment token ([#5933](https://github.com/helix-editor/helix/pull/5933))
- Fix panic in `goto_window_center` at EOF ([#5987](https://github.com/helix-editor/helix/pull/5987))
- Ignore invalid file URIs sent by a language server ([#6000](https://github.com/helix-editor/helix/pull/6000))
- Decode LSP URIs for the workspace diagnostics picker ([#6016](https://github.com/helix-editor/helix/pull/6016))
- Fix incorrect usages of `tab_width` with `indent_width` ([#5918](https://github.com/helix-editor/helix/pull/5918))
- DAP: Send Disconnect if the Terminated event is received ([#5532](https://github.com/helix-editor/helix/pull/5532))
- DAP: Validate key and index exist when requesting variables ([#5628](https://github.com/helix-editor/helix/pull/5628))
- Check LSP renaming support before prompting for rename text ([#6257](https://github.com/helix-editor/helix/pull/6257))
- Fix indent guide rendering ([#6136](https://github.com/helix-editor/helix/pull/6136))
- Fix division by zero panic ([#6155](https://github.com/helix-editor/helix/pull/6155))
- Fix lacking space panic ([#6109](https://github.com/helix-editor/helix/pull/6109))
- Send error replies for malformed and unhandled LSP requests ([#6058](https://github.com/helix-editor/helix/pull/6058))
- Fix table column calculations for dynamic pickers ([#5920](https://github.com/helix-editor/helix/pull/5920))
- Skip adding jumplist entries for `:<n>` line number previews ([#5751](https://github.com/helix-editor/helix/pull/5751))
- Fix completion race conditions ([#6173](https://github.com/helix-editor/helix/pull/6173))
- Fix `shrink_selection` with multiple cursors ([#6093](https://github.com/helix-editor/helix/pull/6093))
- Fix indentation calculation for lines with mixed tabs/spaces ([#6278](https://github.com/helix-editor/helix/pull/6278))
- No-op `client/registerCapability` LSP requests ([#6258](https://github.com/helix-editor/helix/pull/6258))
- Send the STOP signal to all processes in the process group ([#3546](https://github.com/helix-editor/helix/pull/3546))
- Fix workspace edit client capabilities declaration ([7bf168d](https://github.com/helix-editor/helix/commit/7bf168d))
- Fix highlighting in picker results with multiple columns ([#6333](https://github.com/helix-editor/helix/pull/6333))
- Canonicalize paths before stripping the current dir as a prefix ([#6290](https://github.com/helix-editor/helix/pull/6290))
- Fix truncation behavior for long path names in the file picker ([#6410](https://github.com/helix-editor/helix/pull/6410), [67783dd](https://github.com/helix-editor/helix/commit/67783dd))
- Fix theme reloading behavior in `:config-reload` ([ab819d8](https://github.com/helix-editor/helix/commit/ab819d8))
Themes:
- Update `serika` ([#5038](https://github.com/helix-editor/helix/pull/5038), [#6344](https://github.com/helix-editor/helix/pull/6344))
- Update `flatwhite` ([#5036](https://github.com/helix-editor/helix/pull/5036), [#6323](https://github.com/helix-editor/helix/pull/6323))
- Update `autumn` ([#5051](https://github.com/helix-editor/helix/pull/5051), [#5397](https://github.com/helix-editor/helix/pull/5397), [#6280](https://github.com/helix-editor/helix/pull/6280), [#6316](https://github.com/helix-editor/helix/pull/6316))
- Update `acme` ([#5019](https://github.com/helix-editor/helix/pull/5019), [#5486](https://github.com/helix-editor/helix/pull/5486), [#5488](https://github.com/helix-editor/helix/pull/5488))
- Update `gruvbox` themes ([#5066](https://github.com/helix-editor/helix/pull/5066), [#5333](https://github.com/helix-editor/helix/pull/5333), [#5540](https://github.com/helix-editor/helix/pull/5540), [#6285](https://github.com/helix-editor/helix/pull/6285), [#6295](https://github.com/helix-editor/helix/pull/6295))
- Update `base16_transparent` ([#5105](https://github.com/helix-editor/helix/pull/5105))
- Update `dark_high_contrast` ([#5105](https://github.com/helix-editor/helix/pull/5105))
- Update `dracula` ([#5236](https://github.com/helix-editor/helix/pull/5236), [#5627](https://github.com/helix-editor/helix/pull/5627), [#6414](https://github.com/helix-editor/helix/pull/6414))
- Update `monokai_pro_spectrum` ([#5250](https://github.com/helix-editor/helix/pull/5250), [#5602](https://github.com/helix-editor/helix/pull/5602))
- Update `rose_pine` ([#5267](https://github.com/helix-editor/helix/pull/5267), [#5489](https://github.com/helix-editor/helix/pull/5489), [#6384](https://github.com/helix-editor/helix/pull/6384))
- Update `kanagawa` ([#5273](https://github.com/helix-editor/helix/pull/5273), [#5571](https://github.com/helix-editor/helix/pull/5571), [#6085](https://github.com/helix-editor/helix/pull/6085))
- Update `emacs` ([#5334](https://github.com/helix-editor/helix/pull/5334))
- Add `github` themes ([#5353](https://github.com/helix-editor/helix/pull/5353), [efeec12](https://github.com/helix-editor/helix/commit/efeec12))
- Dark themes: `github_dark`, `github_dark_colorblind`, `github_dark_dimmed`, `github_dark_high_contrast`, `github_dark_tritanopia`
- Light themes: `github_light`, `github_light_colorblind`, `github_light_dimmed`, `github_light_high_contrast`, `github_light_tritanopia`
- Update `solarized` variants ([#5445](https://github.com/helix-editor/helix/pull/5445), [#6327](https://github.com/helix-editor/helix/pull/6327))
- Update `catppuccin` variants ([#5404](https://github.com/helix-editor/helix/pull/5404), [#6107](https://github.com/helix-editor/helix/pull/6107), [#6269](https://github.com/helix-editor/helix/pull/6269), [#6464](https://github.com/helix-editor/helix/pull/6464))
- Use curly underlines in built-in themes ([#5419](https://github.com/helix-editor/helix/pull/5419))
- Update `zenburn` ([#5573](https://github.com/helix-editor/helix/pull/5573))
- Rewrite `snazzy` ([#3971](https://github.com/helix-editor/helix/pull/3971))
- Add `monokai_aqua` ([#5578](https://github.com/helix-editor/helix/pull/5578))
- Add `markup.strikethrough` to existing themes ([#5619](https://github.com/helix-editor/helix/pull/5619))
- Update `sonokai` ([#5440](https://github.com/helix-editor/helix/pull/5440))
- Update `onedark` ([#5755](https://github.com/helix-editor/helix/pull/5755))
- Add `ayu_evolve` ([#5638](https://github.com/helix-editor/helix/pull/5638), [#6028](https://github.com/helix-editor/helix/pull/6028), [#6225](https://github.com/helix-editor/helix/pull/6225))
- Add `jellybeans` ([#5719](https://github.com/helix-editor/helix/pull/5719))
- Update `fleet_dark` ([#5605](https://github.com/helix-editor/helix/pull/5605), [#6266](https://github.com/helix-editor/helix/pull/6266), [#6324](https://github.com/helix-editor/helix/pull/6324), [#6375](https://github.com/helix-editor/helix/pull/6375))
- Add `darcula-solid` ([#5778](https://github.com/helix-editor/helix/pull/5778))
- Remove text background from monokai themes ([#6009](https://github.com/helix-editor/helix/pull/6009))
- Update `pop_dark` ([#5992](https://github.com/helix-editor/helix/pull/5992), [#6208](https://github.com/helix-editor/helix/pull/6208), [#6227](https://github.com/helix-editor/helix/pull/6227), [#6292](https://github.com/helix-editor/helix/pull/6292))
- Add `everblush` ([#6086](https://github.com/helix-editor/helix/pull/6086))
- Add `adwaita-dark` ([#6042](https://github.com/helix-editor/helix/pull/6042), [#6342](https://github.com/helix-editor/helix/pull/6342))
- Update `papercolor` ([#6162](https://github.com/helix-editor/helix/pull/6162))
- Update `onelight` ([#6192](https://github.com/helix-editor/helix/pull/6192), [#6276](https://github.com/helix-editor/helix/pull/6276))
- Add `molokai` ([#6260](https://github.com/helix-editor/helix/pull/6260))
- Update `ayu` variants ([#6329](https://github.com/helix-editor/helix/pull/6329))
- Update `tokyonight` variants ([#6349](https://github.com/helix-editor/helix/pull/6349))
- Update `nord` variants ([#6376](https://github.com/helix-editor/helix/pull/6376))
New languages:
- BibTeX ([#5064](https://github.com/helix-editor/helix/pull/5064))
- Mermaid.js ([#5147](https://github.com/helix-editor/helix/pull/5147))
- Crystal ([#4993](https://github.com/helix-editor/helix/pull/4993), [#5205](https://github.com/helix-editor/helix/pull/5205))
- MATLAB/Octave ([#5192](https://github.com/helix-editor/helix/pull/5192))
- `tfvars` (uses HCL) ([#5396](https://github.com/helix-editor/helix/pull/5396))
- Ponylang ([#5416](https://github.com/helix-editor/helix/pull/5416))
- DHall ([1f6809c](https://github.com/helix-editor/helix/commit/1f6809c))
- Sagemath ([#5649](https://github.com/helix-editor/helix/pull/5649))
- MSBuild ([#5793](https://github.com/helix-editor/helix/pull/5793))
- pem ([#5797](https://github.com/helix-editor/helix/pull/5797))
- passwd ([#4959](https://github.com/helix-editor/helix/pull/4959))
- hosts ([#4950](https://github.com/helix-editor/helix/pull/4950), [#5914](https://github.com/helix-editor/helix/pull/5914))
- uxntal ([#6047](https://github.com/helix-editor/helix/pull/6047))
- Yuck ([#6064](https://github.com/helix-editor/helix/pull/6064), [#6242](https://github.com/helix-editor/helix/pull/6242))
- GNU gettext PO ([#5996](https://github.com/helix-editor/helix/pull/5996))
- Sway ([#6023](https://github.com/helix-editor/helix/pull/6023))
- NASM ([#6068](https://github.com/helix-editor/helix/pull/6068))
- PRQL ([#6126](https://github.com/helix-editor/helix/pull/6126))
- reStructuredText ([#6180](https://github.com/helix-editor/helix/pull/6180))
- Smithy ([#6370](https://github.com/helix-editor/helix/pull/6370))
- VHDL ([#5826](https://github.com/helix-editor/helix/pull/5826))
- Rego (OpenPolicy Agent) ([#6415](https://github.com/helix-editor/helix/pull/6415))
- Nim ([#6123](https://github.com/helix-editor/helix/pull/6123))
Updated languages and queries:
- Use diff syntax for patch files ([#5085](https://github.com/helix-editor/helix/pull/5085))
- Add Haskell textobjects ([#5061](https://github.com/helix-editor/helix/pull/5061))
- Fix commonlisp configuration ([#5091](https://github.com/helix-editor/helix/pull/5091))
- Update Scheme ([bae890d](https://github.com/helix-editor/helix/commit/bae890d))
- Add indent queries for Bash ([#5149](https://github.com/helix-editor/helix/pull/5149))
- Recognize `c++` as a C++ extension ([#5183](https://github.com/helix-editor/helix/pull/5183))
- Enable HTTP server in `metals` (Scala) config ([#5551](https://github.com/helix-editor/helix/pull/5551))
- Change V-lang language server to `v ls` from `vls` ([#5677](https://github.com/helix-editor/helix/pull/5677))
- Inject comment grammar into Nix ([#5208](https://github.com/helix-editor/helix/pull/5208))
- Update Rust highlights ([#5238](https://github.com/helix-editor/helix/pull/5238), [#5349](https://github.com/helix-editor/helix/pull/5349))
- Fix HTML injection within Markdown ([#5265](https://github.com/helix-editor/helix/pull/5265))
- Fix comment token for godot ([#5276](https://github.com/helix-editor/helix/pull/5276))
- Expand injections for Vue ([#5268](https://github.com/helix-editor/helix/pull/5268))
- Add `.bash_aliases` as a Bash file-type ([#5347](https://github.com/helix-editor/helix/pull/5347))
- Fix comment token for sshclientconfig ([#5351](https://github.com/helix-editor/helix/pull/5351))
- Update Prisma ([#5417](https://github.com/helix-editor/helix/pull/5417))
- Update C++ ([#5457](https://github.com/helix-editor/helix/pull/5457))
- Add more file-types for Python ([#5593](https://github.com/helix-editor/helix/pull/5593))
- Update tree-sitter-scala ([#5576](https://github.com/helix-editor/helix/pull/5576))
- Add an injection regex for Lua ([#5606](https://github.com/helix-editor/helix/pull/5606))
- Add `build.gradle` to java roots configuration ([#5641](https://github.com/helix-editor/helix/pull/5641))
- Add Hub PR files to markdown file-types ([#5634](https://github.com/helix-editor/helix/pull/5634))
- Add an external formatter configuration for Cue ([#5679](https://github.com/helix-editor/helix/pull/5679))
- Add injections for builders and writers to Nix ([#5629](https://github.com/helix-editor/helix/pull/5629))
- Update tree-sitter-xml to fix whitespace parsing ([#5685](https://github.com/helix-editor/helix/pull/5685))
- Add `Justfile` to the make file-types configuration ([#5687](https://github.com/helix-editor/helix/pull/5687))
- Update tree-sitter-sql and highlight queries ([#5683](https://github.com/helix-editor/helix/pull/5683), [#5772](https://github.com/helix-editor/helix/pull/5772))
- Use the bash grammar and queries for env language ([#5720](https://github.com/helix-editor/helix/pull/5720))
- Add podspec files to ruby file-types ([#5811](https://github.com/helix-editor/helix/pull/5811))
- Recognize `.C` and `.H` file-types as C++ ([#5808](https://github.com/helix-editor/helix/pull/5808))
- Recognize plist and mobileconfig files as XML ([#5863](https://github.com/helix-editor/helix/pull/5863))
- Fix `select` indentation in Go ([#5713](https://github.com/helix-editor/helix/pull/5713))
- Check for external file modifications when writing ([#5805](https://github.com/helix-editor/helix/pull/5805))
- Recognize containerfiles as dockerfile syntax ([#5873](https://github.com/helix-editor/helix/pull/5873))
- Update godot grammar and queries ([#5944](https://github.com/helix-editor/helix/pull/5944), [#6186](https://github.com/helix-editor/helix/pull/6186))
- Improve DHall highlights ([#5959](https://github.com/helix-editor/helix/pull/5959))
- Recognize `.env.dist` and `source.env` as env language ([#6003](https://github.com/helix-editor/helix/pull/6003))
- Update tree-sitter-git-rebase ([#6030](https://github.com/helix-editor/helix/pull/6030), [#6094](https://github.com/helix-editor/helix/pull/6094))
- Improve SQL highlights ([#6041](https://github.com/helix-editor/helix/pull/6041))
- Improve markdown highlights and inject LaTeX ([#6100](https://github.com/helix-editor/helix/pull/6100))
- Add textobject queries for Elm ([#6084](https://github.com/helix-editor/helix/pull/6084))
- Recognize graphql schema file type ([#6159](https://github.com/helix-editor/helix/pull/6159))
- Improve highlighting in comments ([#6143](https://github.com/helix-editor/helix/pull/6143))
- Improve highlighting for JavaScript/TypeScript/ECMAScript languages ([#6205](https://github.com/helix-editor/helix/pull/6205))
- Improve PHP highlights ([#6203](https://github.com/helix-editor/helix/pull/6203), [#6250](https://github.com/helix-editor/helix/pull/6250), [#6299](https://github.com/helix-editor/helix/pull/6299))
- Improve Go highlights ([#6204](https://github.com/helix-editor/helix/pull/6204))
- Highlight unchecked sqlx functions as SQL in Rust ([#6256](https://github.com/helix-editor/helix/pull/6256))
- Improve Erlang highlights ([cdd6c8d](https://github.com/helix-editor/helix/commit/cdd6c8d))
- Improve Nix highlights ([fb4d703](https://github.com/helix-editor/helix/commit/fb4d703))
- Improve gdscript highlights ([#6311](https://github.com/helix-editor/helix/pull/6311))
- Improve Vlang highlights ([#6279](https://github.com/helix-editor/helix/pull/6279))
- Improve Makefile highlights ([#6339](https://github.com/helix-editor/helix/pull/6339))
- Remove auto-pair for `'` in OCaml ([#6381](https://github.com/helix-editor/helix/pull/6381))
- Fix indents in switch statements in ECMA languages ([#6369](https://github.com/helix-editor/helix/pull/6369))
- Recognize xlb and storyboard file-types as XML ([#6407](https://github.com/helix-editor/helix/pull/6407))
- Recognize cts and mts file-types as TypeScript ([#6424](https://github.com/helix-editor/helix/pull/6424))
- Recognize SVG file-type as XML ([#6431](https://github.com/helix-editor/helix/pull/6431))
- Add theme scopes for (un)checked list item markup scopes ([#6434](https://github.com/helix-editor/helix/pull/6434))
- Update git commit grammar and add the comment textobject ([#6439](https://github.com/helix-editor/helix/pull/6439), [#6493](https://github.com/helix-editor/helix/pull/6493))
- Recognize ARB file-type as JSON ([#6452](https://github.com/helix-editor/helix/pull/6452))
- Inject markdown into markdown strings in Julia ([#6489](https://github.com/helix-editor/helix/pull/6489))
Packaging:
- Fix Nix flake devShell for darwin hosts ([#5368](https://github.com/helix-editor/helix/pull/5368))
- Add Appstream metadata file to `contrib/` ([#5643](https://github.com/helix-editor/helix/pull/5643))
- Increase the MSRV to 1.65 ([#5570](https://github.com/helix-editor/helix/pull/5570), [#6185](https://github.com/helix-editor/helix/pull/6185))
- Expose the Nix flake's `wrapper` ([#5994](https://github.com/helix-editor/helix/pull/5994))
# 22.12 (2022-12-06) # 22.12 (2022-12-06)
This is a great big release filled with changes from a 99 contributors. A big _thank you_ to you all! This is a great big release filled with changes from a 99 contributors. A big _thank you_ to you all!

567
Cargo.lock generated

File diff suppressed because it is too large Load Diff

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

@ -4,7 +4,7 @@
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="logo_dark.svg"> <source media="(prefers-color-scheme: dark)" srcset="logo_dark.svg">
<source media="(prefers-color-scheme: light)" srcset="logo_light.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> </picture>
</h1> </h1>
@ -18,7 +18,7 @@
![Screenshot](./screenshot.png) ![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 The editing model is very heavily based on Kakoune; during development I found
myself agreeing with most of Kakoune's design decisions. 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 Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.scm`. `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
[Installation documentation](https://docs.helix-editor.com/install.html). [Installation documentation](https://docs.helix-editor.com/install.html).

@ -1 +1 @@
22.12 23.03

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

@ -11,6 +11,7 @@ Example config:
```toml ```toml
theme = "onedark" theme = "onedark"
icons = "nerdfonts"
[editor] [editor]
line-number = "relative" line-number = "relative"
@ -30,6 +31,9 @@ You can use a custom configuration file by specifying it with the `-c` or
Additionally, you can reload the configuration file by sending the USR1 Additionally, you can reload the configuration file by sending the USR1
signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`. signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.
## Editor ## Editor
### `[editor]` Section ### `[editor]` Section
@ -58,6 +62,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | | `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
### `[editor.statusline]` Section ### `[editor.statusline]` Section
@ -104,6 +109,7 @@ The following statusline elements can be configured:
| `file-line-ending` | The file line endings (CRLF or LF) | | `file-line-ending` | The file line endings (CRLF or LF) |
| `total-line-numbers` | The total line numbers of the opened file | | `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type 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 | | `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace | | `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections | | `selections` | The number of active selections |
@ -123,6 +129,7 @@ The following statusline elements can be configured:
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | | `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` | | `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` | | `display-signature-help-docs` | Display docs under signature help popup | `true` |
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
[^1]: By default, a progress spinner is shown in the statusline beside the file path. [^1]: By default, a progress spinner is shown in the statusline beside the file path.
[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. [^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix.
@ -180,6 +187,8 @@ auto-pairs = false # defaults to `true`
The default pairs are <code>(){}[]''""``</code>, but these can be customized by The default pairs are <code>(){}[]''""``</code>, but these can be customized by
setting `auto-pairs` to a TOML table: setting `auto-pairs` to a TOML table:
Example
```toml ```toml
[editor.auto-pairs] [editor.auto-pairs]
'(' = ')' '(' = ')'
@ -305,7 +314,7 @@ Example:
min-width = 1 min-width = 1
``` ```
#### `[editor.gutters.diagnotics]` Section #### `[editor.gutters.diagnostics]` Section
Currently unused Currently unused
@ -317,6 +326,18 @@ Currently unused
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 ### `[editor.soft-wrap]` Section
Options for soft wrapping lines that exceed the view width: Options for soft wrapping lines that exceed the view width:
@ -338,3 +359,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0 max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it 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` |

@ -41,7 +41,7 @@
| fortran | ✓ | | ✓ | `fortls` | | fortran | ✓ | | ✓ | `fortls` |
| gdscript | ✓ | ✓ | ✓ | | | gdscript | ✓ | ✓ | ✓ | |
| git-attributes | ✓ | | | | | git-attributes | ✓ | | | |
| git-commit | ✓ | | | | | git-commit | ✓ | | | |
| git-config | ✓ | | | | | git-config | ✓ | | | |
| git-ignore | ✓ | | | | | git-ignore | ✓ | | | |
| git-rebase | ✓ | | | | | git-rebase | ✓ | | | |
@ -59,6 +59,7 @@
| heex | ✓ | ✓ | | `elixir-ls` | | heex | ✓ | ✓ | | `elixir-ls` |
| hosts | ✓ | | | | | hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server` | | html | ✓ | | | `vscode-html-language-server` |
| hurl | ✓ | | ✓ | |
| idris | | | | `idris2-lsp` | | idris | | | | `idris2-lsp` |
| iex | ✓ | | | | | iex | ✓ | | | |
| ini | ✓ | | | | | ini | ✓ | | | |
@ -68,7 +69,7 @@
| json | ✓ | | ✓ | `vscode-json-language-server` | | json | ✓ | | ✓ | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` | | jsonnet | ✓ | | | `jsonnet-language-server` |
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` | | jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| julia | ✓ | | | `julia` | | julia | ✓ | | | `julia` |
| kdl | ✓ | | | | | kdl | ✓ | | | |
| kotlin | ✓ | | | `kotlin-language-server` | | kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | ✓ | | `texlab` | | latex | ✓ | ✓ | | `texlab` |
@ -88,6 +89,7 @@
| msbuild | ✓ | | ✓ | | | msbuild | ✓ | | ✓ | |
| nasm | ✓ | ✓ | | | | nasm | ✓ | ✓ | | |
| nickel | ✓ | | ✓ | `nls` | | nickel | ✓ | | ✓ | `nls` |
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
| nix | ✓ | | | `nil` | | nix | ✓ | | | `nil` |
| nu | ✓ | | | | | nu | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` | | ocaml | ✓ | | ✓ | `ocamllsp` |
@ -112,8 +114,10 @@
| r | ✓ | | | `R` | | r | ✓ | | | `R` |
| racket | ✓ | | | `racket` | | racket | ✓ | | | `racket` |
| regex | ✓ | | | | | regex | ✓ | | | |
| rego | ✓ | | | `regols` |
| rescript | ✓ | ✓ | | `rescript-language-server` | | rescript | ✓ | ✓ | | `rescript-language-server` |
| rmarkdown | ✓ | | ✓ | `R` | | rmarkdown | ✓ | | ✓ | `R` |
| robot | ✓ | | | `robotframework_ls` |
| ron | ✓ | | ✓ | | | ron | ✓ | | ✓ | |
| rst | ✓ | | | | | rst | ✓ | | | |
| ruby | ✓ | ✓ | ✓ | `solargraph` | | ruby | ✓ | ✓ | ✓ | `solargraph` |
@ -145,6 +149,7 @@
| v | ✓ | ✓ | ✓ | `v` | | v | ✓ | ✓ | ✓ | `v` |
| vala | ✓ | | | `vala-language-server` | | vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` | | verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | | | vhs | ✓ | | | |
| vue | ✓ | | | `vls` | | vue | ✓ | | | `vls` |
| wast | ✓ | | | | | wast | ✓ | | | |

@ -29,6 +29,7 @@
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | | `: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). | | `: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). | | `: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` | 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. | | `: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. | | `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
@ -70,6 +71,7 @@
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
| `:config-reload` | Refresh user config. | | `:config-reload` | Refresh user config. |
| `:config-open` | Open the user config.toml file. | | `:config-open` | Open the user config.toml file. |
| `:config-open-workspace` | Open the workspace config.toml file. |
| `:log-open` | Open the helix log file. | | `:log-open` | Open the helix log file. |
| `:insert-output` | Run shell command, inserting output before each selection. | | `:insert-output` | Run shell command, inserting output before each selection. |
| `:append-output` | Run shell command, appending output after each selection. | | `:append-output` | Run shell command, appending output after each selection. |

@ -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` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` | | `/` | Global search in workspace folder | `global_search` |
| `?` | Open command palette | `command_palette` | | `?` | 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. > 💡 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 | | `Tab` | Select next completion item |
| `BackTab` | Select previous completion item | | `BackTab` | Select previous completion item |
| `Enter` | Open selected | | `Enter` | Open selected |
# File explorer
Press `?` to see keymaps. Remapping currently not supported.

@ -64,6 +64,7 @@ These configuration keys are available:
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
### File-type detection and the `file-types` key ### File-type detection and the `file-types` key

@ -228,6 +228,8 @@ We use a similar set of scopes as
- `list` - `list`
- `unnumbered` - `unnumbered`
- `numbered` - `numbered`
- `checked`
- `unchecked`
- `bold` - `bold`
- `italic` - `italic`
- `strikethrough` - `strikethrough`
@ -276,8 +278,11 @@ These scopes are used for theming the editor interface:
| `ui.cursor.primary.normal` | | | `ui.cursor.primary.normal` | |
| `ui.cursor.primary.insert` | | | `ui.cursor.primary.insert` | |
| `ui.cursor.primary.select` | | | `ui.cursor.primary.select` | |
| `ui.debug.breakpoint` | Breakpoint indicator, found in the gutter |
| `ui.debug.active` | Indicator for the line at which debugging execution is paused at, found in the gutter |
| `ui.gutter` | Gutter | | `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on | | `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.highlight.frameline` | Line at which debugging execution is paused at |
| `ui.linenr` | Line numbers | | `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on | | `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline | | `ui.statusline` | Statusline |
@ -311,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.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.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]) | | `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
| `warning` | Diagnostics warning (gutter) | | `warning` | Diagnostics warning icon (gutter, statusline, and diagnostic pickers) |
| `error` | Diagnostics error (gutter) | | `error` | Diagnostics error icon (gutter, statusline, and diagnostic pickers) |
| `info` | Diagnostics info (gutter) | | `info` | Diagnostics info icon (gutter, statusline, and diagnostic pickers) |
| `hint` | Diagnostics hint (gutter) | | `hint` | Diagnostics hint icon (gutter, statusline, and diagnostic pickers) |
| `diagnostic` | Diagnostics fallback style (editing area) | | `diagnostic` | Diagnostics fallback style (editing area) |
| `diagnostic.hint` | Diagnostics hint (editing area) | | `diagnostic.hint` | Diagnostics hint (editing area) |
| `diagnostic.info` | Diagnostics info (editing area) | | `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) | | `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) | | `diagnostic.error` | Diagnostics error (editing area) |
| `symbolkind` | Symbol kind icons (symbol picker) |
[editor-section]: ./configuration.md#editor-section [editor-section]: ./configuration.md#editor-section

@ -36,6 +36,9 @@
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="23.03" date="2023-03-31">
<url>https://helix-editor.com/news/release-23-03-highlights/</url>
</release>
<release version="22.12" date="2022-12-6"> <release version="22.12" date="2022-12-6">
<url>https://helix-editor.com/news/release-22-12-highlights/</url> <url>https://helix-editor.com/news/release-22-12-highlights/</url>
</release> </release>

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

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

@ -32,6 +32,7 @@ regex = "1"
bitflags = "2.0" bitflags = "2.0"
ahash = "0.8.3" ahash = "0.8.3"
hashbrown = { version = "0.13.2", features = ["raw"] } hashbrown = { version = "0.13.2", features = ["raw"] }
dunce = "1.0"
log = "0.4" log = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

@ -36,55 +36,12 @@ pub mod unicode {
pub use unicode_width as width; pub use unicode_width as width;
} }
pub use helix_loader::find_workspace;
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> { pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace()) line.chars().position(|ch| !ch.is_whitespace())
} }
/// Find project root.
///
/// Order of detection:
/// * Top-most folder containing a root marker in current git repository
/// * Git repository root if no marker detected
/// * Top-most folder containing a root marker if not git repository detected
/// * Current working directory as fallback
pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::PathBuf {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let root = match root {
Some(root) => {
let root = std::path::Path::new(root);
if root.is_absolute() {
root.to_path_buf()
} else {
current_dir.join(root)
}
}
None => current_dir.clone(),
};
let mut top_marker = None;
for ancestor in root.ancestors() {
if root_markers
.iter()
.any(|marker| ancestor.join(marker).exists())
{
top_marker = Some(ancestor);
}
if ancestor.join(".git").exists() {
// Top marker is repo root if not root marker was detected yet
if top_marker.is_none() {
top_marker = Some(ancestor);
}
// Don't go higher than repo if we're in one
break;
}
}
// Return the found top marker or the current_dir as fallback
top_marker.map_or(current_dir, |a| a.to_path_buf())
}
pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice}; pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
// pub use tendril::StrTendril as Tendril; // pub use tendril::StrTendril as Tendril;
@ -98,7 +55,7 @@ pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes; pub use graphemes::RopeGraphemes;
pub use position::{ pub use position::{
char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor, char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor,
visual_offset_from_block, Position, visual_offset_from_block, Position, VisualOffsetError,
}; };
#[allow(deprecated)] #[allow(deprecated)]
pub use position::{pos_at_visual_coords, visual_coords_at_pos}; pub use position::{pos_at_visual_coords, visual_coords_at_pos};

@ -40,6 +40,21 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
/// needs to improve on. /// needs to improve on.
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81> /// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
pub fn get_normalized_path(path: &Path) -> PathBuf { pub fn get_normalized_path(path: &Path) -> PathBuf {
// normalization strategy is to canonicalize first ancestor path that exists (i.e., canonicalize as much as possible),
// then run handrolled normalization on the non-existent remainder
let (base, path) = path
.ancestors()
.find_map(|base| {
let canonicalized_base = dunce::canonicalize(base).ok()?;
let remainder = path.strip_prefix(base).ok()?.into();
Some((canonicalized_base, remainder))
})
.unwrap_or_else(|| (PathBuf::new(), PathBuf::from(path)));
if path.as_os_str().is_empty() {
return base;
}
let mut components = path.components().peekable(); let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next(); components.next();
@ -63,7 +78,7 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
} }
} }
} }
ret base.join(ret)
} }
/// Returns the canonical, absolute form of a path with all intermediate components normalized. /// Returns the canonical, absolute form of a path with all intermediate components normalized.
@ -82,13 +97,19 @@ pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
} }
pub fn get_relative_path(path: &Path) -> PathBuf { pub fn get_relative_path(path: &Path) -> PathBuf {
let path = PathBuf::from(path);
let path = if path.is_absolute() { let path = if path.is_absolute() {
let cwdir = std::env::current_dir().expect("couldn't determine current directory"); let cwdir = std::env::current_dir()
path.strip_prefix(cwdir).unwrap_or(path) .map(|path| get_normalized_path(&path))
.expect("couldn't determine current directory");
get_normalized_path(&path)
.strip_prefix(cwdir)
.map(PathBuf::from)
.unwrap_or(path)
} else { } else {
path path
}; };
fold_home_dir(path) fold_home_dir(&path)
} }
/// Returns a truncated filepath where the basepart of the path is reduced to the first /// Returns a truncated filepath where the basepart of the path is reduced to the first

@ -109,7 +109,7 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
/// softwrapping positions are estimated with an O(1) algorithm /// softwrapping positions are estimated with an O(1) algorithm
/// to ensure consistent performance for large lines (currently unimplemented) /// to ensure consistent performance for large lines (currently unimplemented)
/// ///
/// Usualy you want to use `visual_offset_from_anchor` instead but this function /// Usually you want to use `visual_offset_from_anchor` instead but this function
/// can be useful (and faster) if /// can be useful (and faster) if
/// * You already know the visual position of the block /// * You already know the visual position of the block
/// * You only care about the horizontal offset (column) and not the vertical offset (row) /// * You only care about the horizontal offset (column) and not the vertical offset (row)
@ -137,6 +137,12 @@ pub fn visual_offset_from_block(
(last_pos, block_start) (last_pos, block_start)
} }
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum VisualOffsetError {
PosBeforeAnchorRow,
PosAfterMaxRow,
}
/// Returns the visual offset from the start of the visual line /// Returns the visual offset from the start of the visual line
/// that contains anchor. /// that contains anchor.
pub fn visual_offset_from_anchor( pub fn visual_offset_from_anchor(
@ -146,28 +152,46 @@ pub fn visual_offset_from_anchor(
text_fmt: &TextFormat, text_fmt: &TextFormat,
annotations: &TextAnnotations, annotations: &TextAnnotations,
max_rows: usize, max_rows: usize,
) -> Option<(Position, usize)> { ) -> Result<(Position, usize), VisualOffsetError> {
let (formatter, block_start) = let (formatter, block_start) =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
let mut char_pos = block_start; let mut char_pos = block_start;
let mut anchor_line = None; let mut anchor_line = None;
let mut found_pos = None;
let mut last_pos = Position::default(); let mut last_pos = Position::default();
if pos < block_start {
return Err(VisualOffsetError::PosBeforeAnchorRow);
}
for (grapheme, vpos) in formatter { for (grapheme, vpos) in formatter {
last_pos = vpos; last_pos = vpos;
char_pos += grapheme.doc_chars(); char_pos += grapheme.doc_chars();
if char_pos > pos {
if let Some(anchor_line) = anchor_line {
last_pos.row -= anchor_line;
return Ok((last_pos, block_start));
} else {
found_pos = Some(last_pos);
}
}
if char_pos > anchor && anchor_line.is_none() { if char_pos > anchor && anchor_line.is_none() {
if let Some(mut found_pos) = found_pos {
return if found_pos.row == last_pos.row {
found_pos.row = 0;
Ok((found_pos, block_start))
} else {
Err(VisualOffsetError::PosBeforeAnchorRow)
};
} else {
anchor_line = Some(last_pos.row); anchor_line = Some(last_pos.row);
} }
if char_pos > pos {
last_pos.row -= anchor_line.unwrap();
return Some((last_pos, block_start));
} }
if let Some(anchor_line) = anchor_line { if let Some(anchor_line) = anchor_line {
if vpos.row >= anchor_line + max_rows { if vpos.row >= anchor_line + max_rows {
return None; return Err(VisualOffsetError::PosAfterMaxRow);
} }
} }
} }
@ -175,7 +199,7 @@ pub fn visual_offset_from_anchor(
let anchor_line = anchor_line.unwrap_or(last_pos.row); let anchor_line = anchor_line.unwrap_or(last_pos.row);
last_pos.row -= anchor_line; last_pos.row -= anchor_line;
Some((last_pos, block_start)) Ok((last_pos, block_start))
} }
/// Convert (line, column) coordinates to a character index. /// Convert (line, column) coordinates to a character index.
@ -267,7 +291,7 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize)
/// ///
/// If no (text) grapheme starts at exactly at the specified column the /// If no (text) grapheme starts at exactly at the specified column the
/// start of the grapheme to the left is returned. If there is no grapheme /// start of the grapheme to the left is returned. If there is no grapheme
/// to the left (for example if the line starts with virtual text) then the positiong /// to the left (for example if the line starts with virtual text) then the positioning
/// of the next grapheme to the right is returned. /// of the next grapheme to the right is returned.
/// ///
/// If the `line` coordinate is beyond the end of the file, the EOF /// If the `line` coordinate is beyond the end of the file, the EOF
@ -285,18 +309,19 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize)
/// on the visual line is returned if the visual line contains any text: /// on the visual line is returned if the visual line contains any text:
/// If the visual line at the specified offset is a virtual line generated by a `LineAnnotation` /// If the visual line at the specified offset is a virtual line generated by a `LineAnnotation`
/// the previous char_index is returned, together with the remaining vertical offset (`virtual_lines`) /// the previous char_index is returned, together with the remaining vertical offset (`virtual_lines`)
pub fn char_idx_at_visual_offset<'a>( pub fn char_idx_at_visual_offset(
text: RopeSlice<'a>, text: RopeSlice,
mut anchor: usize, mut anchor: usize,
mut row_offset: isize, mut row_offset: isize,
column: usize, column: usize,
text_fmt: &TextFormat, text_fmt: &TextFormat,
annotations: &TextAnnotations, annotations: &TextAnnotations,
) -> (usize, usize) { ) -> (usize, usize) {
let mut pos = anchor;
// convert row relative to visual line containing anchor to row relative to a block containing anchor (anchor may change) // convert row relative to visual line containing anchor to row relative to a block containing anchor (anchor may change)
loop { loop {
let (visual_pos_in_block, block_char_offset) = let (visual_pos_in_block, block_char_offset) =
visual_offset_from_block(text, anchor, anchor, text_fmt, annotations); visual_offset_from_block(text, anchor, pos, text_fmt, annotations);
row_offset += visual_pos_in_block.row as isize; row_offset += visual_pos_in_block.row as isize;
anchor = block_char_offset; anchor = block_char_offset;
if row_offset >= 0 { if row_offset >= 0 {
@ -308,10 +333,10 @@ pub fn char_idx_at_visual_offset<'a>(
break; break;
} }
// the row_offset is negative so we need to look at the previous block // the row_offset is negative so we need to look at the previous block
// set the anchor to the last char before the current block // set the anchor to the last char before the current block so that we can compute
// this char index is also always a line earlier so increase the row_offset by 1 // the distance of this block from the start of the previous block
pos = anchor;
anchor -= 1; anchor -= 1;
row_offset += 1;
} }
char_idx_at_visual_block_offset( char_idx_at_visual_block_offset(

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

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

@ -20,7 +20,7 @@ use std::{
fmt, fmt,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
mem::{replace, transmute}, mem::{replace, transmute},
path::Path, path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::Arc, sync::Arc,
}; };
@ -127,6 +127,10 @@ pub struct LanguageConfiguration {
pub auto_pairs: Option<AutoPairs>, pub auto_pairs: Option<AutoPairs>,
pub rulers: Option<Vec<u16>>, // if set, override editor's rulers pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
/// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
/// Falling back to the current working directory if none are configured.
pub workspace_lsp_roots: Option<Vec<PathBuf>>,
} }
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash)]
@ -551,6 +555,8 @@ impl LanguageConfiguration {
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct SoftWrap { pub struct SoftWrap {
/// Soft wrap lines that exceed viewport width. Default to off /// Soft wrap lines that exceed viewport width. Default to off
// NOTE: Option on purpose because the struct is shared between language config and global config.
// By default the option is None so that the language config falls back to the global config unless explicitly set.
pub enable: Option<bool>, pub enable: Option<bool>,
/// Maximum space left free at the end of the line. /// Maximum space left free at the end of the line.
/// This space is used to wrap text at word boundaries. If that is not possible within this limit /// This space is used to wrap text at word boundaries. If that is not possible within this limit

@ -1,5 +1,4 @@
use std::cell::Cell; use std::cell::Cell;
use std::convert::identity;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
@ -113,9 +112,7 @@ impl<A, M> Layer<A, M> {
pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) { pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) {
let new_index = self let new_index = self
.annotations .annotations
.binary_search_by_key(&char_idx, get_char_idx) .partition_point(|annot| get_char_idx(annot) < char_idx);
.unwrap_or_else(identity);
self.current_index.set(new_index); self.current_index.set(new_index);
} }
@ -175,7 +172,7 @@ impl TextAnnotations {
for char_idx in char_range { for char_idx in char_range {
if let Some((_, Some(highlight))) = self.overlay_at(char_idx) { if let Some((_, Some(highlight))) = self.overlay_at(char_idx) {
// we don't know the number of chars the original grapheme takes // we don't know the number of chars the original grapheme takes
// however it doesn't matter as highlight bounderies are automatically // however it doesn't matter as highlight boundaries are automatically
// aligned to grapheme boundaries in the rendering code // aligned to grapheme boundaries in the rendering code
highlights.push((highlight.0, char_idx..char_idx + 1)) highlights.push((highlight.0, char_idx..char_idx + 1))
} }
@ -206,7 +203,7 @@ impl TextAnnotations {
/// Add new grapheme overlays. /// Add new grapheme overlays.
/// ///
/// The overlayed grapheme will be rendered with `highlight` /// The overlaid grapheme will be rendered with `highlight`
/// patched on top of `ui.text`. /// patched on top of `ui.text`.
/// ///
/// The overlays **must be sorted** by their `char_idx`. /// The overlays **must be sorted** by their `char_idx`.

@ -512,4 +512,10 @@ impl Client {
self.call::<requests::SetExceptionBreakpoints>(args) self.call::<requests::SetExceptionBreakpoints>(args)
} }
pub fn current_stack_frame(&self) -> Option<&StackFrame> {
self.stack_frames
.get(&self.thread_id?)?
.get(self.active_frame?)
}
} }

@ -9,9 +9,11 @@ pub fn default_lang_config() -> toml::Value {
/// User configured languages.toml file, merged with the default config. /// User configured languages.toml file, merged with the default config.
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> { pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
let config = crate::local_config_dirs() let config = [
crate::config_dir(),
crate::find_workspace().0.join(".helix"),
]
.into_iter() .into_iter()
.chain([crate::config_dir()].into_iter())
.map(|path| path.join("languages.toml")) .map(|path| path.join("languages.toml"))
.filter_map(|file| { .filter_map(|file| {
std::fs::read_to_string(file) std::fs::read_to_string(file)
@ -20,8 +22,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
}) })
.collect::<Result<Vec<_>, _>>()? .collect::<Result<Vec<_>, _>>()?
.into_iter() .into_iter()
.chain([default_lang_config()].into_iter()) .fold(default_lang_config(), |a, b| {
.fold(toml::Value::Table(toml::value::Table::default()), |a, b| {
// combines for example // combines for example
// b: // b:
// [[language]] // [[language]]
@ -38,7 +39,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
// language-server = { command = "/usr/bin/taplo" } // language-server = { command = "/usr/bin/taplo" }
// //
// thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
crate::merge_toml_values(b, a, 3) crate::merge_toml_values(a, b, 3)
}); });
Ok(config) Ok(config)

@ -1,8 +1,14 @@
pub mod config; pub mod config;
pub mod grammar; pub mod grammar;
use anyhow::{anyhow, Result};
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; 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"); pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
@ -42,7 +48,7 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
let mut rt_dirs = Vec::new(); let mut rt_dirs = Vec::new();
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); let path = PathBuf::from(dir).parent().unwrap().join(RT_DIR);
log::debug!("runtime dir: {}", path.to_string_lossy()); log::debug!("runtime dir: {}", path.to_string_lossy());
rt_dirs.push(path); rt_dirs.push(path);
} }
@ -113,15 +119,6 @@ pub fn config_dir() -> PathBuf {
path path
} }
pub fn local_config_dirs() -> Vec<PathBuf> {
let directories = find_local_config_dirs()
.into_iter()
.map(|path| path.join(".helix"))
.collect();
log::debug!("Located configuration folders: {:?}", directories);
directories
}
pub fn cache_dir() -> PathBuf { pub fn cache_dir() -> PathBuf {
// TODO: allow env var override // TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!"); let strategy = choose_base_strategy().expect("Unable to find the config directory!");
@ -137,6 +134,10 @@ pub fn config_file() -> PathBuf {
.unwrap_or_else(|| config_dir().join("config.toml")) .unwrap_or_else(|| config_dir().join("config.toml"))
} }
pub fn workspace_config_file() -> PathBuf {
find_workspace().0.join(".helix").join("config.toml")
}
pub fn lang_config_file() -> PathBuf { pub fn lang_config_file() -> PathBuf {
config_dir().join("languages.toml") config_dir().join("languages.toml")
} }
@ -145,22 +146,6 @@ pub fn log_file() -> PathBuf {
cache_dir().join("helix.log") cache_dir().join("helix.log")
} }
pub fn find_local_config_dirs() -> Vec<PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let mut directories = Vec::new();
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() {
directories.push(ancestor.to_path_buf());
// Don't go higher than repo if we're in one
break;
} else if ancestor.join(".helix").is_dir() {
directories.push(ancestor.to_path_buf());
}
}
directories
}
/// Merge two TOML documents, merging values from `right` onto `left` /// Merge two TOML documents, merging values from `right` onto `left`
/// ///
/// When an array exists in both `left` and `right`, `right`'s array is /// When an array exists in both `left` and `right`, `right`'s array is
@ -175,8 +160,6 @@ pub fn find_local_config_dirs() -> Vec<PathBuf> {
/// where one usually wants to override or add to the array instead of /// where one usually wants to override or add to the array instead of
/// replacing it altogether. /// replacing it altogether.
pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value { 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> { fn get_name(v: &Value) -> Option<&str> {
v.get("name").and_then(Value::as_str) v.get("name").and_then(Value::as_str)
} }
@ -230,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)] #[cfg(test)]
mod merge_toml_tests { mod merge_toml_tests {
use std::str; use std::str;
@ -302,3 +394,21 @@ mod merge_toml_tests {
) )
} }
} }
/// Finds the current workspace folder.
/// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root
///
/// This function starts searching the FS upward from the CWD
/// and returns the first directory that contains either `.git` or `.helix`.
/// If no workspace was found returns (CWD, true).
/// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return (ancestor.to_owned(), false);
}
}
(current_dir, true)
}

@ -24,6 +24,7 @@ lsp-types = { version = "0.94" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1.26", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio = { version = "1.27", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.12" tokio-stream = "0.1.12"
which = "4.4" which = "4.4"
parking_lot = "0.12.1"

@ -1,22 +1,26 @@
use crate::{ use crate::{
jsonrpc, find_lsp_workspace, jsonrpc,
transport::{Payload, Transport}, transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result, Call, Error, OffsetEncoding, Result,
}; };
use helix_core::{find_root, ChangeSet, Rope}; use helix_core::{find_workspace, path, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH}; use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::PositionEncodingKind; use lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
};
use lsp_types as lsp; use lsp_types as lsp;
use parking_lot::Mutex;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap;
use std::future::Future; use std::future::Future;
use std::process::Stdio; use std::process::Stdio;
use std::sync::{ use std::sync::{
atomic::{AtomicU64, Ordering}, atomic::{AtomicU64, Ordering},
Arc, Arc,
}; };
use std::{collections::HashMap, path::PathBuf};
use tokio::{ use tokio::{
io::{BufReader, BufWriter}, io::{BufReader, BufWriter},
process::{Child, Command}, process::{Child, Command},
@ -26,6 +30,17 @@ use tokio::{
}, },
}; };
fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
lsp::WorkspaceFolder {
name: uri
.path_segments()
.and_then(|segments| segments.last())
.map(|basename| basename.to_string())
.unwrap_or_default(),
uri,
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
id: usize, id: usize,
@ -36,11 +51,121 @@ pub struct Client {
config: Option<Value>, config: Option<Value>,
root_path: std::path::PathBuf, root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>, root_uri: Option<lsp::Url>,
workspace_folders: Vec<lsp::WorkspaceFolder>, workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
initialize_notify: Arc<Notify>,
/// workspace folders added while the server is still initializing
req_timeout: u64, req_timeout: u64,
} }
impl Client { impl Client {
pub fn try_add_doc(
self: &Arc<Self>,
root_markers: &[String],
manual_roots: &[PathBuf],
doc_path: Option<&std::path::PathBuf>,
may_support_workspace: bool,
) -> bool {
let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::get_normalized_path(&workspace);
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers,
manual_roots,
&workspace,
workspace_is_cwd,
);
let root_uri = root
.as_ref()
.and_then(|root| lsp::Url::from_file_path(root).ok());
if self.root_path == root.unwrap_or(workspace)
|| root_uri.as_ref().map_or(false, |root_uri| {
self.workspace_folders
.lock()
.iter()
.any(|workspace| &workspace.uri == root_uri)
})
{
// workspace URI is already registered so we can use this client
return true;
}
// this server definitely doesn't support multiple workspace, no need to check capabilities
if !may_support_workspace {
return false;
}
let Some(capabilities) = self.capabilities.get() else {
let client = Arc::clone(self);
// initialization hasn't finished yet, deal with this new root later
// TODO: In the edgecase that a **new root** is added
// for an LSP that **doesn't support workspace_folders** before initaliation is finished
// the new roots are ignored.
// That particular edgecase would require retroactively spawning new LSP
// clients and therefore also require us to retroactively update the corresponding
// documents LSP client handle. It's doable but a pretty weird edgecase so let's
// wait and see if anyone ever runs into it.
tokio::spawn(async move {
client.initialize_notify.notified().await;
if let Some(workspace_folders_caps) = client
.capabilities()
.workspace
.as_ref()
.and_then(|cap| cap.workspace_folders.as_ref())
.filter(|cap| cap.supported.unwrap_or(false))
{
client.add_workspace_folder(
root_uri,
&workspace_folders_caps.change_notifications,
);
}
});
return true;
};
if let Some(workspace_folders_caps) = capabilities
.workspace
.as_ref()
.and_then(|cap| cap.workspace_folders.as_ref())
.filter(|cap| cap.supported.unwrap_or(false))
{
self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications);
true
} else {
// the server doesn't support multi workspaces, we need a new client
false
}
}
fn add_workspace_folder(
&self,
root_uri: Option<lsp::Url>,
change_notifications: &Option<OneOf<bool, String>>,
) {
// root_uri is None just means that there isn't really any LSP workspace
// associated with this file. For servers that support multiple workspaces
// there is just one server so we can always just use that shared instance.
// No need to add a new workspace root here as there is no logical root for this file
// let the server deal with this
let Some(root_uri) = root_uri else {
return;
};
// server supports workspace folders, let's add the new root to the list
self.workspace_folders
.lock()
.push(workspace_for_uri(root_uri.clone()));
if &Some(OneOf::Left(false)) == change_notifications {
// server specifically opted out of DidWorkspaceChange notifications
// let's assume the server will request the workspace folders itself
// and that we can therefore reuse the client (but are done now)
return;
}
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
}
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn start( pub fn start(
@ -49,6 +174,7 @@ impl Client {
config: Option<Value>, config: Option<Value>,
server_environment: HashMap<String, String>, server_environment: HashMap<String, String>,
root_markers: &[String], root_markers: &[String],
manual_roots: &[PathBuf],
id: usize, id: usize,
req_timeout: u64, req_timeout: u64,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
@ -75,27 +201,26 @@ impl Client {
let (server_rx, server_tx, initialize_notify) = let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id); Transport::start(reader, writer, stderr, id);
let (workspace, workspace_is_cwd) = find_workspace();
let root_path = find_root( let workspace = path::get_normalized_path(&workspace);
doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())), let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers, root_markers,
manual_roots,
&workspace,
workspace_is_cwd,
); );
let root_uri = lsp::Url::from_file_path(root_path.clone()).ok(); // `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
// TODO: support multiple workspace folders
let workspace_folders = root_uri let workspace_folders = root_uri
.clone() .clone()
.map(|root| { .map(|root| vec![workspace_for_uri(root)])
vec![lsp::WorkspaceFolder {
name: root
.path_segments()
.and_then(|segments| segments.last())
.map(|basename| basename.to_string())
.unwrap_or_default(),
uri: root,
}]
})
.unwrap_or_default(); .unwrap_or_default();
let client = Self { let client = Self {
@ -106,10 +231,10 @@ impl Client {
capabilities: OnceCell::new(), capabilities: OnceCell::new(),
config, config,
req_timeout, req_timeout,
root_path, root_path,
root_uri, root_uri,
workspace_folders, workspace_folders: Mutex::new(workspace_folders),
initialize_notify: initialize_notify.clone(),
}; };
Ok((client, server_rx, initialize_notify)) Ok((client, server_rx, initialize_notify))
@ -154,7 +279,7 @@ impl Client {
"utf-16" => Some(OffsetEncoding::Utf16), "utf-16" => Some(OffsetEncoding::Utf16),
"utf-32" => Some(OffsetEncoding::Utf32), "utf-32" => Some(OffsetEncoding::Utf32),
encoding => { encoding => {
log::error!("Server provided invalid position encording {encoding}, defaulting to utf-16"); log::error!("Server provided invalid position encoding {encoding}, defaulting to utf-16");
None None
}, },
}) })
@ -165,8 +290,10 @@ impl Client {
self.config.as_ref() self.config.as_ref()
} }
pub fn workspace_folders(&self) -> &[lsp::WorkspaceFolder] { pub async fn workspace_folders(
&self.workspace_folders &self,
) -> parking_lot::MutexGuard<'_, Vec<lsp::WorkspaceFolder>> {
self.workspace_folders.lock()
} }
/// Execute a RPC request on the language server. /// Execute a RPC request on the language server.
@ -286,7 +413,7 @@ impl Client {
// General messages // General messages
// ------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------
pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> { pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result<lsp::InitializeResult> {
if let Some(config) = &self.config { if let Some(config) = &self.config {
log::info!("Using custom LSP config: {}", config); log::info!("Using custom LSP config: {}", config);
} }
@ -294,7 +421,7 @@ impl Client {
#[allow(deprecated)] #[allow(deprecated)]
let params = lsp::InitializeParams { let params = lsp::InitializeParams {
process_id: Some(std::process::id()), process_id: Some(std::process::id()),
workspace_folders: Some(self.workspace_folders.clone()), workspace_folders: Some(self.workspace_folders.lock().clone()),
// root_path is obsolete, but some clients like pyright still use it so we specify both. // root_path is obsolete, but some clients like pyright still use it so we specify both.
// clients will prefer _uri if possible // clients will prefer _uri if possible
root_path: self.root_path.to_str().map(|path| path.to_owned()), root_path: self.root_path.to_str().map(|path| path.to_owned()),
@ -334,7 +461,7 @@ impl Client {
text_document: Some(lsp::TextDocumentClientCapabilities { text_document: Some(lsp::TextDocumentClientCapabilities {
completion: Some(lsp::CompletionClientCapabilities { completion: Some(lsp::CompletionClientCapabilities {
completion_item: Some(lsp::CompletionItemCapability { completion_item: Some(lsp::CompletionItemCapability {
snippet_support: Some(true), snippet_support: Some(enable_snippets),
resolve_support: Some(lsp::CompletionItemCapabilityResolveSupport { resolve_support: Some(lsp::CompletionItemCapabilityResolveSupport {
properties: vec![ properties: vec![
String::from("documentation"), String::from("documentation"),
@ -413,8 +540,8 @@ impl Client {
}), }),
general: Some(lsp::GeneralClientCapabilities { general: Some(lsp::GeneralClientCapabilities {
position_encodings: Some(vec![ position_encodings: Some(vec![
PositionEncodingKind::UTF32,
PositionEncodingKind::UTF8, PositionEncodingKind::UTF8,
PositionEncodingKind::UTF32,
PositionEncodingKind::UTF16, PositionEncodingKind::UTF16,
]), ]),
..Default::default() ..Default::default()
@ -465,6 +592,16 @@ impl Client {
) )
} }
pub fn did_change_workspace(
&self,
added: Vec<WorkspaceFolder>,
removed: Vec<WorkspaceFolder>,
) -> impl Future<Output = Result<()>> {
self.notify::<DidChangeWorkspaceFolders>(DidChangeWorkspaceFoldersParams {
event: WorkspaceFoldersChangeEvent { added, removed },
})
}
// ------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------
// Text document // Text document
// ------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------

@ -10,11 +10,15 @@ pub use lsp::{Position, Url};
pub use lsp_types as lsp; pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration}; use helix_core::{
path,
syntax::{LanguageConfiguration, LanguageServerConfiguration},
};
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedReceiver;
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::{hash_map::Entry, HashMap},
path::{Path, PathBuf},
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Arc, Arc,
@ -128,7 +132,11 @@ pub mod util {
) -> Option<usize> { ) -> Option<usize> {
let pos_line = pos.line as usize; let pos_line = pos.line as usize;
if pos_line > doc.len_lines() - 1 { if pos_line > doc.len_lines() - 1 {
return None; // If it extends past the end, truncate it to the end. This is because the
// way the LSP describes the range including the last newline is by
// specifying a line number after what we would call the last line.
log::warn!("LSP position {pos:?} out of range assuming EOF");
return Some(doc.len_chars());
} }
// We need to be careful here to fully comply ith the LSP spec. // We need to be careful here to fully comply ith the LSP spec.
@ -144,10 +152,10 @@ pub mod util {
// > \n, \r\n and \r. Positions are line end character agnostic. // > \n, \r\n and \r. Positions are line end character agnostic.
// > So you can not specify a position that denotes \r|\n or \n| where | represents the character offset. // > So you can not specify a position that denotes \r|\n or \n| where | represents the character offset.
// //
// This means that while the line must be in bounds the `charater` // This means that while the line must be in bounds the `character`
// must be capped to the end of the line. // must be capped to the end of the line.
// Note that the end of the line here is **before** the line terminator // Note that the end of the line here is **before** the line terminator
// so we must use `line_end_char_index` istead of `doc.line_to_char(pos_line + 1)` // so we must use `line_end_char_index` instead of `doc.line_to_char(pos_line + 1)`
// //
// FIXME: Helix does not fully comply with the LSP spec for line terminators. // FIXME: Helix does not fully comply with the LSP spec for line terminators.
// The LSP standard requires that line terminators are ['\n', '\r\n', '\r']. // The LSP standard requires that line terminators are ['\n', '\r\n', '\r'].
@ -238,9 +246,20 @@ pub mod util {
pub fn lsp_range_to_range( pub fn lsp_range_to_range(
doc: &Rope, doc: &Rope,
range: lsp::Range, mut range: lsp::Range,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
) -> Option<Range> { ) -> Option<Range> {
// This is sort of an edgecase. It's not clear from the spec how to deal with
// ranges where end < start. They don't make much sense but vscode simply caps start to end
// and because it's not specified quite a few LS rely on this as a result (for example the TS server)
if range.start > range.end {
log::error!(
"Invalid LSP range start {:?} > end {:?}, using an empty range at the end instead",
range.start,
range.end
);
range.start = range.end;
}
let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?; let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?;
let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?; let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?;
@ -605,7 +624,7 @@ impl Notification {
#[derive(Debug)] #[derive(Debug)]
pub struct Registry { pub struct Registry {
inner: HashMap<LanguageId, (usize, Arc<Client>)>, inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>,
counter: AtomicUsize, counter: AtomicUsize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>, pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
@ -629,18 +648,24 @@ impl Registry {
pub fn get_by_id(&self, id: usize) -> Option<&Client> { pub fn get_by_id(&self, id: usize) -> Option<&Client> {
self.inner self.inner
.values() .values()
.flatten()
.find(|(client_id, _)| client_id == &id) .find(|(client_id, _)| client_id == &id)
.map(|(_, client)| client.as_ref()) .map(|(_, client)| client.as_ref())
} }
pub fn remove_by_id(&mut self, id: usize) { pub fn remove_by_id(&mut self, id: usize) {
self.inner.retain(|_, (client_id, _)| client_id != &id) self.inner.retain(|_, clients| {
clients.retain(|&(client_id, _)| client_id != id);
!clients.is_empty()
})
} }
pub fn restart( pub fn restart(
&mut self, &mut self,
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Option<Arc<Client>>> { ) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server { let config = match &language_config.language_server {
Some(config) => config, Some(config) => config,
@ -655,15 +680,23 @@ impl Registry {
// initialize a new client // initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed); let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) = let NewClientResult(client, incoming) = start_client(
start_client(id, language_config, config, doc_path)?; id,
language_config,
config,
doc_path,
root_dirs,
enable_snippets,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming)); self.incoming.push(UnboundedReceiverStream::new(incoming));
let (_, old_client) = entry.insert((id, client.clone())); let old_clients = entry.insert(vec![(id, client.clone())]);
for (_, old_client) in old_clients {
tokio::spawn(async move { tokio::spawn(async move {
let _ = old_client.force_shutdown().await; let _ = old_client.force_shutdown().await;
}); });
}
Ok(Some(client)) Ok(Some(client))
} }
@ -673,41 +706,52 @@ impl Registry {
pub fn stop(&mut self, language_config: &LanguageConfiguration) { pub fn stop(&mut self, language_config: &LanguageConfiguration) {
let scope = language_config.scope.clone(); let scope = language_config.scope.clone();
if let Some((_, client)) = self.inner.remove(&scope) { if let Some(clients) = self.inner.remove(&scope) {
for (_, client) in clients {
tokio::spawn(async move { tokio::spawn(async move {
let _ = client.force_shutdown().await; let _ = client.force_shutdown().await;
}); });
} }
} }
}
pub fn get( pub fn get(
&mut self, &mut self,
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Option<Arc<Client>>> { ) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server { let config = match &language_config.language_server {
Some(config) => config, Some(config) => config,
None => return Ok(None), None => return Ok(None),
}; };
match self.inner.entry(language_config.scope.clone()) { let clients = self.inner.entry(language_config.scope.clone()).or_default();
Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())), // check if we already have a client for this documents root that we can reuse
Entry::Vacant(entry) => { if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| {
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
}) {
return Ok(Some(client.1.clone()));
}
// initialize a new client // initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed); let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) = let NewClientResult(client, incoming) = start_client(
start_client(id, language_config, config, doc_path)?; id,
language_config,
config,
doc_path,
root_dirs,
enable_snippets,
)?;
clients.push((id, client.clone()));
self.incoming.push(UnboundedReceiverStream::new(incoming)); self.incoming.push(UnboundedReceiverStream::new(incoming));
entry.insert((id, client.clone()));
Ok(Some(client)) Ok(Some(client))
} }
}
}
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> { pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().map(|(_, client)| client) self.inner.values().flatten().map(|(_, client)| client)
} }
} }
@ -798,6 +842,8 @@ fn start_client(
config: &LanguageConfiguration, config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration, ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<NewClientResult> { ) -> Result<NewClientResult> {
let (client, incoming, initialize_notify) = Client::start( let (client, incoming, initialize_notify) = Client::start(
&ls_config.command, &ls_config.command,
@ -805,6 +851,7 @@ fn start_client(
config.config.clone(), config.config.clone(),
ls_config.environment.clone(), ls_config.environment.clone(),
&config.roots, &config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
id, id,
ls_config.timeout, ls_config.timeout,
doc_path, doc_path,
@ -820,7 +867,7 @@ fn start_client(
.capabilities .capabilities
.get_or_try_init(|| { .get_or_try_init(|| {
_client _client
.initialize() .initialize(enable_snippets)
.map_ok(|response| response.capabilities) .map_ok(|response| response.capabilities)
}) })
.await; .await;
@ -842,6 +889,65 @@ fn start_client(
Ok(NewClientResult(client, incoming)) Ok(NewClientResult(client, incoming))
} }
/// Find an LSP workspace of a file using the following mechanism:
/// * if the file is outside `workspace` return `None`
/// * start at `file` and search the file tree upward
/// * stop the search at the first `root_dirs` entry that contains `file`
/// * if no `root_dirs` matches `file` stop at workspace
/// * Returns the top most directory that contains a `root_marker`
/// * If no root marker and we stopped at a `root_dirs` entry, return the directory we stopped at
/// * If we stopped at `workspace` instead and `workspace_is_cwd == false` return `None`
/// * If we stopped at `workspace` instead and `workspace_is_cwd == true` return `workspace`
pub fn find_lsp_workspace(
file: &str,
root_markers: &[String],
root_dirs: &[PathBuf],
workspace: &Path,
workspace_is_cwd: bool,
) -> Option<PathBuf> {
let file = std::path::Path::new(file);
let mut file = if file.is_absolute() {
file.to_path_buf()
} else {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
current_dir.join(file)
};
file = path::get_normalized_path(&file);
if !file.starts_with(workspace) {
return None;
}
let mut top_marker = None;
for ancestor in file.ancestors() {
if root_markers
.iter()
.any(|marker| ancestor.join(marker).exists())
{
top_marker = Some(ancestor);
}
if root_dirs
.iter()
.any(|root_dir| path::get_normalized_path(&workspace.join(root_dir)) == ancestor)
{
// if the worskapce is the cwd do not search any higher for workspaces
// but specify
return Some(top_marker.unwrap_or(workspace).to_owned());
}
if ancestor == workspace {
// if the workspace is the CWD, let the LSP decide what the workspace
// is
return top_marker
.or_else(|| (!workspace_is_cwd).then_some(workspace))
.map(Path::to_owned);
}
}
debug_assert!(false, "workspace must be an ancestor of <file>");
None
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{lsp, util::*, OffsetEncoding}; use super::{lsp, util::*, OffsetEncoding};
@ -860,16 +966,16 @@ mod tests {
test_case!("", (0, 0) => Some(0)); test_case!("", (0, 0) => Some(0));
test_case!("", (0, 1) => Some(0)); test_case!("", (0, 1) => Some(0));
test_case!("", (1, 0) => None); test_case!("", (1, 0) => Some(0));
test_case!("\n\n", (0, 0) => Some(0)); test_case!("\n\n", (0, 0) => Some(0));
test_case!("\n\n", (1, 0) => Some(1)); test_case!("\n\n", (1, 0) => Some(1));
test_case!("\n\n", (1, 1) => Some(1)); test_case!("\n\n", (1, 1) => Some(1));
test_case!("\n\n", (2, 0) => Some(2)); test_case!("\n\n", (2, 0) => Some(2));
test_case!("\n\n", (3, 0) => None); test_case!("\n\n", (3, 0) => Some(2));
test_case!("test\n\n\n\ncase", (4, 3) => Some(11)); test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
test_case!("test\n\n\n\ncase", (4, 4) => Some(12)); test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
test_case!("test\n\n\n\ncase", (4, 5) => Some(12)); test_case!("test\n\n\n\ncase", (4, 5) => Some(12));
test_case!("", (u32::MAX, u32::MAX) => None); test_case!("", (u32::MAX, u32::MAX) => Some(0));
} }
#[test] #[test]

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

@ -1 +1,4 @@
/target /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, document::DocumentSavedEventResult,
editor::{ConfigEvent, EditorEvent}, editor::{ConfigEvent, EditorEvent},
graphics::Rect, graphics::Rect,
theme, icons, theme,
tree::Layout, tree::Layout,
Align, Editor, Align, Editor,
}; };
@ -21,11 +21,11 @@ use tui::backend::Backend;
use crate::{ use crate::{
args::Args, args::Args,
commands::apply_workspace_edit, commands::apply_workspace_edit,
compositor::{Compositor, Event}, compositor::{self, Compositor, Event},
config::Config, config::Config,
job::Jobs, job::Jobs,
keymap::Keymaps, keymap::Keymaps,
ui::{self, overlay::overlayed}, ui::{self, overlay::overlaid as overlayed, Explorer},
}; };
use log::{debug, error, warn}; use log::{debug, error, warn};
@ -69,6 +69,7 @@ pub struct Application {
#[allow(dead_code)] #[allow(dead_code)]
theme_loader: Arc<theme::Loader>, theme_loader: Arc<theme::Loader>,
icons_loader: Arc<icons::Loader>,
#[allow(dead_code)] #[allow(dead_code)]
syn_loader: Arc<syntax::Loader>, syn_loader: Arc<syntax::Loader>,
@ -111,9 +112,9 @@ impl Application {
use helix_view::editor::Action; use helix_view::editor::Action;
let mut theme_parent_dirs = vec![helix_loader::config_dir()]; let mut theme_and_icons_parent_dirs = vec![helix_loader::config_dir()];
theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); theme_and_icons_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs)); 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 true_color = config.editor.true_color || crate::true_color();
let theme = config let theme = config
@ -131,6 +132,21 @@ impl Application {
}) })
.unwrap_or_else(|| theme_loader.default_theme(true_color)); .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)); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
#[cfg(not(feature = "integration"))] #[cfg(not(feature = "integration"))]
@ -146,16 +162,34 @@ impl Application {
let mut editor = Editor::new( let mut editor = Editor::new(
area, area,
theme_loader.clone(), theme_loader.clone(),
icons_loader.clone(),
syn_loader.clone(), syn_loader.clone(),
Arc::new(Map::new(Arc::clone(&config), |config: &Config| { Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.editor &config.editor
})), })),
); );
editor.set_theme(theme);
editor.set_icons(icons);
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.keys &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); compositor.push(editor_view);
if args.load_tutor { if args.load_tutor {
@ -168,7 +202,7 @@ impl Application {
if first.is_dir() { if first.is_dir() {
std::env::set_current_dir(first).context("set current dir")?; std::env::set_current_dir(first).context("set current dir")?;
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
let picker = ui::file_picker(".".into(), &config.load().editor); let picker = ui::file_picker(".".into(), &config.load().editor, &editor.icons);
compositor.push(Box::new(overlayed(picker))); compositor.push(Box::new(overlayed(picker)));
} else { } else {
let nr_of_files = args.files.len(); let nr_of_files = args.files.len();
@ -225,8 +259,6 @@ impl Application {
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
} }
editor.set_theme(theme);
#[cfg(windows)] #[cfg(windows)]
let signals = futures_util::stream::empty(); let signals = futures_util::stream::empty();
#[cfg(not(windows))] #[cfg(not(windows))]
@ -241,10 +273,11 @@ impl Application {
config, config,
theme_loader, theme_loader,
icons_loader,
syn_loader, syn_loader,
signals, signals,
jobs: Jobs::new(), jobs,
lsp_progress: LspProgressMap::new(), lsp_progress: LspProgressMap::new(),
last_render: Instant::now(), last_render: Instant::now(),
}; };
@ -393,18 +426,35 @@ impl Application {
/// Refresh theme after config change /// Refresh theme after config change
fn refresh_theme(&mut self, config: &Config) -> Result<(), Error> { fn refresh_theme(&mut self, config: &Config) -> Result<(), Error> {
if let Some(theme) = config.theme.clone() { let true_color = config.editor.true_color || crate::true_color();
let true_color = self.true_color(); let theme = config
let theme = self .theme
.theme_loader .as_ref()
.load(&theme) .and_then(|theme| {
.map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; self.theme_loader
.load(theme)
if true_color || theme.is_16_color() { .map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, e);
e
})
.ok()
.filter(|theme| (true_color || theme.is_16_color()))
})
.unwrap_or_else(|| self.theme_loader.default_theme(true_color));
self.editor.set_theme(theme); self.editor.set_theme(theme);
} else { Ok(())
anyhow::bail!("theme requires truecolor support, which is not available")
} }
/// 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(()) Ok(())
@ -416,6 +466,7 @@ impl Application {
.map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?; .map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?;
self.refresh_language_config()?; self.refresh_language_config()?;
self.refresh_theme(&default_config)?; self.refresh_theme(&default_config)?;
self.refresh_icons(&default_config)?;
// Store new config // Store new config
self.config.store(Arc::new(default_config)); self.config.store(Arc::new(default_config));
Ok(()) Ok(())
@ -431,10 +482,6 @@ impl Application {
} }
} }
fn true_color(&self) -> bool {
self.config.load().editor.true_color || crate::true_color()
}
#[cfg(windows)] #[cfg(windows)]
// no signal handling available on windows // no signal handling available on windows
pub async fn handle_signals(&mut self, _signal: ()) {} pub async fn handle_signals(&mut self, _signal: ()) {}
@ -1018,7 +1065,7 @@ impl Application {
let language_server = let language_server =
self.editor.language_servers.get_by_id(server_id).unwrap(); self.editor.language_servers.get_by_id(server_id).unwrap();
Ok(json!(language_server.workspace_folders())) Ok(json!(&*language_server.workspace_folders().await))
} }
Ok(MethodCall::WorkspaceConfiguration(params)) => { Ok(MethodCall::WorkspaceConfiguration(params)) => {
let result: Vec<_> = params let result: Vec<_> = params

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

@ -6,13 +6,13 @@ pub use dap::*;
use helix_vcs::Hunk; use helix_vcs::Hunk;
pub use lsp::*; pub use lsp::*;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tui::widgets::Row; use tui::{text::Span, widgets::Row};
pub use typed::*; pub use typed::*;
use helix_core::{ use helix_core::{
char_idx_at_visual_offset, comment, char_idx_at_visual_offset, comment,
doc_formatter::TextFormat, doc_formatter::TextFormat,
encoding, find_first_non_whitespace_char, find_root, graphemes, encoding, find_first_non_whitespace_char, find_workspace, graphemes,
history::UndoKind, history::UndoKind,
increment, indent, increment, indent,
indent::IndentStyle, indent::IndentStyle,
@ -34,6 +34,7 @@ use helix_view::{
clipboard::ClipboardType, clipboard::ClipboardType,
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion}, editor::{Action, Motion},
icons::Icons,
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
keyboard::KeyCode, keyboard::KeyCode,
@ -54,8 +55,8 @@ use crate::{
job::Callback, job::Callback,
keymap::ReverseKeymap, keymap::ReverseKeymap,
ui::{ ui::{
self, editor::InsertEvent, overlay::overlayed, FilePicker, Picker, Popup, Prompt, self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker,
PromptEvent, Popup, Prompt, PromptEvent,
}, },
}; };
@ -472,6 +473,8 @@ impl MappableCommand {
record_macro, "Record macro", record_macro, "Record macro",
replay_macro, "Replay macro", replay_macro, "Replay macro",
command_palette, "Open command palette", command_palette, "Open command palette",
open_or_focus_explorer, "Open or focus explorer",
reveal_current_file, "Reveal current file in explorer",
); );
} }
@ -501,7 +504,7 @@ impl std::str::FromStr for MappableCommand {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(suffix) = s.strip_prefix(':') { if let Some(suffix) = s.strip_prefix(':') {
let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim()); let mut typable_command = suffix.split(' ').map(|arg| arg.trim());
let name = typable_command let name = typable_command
.next() .next()
.ok_or_else(|| anyhow!("Expected typable command name"))?; .ok_or_else(|| anyhow!("Expected typable command name"))?;
@ -1470,7 +1473,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
let cursor = range.cursor(text); let cursor = range.cursor(text);
let height = view.inner_height(); let height = view.inner_height();
let scrolloff = config.scrolloff.min(height / 2); let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2);
let offset = match direction { let offset = match direction {
Forward => offset as isize, Forward => offset as isize,
Backward => -(offset as isize), Backward => -(offset as isize),
@ -1489,18 +1492,19 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
&annotations, &annotations,
); );
let head; let mut head;
match direction { match direction {
Forward => { Forward => {
head = char_idx_at_visual_offset( let off;
(head, off) = char_idx_at_visual_offset(
doc_text, doc_text,
view.offset.anchor, view.offset.anchor,
(view.offset.vertical_offset + scrolloff) as isize, (view.offset.vertical_offset + scrolloff) as isize,
0, 0,
&text_fmt, &text_fmt,
&annotations, &annotations,
) );
.0; head += (off != 0) as usize;
if head <= cursor { if head <= cursor {
return; return;
} }
@ -1509,7 +1513,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
head = char_idx_at_visual_offset( head = char_idx_at_visual_offset(
doc_text, doc_text,
view.offset.anchor, view.offset.anchor,
(view.offset.vertical_offset + height - scrolloff) as isize, (view.offset.vertical_offset + height - scrolloff - 1) as isize,
0, 0,
&text_fmt, &text_fmt,
&annotations, &annotations,
@ -1560,7 +1564,7 @@ fn half_page_down(cx: &mut Context) {
} }
#[allow(deprecated)] #[allow(deprecated)]
// currently uses the deprected `visual_coords_at_pos`/`pos_at_visual_coords` functions // currently uses the deprecated `visual_coords_at_pos`/`pos_at_visual_coords` functions
// as this function ignores softwrapping (and virtual text) and instead only cares // as this function ignores softwrapping (and virtual text) and instead only cares
// about "text visual position" // about "text visual position"
// //
@ -1988,11 +1992,12 @@ fn global_search(cx: &mut Context) {
impl ui::menu::Item for FileResult { impl ui::menu::Item for FileResult {
type Data = Option<PathBuf>; 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) let relative_path = helix_core::path::get_relative_path(&self.path)
.to_string_lossy() .to_string_lossy()
.into_owned(); .into_owned();
if current_path let path_span: Span = if current_path
.as_ref() .as_ref()
.map(|p| p == &self.path) .map(|p| p == &self.path)
.unwrap_or(false) .unwrap_or(false)
@ -2000,6 +2005,12 @@ fn global_search(cx: &mut Context) {
format!("{} (*)", relative_path).into() format!("{} (*)", relative_path).into()
} else { } else {
relative_path.into() relative_path.into()
};
if let Some(icon) = icon {
Row::new([icon.into(), path_span])
} else {
path_span.into()
} }
} }
} }
@ -2116,6 +2127,7 @@ fn global_search(cx: &mut Context) {
let picker = FilePicker::new( let picker = FilePicker::new(
all_matches, all_matches,
current_path, current_path,
editor.config().icons.picker.then_some(&editor.icons),
move |cx, FileResult { path, line_num }, action| { move |cx, FileResult { path, line_num }, action| {
match cx.editor.open(path, action) { match cx.editor.open(path, action) {
Ok(_) => {} Ok(_) => {}
@ -2146,7 +2158,7 @@ fn global_search(cx: &mut Context) {
Some((path.clone().into(), Some((*line_num, *line_num)))) Some((path.clone().into(), Some((*line_num, *line_num))))
}, },
); );
compositor.push(Box::new(overlayed(picker))); compositor.push(Box::new(overlaid(picker)));
}, },
)); ));
Ok(call) Ok(call)
@ -2418,11 +2430,9 @@ fn append_mode(cx: &mut Context) {
} }
fn file_picker(cx: &mut Context) { fn file_picker(cx: &mut Context) {
// We don't specify language markers, root will be the root of the current let root = find_workspace().0;
// git repo or the current dir if we're not in a repo let picker = ui::file_picker(root, &cx.editor.config(), &cx.editor.icons);
let root = find_root(None, &[]); cx.push_layer(Box::new(overlaid(picker)));
let picker = ui::file_picker(root, &cx.editor.config());
cx.push_layer(Box::new(overlayed(picker)));
} }
fn file_picker_in_current_buffer_directory(cx: &mut Context) { fn file_picker_in_current_buffer_directory(cx: &mut Context) {
@ -2438,13 +2448,56 @@ 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(overlayed(picker))); cx.push_layer(Box::new(overlaid(picker)));
} }
fn file_picker_in_current_directory(cx: &mut Context) { fn file_picker_in_current_directory(cx: &mut Context) {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./")); 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(overlayed(picker))); 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) { fn buffer_picker(cx: &mut Context) {
@ -2460,7 +2513,7 @@ fn buffer_picker(cx: &mut Context) {
impl ui::menu::Item for BufferMeta { impl ui::menu::Item for BufferMeta {
type Data = (); type Data = ();
fn format(&self, _data: &Self::Data) -> Row { fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row {
let path = self let path = self
.path .path
.as_deref() .as_deref()
@ -2470,6 +2523,9 @@ fn buffer_picker(cx: &mut Context) {
None => SCRATCH_BUFFER_NAME, 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(); let mut flags = String::new();
if self.is_modified { if self.is_modified {
flags.push('+'); flags.push('+');
@ -2478,9 +2534,19 @@ fn buffer_picker(cx: &mut Context) {
flags.push('*'); flags.push('*');
} }
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()]) Row::new([self.id.to_string(), flags, path.to_string()])
} }
} }
}
let new_meta = |doc: &Document| BufferMeta { let new_meta = |doc: &Document| BufferMeta {
id: doc.id(), id: doc.id(),
@ -2496,6 +2562,7 @@ fn buffer_picker(cx: &mut Context) {
.map(|doc| new_meta(doc)) .map(|doc| new_meta(doc))
.collect(), .collect(),
(), (),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
|cx, meta, action| { |cx, meta, action| {
cx.editor.switch(meta.id, action); cx.editor.switch(meta.id, action);
}, },
@ -2509,7 +2576,7 @@ fn buffer_picker(cx: &mut Context) {
Some((meta.id.into(), Some((line, line)))) Some((meta.id.into(), Some((line, line))))
}, },
); );
cx.push_layer(Box::new(overlayed(picker))); cx.push_layer(Box::new(overlaid(picker)));
} }
fn jumplist_picker(cx: &mut Context) { fn jumplist_picker(cx: &mut Context) {
@ -2524,7 +2591,10 @@ fn jumplist_picker(cx: &mut Context) {
impl ui::menu::Item for JumpMeta { impl ui::menu::Item for JumpMeta {
type Data = (); 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 let path = self
.path .path
.as_deref() .as_deref()
@ -2544,7 +2614,13 @@ fn jumplist_picker(cx: &mut Context) {
} else { } else {
format!(" ({})", flags.join("")) 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()
}
} }
} }
@ -2578,6 +2654,7 @@ fn jumplist_picker(cx: &mut Context) {
}) })
.collect(), .collect(),
(), (),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
|cx, meta, action| { |cx, meta, action| {
cx.editor.switch(meta.id, action); cx.editor.switch(meta.id, action);
let config = cx.editor.config(); let config = cx.editor.config();
@ -2591,13 +2668,13 @@ fn jumplist_picker(cx: &mut Context) {
Some((meta.path.clone()?.into(), Some((line, line)))) Some((meta.path.clone()?.into(), Some((line, line))))
}, },
); );
cx.push_layer(Box::new(overlayed(picker))); cx.push_layer(Box::new(overlaid(picker)));
} }
impl ui::menu::Item for MappableCommand { impl ui::menu::Item for MappableCommand {
type Data = ReverseKeymap; 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 { let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
bindings.iter().fold(String::new(), |mut acc, bind| { bindings.iter().fold(String::new(), |mut acc, bind| {
if !acc.is_empty() { if !acc.is_empty() {
@ -2639,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 { let mut ctx = Context {
register: None, register: None,
count: std::num::NonZeroUsize::new(1), count: std::num::NonZeroUsize::new(1),
@ -2665,7 +2742,7 @@ pub fn command_palette(cx: &mut Context) {
} }
} }
}); });
compositor.push(Box::new(overlayed(picker))); compositor.push(Box::new(overlaid(picker)));
}, },
)); ));
} }
@ -4186,7 +4263,7 @@ pub fn completion(cx: &mut Context) {
None => return, None => return,
}; };
// setup a chanel that allows the request to be canceled // setup a channel that allows the request to be canceled
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
// set completion_request so that this request can be canceled // set completion_request so that this request can be canceled
// by setting completion_request, the old channel stored there is dropped // by setting completion_request, the old channel stored there is dropped
@ -4239,7 +4316,7 @@ pub fn completion(cx: &mut Context) {
let (view, doc) = current_ref!(editor); let (view, doc) = current_ref!(editor);
// check if the completion request is stale. // check if the completion request is stale.
// //
// Completions are completed asynchrounsly and therefore the user could // Completions are completed asynchronously and therefore the user could
//switch document/view or leave insert mode. In all of thoise cases the //switch document/view or leave insert mode. In all of thoise cases the
// completion should be discarded // completion should be discarded
if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc { if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc {
@ -4262,7 +4339,7 @@ pub fn completion(cx: &mut Context) {
} }
let size = compositor.size(); let size = compositor.size();
let ui = compositor.find::<ui::EditorView>().unwrap(); let ui = compositor.find::<ui::EditorView>().unwrap();
ui.set_completion( let completion_area = ui.set_completion(
editor, editor,
savepoint, savepoint,
items, items,
@ -4271,6 +4348,15 @@ pub fn completion(cx: &mut Context) {
trigger_offset, trigger_offset,
size, size,
); );
let size = compositor.size();
let signature_help_area = compositor
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
.map(|signature_help| signature_help.area(size, editor));
// Delete the signature help popup if they intersect.
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b))
{
compositor.remove(SignatureHelp::ID);
}
}, },
); );
} }

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

@ -3,15 +3,12 @@ use helix_lsp::{
block_on, block_on,
lsp::{ lsp::{
self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity, self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity,
NumberOrString, NumberOrString, SymbolKind,
}, },
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding, OffsetEncoding,
}; };
use tui::{ use tui::{text::Span, widgets::Row};
text::{Span, Spans},
widgets::Row,
};
use super::{align_view, push_jump, Align, Context, Editor, Open}; 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::{ use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
editor::Action, editor::Action,
icons::{self, Icon, Icons},
theme::Style, theme::Style,
Document, View, Document, View,
}; };
@ -26,7 +24,7 @@ use helix_view::{
use crate::{ use crate::{
compositor::{self, Compositor}, compositor::{self, Compositor},
ui::{ ui::{
self, lsp::SignatureHelp, overlay::overlayed, DynamicPicker, FileLocation, FilePicker, self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
Popup, PromptEvent, Popup, PromptEvent,
}, },
}; };
@ -57,7 +55,7 @@ impl ui::menu::Item for lsp::Location {
/// Current working directory. /// Current working directory.
type Data = PathBuf; 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 // 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://". // 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 // Those extra chars will be used to avoid allocating when writing the line number (in the
@ -81,7 +79,7 @@ impl ui::menu::Item for lsp::Location {
// Most commonly, this will not allocate, especially on Unix systems where the root prefix // Most commonly, this will not allocate, especially on Unix systems where the root prefix
// is a simple `/` and not `C:\` (with whatever drive letter) // is a simple `/` and not `C:\` (with whatever drive letter)
write!(&mut res, ":{}", self.range.start.line) write!(&mut res, ":{}", self.range.start.line + 1)
.expect("Will only failed if allocating fail"); .expect("Will only failed if allocating fail");
res.into() res.into()
} }
@ -91,16 +89,58 @@ impl ui::menu::Item for lsp::SymbolInformation {
/// Path to currently focussed document /// Path to currently focussed document
type Data = Option<lsp::Url>; 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) { if current_doc_path.as_ref() == Some(&self.location.uri) {
if let Some(icon) = icon {
Row::new([Span::from(icon), self.name.as_str().into()])
} else {
self.name.as_str().into() self.name.as_str().into()
}
} else { } else {
match self.location.uri.to_file_path() { let symbol_span: Span = match self.location.uri.to_file_path() {
Ok(path) => { Ok(path) => {
let get_relative_path = path::get_relative_path(path.as_path()); let get_relative_path = path::get_relative_path(path.as_path());
format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into() format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
} }
Err(_) => format!("{} ({})", &self.name, &self.location.uri).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 { impl ui::menu::Item for PickerDiagnostic {
type Data = (DiagnosticStyles, DiagnosticsFormat); 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 let mut style = self
.diag .diag
.severity .severity
@ -152,12 +203,20 @@ impl ui::menu::Item for PickerDiagnostic {
} }
}; };
Spans::from(vec![ 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::raw(path),
Span::styled(&self.diag.message, style), Span::styled(&self.diag.message, style),
Span::styled(code, style), Span::styled(code, style),
]) ])
.into() }
} }
} }
@ -213,11 +272,13 @@ fn sym_picker(
symbols: Vec<lsp::SymbolInformation>, symbols: Vec<lsp::SymbolInformation>,
current_path: Option<lsp::Url>, current_path: Option<lsp::Url>,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
editor: &Editor,
) -> FilePicker<lsp::SymbolInformation> { ) -> FilePicker<lsp::SymbolInformation> {
// TODO: drop current_path comparison and instead use workspace: bool flag? // TODO: drop current_path comparison and instead use workspace: bool flag?
FilePicker::new( FilePicker::new(
symbols, symbols,
current_path.clone(), current_path.clone(),
editor.config().icons.picker.then_some(&editor.icons),
move |cx, symbol, action| { move |cx, symbol, action| {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
push_jump(view, doc); push_jump(view, doc);
@ -293,6 +354,7 @@ fn diag_picker(
FilePicker::new( FilePicker::new(
flat_diag, flat_diag,
(styles, format), (styles, format),
cx.editor.config().icons.picker.then_some(&cx.editor.icons),
move |cx, PickerDiagnostic { url, diag }, action| { move |cx, PickerDiagnostic { url, diag }, action| {
if current_path.as_ref() == Some(url) { if current_path.as_ref() == Some(url) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -371,8 +433,8 @@ pub fn symbol_picker(cx: &mut Context) {
} }
}; };
let picker = sym_picker(symbols, current_url, offset_encoding); let picker = sym_picker(symbols, current_url, offset_encoding, editor);
compositor.push(Box::new(overlayed(picker))) compositor.push(Box::new(overlaid(picker)))
} }
}, },
) )
@ -394,9 +456,9 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
cx.callback( cx.callback(
future, future,
move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| { move |editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
let symbols = response.unwrap_or_default(); 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 get_symbols = |query: String, editor: &mut Editor| {
let doc = doc!(editor); let doc = doc!(editor);
let language_server = match doc.language_server() { let language_server = match doc.language_server() {
@ -431,7 +493,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
future.boxed() future.boxed()
}; };
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols)); let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
compositor.push(Box::new(overlayed(dyn_picker))) compositor.push(Box::new(overlaid(dyn_picker)))
}, },
) )
} }
@ -454,7 +516,7 @@ pub fn diagnostics_picker(cx: &mut Context) {
DiagnosticsFormat::HideSourcePath, DiagnosticsFormat::HideSourcePath,
offset_encoding, offset_encoding,
); );
cx.push_layer(Box::new(overlayed(picker))); cx.push_layer(Box::new(overlaid(picker)));
} }
} }
@ -471,12 +533,12 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) {
DiagnosticsFormat::ShowSourcePath, DiagnosticsFormat::ShowSourcePath,
offset_encoding, offset_encoding,
); );
cx.push_layer(Box::new(overlayed(picker))); cx.push_layer(Box::new(overlaid(picker)));
} }
impl ui::menu::Item for lsp::CodeActionOrCommand { impl ui::menu::Item for lsp::CodeActionOrCommand {
type Data = (); type Data = ();
fn format(&self, _data: &Self::Data) -> Row { fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row {
match self { match self {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(), lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
@ -491,7 +553,7 @@ impl ui::menu::Item for lsp::CodeActionOrCommand {
/// ///
/// While the `kind` field is defined as open ended in the LSP spec (any value may be used) /// While the `kind` field is defined as open ended in the LSP spec (any value may be used)
/// in practice a closed set of common values (mostly suggested in the LSP spec) are used. /// in practice a closed set of common values (mostly suggested in the LSP spec) are used.
/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker) /// VSCode displays each of these categories separately (separated by a heading in the codeactions picker)
/// to make them easier to navigate. Helix does not display these headings to the user. /// to make them easier to navigate. Helix does not display these headings to the user.
/// However it does sort code actions by their categories to achieve the same order as the VScode picker, /// However it does sort code actions by their categories to achieve the same order as the VScode picker,
/// just without the headings. /// just without the headings.
@ -521,7 +583,7 @@ fn action_category(action: &CodeActionOrCommand) -> u32 {
} }
} }
fn action_prefered(action: &CodeActionOrCommand) -> bool { fn action_preferred(action: &CodeActionOrCommand) -> bool {
matches!( matches!(
action, action,
CodeActionOrCommand::CodeAction(CodeAction { CodeActionOrCommand::CodeAction(CodeAction {
@ -600,12 +662,12 @@ pub fn code_action(cx: &mut Context) {
} }
// Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec. // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
// Many details are modeled after vscode because langauge servers are usually tested against it. // Many details are modeled after vscode because language servers are usually tested against it.
// VScode sorts the codeaction two times: // VScode sorts the codeaction two times:
// //
// First the codeactions that fix some diagnostics are moved to the front. // First the codeactions that fix some diagnostics are moved to the front.
// If both codeactions fix some diagnostics (or both fix none) the codeaction // If both codeactions fix some diagnostics (or both fix none) the codeaction
// that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate // that is marked with `is_preferred` is shown first. The codeactions are then shown in separate
// submenus that only contain a certain category (see `action_category`) of actions. // submenus that only contain a certain category (see `action_category`) of actions.
// //
// Below this done in in a single sorting step // Below this done in in a single sorting step
@ -627,10 +689,10 @@ pub fn code_action(cx: &mut Context) {
return order; return order;
} }
// if one of the codeactions is marked as prefered show it first // if one of the codeactions is marked as preferred show it first
// otherwise keep the original LSP sorting // otherwise keep the original LSP sorting
action_prefered(action1) action_preferred(action1)
.cmp(&action_prefered(action2)) .cmp(&action_preferred(action2))
.reverse() .reverse()
}); });
@ -672,7 +734,7 @@ pub fn code_action(cx: &mut Context) {
impl ui::menu::Item for lsp::Command { impl ui::menu::Item for lsp::Command {
type Data = (); 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() self.title.as_str().into()
} }
} }
@ -950,12 +1012,13 @@ fn goto_impl(
let picker = FilePicker::new( let picker = FilePicker::new(
locations, locations,
cwdir, cwdir,
None,
move |cx, location, action| { move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action) jump_to_location(cx.editor, location, offset_encoding, action)
}, },
move |_editor, location| Some(location_to_file_location(location)), move |_editor, location| Some(location_to_file_location(location)),
); );
compositor.push(Box::new(overlayed(picker))); compositor.push(Box::new(overlaid(picker)));
} }
} }
} }
@ -1221,10 +1284,25 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
contents.set_active_param_range(active_param_range()); contents.set_active_param_range(active_param_range());
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID); let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
let popup = Popup::new(SignatureHelp::ID, contents) let mut popup = Popup::new(SignatureHelp::ID, contents)
.position(old_popup.and_then(|p| p.get_position())) .position(old_popup.and_then(|p| p.get_position()))
.position_bias(Open::Above) .position_bias(Open::Above)
.ignore_escape_key(true); .ignore_escape_key(true);
// Don't create a popup if it intersects the auto-complete menu.
let size = compositor.size();
if compositor
.find::<ui::EditorView>()
.unwrap()
.completion
.as_mut()
.map(|completion| completion.area(size, editor))
.filter(|area| area.intersects(popup.area(size, editor)))
.is_some()
{
return;
}
compositor.replace_or_push(SignatureHelp::ID, popup); compositor.replace_or_push(SignatureHelp::ID, popup);
}, },
); );

@ -115,8 +115,8 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
let callback = async move { let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new( let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| { 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(overlayed(picker))); compositor.push(Box::new(overlaid(picker)));
}, },
)); ));
Ok(call) Ok(call)
@ -298,6 +298,30 @@ fn force_buffer_close_all(
buffer_close_by_ids_impl(cx, &document_ids, true) 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( fn buffer_next(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[Cow<str>], _args: &[Cow<str>],
@ -853,6 +877,30 @@ fn theme(
Ok(()) 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( fn yank_main_selection_to_clipboard(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[Cow<str>], _args: &[Cow<str>],
@ -1332,10 +1380,10 @@ fn lsp_workspace_command(
let callback = async move { let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new( let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| { 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()); execute_lsp_command(cx.editor, command.clone());
}); });
compositor.push(Box::new(overlayed(picker))) compositor.push(Box::new(overlaid(picker)))
}, },
)); ));
Ok(call) Ok(call)
@ -1371,13 +1419,19 @@ fn lsp_restart(
return Ok(()); return Ok(());
} }
let editor_config = cx.editor.config.load();
let (_view, doc) = current!(cx.editor); let (_view, doc) = current!(cx.editor);
let config = doc let config = doc
.language_config() .language_config()
.context("LSP not defined for the current document")?; .context("LSP not defined for the current document")?;
let scope = config.scope.clone(); let scope = config.scope.clone();
cx.editor.language_servers.restart(config, doc.path())?; cx.editor.language_servers.restart(
config,
doc.path(),
&editor_config.workspace_lsp_roots,
editor_config.lsp.snippets,
)?;
// This collect is needed because refresh_language_server would need to re-borrow editor. // This collect is needed because refresh_language_server would need to re-borrow editor.
let document_ids_to_refresh: Vec<DocumentId> = cx let document_ids_to_refresh: Vec<DocumentId> = cx
@ -1970,6 +2024,20 @@ fn open_config(
Ok(()) Ok(())
} }
fn open_workspace_config(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
cx.editor
.open(&helix_loader::workspace_config_file(), Action::Replace)?;
Ok(())
}
fn open_log( fn open_log(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[Cow<str>], _args: &[Cow<str>],
@ -2105,20 +2173,16 @@ fn reset_diff_change(
let scrolloff = editor.config().scrolloff; let scrolloff = editor.config().scrolloff;
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
// TODO refactor to use let..else once MSRV is raised to 1.65 let Some(handle) = doc.diff_handle() else {
let handle = match doc.diff_handle() { bail!("Diff is not available in the current buffer")
Some(handle) => handle,
None => bail!("Diff is not available in the current buffer"),
}; };
let diff = handle.load(); let diff = handle.load();
let doc_text = doc.text().slice(..); let doc_text = doc.text().slice(..);
let line = doc.selection(view.id).primary().cursor_line(doc_text); let line = doc.selection(view.id).primary().cursor_line(doc_text);
// TODO refactor to use let..else once MSRV is raised to 1.65 let Some(hunk_idx) = diff.hunk_at(line as u32, true) else {
let hunk_idx = match diff.hunk_at(line as u32, true) { bail!("There is no change at the cursor")
Some(hunk_idx) => hunk_idx,
None => bail!("There is no change at the cursor"),
}; };
let hunk = diff.nth_hunk(hunk_idx); let hunk = diff.nth_hunk(hunk_idx);
let diff_base = diff.diff_base(); let diff_base = diff.diff_base();
@ -2213,6 +2277,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: force_buffer_close_all, fun: force_buffer_close_all,
signature: CommandSignature::none(), 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 { TypableCommand {
name: "buffer-next", name: "buffer-next",
aliases: &["bn", "bnext"], aliases: &["bn", "bnext"],
@ -2358,6 +2429,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: theme, fun: theme,
signature: CommandSignature::positional(&[completers::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 { TypableCommand {
name: "clipboard-yank", name: "clipboard-yank",
aliases: &[], aliases: &[],
@ -2646,6 +2724,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: open_config, fun: open_config,
signature: CommandSignature::none(), signature: CommandSignature::none(),
}, },
TypableCommand {
name: "config-open-workspace",
aliases: &[],
doc: "Open the workspace config.toml file.",
fun: open_workspace_config,
signature: CommandSignature::none(),
},
TypableCommand { TypableCommand {
name: "log-open", name: "log-open",
aliases: &[], aliases: &[],

@ -34,6 +34,48 @@ impl<'a> Context<'a> {
tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?; tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?;
Ok(()) 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 { pub trait Component: Any + AnyComponent {
@ -72,6 +114,21 @@ pub trait Component: Any + AnyComponent {
fn id(&self) -> Option<&'static str> { fn id(&self) -> Option<&'static str> {
None 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 { pub struct Compositor {

@ -1,27 +1,37 @@
use crate::keymap::{default::default, merge_keys, Keymap}; use crate::keymap;
use crate::keymap::{merge_keys, Keymap};
use helix_loader::merge_toml_values;
use helix_view::document::Mode; use helix_view::document::Mode;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::fs;
use std::io::Error as IOError; use std::io::Error as IOError;
use std::path::PathBuf;
use toml::de::Error as TomlError; use toml::de::Error as TomlError;
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Config { pub struct Config {
pub theme: Option<String>, pub theme: Option<String>,
#[serde(default = "default")] pub icons: Option<String>,
pub keys: HashMap<Mode, Keymap>, pub keys: HashMap<Mode, Keymap>,
#[serde(default)]
pub editor: helix_view::editor::Config, pub editor: helix_view::editor::Config,
} }
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[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>,
}
impl Default for Config { impl Default for Config {
fn default() -> Config { fn default() -> Config {
Config { Config {
theme: None, theme: None,
keys: default(), icons: None,
keys: keymap::default(),
editor: helix_view::editor::Config::default(), editor: helix_view::editor::Config::default(),
} }
} }
@ -33,6 +43,12 @@ pub enum ConfigLoadError {
Error(IOError), Error(IOError),
} }
impl Default for ConfigLoadError {
fn default() -> Self {
ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder"))
}
}
impl Display for ConfigLoadError { impl Display for ConfigLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -43,17 +59,74 @@ impl Display for ConfigLoadError {
} }
impl Config { impl Config {
pub fn load(config_path: PathBuf) -> Result<Config, ConfigLoadError> { pub fn load(
match std::fs::read_to_string(config_path) { global: Result<String, ConfigLoadError>,
Ok(config) => toml::from_str(&config) local: Result<String, ConfigLoadError>,
.map(merge_keys) ) -> Result<Config, ConfigLoadError> {
.map_err(ConfigLoadError::BadConfig), let global_config: Result<ConfigRaw, ConfigLoadError> =
Err(err) => Err(ConfigLoadError::Error(err)), global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
let local_config: Result<ConfigRaw, ConfigLoadError> =
local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
let res = match (global_config, local_config) {
(Ok(global), Ok(local)) => {
let mut keys = keymap::default();
if let Some(global_keys) = global.keys {
merge_keys(&mut keys, global_keys)
}
if let Some(local_keys) = local.keys {
merge_keys(&mut keys, local_keys)
}
let editor = match (global.editor, local.editor) {
(None, None) => helix_view::editor::Config::default(),
(None, Some(val)) | (Some(val), None) => {
val.try_into().map_err(ConfigLoadError::BadConfig)?
}
(Some(global), Some(local)) => merge_toml_values(global, local, 3)
.try_into()
.map_err(ConfigLoadError::BadConfig)?,
};
Config {
theme: local.theme.or(global.theme),
icons: local.icons.or(global.icons),
keys,
editor,
} }
} }
// if any configs are invalid return that first
(_, Err(ConfigLoadError::BadConfig(err)))
| (Err(ConfigLoadError::BadConfig(err)), _) => {
return Err(ConfigLoadError::BadConfig(err))
}
(Ok(config), Err(_)) | (Err(_), Ok(config)) => {
let mut keys = keymap::default();
if let Some(keymap) = config.keys {
merge_keys(&mut keys, keymap);
}
Config {
theme: config.theme,
icons: config.icons,
keys,
editor: config.editor.map_or_else(
|| Ok(helix_view::editor::Config::default()),
|val| val.try_into().map_err(ConfigLoadError::BadConfig),
)?,
}
}
// these are just two io errors return the one for the global config
(Err(err), Err(_)) => return Err(err),
};
Ok(res)
}
pub fn load_default() -> Result<Config, ConfigLoadError> { pub fn load_default() -> Result<Config, ConfigLoadError> {
Config::load(helix_loader::config_file()) let global_config =
fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error);
let local_config = fs::read_to_string(helix_loader::workspace_config_file())
.map_err(ConfigLoadError::Error);
Config::load(global_config, local_config)
} }
} }
@ -61,6 +134,12 @@ impl Config {
mod tests { mod tests {
use super::*; use super::*;
impl Config {
fn load_test(config: &str) -> Config {
Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap()
}
}
#[test] #[test]
fn parsing_keymaps_config_file() { fn parsing_keymaps_config_file() {
use crate::keymap; use crate::keymap;
@ -77,10 +156,10 @@ mod tests {
A-F12 = "move_next_word_end" A-F12 = "move_next_word_end"
"#; "#;
assert_eq!( let mut keys = keymap::default();
toml::from_str::<Config>(sample_keymaps).unwrap(), merge_keys(
Config { &mut keys,
keys: hashmap! { hashmap! {
Mode::Insert => Keymap::new(keymap!({ "Insert mode" Mode::Insert => Keymap::new(keymap!({ "Insert mode"
"y" => move_line_down, "y" => move_line_down,
"S-C-a" => delete_selection, "S-C-a" => delete_selection,
@ -89,6 +168,12 @@ mod tests {
"A-F12" => move_next_word_end, "A-F12" => move_next_word_end,
})), })),
}, },
);
assert_eq!(
Config::load_test(sample_keymaps),
Config {
keys,
..Default::default() ..Default::default()
} }
); );
@ -97,11 +182,11 @@ mod tests {
#[test] #[test]
fn keys_resolve_to_correct_defaults() { fn keys_resolve_to_correct_defaults() {
// From serde default // From serde default
let default_keys = toml::from_str::<Config>("").unwrap().keys; let default_keys = Config::load_test("").keys;
assert_eq!(default_keys, default()); assert_eq!(default_keys, keymap::default());
// From the Default trait // From the Default trait
let default_keys = Config::default().keys; let default_keys = Config::default().keys;
assert_eq!(default_keys, default()); assert_eq!(default_keys, keymap::default());
} }
} }

@ -2,7 +2,6 @@ pub mod default;
pub mod macros; pub mod macros;
pub use crate::commands::MappableCommand; pub use crate::commands::MappableCommand;
use crate::config::Config;
use arc_swap::{ use arc_swap::{
access::{DynAccess, DynGuard}, access::{DynAccess, DynGuard},
ArcSwap, ArcSwap,
@ -16,7 +15,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use default::default; pub use default::default;
use macros::key; use macros::key;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -417,12 +416,10 @@ impl Default for Keymaps {
} }
/// Merge default config keys with user overwritten keys for custom user config. /// Merge default config keys with user overwritten keys for custom user config.
pub fn merge_keys(mut config: Config) -> Config { pub fn merge_keys(dst: &mut HashMap<Mode, Keymap>, mut delta: HashMap<Mode, Keymap>) {
let mut delta = std::mem::replace(&mut config.keys, default()); for (mode, keys) in dst {
for (mode, keys) in &mut config.keys {
keys.merge(delta.remove(mode).unwrap_or_default()) keys.merge(delta.remove(mode).unwrap_or_default())
} }
config
} }
#[cfg(test)] #[cfg(test)]
@ -449,8 +446,7 @@ mod tests {
#[test] #[test]
fn merge_partial_keys() { fn merge_partial_keys() {
let config = Config { let keymap = hashmap! {
keys: hashmap! {
Mode::Normal => Keymap::new( Mode::Normal => Keymap::new(
keymap!({ "Normal mode" keymap!({ "Normal mode"
"i" => normal_mode, "i" => normal_mode,
@ -462,13 +458,12 @@ mod tests {
}, },
}) })
) )
},
..Default::default()
}; };
let mut merged_config = merge_keys(config.clone()); let mut merged_keyamp = default();
assert_ne!(config, merged_config); merge_keys(&mut merged_keyamp, keymap.clone());
assert_ne!(keymap, merged_keyamp);
let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone())));
assert_eq!( assert_eq!(
keymap.get(Mode::Normal, key!('i')), keymap.get(Mode::Normal, key!('i')),
KeymapResult::Matched(MappableCommand::normal_mode), KeymapResult::Matched(MappableCommand::normal_mode),
@ -486,7 +481,7 @@ mod tests {
"Leaf should replace node" "Leaf should replace node"
); );
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap // Assumes that `g` is a node in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(), keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
@ -506,14 +501,13 @@ mod tests {
"Old leaves in subnode should be present in merged node" "Old leaves in subnode should be present in merged node"
); );
assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0);
} }
#[test] #[test]
fn order_should_be_set() { fn order_should_be_set() {
let config = Config { let keymap = hashmap! {
keys: hashmap! {
Mode::Normal => Keymap::new( Mode::Normal => Keymap::new(
keymap!({ "Normal mode" keymap!({ "Normal mode"
"space" => { "" "space" => { ""
@ -524,12 +518,11 @@ mod tests {
}, },
}) })
) )
},
..Default::default()
}; };
let mut merged_config = merge_keys(config.clone()); let mut merged_keyamp = default();
assert_ne!(config, merged_config); merge_keys(&mut merged_keyamp, keymap.clone());
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); assert_ne!(keymap, merged_keyamp);
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works // Make sure mapping works
assert_eq!( assert_eq!(
keymap keymap

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

@ -3,7 +3,7 @@ use crossterm::event::EventStream;
use helix_loader::VERSION_AND_GIT_HASH; use helix_loader::VERSION_AND_GIT_HASH;
use helix_term::application::Application; use helix_term::application::Application;
use helix_term::args::Args; use helix_term::args::Args;
use helix_term::config::Config; use helix_term::config::{Config, ConfigLoadError};
use std::path::PathBuf; use std::path::PathBuf;
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@ -126,18 +126,19 @@ FLAGS:
helix_loader::initialize_config_file(args.config_file.clone()); helix_loader::initialize_config_file(args.config_file.clone());
let config = match std::fs::read_to_string(helix_loader::config_file()) { let config = match Config::load_default() {
Ok(config) => toml::from_str(&config) Ok(config) => config,
.map(helix_term::keymap::merge_keys) Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => {
.unwrap_or_else(|err| { Config::default()
}
Err(ConfigLoadError::Error(err)) => return Err(Error::new(err)),
Err(ConfigLoadError::BadConfig(err)) => {
eprintln!("Bad config: {}", err); eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config"); eprintln!("Press <ENTER> to continue with default config");
use std::io::Read; use std::io::Read;
let _ = std::io::stdin().read(&mut []); let _ = std::io::stdin().read(&mut []);
Config::default() Config::default()
}), }
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
}; };
let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| { let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {

@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::{ use helix_view::{
document::SavePoint, document::SavePoint,
editor::CompleteAction, editor::CompleteAction,
icons::Icons,
theme::{Modifier, Style}, theme::{Modifier, Style},
ViewId, ViewId,
}; };
@ -33,7 +34,8 @@ impl menu::Item for CompletionItem {
.into() .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() let deprecated = self.deprecated.unwrap_or_default()
|| self.tags.as_ref().map_or(false, |tags| { || self.tags.as_ref().map_or(false, |tags| {
tags.contains(&lsp::CompletionItemTag::DEPRECATED) tags.contains(&lsp::CompletionItemTag::DEPRECATED)
@ -141,17 +143,13 @@ impl Completion {
} }
}; };
let start_offset = let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
match util::lsp_pos_to_pos(doc.text(), edit.range.start, offset_encoding) { return Transaction::new(doc.text());
Some(start) => start as i128 - primary_cursor as i128,
None => return Transaction::new(doc.text()),
};
let end_offset =
match util::lsp_pos_to_pos(doc.text(), edit.range.end, offset_encoding) {
Some(end) => end as i128 - primary_cursor as i128,
None => return Transaction::new(doc.text()),
}; };
let start_offset = range.anchor as i128 - primary_cursor as i128;
let end_offset = range.head as i128 - primary_cursor as i128;
(Some((start_offset, end_offset)), edit.new_text) (Some((start_offset, end_offset)), edit.new_text)
} else { } else {
let new_text = item let new_text = item
@ -414,6 +412,10 @@ impl Completion {
true true
} }
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
self.popup.area(viewport, editor)
}
} }
impl Component for Completion { impl Component for Completion {
@ -481,7 +483,7 @@ impl Component for Completion {
}; };
let popup_area = { let popup_area = {
let (popup_x, popup_y) = self.popup.get_rel_position(area, cx); let (popup_x, popup_y) = self.popup.get_rel_position(area, cx.editor);
let (popup_width, popup_height) = self.popup.get_size(); let (popup_width, popup_height) = self.popup.get_size();
Rect::new(popup_x, popup_y, popup_width, popup_height) Rect::new(popup_x, popup_y, popup_width, popup_height)
}; };

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

@ -6,7 +6,7 @@ use crate::{
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, Keymaps},
ui::{ ui::{
document::{render_document, LinePos, TextRenderer, TranslatedPosition}, document::{render_document, LinePos, TextRenderer, TranslatedPosition},
Completion, ProgressSpinners, Completion, Explorer, ProgressSpinners,
}, },
}; };
@ -23,7 +23,7 @@ use helix_core::{
}; };
use helix_view::{ use helix_view::{
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig}, editor::{CompleteAction, CursorShapeConfig, ExplorerPosition},
graphics::{Color, CursorKind, Modifier, Rect, Style}, graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers}, keyboard::{KeyCode, KeyModifiers},
@ -43,6 +43,7 @@ pub struct EditorView {
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>), pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>, pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
pub(crate) explorer: Option<Explorer>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -68,6 +69,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()), last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
explorer: None,
} }
} }
@ -93,47 +95,30 @@ impl EditorView {
let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new(); let mut line_decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
let mut translated_positions: Vec<TranslatedPosition> = Vec::new(); let mut translated_positions: Vec<TranslatedPosition> = Vec::new();
// DAP: Highlight current stack frame position if is_focused && config.cursorline {
let stack_frame = editor.debugger.as_ref().and_then(|debugger| { line_decorations.push(Self::cursorline_decorator(doc, view, theme))
if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
debugger
.stack_frames
.get(&thread_id)
.and_then(|bt| bt.get(frame))
} else {
None
} }
});
if let Some(frame) = stack_frame { if is_focused && config.cursorcolumn {
if doc.path().is_some() Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations);
&& frame }
.source
.as_ref() // Set DAP highlights, if needed.
.and_then(|source| source.path.as_ref()) if let Some(frame) = editor.current_stack_frame() {
== doc.path() let dap_line = frame.line.saturating_sub(1) as usize;
{ let style = theme.get("ui.highlight.frameline");
let line = frame.line - 1; // convert to 0-indexing
let style = theme.get("ui.highlight");
let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
if pos.doc_line != line { if pos.doc_line != dap_line {
return; return;
} }
renderer renderer.surface.set_style(
.surface Rect::new(inner.x, inner.y + pos.visual_line, inner.width, 1),
.set_style(Rect::new(area.x, pos.visual_line, area.width, 1), style); style,
);
}; };
line_decorations.push(Box::new(line_decoration)); line_decorations.push(Box::new(line_decoration));
} }
}
if is_focused && config.cursorline {
line_decorations.push(Self::cursorline_decorator(doc, view, theme))
}
if is_focused && config.cursorcolumn {
Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations);
}
let mut highlights = let mut highlights =
Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme); Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme);
@ -422,6 +407,7 @@ impl EditorView {
let primary_selection_scope = theme let primary_selection_scope = theme
.find_scope_index_exact("ui.selection.primary") .find_scope_index_exact("ui.selection.primary")
.unwrap_or(selection_scope); .unwrap_or(selection_scope);
let base_cursor_scope = theme let base_cursor_scope = theme
.find_scope_index_exact("ui.cursor") .find_scope_index_exact("ui.cursor")
.unwrap_or(selection_scope); .unwrap_or(selection_scope);
@ -547,8 +533,24 @@ impl EditorView {
let mut x = viewport.x; let mut x = viewport.x;
let current_doc = view!(editor).doc; let current_doc = view!(editor).doc;
let config = editor.config();
let icons_enabled = config.icons.bufferline;
for doc in editor.documents() { 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 let fname = doc
.path() .path()
.unwrap_or(&scratch) .unwrap_or(&scratch)
@ -567,6 +569,22 @@ impl EditorView {
let used_width = viewport.x.saturating_sub(x); let used_width = viewport.x.saturating_sub(x);
let rem_width = surface.area.width.saturating_sub(used_width); 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 x = surface
.set_stringn(x, viewport.y, text, rem_width as usize, style) .set_stringn(x, viewport.y, text, rem_width as usize, style)
.0; .0;
@ -968,7 +986,7 @@ impl EditorView {
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
size: Rect, size: Rect,
) { ) -> Option<Rect> {
let mut completion = Completion::new( let mut completion = Completion::new(
editor, editor,
savepoint, savepoint,
@ -980,15 +998,17 @@ impl EditorView {
if completion.is_empty() { if completion.is_empty() {
// skip if we got no completion results // skip if we got no completion results
return; return None;
} }
let area = completion.area(size, editor);
editor.last_completion = None; editor.last_completion = None;
self.last_insert.1.push(InsertEvent::TriggerCompletion); self.last_insert.1.push(InsertEvent::TriggerCompletion);
// TODO : propagate required size on resize to completion too // TODO : propagate required size on resize to completion too
completion.required_size((size.width, size.height)); completion.required_size((size.width, size.height));
self.completion = Some(completion); self.completion = Some(completion);
Some(area)
} }
pub fn clear_completion(&mut self, editor: &mut Editor) { pub fn clear_completion(&mut self, editor: &mut Editor) {
@ -1038,9 +1058,14 @@ impl EditorView {
.. ..
} = *event; } = *event;
let pos_and_view = |editor: &Editor, row, column| { let pos_and_view = |editor: &Editor, row, column, ignore_virtual_text| {
editor.tree.views().find_map(|(view, _focus)| { editor.tree.views().find_map(|(view, _focus)| {
view.pos_at_screen_coords(&editor.documents[&view.doc], row, column, true) view.pos_at_screen_coords(
&editor.documents[&view.doc],
row,
column,
ignore_virtual_text,
)
.map(|pos| (pos, view.id)) .map(|pos| (pos, view.id))
}) })
}; };
@ -1056,7 +1081,7 @@ impl EditorView {
MouseEventKind::Down(MouseButton::Left) => { MouseEventKind::Down(MouseButton::Left) => {
let editor = &mut cxt.editor; let editor = &mut cxt.editor;
if let Some((pos, view_id)) = pos_and_view(editor, row, column) { if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
let doc = doc_mut!(editor, &view!(editor, view_id).doc); let doc = doc_mut!(editor, &view!(editor, view_id).doc);
if modifiers == KeyModifiers::ALT { if modifiers == KeyModifiers::ALT {
@ -1120,7 +1145,7 @@ impl EditorView {
_ => unreachable!(), _ => unreachable!(),
}; };
match pos_and_view(cxt.editor, row, column) { match pos_and_view(cxt.editor, row, column, false) {
Some((_, view_id)) => cxt.editor.tree.focus = view_id, Some((_, view_id)) => cxt.editor.tree.focus = view_id,
None => return EventResult::Ignored(None), None => return EventResult::Ignored(None),
} }
@ -1191,7 +1216,7 @@ impl EditorView {
return EventResult::Consumed(None); return EventResult::Consumed(None);
} }
if let Some((pos, view_id)) = pos_and_view(editor, row, column) { if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
let doc = doc_mut!(editor, &view!(editor, view_id).doc); let doc = doc_mut!(editor, &view!(editor, view_id).doc);
doc.set_selection(view_id, Selection::point(pos)); doc.set_selection(view_id, Selection::point(pos));
cxt.editor.focus(view_id); cxt.editor.focus(view_id);
@ -1214,6 +1239,11 @@ impl Component for EditorView {
event: &Event, event: &Event,
context: &mut crate::compositor::Context, context: &mut crate::compositor::Context,
) -> EventResult { ) -> 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 { let mut cx = commands::Context {
editor: context.editor, editor: context.editor,
count: None, count: None,
@ -1267,13 +1297,15 @@ impl Component for EditorView {
// let completion swallow the event if necessary // let completion swallow the event if necessary
let mut consumed = false; let mut consumed = false;
if let Some(completion) = &mut self.completion { if let Some(completion) = &mut self.completion {
let res = {
// use a fake context here // use a fake context here
let mut cx = Context { let mut cx = Context {
editor: cx.editor, editor: cx.editor,
jobs: cx.jobs, jobs: cx.jobs,
scroll: None, scroll: None,
}; };
let res = completion.handle_event(event, &mut cx); completion.handle_event(event, &mut cx)
};
if let EventResult::Consumed(callback) = res { if let EventResult::Consumed(callback) = res {
consumed = true; consumed = true;
@ -1281,6 +1313,12 @@ impl Component for EditorView {
if callback.is_some() { if callback.is_some() {
// assume close_fn // assume close_fn
self.clear_completion(cx.editor); self.clear_completion(cx.editor);
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
commands::signature_help_impl(
&mut cx,
commands::SignatureHelpInvoked::Automatic,
);
} }
} }
} }
@ -1362,6 +1400,8 @@ impl Component for EditorView {
surface.set_style(area, cx.editor.theme.get("ui.background")); surface.set_style(area, cx.editor.theme.get("ui.background"));
let config = cx.editor.config(); let config = cx.editor.config();
let editor_area = area.clip_bottom(1);
// check if bufferline should be rendered // check if bufferline should be rendered
use helix_view::editor::BufferLine; use helix_view::editor::BufferLine;
let use_bufferline = match config.bufferline { let use_bufferline = match config.bufferline {
@ -1370,15 +1410,43 @@ impl Component for EditorView {
_ => false, _ => false,
}; };
// -1 for commandline and -1 for bufferline let editor_area = if use_bufferline {
let mut editor_area = area.clip_bottom(1); editor_area.clip_top(1)
if use_bufferline { } else {
editor_area = editor_area.clip_top(1); 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 // if the terminal size suddenly changed, we need to trigger a resize
cx.editor.resize(editor_area); 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 { if use_bufferline {
Self::render_bufferline(cx.editor, area.with_height(1), surface); Self::render_bufferline(cx.editor, area.with_height(1), surface);
} }
@ -1457,9 +1525,47 @@ impl Component for EditorView {
if let Some(completion) = self.completion.as_mut() { if let Some(completion) = self.completion.as_mut() {
completion.render(area, surface, cx); 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) { 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() { match editor.cursor() {
// All block cursors are drawn manually // All block cursors are drawn manually
(pos, CursorKind::Block) => (pos, CursorKind::Hidden), (pos, CursorKind::Block) => (pos, CursorKind::Hidden),

File diff suppressed because it is too large Load Diff

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

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

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

@ -1,6 +1,7 @@
mod completion; mod completion;
mod document; mod document;
pub(crate) mod editor; pub(crate) mod editor;
mod explorer;
mod fuzzy_match; mod fuzzy_match;
mod info; mod info;
pub mod lsp; pub mod lsp;
@ -13,12 +14,14 @@ mod prompt;
mod spinner; mod spinner;
mod statusline; mod statusline;
mod text; mod text;
mod tree;
use crate::compositor::{Component, Compositor}; use crate::compositor::{Component, Compositor};
use crate::filter_picker_entry; use crate::filter_picker_entry;
use crate::job::{self, Callback}; use crate::job::{self, Callback};
pub use completion::Completion; pub use completion::Completion;
pub use editor::EditorView; pub use editor::EditorView;
use helix_view::icons::Icons;
pub use markdown::Markdown; pub use markdown::Markdown;
pub use menu::Menu; pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
@ -26,7 +29,9 @@ pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent}; pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner}; pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text; pub use text::Text;
pub use tree::{TreeOp, TreeView, TreeViewItem};
pub use explorer::Explorer;
use helix_core::regex::Regex; use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder; use helix_core::regex::RegexBuilder;
use helix_view::Editor; use helix_view::Editor;
@ -158,7 +163,11 @@ pub fn regex_prompt(
cx.push_layer(Box::new(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 ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant; use std::time::Instant;
@ -220,6 +229,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
FilePicker::new( FilePicker::new(
files, files,
root, root,
config.icons.picker.then_some(icons),
move |cx, path: &PathBuf, action| { move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) { if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() { 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::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme;
use helix_view::{editor::Config, Editor}; use helix_view::{editor::Config, Editor};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::borrow::Cow; use std::borrow::Cow;
@ -280,9 +290,9 @@ pub mod completers {
} }
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> { 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() { 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("default".into());
names.push("base16_default".into()); names.push("base16_default".into());
@ -311,6 +321,37 @@ pub mod completers {
names 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 /// 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>) { fn get_keys(value: &serde_json::Value, vec: &mut Vec<String>, scope: Option<&str>) {
if let Some(map) = value.as_object() { if let Some(map) = value.as_object() {

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

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

@ -6,7 +6,10 @@ use crate::{
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use helix_core::Position; use helix_core::Position;
use helix_view::graphics::{Margin, Rect}; use helix_view::{
graphics::{Margin, Rect},
Editor,
};
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
// a width/height hint. maybe Popup(Box<Component>) // a width/height hint. maybe Popup(Box<Component>)
@ -88,10 +91,10 @@ impl<T: Component> Popup<T> {
/// Calculate the position where the popup should be rendered and return the coordinates of the /// Calculate the position where the popup should be rendered and return the coordinates of the
/// top left corner. /// top left corner.
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { pub fn get_rel_position(&mut self, viewport: Rect, editor: &Editor) -> (u16, u16) {
let position = self let position = self
.position .position
.get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); .get_or_insert_with(|| editor.cursor().0.unwrap_or_default());
let (width, height) = self.size; let (width, height) = self.size;
@ -155,6 +158,16 @@ impl<T: Component> Popup<T> {
pub fn contents_mut(&mut self) -> &mut T { pub fn contents_mut(&mut self) -> &mut T {
&mut self.contents &mut self.contents
} }
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
// trigger required_size so we recalculate if the child changed
self.required_size((viewport.width, viewport.height));
let (rel_x, rel_y) = self.get_rel_position(viewport, editor);
// clip to viewport
viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1))
}
} }
impl<T: Component> Component for Popup<T> { impl<T: Component> Component for Popup<T> {
@ -232,16 +245,9 @@ impl<T: Component> Component for Popup<T> {
} }
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
// trigger required_size so we recalculate if the child changed let area = self.area(viewport, cx.editor);
self.required_size((viewport.width, viewport.height));
cx.scroll = Some(self.scroll); cx.scroll = Some(self.scroll);
let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
// clip to viewport
let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
// clear area // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background); surface.clear_with(area, background);

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

@ -4,6 +4,7 @@ use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::{ use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME}, document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect, graphics::Rect,
icons::Icon,
theme::Style, theme::Style,
Document, Editor, View, Document, Editor, View,
}; };
@ -21,6 +22,7 @@ pub struct RenderContext<'a> {
pub focused: bool, pub focused: bool,
pub spinners: &'a ProgressSpinners, pub spinners: &'a ProgressSpinners,
pub parts: RenderBuffer<'a>, pub parts: RenderBuffer<'a>,
pub icons: RenderContextIcons<'a>,
} }
impl<'a> RenderContext<'a> { impl<'a> RenderContext<'a> {
@ -31,6 +33,25 @@ impl<'a> RenderContext<'a> {
focused: bool, focused: bool,
spinners: &'a ProgressSpinners, spinners: &'a ProgressSpinners,
) -> Self { ) -> 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 { RenderContext {
editor, editor,
doc, doc,
@ -38,10 +59,21 @@ impl<'a> RenderContext<'a> {
focused, focused,
spinners, spinners,
parts: RenderBuffer::default(), 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)] #[derive(Default)]
pub struct RenderBuffer<'a> { pub struct RenderBuffer<'a> {
pub left: Spans<'a>, pub left: Spans<'a>,
@ -148,6 +180,7 @@ where
helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding, helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending, helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type, 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::Diagnostics => render_diagnostics,
helix_view::editor::StatusLineElement::WorkspaceDiagnostics => render_workspace_diagnostics, helix_view::editor::StatusLineElement::WorkspaceDiagnostics => render_workspace_diagnostics,
helix_view::editor::StatusLineElement::Selections => render_selections, helix_view::editor::StatusLineElement::Selections => render_selections,
@ -240,7 +273,13 @@ where
if warnings > 0 { if warnings > 0 {
write( write(
context, context,
"●".to_string(), context
.editor
.icons
.diagnostic
.warning
.icon_char
.to_string(),
Some(context.editor.theme.get("warning")), Some(context.editor.theme.get("warning")),
); );
write(context, format!(" {} ", warnings), None); write(context, format!(" {} ", warnings), None);
@ -249,7 +288,7 @@ where
if errors > 0 { if errors > 0 {
write( write(
context, context,
"●".to_string(), context.editor.icons.diagnostic.error.icon_char.to_string(),
Some(context.editor.theme.get("error")), Some(context.editor.theme.get("error")),
); );
write(context, format!(" {} ", errors), None); write(context, format!(" {} ", errors), None);
@ -282,7 +321,13 @@ where
if warnings > 0 { if warnings > 0 {
write( write(
context, context,
"●".to_string(), context
.editor
.icons
.diagnostic
.warning
.icon_char
.to_string(),
Some(context.editor.theme.get("warning")), Some(context.editor.theme.get("warning")),
); );
write(context, format!(" {} ", warnings), None); write(context, format!(" {} ", warnings), None);
@ -291,7 +336,7 @@ where
if errors > 0 { if errors > 0 {
write( write(
context, context,
"●".to_string(), context.editor.icons.diagnostic.error.icon_char.to_string(),
Some(context.editor.theme.get("error")), Some(context.editor.theme.get("error")),
); );
write(context, format!(" {} ", errors), None); write(context, format!(" {} ", errors), None);
@ -412,6 +457,21 @@ where
write(context, format!(" {} ", file_type), None); 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) fn render_file_name<F>(context: &mut RenderContext, write: F)
where where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
@ -482,11 +542,12 @@ fn render_version_control<F>(context: &mut RenderContext, write: F)
where where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{ {
let head = context let head = context.doc.version_control_head().unwrap_or_default();
.doc
.version_control_head()
.unwrap_or_default()
.to_string();
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

@ -2,8 +2,6 @@
mod test { mod test {
mod helpers; mod helpers;
use std::path::PathBuf;
use helix_core::{syntax::AutoPairConfig, Selection}; use helix_core::{syntax::AutoPairConfig, Selection};
use helix_term::config::Config; use helix_term::config::Config;

@ -3,7 +3,7 @@ use std::{
ops::RangeInclusive, ops::RangeInclusive,
}; };
use helix_core::diagnostic::Severity; use helix_core::{diagnostic::Severity, path::get_normalized_path};
use helix_view::doc; use helix_view::doc;
use super::*; use super::*;
@ -23,7 +23,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
assert_eq!(1, docs.len()); assert_eq!(1, docs.len());
let doc = docs.pop().unwrap(); let doc = docs.pop().unwrap();
assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path)); assert_eq!(Some(&get_normalized_path(file.path())), doc.path());
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
}), }),
false, false,
@ -269,7 +269,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> {
assert_eq!(1, docs.len()); assert_eq!(1, docs.len());
let doc = docs.pop().unwrap(); let doc = docs.pop().unwrap();
assert_eq!(Some(&file.path().to_path_buf()), doc.path()); assert_eq!(Some(&get_normalized_path(file.path())), doc.path());
}), }),
false, false,
) )
@ -341,7 +341,7 @@ async fn test_write_new_path() -> anyhow::Result<()> {
Some(&|app| { Some(&|app| {
let doc = doc!(app.editor); let doc = doc!(app.editor);
assert!(!app.editor.is_err()); assert!(!app.editor.is_err());
assert_eq!(file1.path(), doc.path().unwrap()); assert_eq!(&get_normalized_path(file1.path()), doc.path().unwrap());
}), }),
), ),
( (
@ -349,7 +349,7 @@ async fn test_write_new_path() -> anyhow::Result<()> {
Some(&|app| { Some(&|app| {
let doc = doc!(app.editor); let doc = doc!(app.editor);
assert!(!app.editor.is_err()); assert!(!app.editor.is_err());
assert_eq!(file2.path(), doc.path().unwrap()); assert_eq!(&get_normalized_path(file2.path()), doc.path().unwrap());
assert!(app.editor.document_by_path(file1.path()).is_none()); assert!(app.editor.document_by_path(file1.path()).is_none());
}), }),
), ),

@ -1,6 +1,7 @@
use std::{ use std::{
fs::File, fs::File,
io::{Read, Write}, io::{Read, Write},
mem::replace,
path::PathBuf, path::PathBuf,
time::Duration, time::Duration,
}; };
@ -222,10 +223,11 @@ pub fn temp_file_with_contents<S: AsRef<str>>(
/// Generates a config with defaults more suitable for integration tests /// Generates a config with defaults more suitable for integration tests
pub fn test_config() -> Config { pub fn test_config() -> Config {
merge_keys(Config { Config {
editor: test_editor_config(), editor: test_editor_config(),
keys: helix_term::keymap::default(),
..Default::default() ..Default::default()
}) }
} }
pub fn test_editor_config() -> helix_view::editor::Config { pub fn test_editor_config() -> helix_view::editor::Config {
@ -300,8 +302,10 @@ impl AppBuilder {
// Remove this attribute once `with_config` is used in a test: // Remove this attribute once `with_config` is used in a test:
#[allow(dead_code)] #[allow(dead_code)]
pub fn with_config(mut self, config: Config) -> Self { pub fn with_config(mut self, mut config: Config) -> Self {
self.config = helix_term::keymap::merge_keys(config); let keys = replace(&mut config.keys, helix_term::keymap::default());
merge_keys(&mut config.keys, keys);
self.config = config;
self self
} }

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

@ -1,5 +1,7 @@
use super::*; use super::*;
use helix_core::path::get_normalized_path;
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_split_write_quit_all() -> anyhow::Result<()> { async fn test_split_write_quit_all() -> anyhow::Result<()> {
let mut file1 = tempfile::NamedTempFile::new()?; let mut file1 = tempfile::NamedTempFile::new()?;
@ -25,21 +27,21 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> {
let doc1 = docs let doc1 = docs
.iter() .iter()
.find(|doc| doc.path().unwrap() == file1.path()) .find(|doc| doc.path().unwrap() == &get_normalized_path(file1.path()))
.unwrap(); .unwrap();
assert_eq!("hello1", doc1.text().to_string()); assert_eq!("hello1", doc1.text().to_string());
let doc2 = docs let doc2 = docs
.iter() .iter()
.find(|doc| doc.path().unwrap() == file2.path()) .find(|doc| doc.path().unwrap() == &get_normalized_path(file2.path()))
.unwrap(); .unwrap();
assert_eq!("hello2", doc2.text().to_string()); assert_eq!("hello2", doc2.text().to_string());
let doc3 = docs let doc3 = docs
.iter() .iter()
.find(|doc| doc.path().unwrap() == file3.path()) .find(|doc| doc.path().unwrap() == &get_normalized_path(file3.path()))
.unwrap(); .unwrap();
assert_eq!("hello3", doc3.text().to_string()); assert_eq!("hello3", doc3.text().to_string());

@ -78,21 +78,20 @@ where
} }
#[inline] #[inline]
fn supports_keyboard_enhancement_protocol(&self) -> io::Result<bool> { fn supports_keyboard_enhancement_protocol(&self) -> bool {
self.supports_keyboard_enhancement_protocol *self.supports_keyboard_enhancement_protocol
.get_or_try_init(|| { .get_or_init(|| {
use std::time::Instant; use std::time::Instant;
let now = Instant::now(); let now = Instant::now();
let support = terminal::supports_keyboard_enhancement(); let supported = matches!(terminal::supports_keyboard_enhancement(), Ok(true));
log::debug!( log::debug!(
"The keyboard enhancement protocol is {}supported in this terminal (checked in {:?})", "The keyboard enhancement protocol is {}supported in this terminal (checked in {:?})",
if matches!(support, Ok(true)) { "" } else { "not " }, if supported { "" } else { "not " },
Instant::now().duration_since(now) Instant::now().duration_since(now)
); );
support supported
}) })
.copied()
} }
} }
@ -125,7 +124,7 @@ where
if config.enable_mouse_capture { if config.enable_mouse_capture {
execute!(self.buffer, EnableMouseCapture)?; execute!(self.buffer, EnableMouseCapture)?;
} }
if self.supports_keyboard_enhancement_protocol()? { if self.supports_keyboard_enhancement_protocol() {
execute!( execute!(
self.buffer, self.buffer,
PushKeyboardEnhancementFlags( PushKeyboardEnhancementFlags(
@ -143,7 +142,7 @@ where
if config.enable_mouse_capture { if config.enable_mouse_capture {
execute!(self.buffer, DisableMouseCapture)?; execute!(self.buffer, DisableMouseCapture)?;
} }
if self.supports_keyboard_enhancement_protocol()? { if self.supports_keyboard_enhancement_protocol() {
execute!(self.buffer, PopKeyboardEnhancementFlags)?; execute!(self.buffer, PopKeyboardEnhancementFlags)?;
} }
execute!( execute!(
@ -345,9 +344,9 @@ impl ModifierDiff {
} }
} }
/// Crossterm uses semicolon as a seperator for colors /// Crossterm uses semicolon as a separator for colors
/// this is actually not spec compliant (altough commonly supported) /// this is actually not spec compliant (although commonly supported)
/// However the correct approach is to use colons as a seperator. /// However the correct approach is to use colons as a separator.
/// This usually doesn't make a difference for emulators that do support colored underlines. /// This usually doesn't make a difference for emulators that do support colored underlines.
/// However terminals that do not support colored underlines will ignore underlines colors with colons /// However terminals that do not support colored underlines will ignore underlines colors with colons
/// while escape sequences with semicolons are always processed which leads to weird visual artifacts. /// while escape sequences with semicolons are always processed which leads to weird visual artifacts.

@ -433,6 +433,47 @@ impl Buffer {
(x_offset as u16, y) (x_offset as u16, y)
} }
pub fn set_spans_truncated(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) {
// prevent panic if out of range
if !self.in_bounds(x, y) || width == 0 {
return (x, y);
}
let mut x_offset = x as usize;
let max_offset = min(self.area.right(), width.saturating_add(x));
let mut start_index = self.index_of(x, y);
let mut index = self.index_of(max_offset as u16, y);
let content_width = spans.width();
let truncated = content_width > width as usize;
if truncated {
self.content[start_index].set_symbol("…");
start_index += 1;
} else {
index -= width as usize - content_width;
}
for span in spans.0.iter().rev() {
for s in span.content.graphemes(true).rev() {
let width = s.width();
if width == 0 {
continue;
}
let start = index - width;
if start < start_index {
break;
}
self.content[start].set_symbol(s);
self.content[start].set_style(span.style);
for i in start + 1..index {
self.content[i].reset();
}
index -= width;
x_offset += width;
}
}
(x_offset as u16, y)
}
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) { pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) {
let mut remaining_width = width; let mut remaining_width = width;
let mut x = x; let mut x = x;

@ -49,6 +49,7 @@
use helix_core::line_ending::str_is_line_ending; use helix_core::line_ending::str_is_line_ending;
use helix_core::unicode::width::UnicodeWidthStr; use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::Style; use helix_view::graphics::Style;
use helix_view::icons::Icon;
use std::borrow::Cow; use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation; 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. /// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Spans<'a>(pub Vec<Span<'a>>); pub struct Spans<'a>(pub Vec<Span<'a>>);

@ -7,8 +7,9 @@ use crate::{
use helix_view::graphics::{Rect, Style}; use helix_view::graphics::{Rect, Style};
/// Border render type. Defaults to [`BorderType::Plain`]. /// Border render type. Defaults to [`BorderType::Plain`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum BorderType { pub enum BorderType {
#[default]
Plain, Plain,
Rounded, Rounded,
Double, Double,
@ -26,12 +27,6 @@ impl BorderType {
} }
} }
impl Default for BorderType {
fn default() -> BorderType {
BorderType::Plain
}
}
/// Base widget to be used with all upper level ones. It may be used to display a box border around /// Base widget to be used with all upper level ones. It may be used to display a box border around
/// the widget and/or add a title. /// the widget and/or add a title.
/// ///

@ -354,7 +354,13 @@ impl TableState {
impl<'a> Table<'a> { impl<'a> Table<'a> {
// type State = TableState; // type State = TableState;
pub fn render_table(mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) { pub fn render_table(
mut self,
area: Rect,
buf: &mut Buffer,
state: &mut TableState,
truncate: bool,
) {
if area.area() == 0 { if area.area() == 0 {
return; return;
} }
@ -401,6 +407,7 @@ impl<'a> Table<'a> {
width: *width, width: *width,
height: max_header_height, height: max_header_height,
}, },
truncate,
); );
col += *width + self.column_spacing; col += *width + self.column_spacing;
} }
@ -457,6 +464,7 @@ impl<'a> Table<'a> {
width: *width, width: *width,
height: table_row.height, height: table_row.height,
}, },
truncate,
); );
col += *width + self.column_spacing; col += *width + self.column_spacing;
} }
@ -464,20 +472,24 @@ impl<'a> Table<'a> {
} }
} }
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) { fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect, truncate: bool) {
buf.set_style(area, cell.style); buf.set_style(area, cell.style);
for (i, spans) in cell.content.lines.iter().enumerate() { for (i, spans) in cell.content.lines.iter().enumerate() {
if i as u16 >= area.height { if i as u16 >= area.height {
break; break;
} }
if truncate {
buf.set_spans_truncated(area.x, area.y + i as u16, spans, area.width);
} else {
buf.set_spans(area.x, area.y + i as u16, spans, area.width); buf.set_spans(area.x, area.y + i as u16, spans, area.width);
} }
} }
}
impl<'a> Widget for Table<'a> { impl<'a> Widget for Table<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default(); let mut state = TableState::default();
Table::render_table(self, area, buf, &mut state); Table::render_table(self, area, buf, &mut state, false);
} }
} }

@ -17,8 +17,9 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
parking_lot = "0.12" parking_lot = "0.12"
arc-swap = { version = "1.6.0" } arc-swap = { version = "1.6.0" }
gix = { version = "0.41.0", default-features = false , optional = true } gix = { version = "0.43.0", default-features = false , optional = true }
imara-diff = "0.1.5" imara-diff = "0.1.5"
anyhow = "1"
log = "0.4" log = "0.4"

@ -1,3 +1,4 @@
use anyhow::{bail, Context, Result};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
@ -14,7 +15,7 @@ mod test;
pub struct Git; pub struct Git;
impl Git { impl Git {
fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Option<ThreadSafeRepository> { fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Result<ThreadSafeRepository> {
// custom open options // custom open options
let mut git_open_opts_map = gix::sec::trust::Mapping::<gix::open::Options>::default(); let mut git_open_opts_map = gix::sec::trust::Mapping::<gix::open::Options>::default();
@ -45,26 +46,31 @@ impl Git {
open_options.ceiling_dirs = vec![ceiling_dir.to_owned()]; open_options.ceiling_dirs = vec![ceiling_dir.to_owned()];
} }
ThreadSafeRepository::discover_with_environment_overrides_opts( let res = ThreadSafeRepository::discover_with_environment_overrides_opts(
path, path,
open_options, open_options,
git_open_opts_map, git_open_opts_map,
) )?;
.ok()
Ok(res)
} }
} }
impl DiffProvider for Git { impl DiffProvider for Git {
fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> { fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
debug_assert!(!file.exists() || file.is_file()); debug_assert!(!file.exists() || file.is_file());
debug_assert!(file.is_absolute()); debug_assert!(file.is_absolute());
// TODO cache repository lookup // TODO cache repository lookup
let repo = Git::open_repo(file.parent()?, None)?.to_thread_local();
let head = repo.head_commit().ok()?; let repo_dir = file.parent().context("file has no parent directory")?;
let repo = Git::open_repo(repo_dir, None)
.context("failed to open git repo")?
.to_thread_local();
let head = repo.head_commit()?;
let file_oid = find_file_in_commit(&repo, &head, file)?; let file_oid = find_file_in_commit(&repo, &head, file)?;
let file_object = repo.find_object(file_oid).ok()?; let file_object = repo.find_object(file_oid)?;
let mut data = file_object.detach().data; let mut data = file_object.detach().data;
// convert LF to CRLF if configured to avoid showing every line as changed // convert LF to CRLF if configured to avoid showing every line as changed
if repo if repo
@ -87,35 +93,42 @@ impl DiffProvider for Git {
} }
data = normalized_file data = normalized_file
} }
Some(data) Ok(data)
} }
fn get_current_head_name(&self, file: &Path) -> Option<Arc<ArcSwap<Box<str>>>> { fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
debug_assert!(!file.exists() || file.is_file()); debug_assert!(!file.exists() || file.is_file());
debug_assert!(file.is_absolute()); debug_assert!(file.is_absolute());
let repo = Git::open_repo(file.parent()?, None)?.to_thread_local(); let repo_dir = file.parent().context("file has no parent directory")?;
let head_ref = repo.head_ref().ok()?; let repo = Git::open_repo(repo_dir, None)
let head_commit = repo.head_commit().ok()?; .context("failed to open git repo")?
.to_thread_local();
let head_ref = repo.head_ref()?;
let head_commit = repo.head_commit()?;
let name = match head_ref { let name = match head_ref {
Some(reference) => reference.name().shorten().to_string(), Some(reference) => reference.name().shorten().to_string(),
None => head_commit.id.to_hex_with_len(8).to_string(), None => head_commit.id.to_hex_with_len(8).to_string(),
}; };
Some(Arc::new(ArcSwap::from_pointee(name.into_boxed_str()))) Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str())))
} }
} }
/// Finds the object that contains the contents of a file at a specific commit. /// Finds the object that contains the contents of a file at a specific commit.
fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Option<ObjectId> { fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Result<ObjectId> {
let repo_dir = repo.work_dir()?; let repo_dir = repo.work_dir().context("repo has no worktree")?;
let rel_path = file.strip_prefix(repo_dir).ok()?; let rel_path = file.strip_prefix(repo_dir)?;
let tree = commit.tree().ok()?; let tree = commit.tree()?;
let tree_entry = tree.lookup_entry_by_path(rel_path).ok()??; let tree_entry = tree
.lookup_entry_by_path(rel_path)?
.context("file is untracked")?;
match tree_entry.mode() { match tree_entry.mode() {
// not a file, everything is new, do not show diff // not a file, everything is new, do not show diff
EntryMode::Tree | EntryMode::Commit | EntryMode::Link => None, mode @ (EntryMode::Tree | EntryMode::Commit | EntryMode::Link) => {
bail!("entry at {} is not a file but a {mode:?}", file.display())
}
// found a file // found a file
EntryMode::Blob | EntryMode::BlobExecutable => Some(tree_entry.object_id()), EntryMode::Blob | EntryMode::BlobExecutable => Ok(tree_entry.object_id()),
} }
} }

@ -54,7 +54,7 @@ fn missing_file() {
let file = temp_git.path().join("file.txt"); let file = temp_git.path().join("file.txt");
File::create(&file).unwrap().write_all(b"foo").unwrap(); File::create(&file).unwrap().write_all(b"foo").unwrap();
assert_eq!(Git.get_diff_base(&file), None); assert!(Git.get_diff_base(&file).is_err());
} }
#[test] #[test]
@ -64,7 +64,7 @@ fn unmodified_file() {
let contents = b"foo".as_slice(); let contents = b"foo".as_slice();
File::create(&file).unwrap().write_all(contents).unwrap(); File::create(&file).unwrap().write_all(contents).unwrap();
create_commit(temp_git.path(), true); create_commit(temp_git.path(), true);
assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents))); assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents));
} }
#[test] #[test]
@ -76,7 +76,7 @@ fn modified_file() {
create_commit(temp_git.path(), true); create_commit(temp_git.path(), true);
File::create(&file).unwrap().write_all(b"bar").unwrap(); File::create(&file).unwrap().write_all(b"bar").unwrap();
assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents))); assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents));
} }
/// Test that `get_file_head` does not return content for a directory. /// Test that `get_file_head` does not return content for a directory.
@ -95,7 +95,7 @@ fn directory() {
std::fs::remove_dir_all(&dir).unwrap(); std::fs::remove_dir_all(&dir).unwrap();
File::create(&dir).unwrap().write_all(b"bar").unwrap(); File::create(&dir).unwrap().write_all(b"bar").unwrap();
assert_eq!(Git.get_diff_base(&dir), None); assert!(Git.get_diff_base(&dir).is_err());
} }
/// Test that `get_file_head` does not return content for a symlink. /// Test that `get_file_head` does not return content for a symlink.
@ -116,6 +116,6 @@ fn symlink() {
symlink("file.txt", &file_link).unwrap(); symlink("file.txt", &file_link).unwrap();
create_commit(temp_git.path(), true); create_commit(temp_git.path(), true);
assert_eq!(Git.get_diff_base(&file_link), None); assert!(Git.get_diff_base(&file_link).is_err());
assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents))); assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents));
} }

@ -1,3 +1,4 @@
use anyhow::{bail, Result};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
@ -18,19 +19,19 @@ pub trait DiffProvider {
/// if this provider is used. /// if this provider is used.
/// The data is returned as raw byte without any decoding or encoding performed /// The data is returned as raw byte without any decoding or encoding performed
/// to ensure all file encodings are handled correctly. /// to ensure all file encodings are handled correctly.
fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>>; fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>>;
fn get_current_head_name(&self, file: &Path) -> Option<Arc<ArcSwap<Box<str>>>>; fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>>;
} }
#[doc(hidden)] #[doc(hidden)]
pub struct Dummy; pub struct Dummy;
impl DiffProvider for Dummy { impl DiffProvider for Dummy {
fn get_diff_base(&self, _file: &Path) -> Option<Vec<u8>> { fn get_diff_base(&self, _file: &Path) -> Result<Vec<u8>> {
None bail!("helix was compiled without git support")
} }
fn get_current_head_name(&self, _file: &Path) -> Option<Arc<ArcSwap<Box<str>>>> { fn get_current_head_name(&self, _file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
None bail!("helix was compiled without git support")
} }
} }
@ -42,13 +43,27 @@ impl DiffProviderRegistry {
pub fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> { pub fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
self.providers self.providers
.iter() .iter()
.find_map(|provider| provider.get_diff_base(file)) .find_map(|provider| match provider.get_diff_base(file) {
Ok(res) => Some(res),
Err(err) => {
log::error!("{err:#?}");
log::error!("failed to open diff base for {}", file.display());
None
}
})
} }
pub fn get_current_head_name(&self, file: &Path) -> Option<Arc<ArcSwap<Box<str>>>> { pub fn get_current_head_name(&self, file: &Path) -> Option<Arc<ArcSwap<Box<str>>>> {
self.providers self.providers
.iter() .iter()
.find_map(|provider| provider.get_current_head_name(file)) .find_map(|provider| match provider.get_current_head_name(file) {
Ok(res) => Some(res),
Err(err) => {
log::error!("{err:#?}");
log::error!("failed to obtain current head name for {}", file.display());
None
}
})
} }
} }

@ -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 /// If supported, returns the changes that should be applied to this document in order
/// to format it nicely. /// to format it nicely.
// We can't use anyhow::Result here since the output of the future has to be // 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}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode}, document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
icons::{self, Icons},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
theme::{self, Theme}, theme::{self, Theme},
@ -10,6 +11,7 @@ use crate::{
view::ViewPosition, view::ViewPosition,
Align, Document, DocumentId, View, ViewId, Align, Document, DocumentId, View, ViewId,
}; };
use dap::StackFrame;
use helix_vcs::DiffProviderRegistry; use helix_vcs::DiffProviderRegistry;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
@ -210,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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config { pub struct Config {
@ -280,7 +327,13 @@ pub struct Config {
pub indent_guides: IndentGuidesConfig, pub indent_guides: IndentGuidesConfig,
/// Whether to color modes with different colors. Defaults to `false`. /// Whether to color modes with different colors. Defaults to `false`.
pub color_modes: bool, pub color_modes: bool,
/// explore config
pub explorer: ExplorerConfig,
pub soft_wrap: SoftWrap, 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)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -349,6 +402,8 @@ pub struct LspConfig {
pub display_signature_help_docs: bool, pub display_signature_help_docs: bool,
/// Display inlay hints /// Display inlay hints
pub display_inlay_hints: bool, pub display_inlay_hints: bool,
/// Whether to enable snippet support
pub snippets: bool,
} }
impl Default for LspConfig { impl Default for LspConfig {
@ -359,6 +414,7 @@ impl Default for LspConfig {
auto_signature_help: true, auto_signature_help: true,
display_signature_help_docs: true, display_signature_help_docs: true,
display_inlay_hints: false, display_inlay_hints: false,
snippets: true,
} }
} }
} }
@ -446,6 +502,9 @@ pub enum StatusLineElement {
/// The file type (language ID or "text") /// The file type (language ID or "text")
FileType, FileType,
/// The file type icon (from file path)
FileTypeIcon,
/// A summary of the number of errors and warnings /// A summary of the number of errors and warnings
Diagnostics, Diagnostics,
@ -532,10 +591,11 @@ impl Default for CursorShapeConfig {
} }
/// bufferline render modes /// bufferline render modes
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum BufferLine { pub enum BufferLine {
/// Don't render bufferline /// Don't render bufferline
#[default]
Never, Never,
/// Always render /// Always render
Always, Always,
@ -543,12 +603,6 @@ pub enum BufferLine {
Multiple, Multiple,
} }
impl Default for BufferLine {
fn default() -> Self {
BufferLine::Never
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum LineNumber { pub enum LineNumber {
@ -748,9 +802,12 @@ impl Default for Config {
bufferline: BufferLine::default(), bufferline: BufferLine::default(),
indent_guides: IndentGuidesConfig::default(), indent_guides: IndentGuidesConfig::default(),
color_modes: false, color_modes: false,
explorer: ExplorerConfig::default(),
soft_wrap: SoftWrap::default(), soft_wrap: SoftWrap::default(),
text_width: 80, text_width: 80,
completion_replace: false, completion_replace: false,
workspace_lsp_roots: Vec::new(),
icons: IconsConfig::default(),
} }
} }
} }
@ -827,6 +884,8 @@ pub struct Editor {
/// The currently applied editor theme. While previewing a theme, the previewed theme /// The currently applied editor theme. While previewing a theme, the previewed theme
/// is set here. /// is set here.
pub theme: Theme, 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 /// 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 /// restored when the preview is aborted, or added to the jumplist when it is
@ -921,15 +980,30 @@ pub enum CloseError {
SaveError(anyhow::Error), 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 { impl Editor {
pub fn new( pub fn new(
mut area: Rect, mut area: Rect,
theme_loader: Arc<theme::Loader>, theme_loader: Arc<theme::Loader>,
icons_loader: Arc<icons::Loader>,
syn_loader: Arc<syntax::Loader>, syn_loader: Arc<syntax::Loader>,
config: Arc<dyn DynAccess<Config>>, config: Arc<dyn DynAccess<Config>>,
) -> Self { ) -> Self {
let conf = config.load(); let conf = config.load();
let auto_pairs = (&conf.auto_pairs).into(); 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 // HAXX: offset the render area height by 1 to account for prompt/commandline
area.height -= 1; area.height -= 1;
@ -972,6 +1046,8 @@ impl Editor {
needs_redraw: false, needs_redraw: false,
cursor_cache: Cell::new(None), cursor_cache: Cell::new(None),
completion_request_handle: None, completion_request_handle: None,
icons,
icons_loader,
} }
} }
@ -1072,6 +1148,9 @@ impl Editor {
} }
ThemeAction::Set => { ThemeAction::Set => {
self.last_theme = None; 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; self.theme = theme;
} }
} }
@ -1079,6 +1158,11 @@ impl Editor {
self._refresh(); self._refresh();
} }
pub fn set_icons(&mut self, icons: Icons) {
self.icons = icons;
self._refresh();
}
/// Refreshes the language server for a given document /// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
self.launch_language_server(doc_id) self.launch_language_server(doc_id)
@ -1091,15 +1175,15 @@ impl Editor {
} }
// if doc doesn't have a URL it's a scratch buffer, ignore it // if doc doesn't have a URL it's a scratch buffer, ignore it
let (lang, path) = {
let doc = self.document(doc_id)?; let doc = self.document(doc_id)?;
(doc.language.clone(), doc.path().cloned()) let (lang, path) = (doc.language.clone(), doc.path().cloned());
}; let config = doc.config.load();
let root_dirs = &config.workspace_lsp_roots;
// try to find a language server based on the language name // try to find a language server based on the language name
let language_server = lang.as_ref().and_then(|language| { let language_server = lang.as_ref().and_then(|language| {
self.language_servers self.language_servers
.get(language, path.as_ref()) .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
.map_err(|e| { .map_err(|e| {
log::error!( log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}", "Failed to initialize the LSP for `{}` {{ {} }}",
@ -1657,6 +1741,12 @@ impl Editor {
doc.restore_cursor = false; doc.restore_cursor = false;
} }
} }
pub fn current_stack_frame(&self) -> Option<&StackFrame> {
self.debugger
.as_ref()
.and_then(|debugger| debugger.current_stack_frame())
}
} }
fn try_restore_indent(doc: &mut Document, view: &mut View) { fn try_restore_indent(doc: &mut Document, view: &mut View) {

@ -248,6 +248,34 @@ impl Rect {
&& self.y < other.y + other.height && self.y < other.y + other.height
&& self.y + self.height > other.y && 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

@ -2,7 +2,7 @@ use std::fmt::Write;
use crate::{ use crate::{
editor::GutterType, editor::GutterType,
graphics::{Color, Style, UnderlineStyle}, graphics::{Style, UnderlineStyle},
Document, Editor, Theme, View, Document, Editor, Theme, View,
}; };
@ -45,7 +45,7 @@ impl GutterType {
} }
pub fn diagnostic<'doc>( pub fn diagnostic<'doc>(
_editor: &'doc Editor, editor: &'doc Editor,
doc: &'doc Document, doc: &'doc Document,
_view: &View, _view: &View,
theme: &Theme, 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. // 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(); 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 { return Some(match diagnostic.severity {
Some(Severity::Error) => error, Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning, Some(Severity::Warning) | None => warning,
@ -90,19 +96,20 @@ pub fn diagnostic<'doc>(
} }
pub fn diff<'doc>( pub fn diff<'doc>(
_editor: &'doc Editor, editor: &'doc Editor,
doc: &'doc Document, doc: &'doc Document,
_view: &View, _view: &View,
theme: &Theme, theme: &Theme,
_is_focused: bool, _is_focused: bool,
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
if let Some(diff_handle) = doc.diff_handle() {
let added = theme.get("diff.plus"); let added = theme.get("diff.plus");
let deleted = theme.get("diff.minus"); let deleted = theme.get("diff.minus");
let modified = theme.get("diff.delta"); let modified = theme.get("diff.delta");
if let Some(diff_handle) = doc.diff_handle() {
let hunks = diff_handle.load(); let hunks = diff_handle.load();
let mut hunk_i = 0; let mut hunk_i = 0;
let mut hunk = hunks.nth_hunk(hunk_i); let mut hunk = hunks.nth_hunk(hunk_i);
let icons = &editor.icons;
Box::new( Box::new(
move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
// truncating the line is fine here because we don't compute diffs // 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() { let (icon, style) = if hunk.is_pure_insertion() {
("▍", added) (&icons.diff.added, added)
} else if hunk.is_pure_removal() { } else if hunk.is_pure_removal() {
if !first_visual_line { if !first_visual_line {
return None; return None;
} }
("▔", deleted) (&icons.diff.deleted, deleted)
} else { } else {
("▍", modified) (&icons.diff.modified, modified)
}; };
write!(out, "{}", icon).unwrap(); write!(out, "{}", icon.icon_char).unwrap();
Some(style) icon.style.map(|i| i.into()).or(Some(style))
}, },
) )
} else { } else {
@ -245,9 +252,9 @@ pub fn breakpoints<'doc>(
theme: &Theme, theme: &Theme,
_is_focused: bool, _is_focused: bool,
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
let warning = theme.get("warning");
let error = theme.get("error"); let error = theme.get("error");
let info = theme.get("info"); let info = theme.get("info");
let breakpoint_style = theme.get("ui.debug.breakpoint");
let breakpoints = doc.path().and_then(|path| editor.breakpoints.get(path)); let breakpoints = doc.path().and_then(|path| editor.breakpoints.get(path));
@ -265,30 +272,56 @@ pub fn breakpoints<'doc>(
.iter() .iter()
.find(|breakpoint| breakpoint.line == line)?; .find(|breakpoint| breakpoint.line == line)?;
let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { let style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
error.underline_style(UnderlineStyle::Line) error.underline_style(UnderlineStyle::Line)
} else if breakpoint.condition.is_some() { } else if breakpoint.condition.is_some() {
error error
} else if breakpoint.log_message.is_some() { } else if breakpoint.log_message.is_some() {
info info
} else { } else {
warning breakpoint_style
}; };
if !breakpoint.verified { let sym = if breakpoint.verified {
// Faded colors editor.icons.breakpoint.verified.icon_char
style = if let Some(Color::Rgb(r, g, b)) = style.fg {
style.fg(Color::Rgb(
((r as f32) * 0.4).floor() as u8,
((g as f32) * 0.4).floor() as u8,
((b as f32) * 0.4).floor() as u8,
))
} else { } else {
style.fg(Color::Gray) editor.icons.breakpoint.unverified.icon_char
}
}; };
write!(out, "{}", sym).unwrap();
Some(style)
},
)
}
fn execution_pause_indicator<'doc>(
editor: &'doc Editor,
doc: &'doc Document,
theme: &Theme,
is_focused: bool,
) -> GutterFn<'doc> {
let style = theme.get("ui.debug.active");
let current_stack_frame = editor.current_stack_frame();
let frame_line = current_stack_frame.map(|frame| frame.line - 1);
let frame_source_path = current_stack_frame.map(|frame| {
frame
.source
.as_ref()
.and_then(|source| source.path.as_ref())
});
let should_display_for_current_doc =
doc.path().is_some() && frame_source_path.unwrap_or(None) == doc.path();
Box::new(
move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
if !first_visual_line
|| !is_focused
|| line != frame_line?
|| !should_display_for_current_doc
{
return None;
}
let sym = if breakpoint.verified { "▲" } else { "⊚" }; let sym = editor.icons.breakpoint.pause_indicator.icon_char;
write!(out, "{}", sym).unwrap(); write!(out, "{}", sym).unwrap();
Some(style) Some(style)
}, },
@ -304,9 +337,11 @@ pub fn diagnostics_or_breakpoints<'doc>(
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused); let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused); let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
let mut execution_pause_indicator = execution_pause_indicator(editor, doc, theme, is_focused);
Box::new(move |line, selected, first_visual_line: bool, out| { Box::new(move |line, selected, first_visual_line: bool, out| {
breakpoints(line, selected, first_visual_line, out) execution_pause_indicator(line, selected, first_visual_line, out)
.or_else(|| breakpoints(line, selected, first_visual_line, out))
.or_else(|| diagnostics(line, selected, first_visual_line, out)) .or_else(|| diagnostics(line, selected, first_visual_line, out))
}) })
} }

@ -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 lsp;
} }
pub mod base64; pub mod base64;
pub mod icons;
pub mod info; pub mod info;
pub mod input; pub mod input;
pub mod keyboard; pub mod keyboard;

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

@ -7,9 +7,13 @@ use crate::{
}; };
use helix_core::{ use helix_core::{
char_idx_at_visual_offset, doc_formatter::TextFormat, syntax::Highlight, char_idx_at_visual_offset,
text_annotations::TextAnnotations, visual_offset_from_anchor, visual_offset_from_block, doc_formatter::TextFormat,
Position, RopeSlice, Selection, Transaction, syntax::Highlight,
text_annotations::TextAnnotations,
visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection,
Transaction,
VisualOffsetError::{PosAfterMaxRow, PosBeforeAnchorRow},
}; };
use std::{ use std::{
@ -213,12 +217,14 @@ impl View {
// - 1 so we have at least one gap in the middle. // - 1 so we have at least one gap in the middle.
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
// as we type // as we type
let scrolloff = scrolloff.min(viewport.height.saturating_sub(1) as usize / 2); let scrolloff = if CENTERING {
0
} else {
scrolloff.min(viewport.height.saturating_sub(1) as usize / 2)
};
let cursor = doc.selection(self.id).primary().cursor(doc_text); let cursor = doc.selection(self.id).primary().cursor(doc_text);
let mut offset = self.offset; let mut offset = self.offset;
let (visual_off, mut at_top) = if cursor >= offset.anchor {
let off = visual_offset_from_anchor( let off = visual_offset_from_anchor(
doc_text, doc_text,
offset.anchor, offset.anchor,
@ -227,32 +233,22 @@ impl View {
&annotations, &annotations,
vertical_viewport_end, vertical_viewport_end,
); );
(off, false)
} else if CENTERING {
// cursor out of view
return None;
} else {
(None, true)
};
let new_anchor = match visual_off { let (new_anchor, at_top) = match off {
Some((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => { Ok((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => {
if CENTERING && visual_pos.row < offset.vertical_offset { if CENTERING {
// cursor out of view // cursor out of view
return None; return None;
} }
at_top = true; (true, true)
true
}
Some((visual_pos, _)) if visual_pos.row + scrolloff + 1 >= vertical_viewport_end => {
if CENTERING && visual_pos.row >= vertical_viewport_end {
// cursor out of view
return None;
} }
true Ok((visual_pos, _)) if visual_pos.row + scrolloff >= vertical_viewport_end => {
(true, false)
} }
Some(_) => false, Ok((_, _)) => (false, false),
None => true, Err(_) if CENTERING => return None,
Err(PosBeforeAnchorRow) => (true, true),
Err(PosAfterMaxRow) => (true, false),
}; };
if new_anchor { if new_anchor {
@ -269,8 +265,8 @@ impl View {
offset.horizontal_offset = 0; offset.horizontal_offset = 0;
} else { } else {
// determine the current visual column of the text // determine the current visual column of the text
let col = visual_off let col = off
.unwrap_or_else(|| { .unwrap_or_else(|_| {
visual_offset_from_block( visual_offset_from_block(
doc_text, doc_text,
offset.anchor, offset.anchor,
@ -360,8 +356,9 @@ impl View {
); );
match pos { match pos {
Some((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset), Ok((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset),
None => visual_height.saturating_sub(1), Err(PosAfterMaxRow) => visual_height.saturating_sub(1),
Err(PosBeforeAnchorRow) => 0,
} }
} }
@ -390,7 +387,8 @@ impl View {
&text_fmt, &text_fmt,
&annotations, &annotations,
viewport.height as usize, viewport.height as usize,
)? )
.ok()?
.0; .0;
if pos.row < self.offset.vertical_offset { if pos.row < self.offset.vertical_offset {
return None; return None;

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

@ -160,7 +160,7 @@ indent = { tab-width = 2, unit = " " }
name = "json" name = "json"
scope = "source.json" scope = "source.json"
injection-regex = "json" injection-regex = "json"
file-types = ["json", "jsonc"] file-types = ["json", "jsonc", "arb"]
roots = [] roots = []
language-server = { command = "vscode-json-language-server", args = ["--stdio"] } language-server = { command = "vscode-json-language-server", args = ["--stdio"] }
auto-format = true auto-format = true
@ -212,7 +212,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd
name = "cpp" name = "cpp"
scope = "source.cpp" scope = "source.cpp"
injection-regex = "cpp" injection-regex = "cpp"
file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H"] file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
roots = [] roots = []
comment-token = "//" comment-token = "//"
language-server = { command = "clangd" } language-server = { command = "clangd" }
@ -453,7 +453,7 @@ includeInlayVariableTypeHints = true
name = "typescript" name = "typescript"
scope = "source.ts" scope = "source.ts"
injection-regex = "(ts|typescript)" injection-regex = "(ts|typescript)"
file-types = ["ts"] file-types = ["ts", "mts", "cts"]
shebangs = [] shebangs = []
roots = [] roots = []
# TODO: highlights-params # TODO: highlights-params
@ -606,7 +606,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "ruby" name = "ruby"
source = { git = "https://github.com/tree-sitter/tree-sitter-ruby", rev = "4c600a463d97e36a0ca5ac57e11f3ac8c297a0fa" } source = { git = "https://github.com/tree-sitter/tree-sitter-ruby", rev = "206c7077164372c596ffa8eaadb9435c28941364" }
[[language]] [[language]]
name = "bash" name = "bash"
@ -1194,7 +1194,7 @@ text-width = 72
[[grammar]] [[grammar]]
name = "git-commit" name = "git-commit"
source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "318dd72abfaa7b8044c1d1fbeabcd06deaaf038f" } source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "bd0ca5a6065f2cada3ac6a82a66db3ceff55fa6b" }
[[language]] [[language]]
name = "diff" name = "diff"
@ -1437,6 +1437,20 @@ comment-token = "//"
indent = { tab-width = 4, unit = " " } indent = { tab-width = 4, unit = " " }
grammar = "rust" grammar = "rust"
[[language]]
name = "robot"
scope = "source.robot"
injection-regex = "robot"
file-types = ["robot", "resource"]
comment-token = "#"
roots = []
indent = { tab-width = 4, unit = " " }
language-server = { command = "robotframework_ls" }
[[grammar]]
name = "robot"
source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "f1142bfaa6acfce95e25d2c6d18d218f4f533927" }
[[language]] [[language]]
name = "r" name = "r"
scope = "source.r" scope = "source.r"
@ -1446,7 +1460,7 @@ shebangs = ["r", "R"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-server = { command = "R", args = ["--slave", "-e", "languageserver::run()"] } language-server = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
[[grammar]] [[grammar]]
name = "r" name = "r"
@ -1545,6 +1559,7 @@ file-types = ["gd"]
shebangs = [] shebangs = []
roots = ["project.godot"] roots = ["project.godot"]
auto-format = true auto-format = true
formatter = { command = "gdformat", args = ["-"] }
comment-token = "#" comment-token = "#"
indent = { tab-width = 4, unit = "\t" } indent = { tab-width = 4, unit = "\t" }
@ -2068,7 +2083,7 @@ source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d1
name = "xml" name = "xml"
scope = "source.xml" scope = "source.xml"
injection-regex = "xml" injection-regex = "xml"
file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard"] file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg", "xsd"]
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
roots = [] roots = []
@ -2369,3 +2384,69 @@ language-server = { command = "cs", args = ["launch", "com.disneystreaming.smith
[[grammar]] [[grammar]]
name = "smithy" name = "smithy"
source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "cf8c7eb9faf7c7049839585eac19c94af231e6a0" } source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "cf8c7eb9faf7c7049839585eac19c94af231e6a0" }
[[language]]
name = "vhdl"
scope = "source.vhdl"
file-types = ["vhd", "vhdl"]
roots = []
comment-token = "--"
language-server = { command = "vhdl_ls", args = [] }
indent = { tab-width = 2, unit = " " }
injection-regex = "vhdl"
[[grammar]]
name = "vhdl"
source = { git = "https://github.com/teburd/tree-sitter-vhdl", rev = "c57313adee2231100db0a7880033f6865deeadb2" }
[[language]]
name = "rego"
roots = []
scope = "source.rego"
injection-regex = "rego"
file-types = ["rego"]
auto-format = true
comment-token = "#"
language-server = { command = "regols" }
grammar = "rego"
[[grammar]]
name = "rego"
source = { git = "https://github.com/FallenAngel97/tree-sitter-rego", rev = "b2667c975f07b33be3ceb83bea5cfbad88095866" }
[[language]]
name = "nim"
scope = "source.nim"
injection-regex = "nim"
file-types = ["nim", "nims", "nimble"]
shebangs = []
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "nimlangserver" }
[language.auto-pairs]
'(' = ')'
'[' = ']'
'"' = '"'
"'" = "'"
'{' = '}'
# Nim's tree-sitter grammar is in heavy development.
[[grammar]]
name = "nim"
source = { git = "https://github.com/aMOPel/tree-sitter-nim", rev = "240239b232550e431d67de250d1b5856209e7f06" }
[[language]]
name = "hurl"
scope = "source.hurl"
injection-regex = "hurl"
file-types = ["hurl"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "hurl"
source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "264c42064b61ee21abe88d0061f29a0523352e22" }

@ -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" sodipodi:docname="logo_dark.svg"
width="2087.0059" width="2087.0059"
height="903.71997" 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:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -53,13 +53,13 @@
inkscape:pagecheckerboard="0" inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" inkscape:deskcolor="#d1d1d1"
showgrid="false" showgrid="false"
inkscape:zoom="0.28409405" inkscape:zoom="0.40176966"
inkscape:cx="1904.2989" inkscape:cx="801.45425"
inkscape:cy="633.59299" inkscape:cy="775.31987"
inkscape:window-width="1908" inkscape:window-width="1908"
inkscape:window-height="2075" inkscape:window-height="996"
inkscape:window-x="26" inkscape:window-x="4"
inkscape:window-y="23" inkscape:window-y="0"
inkscape:window-maximized="0" inkscape:window-maximized="0"
inkscape:current-layer="svg22" /> <g inkscape:current-layer="svg22" /> <g
transform="translate(-31352.726,-1817.2547)" transform="translate(-31352.726,-1817.2547)"
@ -112,4 +112,24 @@
xml:space="preserve" xml:space="preserve"
transform="translate(663.38,37.570044)" transform="translate(663.38,37.570044)"
id="text14661" 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" sodipodi:docname="logo_light.svg"
width="2087.0059" width="2087.0059"
height="903.71997" 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:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -53,13 +53,13 @@
inkscape:pagecheckerboard="0" inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" inkscape:deskcolor="#d1d1d1"
showgrid="false" showgrid="false"
inkscape:zoom="0.28409405" inkscape:zoom="0.40176966"
inkscape:cx="1904.2989" inkscape:cx="752.91897"
inkscape:cy="633.59299" inkscape:cy="551.31092"
inkscape:window-width="1908" inkscape:window-width="1908"
inkscape:window-height="2075" inkscape:window-height="996"
inkscape:window-x="26" inkscape:window-x="4"
inkscape:window-y="23" inkscape:window-y="0"
inkscape:window-maximized="0" inkscape:window-maximized="0"
inkscape:current-layer="svg22" /> <g inkscape:current-layer="svg22" /> <g
transform="translate(-31352.726,-1817.2547)" transform="translate(-31352.726,-1817.2547)"
@ -112,4 +112,24 @@
xml:space="preserve" xml:space="preserve"
transform="translate(663.38,37.570044)" transform="translate(663.38,37.570044)"
id="text14661" 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,2 @@
(comment) @comment.inside
(comment)+ @comment.around

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

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

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

@ -0,0 +1,16 @@
[
(struct_definition)
(macro_definition)
(function_definition)
(compound_expression)
(let_statement)
(if_statement)
(for_statement)
(while_statement)
(do_clause)
(parameter_list)
] @indent
[
"end"
] @outdent

@ -26,3 +26,9 @@
prefix: (identifier) @function.macro) @injection.content prefix: (identifier) @function.macro) @injection.content
(#eq? @function.macro "re") (#eq? @function.macro "re")
(#set! injection.language "regex")) (#set! injection.language "regex"))
(
(prefixed_string_literal
prefix: (identifier) @function.macro) @injection.content
(#eq? @function.macro "md")
(#set! injection.language "markdown"))

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

@ -39,6 +39,9 @@
(list_marker_parenthesis) (list_marker_parenthesis)
] @markup.list.numbered ] @markup.list.numbered
(task_list_marker_checked) @markup.list.checked
(task_list_marker_unchecked) @markup.list.unchecked
(thematic_break) @punctuation.special (thematic_break) @punctuation.special
[ [

@ -0,0 +1,315 @@
;; Constants, Comments, and Literals
(comment) @comment.line
(multilineComment) @comment.block
(docComment) @comment.block.documentation
(multilineDocComment) @comment.block.documentation
; comments
[(literal) (generalizedLit)] @constant
[(nil_lit)] @constant.builtin
[(bool_lit)] @constant.builtin.boolean
[(char_lit)] @constant.character
[(char_esc_seq) (str_esc_seq)] @constant.character.escape
[(custom_numeric_lit)] @constant.numeric
[(int_lit) (int_suffix)] @constant.numeric.integer
[(float_lit) (float_suffix)] @constant.numeric.float
; literals
; note: somewhat irritatingly for testing, lits have the same syntax highlighting as types
[
(str_lit)
(triplestr_lit)
(rstr_lit)
(generalized_str_lit)
(generalized_triplestr_lit)
(interpolated_str_lit)
(interpolated_triplestr_lit)
] @string
; [] @string.regexp
; string literals
[
"."
","
";"
":"
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
"{."
".}"
"#["
"]#"
] @punctuation.bracket
(interpolated_str_lit "&" @punctuation.special)
(interpolated_str_lit "{" @punctuation.special)
(interpolated_str_lit "}" @punctuation.special)
; punctuation
[
"and"
"or"
"xor"
"not"
"in"
"notin"
"is"
"isnot"
"div"
"mod"
"shl"
"shr"
] @keyword.operator
; operators: we list them explicitly to deliminate them from symbolic operators
[(operator) (opr) "="] @operator
; all operators (must come after @keyword.operator)
(pragma) @attribute
; pragmas
;; Imports and Exports
(importStmt
(keyw) @keyword.control.import
(expr (primary (symbol) @namespace))?
(expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?)
(exportStmt
(keyw) @keyword.control.import
(expr (primary (symbol) @namespace))?
(expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?)
(fromStmt
(keyw) @keyword.control.import
(expr (primary (symbol) @namespace))?
(expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?)
(includeStmt
(keyw) @keyword.control.import
(expr (primary (symbol) @namespace))?
(expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?)
(importExceptStmt
(keyw) @keyword.control.import
(expr (primary (symbol) @namespace))?
(expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?)
; import statements
; yeah, this is a bit gross.
;; Control Flow
(ifStmt (keyw) @keyword.control.conditional)
(whenStmt (keyw) @keyword.control.conditional)
(elifStmt (keyw) @keyword.control.conditional)
(elseStmt (keyw) @keyword.control.conditional)
(caseStmt (keyw) @keyword.control.conditional)
(ofBranch (keyw) @keyword.control.conditional)
(inlineIfStmt (keyw) @keyword.control.conditional)
(inlineWhenStmt (keyw) @keyword.control.conditional)
; conditional statements
; todo: do block
(forStmt
. (keyw) @keyword.control.repeat
. (symbol) @variable
. (keyw) @keyword.control.repeat)
(whileStmt (keyw) @keyword.control.repeat)
; loop statements
(returnStmt (keyw) @keyword.control.repeat)
(yieldStmt (keyw) @keyword.control.repeat)
(discardStmt (keyw) @keyword.control.repeat)
(breakStmt (keyw) @keyword.control.repeat)
(continueStmt (keyw) @keyword.control.repeat)
; control flow statements
(raiseStmt (keyw) @keyword.control.exception)
(tryStmt (keyw) @keyword.control.exception)
(tryExceptStmt (keyw) @keyword.control.exception)
(tryFinallyStmt (keyw) @keyword.control.exception)
(inlineTryStmt (keyw) @keyword.control.exception)
; (inlineTryExceptStmt (keyw) @keyword.control.exception)
; (inlineTryFinallyStmt (keyw) @keyword.control.exception)
; exception handling statements
(staticStmt (keyw) @keyword)
(deferStmt (keyw) @keyword)
(asmStmt (keyw) @keyword)
(bindStmt (keyw) @keyword)
(mixinStmt (keyw) @keyword)
; miscellaneous blocks
(blockStmt
(keyw) @keyword.control
(symbol) @label)
; block statements
;; Types and Type Declarations
(typeDef
(keyw) @keyword.storage.type
(symbol) @type)
; names of new types type declarations
(exprColonEqExpr
. (expr (primary (symbol) @variable))
. (expr (primary (symbol) @type)))
; variables in inline tuple declarations
(primarySuffix
(indexSuffix
(exprColonEqExprList
(exprColonEqExpr
(expr
(primary
(symbol) @type))))))
; nested types in brackets, i.e. seq[string]
(primaryTypeDef (symbol) @type)
; primary types of type declarations (NOT nested types)
(primaryTypeDef (primaryPrefix (keyw) @type))
; for consistency
(primaryTypeDesc (symbol) @type)
; type annotations, on declarations or in objects
(primaryTypeDesc (primaryPrefix (keyw) @type))
; var types etc
(genericParamList (genericParam (symbol) @type))
; types in generic blocks
(enumDecl (keyw) @keyword.storage.type)
(enumElement (symbol) @type.enum.variant)
; enum declarations and elements
(tupleDecl (keyw) @keyword.storage.type)
; tuple declarations
(objectDecl (keyw) @keyword.storage.type)
(objectPart (symbol) @variable.other.member)
; object declarations and fields
(objectCase
(keyw) @keyword.control.conditional
(symbol) @variable.other.member)
(objectBranch (keyw) @keyword.control.conditional)
(objectElif (keyw) @keyword.control.conditional)
(objectElse (keyw) @keyword.control.conditional)
(objectWhen (keyw) @keyword.control.conditional)
; variant objects
(conceptDecl (keyw) @keyword.storage.type)
(conceptParam (keyw) @type)
(conceptParam (symbol) @variable)
; concept declarations, parameters, and qualifiers on those parameters
((expr
(primary (symbol))
(operator) @operator
(primary (symbol) @type))
(#match? @operator "is"))
((exprStmt
(primary (symbol))
(operator) @operator
(primary (symbol) @type))
(#match? @operator "is"))
; symbols likely to be types: "x is t" means t is either a type or a type variable
; distinct?
;; Functions
(routine
. (keyw) @keyword.function
. (symbol) @function)
; function declarations
(routineExpr (keyw) @keyword.function)
; discarded function
(routineExprTypeDesc (keyw) @keyword.function)
; function declarations as types
(primary
. (symbol) @function.call
. (primarySuffix (functionCall)))
; regular function calls
(primary
. (symbol) @function.call
. (primarySuffix (cmdCall)))
; function calls without parenthesis
(primary
(primarySuffix (qualifiedSuffix (symbol) @function.call))
. (primarySuffix (functionCall)))
; uniform function call syntax calls
(primary
(primarySuffix (qualifiedSuffix (symbol) @function.call))
. (primarySuffix (cmdCall)))
; just in case
(primary
(symbol) @constructor
(primarySuffix (objectConstr)))
; object constructor
; does not appear to be a way to distinguish these without verbatium matching
; [] @function.builtin
; [] @function.method
; [] @function.macro
; [] @function.special
;; Variables
(paramList (paramColonEquals (symbol) @variable.parameter))
; parameter identifiers
(identColon (ident) @variable.other.member)
; named parts of tuples
(symbolColonExpr (symbol) @variable)
; object constructor parameters
(symbolEqExpr (symbol) @variable)
; named parameters
(variable
(keyw) @keyword.storage.type
(declColonEquals (symbol) @variable))
; let, var, const expressions
((primary (symbol) @variable.builtin)
(#match? @variable.builtin "result"))
; `result` is an implicit builtin variable inside function scopes
((primary (symbol) @type)
(#match? @type "^[A-Z]"))
; assume PascalCase identifiers to be types
((primary
(primarySuffix
(qualifiedSuffix
(symbol) @type)))
(#match? @type "^[A-Z]"))
; assume PascalCase member variables to be enum entries
(primary (symbol) @variable)
; overzealous, matches variables
(primary (primarySuffix (qualifiedSuffix (symbol) @variable.other.member)))
; overzealous, matches member variables: i.e. x in foo.x
(keyw) @keyword
; more specific matches are done above whenever possible

@ -0,0 +1,54 @@
[
(typeDef)
(ifStmt)
(whenStmt)
(elifStmt)
(elseStmt)
(ofBranch) ; note: not caseStmt
(whileStmt)
(tryStmt)
(tryExceptStmt)
(tryFinallyStmt)
(forStmt)
(blockStmt)
(staticStmt)
(deferStmt)
(asmStmt)
; exprStmt?
] @indent
;; increase the indentation level
[
(ifStmt)
(whenStmt)
(elifStmt)
(elseStmt)
(ofBranch) ; note: not caseStmt
(whileStmt)
(tryStmt)
(tryExceptStmt)
(tryFinallyStmt)
(forStmt)
(blockStmt)
(staticStmt)
(deferStmt)
(asmStmt)
; exprStmt?
] @extend
;; ???
[
(returnStmt)
(raiseStmt)
(yieldStmt)
(breakStmt)
(continueStmt)
] @extend.prevent-once
;; end a level of indentation while staying indented
[
")" ; tuples
"]" ; arrays, seqs
"}" ; sets
] @outdent
;; end a level of indentation and unindent the line

@ -0,0 +1,19 @@
(routine
(block) @function.inside) @function.around
; @class.inside (types?)
; @class.around
; paramListSuffix is strange and i do not understand it
(paramList
(paramColonEquals) @parameter.inside) @parameter.around
(comment) @comment.inside
(multilineComment) @comment.inside
(docComment) @comment.inside
(multilineDocComment) @comment.inside
(comment)+ @comment.around
(multilineComment) @comment.around
(docComment)+ @comment.around
(multilineDocComment) @comment.around

@ -0,0 +1,68 @@
[
(import)
] @keyword.control.import
[
(package)
] @namespace
[
(with)
(as)
(every)
(some)
(in)
(default)
"null"
] @keyword.control
[
(not)
(if)
(contains)
(else)
] @keyword.control.conditional
[
(boolean)
] @constant.builtin.boolean
[
(assignment_operator)
(bool_operator)
(arith_operator)
(bin_operator)
] @operator
[
(string)
(raw_string)
] @string
(term (ref (var))) @variable
(comment) @comment.line
(number) @constant.numeric.integer
(expr_call func_name: (fn_name (var) @function .))
(expr_call func_arguments: (fn_args (expr) @variable.parameter))
(rule_args (term) @variable.parameter)
[
(open_paren)
(close_paren)
(open_bracket)
(close_bracket)
(open_curly)
(close_curly)
] @punctuation.bracket
(rule (rule_head (var) @function.method))
(rule
(rule_head (term (ref (var) @namespace)))
(rule_body (query (literal (expr (expr_infix (expr (term (ref (var)) @_output)))))) (#eq? @_output @namespace))
)

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save