diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9518a537..b509ff9e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,7 @@ jobs: rust: stable target: x86_64-pc-windows-msvc cross: false + # 23.03: build issues - build: aarch64-macos os: macos-latest rust: stable @@ -113,6 +114,12 @@ jobs: mkdir -p runtime/grammars/sources tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources + # The rust-toolchain action ignores rust-toolchain.toml files. + # Removing this before building with cargo ensures that the rust-toolchain + # is considered the same between installation and usage. + - name: Remove the rust-toolchain.toml file + run: rm rust-toolchain.toml + - name: Install ${{ matrix.rust }} toolchain uses: dtolnay/rust-toolchain@master with: @@ -155,6 +162,10 @@ jobs: shell: bash if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux' run: | + # Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE + sudo add-apt-repository universe + sudo apt install libfuse2 + mkdir dist name=dev @@ -244,7 +255,7 @@ jobs: exe=".exe" fi pkgname=helix-$GITHUB_REF_NAME-$platform - mkdir $pkgname + mkdir -p $pkgname cp $source/LICENSE $source/README.md $pkgname mkdir $pkgname/contrib cp -r $source/contrib/completion $pkgname/contrib diff --git a/CHANGELOG.md b/CHANGELOG.md index dc91c9ff..01184571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` ([#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 `:` 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) This is a great big release filled with changes from a 99 contributors. A big _thank you_ to you all! diff --git a/Cargo.lock b/Cargo.lock index c86b5aac..26448c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" [[package]] name = "arc-swap" @@ -81,9 +81,9 @@ checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" [[package]] name = "bstr" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", "once_cell", @@ -102,9 +102,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytecount" @@ -114,9 +114,9 @@ checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cassowary" @@ -149,9 +149,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "num-integer", @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.82" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" dependencies = [ "cc", "cxxbridge-flags", @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.82" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" dependencies = [ "cc", "codespan-reporting", @@ -260,24 +260,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 1.0.104", + "syn 2.0.11", ] [[package]] name = "cxxbridge-flags" -version = "1.0.82" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" [[package]] name = "cxxbridge-macro" -version = "1.0.82" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.11", ] [[package]] @@ -329,9 +329,9 @@ checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "encoding_rs" @@ -353,13 +353,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.8" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys", ] [[package]] @@ -395,32 +395,32 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "fern" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" dependencies = [ "log", ] [[package]] name = "filetime" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" dependencies = [ "cfg-if", "libc", - "redox_syscall", - "windows-sys 0.42.0", + "redox_syscall 0.2.16", + "windows-sys", ] [[package]] @@ -450,15 +450,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -467,15 +467,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-task", @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "gix" -version = "0.41.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1853c840375a04e02315cb225b6d802291abfabbf08e500727c98a8670f1bf1" +checksum = "c256ea71cc1967faaefdaad15f334146b7c806f12460dcafd3afed845c8c78dd" dependencies = [ "gix-actor", "gix-attributes", @@ -604,9 +604,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aa7d7dd60256b7a0c0506a1d708ec92767c2662ee57b3301b538eaa3e064f8a" +checksum = "7fbad5ce54a8fc997acc50febd89ec80fa6e97cb7f8d0654cb229936407489d8" dependencies = [ "bstr", "gix-config-value", @@ -615,6 +615,7 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", + "log", "memchr", "nom", "once_cell", @@ -625,9 +626,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d4a4ba0531e46fe558459557a5b29fb86c3e4b2666c1c0861d93c7c678331" +checksum = "d09154c0c8677e4da0ec35e896f56ee3e338e741b9599fae06075edd83a4081c" dependencies = [ "bitflags 1.3.2", "bstr", @@ -666,9 +667,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585b0834d4b6791a848637c4e109545fda9b0f29b591ba55edb33ceda6e7856b" +checksum = "103a0fa79b0d438f5ecb662502f052e530ace4fe1fe8e1c83c0c6da76d728e67" dependencies = [ "gix-hash", "gix-object", @@ -678,9 +679,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.16.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b58931ab475a977deff03417e041a66e4bcb76c4e5797e7ec2fcb272ebce01c" +checksum = "6eba8ba458cb8f4a6c33409b0fe650b1258655175a7ffd1d24fafd3ed31d880b" dependencies = [ "bstr", "dunce", @@ -693,9 +694,9 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6a9dfa7b3c1a99315203e8b97f8f99f3bd95731590607abeaa5ca31bc41fe3" +checksum = "0b76f9a80f6dd7be66442ae86e1f534effad9546676a392acc95e269d0c21c22" dependencies = [ "crc32fast", "flate2", @@ -741,9 +742,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "546ee7855d5d8731288f05a63c07ab41b59cb406660a825ed3fe89d7223823df" +checksum = "717ab601ece7921f59fe86849dbe27d44a46ebb883b5885732c4f30df4996177" dependencies = [ "bitflags 1.3.2", "bstr", @@ -804,9 +805,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa63fce01e5bce663bb24ad01fa2b77266e91b1d1982aab3f67cb0aed8af8169" +checksum = "e83af2e3e36005bfe010927f0dff41fb5acc3e3d89c6f1174135b3a34086bda2" dependencies = [ "arc-swap", "gix-features", @@ -822,9 +823,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bc9d22f0d0620d013003ab05ead5c60124b6e1101bc245be9be4fd7e2330cb" +checksum = "9401911c7fe032ad7b31c6a6b5be59cb283d1d6c999417a8215056efe6d635f3" dependencies = [ "clru", "gix-chunk", @@ -844,9 +845,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c104a66dec149cb8f7aaafc6ab797654cf82d67f050fd0cb7e7294e328354b" +checksum = "32370dce200bb951df013e03dff35b4233fc7a89458642b047629b91734a7e19" dependencies = [ "bstr", "thiserror", @@ -854,9 +855,9 @@ dependencies = [ [[package]] name = "gix-prompt" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20cebf73229debaa82574c4fd20dcaf00fa8d4bfce823a862c4e990d7a0b5b4" +checksum = "0f3034d4d935aef2c7bf719aaa54b88c520e82413118d886ae880a31d5bdee57" dependencies = [ "gix-command", "gix-config-value", @@ -878,9 +879,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.27.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de7cd1050fa82be4240994defc1f1f2fd9def5d8815dcd005f5ddc5e3dc7511" +checksum = "e4e909396ed3b176823991ccc391c276ae2a015e54edaafa3566d35123cfac9d" dependencies = [ "gix-actor", "gix-features", @@ -911,9 +912,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed98e4a0254953c64bc913bd23146a1de662067d5cf974cbdde396958b39e5b0" +checksum = "b12fc4bbc3161a5b2d68079fce93432cef8771ff88ca017abb01187fddfc41a1" dependencies = [ "bstr", "gix-date", @@ -933,14 +934,14 @@ dependencies = [ "dirs", "gix-path", "libc", - "windows", + "windows 0.43.0", ] [[package]] name = "gix-tempfile" -version = "5.0.1" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aed73ef9642f779d609fd19acc332ac1597b978ee87ec11743a68eefaed65bfa" +checksum = "c2ceb30a610e3f5f2d5f9a5114689fde507ba9417705a8cf3429604275b2153c" dependencies = [ "libc", "once_cell", @@ -978,9 +979,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69ddb780ea1465255e66818d75b7098371c58dbc9560da4488a44b9f5c7e443" +checksum = "7bd629d3680773e1785e585d76fd4295b740b559cad9141517300d99a0c8c049" dependencies = [ "bstr", "thiserror", @@ -988,9 +989,9 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "992b8fdade33e079dc61c29f2388ab8e049965ebf7be40efa7f8b80e3c4543fe" +checksum = "54ec9a000b4f24af706c3cc680c7cda235656cbe3216336522f5692773b8a301" dependencies = [ "bstr", "gix-attributes", @@ -1082,6 +1083,7 @@ dependencies = [ "arc-swap", "bitflags 2.0.2", "chrono", + "dunce", "encoding_rs", "etcetera", "hashbrown 0.13.2", @@ -1149,6 +1151,7 @@ dependencies = [ "helix-parsec", "log", "lsp-types", + "parking_lot", "serde", "serde_json", "thiserror", @@ -1220,6 +1223,7 @@ dependencies = [ name = "helix-vcs" version = "0.6.0" dependencies = [ + "anyhow", "arc-swap", "gix", "helix-core", @@ -1263,13 +1267,19 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -1287,16 +1297,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "716f12fbcfac6ffab0a5e9ec51d0a0ff70503742bb2dc7b99396394c9dc323f0" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows 0.47.0", ] [[package]] @@ -1348,9 +1358,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -1383,25 +1393,26 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.4" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" dependencies = [ + "hermit-abi 0.3.1", "libc", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" dependencies = [ "wasm-bindgen", ] @@ -1430,18 +1441,18 @@ dependencies = [ [[package]] name = "link-cplusplus" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" dependencies = [ "cc", ] [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d" [[package]] name = "lock_api" @@ -1483,9 +1494,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] @@ -1507,21 +1518,21 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "nix" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -1531,23 +1542,14 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -1569,11 +1571,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -1604,15 +1606,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -1635,18 +1637,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" -version = "23.1.1" +version = "23.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73c6b64cb5b99eb63ca97d378685712617ec0172ff5c04cd47a489d3e2c51f8" +checksum = "9516b775656bc3e8985e19cd4b8c0c0de045095074e453d2c0a513b5f978392d" [[package]] name = "pulldown-cmark" @@ -1704,6 +1706,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -1711,15 +1722,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.7.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -1734,9 +1745,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "ropey" @@ -1750,23 +1761,23 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.7" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +checksum = "0e78cc525325c06b4a7ff02db283472f3c042b7ff0c391f96c6d5ac6f4f91b75" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "same-file" @@ -1785,35 +1796,35 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.158" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.158" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.11", ] [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" dependencies = [ "itoa", "ryu", @@ -1822,13 +1833,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.11", ] [[package]] @@ -1869,9 +1880,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] @@ -1890,9 +1901,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] @@ -1931,9 +1942,9 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -1953,15 +1964,15 @@ checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "str_indices" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0" +checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd" [[package]] name = "syn" -version = "1.0.104" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -1970,9 +1981,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.4" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" +checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40" dependencies = [ "proc-macro2", "quote", @@ -1981,22 +1992,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -2023,30 +2034,31 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.11", ] [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] @@ -2061,9 +2073,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.17" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa", "libc", @@ -2081,9 +2093,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" dependencies = [ "time-core", ] @@ -2099,20 +2111,19 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.26.0" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", @@ -2120,18 +2131,18 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.11", ] [[package]] @@ -2147,9 +2158,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" dependencies = [ "serde", "serde_spanned", @@ -2168,21 +2179,22 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.3" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap", - "nom8", "serde", "serde_spanned", "toml_datetime", + "winnow", ] [[package]] name = "tree-sitter" -version = "0.20.9" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" dependencies = [ "cc", "regex", @@ -2199,9 +2211,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-bom" @@ -2217,9 +2229,9 @@ checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-linebreak" @@ -2272,12 +2284,11 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi", "winapi-util", ] @@ -2289,9 +2300,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2299,24 +2310,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.104", + "syn 1.0.109", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2324,22 +2335,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "which" @@ -2389,28 +2400,22 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "2649ff315bee4c98757f15dac226efe3d81927adbb6e882084bb1ee3e0c330a7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-targets 0.47.0", ] [[package]] @@ -2419,65 +2424,131 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "2f8996d3f43b4b2d44327cd71b7b0efd1284ab60e6e9d0e8b630e18555d87d3e" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.47.0", + "windows_aarch64_msvc 0.47.0", + "windows_i686_gnu 0.47.0", + "windows_i686_msvc 0.47.0", + "windows_x86_64_gnu 0.47.0", + "windows_x86_64_gnullvm 0.47.0", + "windows_x86_64_msvc 0.47.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831d567d53d4f3cb1db332b68e6e2b6260228eb4d99a777d8b2e8ed794027c90" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a42d54a417c60ce4f0e31661eed628f0fa5aca73448c093ec4d45fab4c51cdf" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "c1925beafdbb22201a53a483db861a5644123157c1c3cee83323a2ed565d71e3" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8ef8f2f1711b223947d9b69b596cf5a4e452c930fb58b6fc3fdae7d0ec6b31" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acaa0c2cf0d2ef99b61c308a0c3dbae430a51b7345dedec470bd8f53f5a3642" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a0628f71be1d11e17ca4a0e9e15b3a5180f6fbf1c2d55e3ba3f850378052c1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9d6e62c256dc6d40b8c8707df17df8d774e60e39db723675241e7c15e910bce7" + +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] [[package]] name = "xtask" diff --git a/Cargo.toml b/Cargo.toml index aaa21659..c6351889 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,3 @@ inherits = "test" package.helix-core.opt-level = 2 package.helix-tui.opt-level = 2 package.helix-term.opt-level = 2 - -[patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" } diff --git a/README.md b/README.md index cba52a7a..6d8d0942 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - Helix + Helix @@ -18,7 +18,7 @@ ![Screenshot](./screenshot.png) -A Kakoune / Neovim inspired editor, written in Rust. +This is a fork of helix, a Kakoune / Neovim inspired editor, written in Rust. The editing model is very heavily based on Kakoune; during development I found myself agreeing with most of Kakoune's design decisions. @@ -43,6 +43,16 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer Note: Only certain languages have indentation definitions at the moment. Check `runtime/queries//` for `indents.scm`. +# Additional Features of helix-plus + +- [File Explorer](https://github.com/helix-editor/helix/pull/5768) + - Added automatic update of the current file in the tree view + - Added icon support + - Added `--show-explorer` CLI flag to show the explorer when opening helix +- [Icons](https://github.com/helix-editor/helix/pull/2869) +- `rm` command to delete the file associated with the current buffer +- `dracula-purple` theme which is a combination of `dracula` and `boo_berry` + # Installation [Installation documentation](https://docs.helix-editor.com/install.html). diff --git a/VERSION b/VERSION index e70b3aeb..35371314 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -22.12 \ No newline at end of file +23.03 \ No newline at end of file diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 6e780b87..0976b376 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -10,6 +10,7 @@ - [Migrating from Vim](./from-vim.md) - [Configuration](./configuration.md) - [Themes](./themes.md) + - [Icons](./icons.md) - [Key remapping](./remapping.md) - [Languages](./languages.md) - [Guides](./guides/README.md) diff --git a/book/src/configuration.md b/book/src/configuration.md index bf314993..152f364f 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -11,6 +11,7 @@ Example config: ```toml theme = "onedark" +icons = "nerdfonts" [editor] 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 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]` 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` | | `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` | +| `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 @@ -104,6 +109,7 @@ The following statusline elements can be configured: | `file-line-ending` | The file line endings (CRLF or LF) | | `total-line-numbers` | The total line numbers of the opened file | | `file-type` | The type of the opened file | +| `file-type-icon` | The icon representing the language of the open file, or else its file type (see `[editor.icons]` section) | | `diagnostics` | The number of warnings and/or errors | | `workspace-diagnostics` | The number of warnings and/or errors on workspace | | `selections` | The number of active selections | @@ -123,6 +129,7 @@ The following statusline elements can be configured: | `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | | `display-inlay-hints` | Display inlay hints[^2] | `false` | | `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. [^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 (){}[]''""``, but these can be customized by setting `auto-pairs` to a TOML table: +Example + ```toml [editor.auto-pairs] '(' = ')' @@ -305,7 +314,7 @@ Example: min-width = 1 ``` -#### `[editor.gutters.diagnotics]` Section +#### `[editor.gutters.diagnostics]` Section Currently unused @@ -317,6 +326,18 @@ Currently unused Currently unused +### `[editor.icons]` Section + +Option for displaying icons within the editor. + +> Warning: some symbols (such as file-type and symbol-kind icons that you would see in the picker) are not available in the "default" icon set. They usually require a patched font such as [NerdFonts](https://www.nerdfonts.com/) to be installed and configured in your terminal emulator, and the corresponding icon set to be configured in the editor (for example, using `icons = "nerdfonts"` in your configuration file). + +| Key | Description | Default | +| --- | --- | --- | +| `picker` | Whether icons in pickers are enabled. | `true` | +| `bufferline` | Whether icons in the buffer line are enabled. | `true` | +| `statusline` | Whether icons in the status line are enabled. | `true` | + ### `[editor.soft-wrap]` Section Options for soft wrapping lines that exceed the view width: @@ -338,3 +359,11 @@ max-wrap = 25 # increase value to reduce forced mid-word wrapping max-indent-retain = 0 wrap-indicator = "" # set wrap-indicator to "" to hide it ``` + +### `[editor.explorer]` Section +Sets explorer side width and style. + + | Key | Description | Default | + | --- | ----------- | ------- | + | `column-width` | explorer side width | 30 | + | `position` | explorer widget position, `left` or `right` | `left` | \ No newline at end of file diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 3c18956a..3f56dd60 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -41,7 +41,7 @@ | fortran | ✓ | | ✓ | `fortls` | | gdscript | ✓ | ✓ | ✓ | | | git-attributes | ✓ | | | | -| git-commit | ✓ | | | | +| git-commit | ✓ | ✓ | | | | git-config | ✓ | | | | | git-ignore | ✓ | | | | | git-rebase | ✓ | | | | @@ -59,6 +59,7 @@ | heex | ✓ | ✓ | | `elixir-ls` | | hosts | ✓ | | | | | html | ✓ | | | `vscode-html-language-server` | +| hurl | ✓ | | ✓ | | | idris | | | | `idris2-lsp` | | iex | ✓ | | | | | ini | ✓ | | | | @@ -68,7 +69,7 @@ | json | ✓ | | ✓ | `vscode-json-language-server` | | jsonnet | ✓ | | | `jsonnet-language-server` | | jsx | ✓ | ✓ | ✓ | `typescript-language-server` | -| julia | ✓ | | | `julia` | +| julia | ✓ | ✓ | ✓ | `julia` | | kdl | ✓ | | | | | kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | ✓ | | `texlab` | @@ -88,6 +89,7 @@ | msbuild | ✓ | | ✓ | | | nasm | ✓ | ✓ | | | | nickel | ✓ | | ✓ | `nls` | +| nim | ✓ | ✓ | ✓ | `nimlangserver` | | nix | ✓ | | | `nil` | | nu | ✓ | | | | | ocaml | ✓ | | ✓ | `ocamllsp` | @@ -112,8 +114,10 @@ | r | ✓ | | | `R` | | racket | ✓ | | | `racket` | | regex | ✓ | | | | +| rego | ✓ | | | `regols` | | rescript | ✓ | ✓ | | `rescript-language-server` | | rmarkdown | ✓ | | ✓ | `R` | +| robot | ✓ | | | `robotframework_ls` | | ron | ✓ | | ✓ | | | rst | ✓ | | | | | ruby | ✓ | ✓ | ✓ | `solargraph` | @@ -145,6 +149,7 @@ | v | ✓ | ✓ | ✓ | `v` | | vala | ✓ | | | `vala-language-server` | | verilog | ✓ | ✓ | | `svlangserver` | +| vhdl | ✓ | | | `vhdl_ls` | | vhs | ✓ | | | | | vue | ✓ | | | `vls` | | wast | ✓ | | | | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 8b367aad..2c5356b0 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -29,6 +29,7 @@ | `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | | `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). | | `:theme` | Change the editor theme (show current theme if no name specified). | +| `:icons` | Change the editor icon flavor (show current flavor if no name specified). | | `:clipboard-yank` | Yank main selection into system clipboard. | | `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. | | `:primary-clipboard-yank` | Yank main selection into system primary clipboard. | @@ -70,6 +71,7 @@ | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:config-reload` | Refresh user config. | | `:config-open` | Open the user config.toml file. | +| `:config-open-workspace` | Open the workspace config.toml file. | | `:log-open` | Open the helix log file. | | `:insert-output` | Run shell command, inserting output before each selection. | | `:append-output` | Run shell command, appending output after each selection. | diff --git a/book/src/icons.md b/book/src/icons.md new file mode 100644 index 00000000..a499a979 --- /dev/null +++ b/book/src/icons.md @@ -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 = ""` to your [`config.toml`](./configuration.md) at the very top of the file before the first section or select it during runtime using `:icons `. + +## 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 = "#…" } +``` diff --git a/book/src/keymap.md b/book/src/keymap.md index 173728f2..7d5c4c79 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -291,6 +291,8 @@ This layer is a kludge of mappings, mostly pickers. | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | | `/` | Global search in workspace folder | `global_search` | | `?` | Open command palette | `command_palette` | +| `e` | Reveal current file in explorer | `reveal_current_file` | + > 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file. @@ -442,3 +444,7 @@ Keys to use within prompt, Remapping currently not supported. | `Tab` | Select next completion item | | `BackTab` | Select previous completion item | | `Enter` | Open selected | + +# File explorer + +Press `?` to see keymaps. Remapping currently not supported. diff --git a/book/src/languages.md b/book/src/languages.md index 5ed69505..f44509fc 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -64,6 +64,7 @@ These configuration keys are available: | `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 | | `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 diff --git a/book/src/themes.md b/book/src/themes.md index 994542c5..eb8bff89 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -228,6 +228,8 @@ We use a similar set of scopes as - `list` - `unnumbered` - `numbered` + - `checked` + - `unchecked` - `bold` - `italic` - `strikethrough` @@ -276,8 +278,11 @@ These scopes are used for theming the editor interface: | `ui.cursor.primary.normal` | | | `ui.cursor.primary.insert` | | | `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.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.selected` | Line number for the line the cursor is on | | `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.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) | | `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) | -| `warning` | Diagnostics warning (gutter) | -| `error` | Diagnostics error (gutter) | -| `info` | Diagnostics info (gutter) | -| `hint` | Diagnostics hint (gutter) | +| `warning` | Diagnostics warning icon (gutter, statusline, and diagnostic pickers) | +| `error` | Diagnostics error icon (gutter, statusline, and diagnostic pickers) | +| `info` | Diagnostics info icon (gutter, statusline, and diagnostic pickers) | +| `hint` | Diagnostics hint icon (gutter, statusline, and diagnostic pickers) | | `diagnostic` | Diagnostics fallback style (editing area) | | `diagnostic.hint` | Diagnostics hint (editing area) | | `diagnostic.info` | Diagnostics info (editing area) | | `diagnostic.warning` | Diagnostics warning (editing area) | | `diagnostic.error` | Diagnostics error (editing area) | +| `symbolkind` | Symbol kind icons (symbol picker) | [editor-section]: ./configuration.md#editor-section diff --git a/contrib/Helix.appdata.xml b/contrib/Helix.appdata.xml index a2428497..b99738a1 100644 --- a/contrib/Helix.appdata.xml +++ b/contrib/Helix.appdata.xml @@ -36,6 +36,9 @@ + + https://helix-editor.com/news/release-23-03-highlights/ + https://helix-editor.com/news/release-22-12-highlights/ diff --git a/flake.lock b/flake.lock index fa292273..d33c404e 100644 --- a/flake.lock +++ b/flake.lock @@ -18,9 +18,6 @@ }, "dream2nix": { "inputs": { - "alejandra": [ - "nci" - ], "all-cabal-json": [ "nci" ], @@ -28,6 +25,8 @@ "devshell": [ "nci" ], + "drv-parts": "drv-parts", + "flake-compat": "flake-compat", "flake-parts": [ "nci", "parts" @@ -51,6 +50,7 @@ "nci", "nixpkgs" ], + "nixpkgsV1": "nixpkgsV1", "poetry2nix": [ "nci" ], @@ -62,11 +62,11 @@ ] }, "locked": { - "lastModified": 1677289985, - "narHash": "sha256-lUp06cTTlWubeBGMZqPl9jODM99LpWMcwxRiscFAUJg=", + "lastModified": 1680258209, + "narHash": "sha256-lEo50RXI/17/a9aCIun8Hz62ZJ5JM5RGeTgclIP+Lgc=", "owner": "nix-community", "repo": "dream2nix", - "rev": "28b973a8d4c30cc1cbb3377ea2023a76bc3fb889", + "rev": "6f512b5a220fdb26bd3c659f7b55e4f052ec8b35", "type": "github" }, "original": { @@ -75,6 +75,54 @@ "type": "github" } }, + "drv-parts": { + "inputs": { + "flake-compat": [ + "nci", + "dream2nix", + "flake-compat" + ], + "flake-parts": [ + "nci", + "dream2nix", + "flake-parts" + ], + "nixpkgs": [ + "nci", + "dream2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1680172861, + "narHash": "sha256-QMyI338xRxaHFDlCXdLCtgelGQX2PdlagZALky4ZXJ8=", + "owner": "davhau", + "repo": "drv-parts", + "rev": "ced8a52f62b0a94244713df2225c05c85b416110", + "type": "github" + }, + "original": { + "owner": "davhau", + "repo": "drv-parts", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "locked": { "lastModified": 1659877975, @@ -119,11 +167,11 @@ ] }, "locked": { - "lastModified": 1677297103, - "narHash": "sha256-ArlJIbp9NGV9yvhZdV0SOUFfRlI/kHeKoCk30NbSiLc=", + "lastModified": 1680329418, + "narHash": "sha256-+KN0eQLSZvL1J0kDO8/fxv0UCHTyZCADLmpIfeeiSGo=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "a79272a2cb0942392bb3a5bf9a3ec6bc568795b2", + "rev": "98c1d2ff5155f0fee5d290f6b982cb990839d540", "type": "github" }, "original": { @@ -134,11 +182,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1677063315, - "narHash": "sha256-qiB4ajTeAOVnVSAwCNEEkoybrAlA+cpeiBxLobHndE8=", + "lastModified": 1680213900, + "narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "988cc958c57ce4350ec248d2d53087777f9e1949", + "rev": "e3652e0735fbec227f342712f180f4f21f0594f2", "type": "github" }, "original": { @@ -151,11 +199,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1675183161, - "narHash": "sha256-Zq8sNgAxDckpn7tJo7V1afRSk2eoVbu3OjI1QklGLNg=", + "lastModified": 1678375444, + "narHash": "sha256-XIgHfGvjFvZQ8hrkfocanCDxMefc/77rXeHvYdzBMc8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e1e1b192c1a5aab2960bf0a0bd53a2e8124fa18e", + "rev": "130fa0baaa2b93ec45523fdcde942f6844ee9f6e", "type": "github" }, "original": { @@ -166,6 +214,21 @@ "type": "github" } }, + "nixpkgsV1": { + "locked": { + "lastModified": 1678500271, + "narHash": "sha256-tRBLElf6f02HJGG0ZR7znMNFv/Uf7b2fFInpTHiHaSE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5eb98948b66de29f899c7fe27ae112a47964baf8", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-22.11", + "type": "indirect" + } + }, "parts": { "inputs": { "nixpkgs-lib": [ @@ -174,11 +237,11 @@ ] }, "locked": { - "lastModified": 1675933616, - "narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=", + "lastModified": 1679737941, + "narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "47478a4a003e745402acf63be7f9a092d51b83d7", + "rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c", "type": "github" }, "original": { @@ -192,11 +255,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1675933616, - "narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=", + "lastModified": 1679737941, + "narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "47478a4a003e745402acf63be7f9a092d51b83d7", + "rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c", "type": "github" }, "original": { @@ -221,11 +284,11 @@ ] }, "locked": { - "lastModified": 1677292251, - "narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=", + "lastModified": 1680315536, + "narHash": "sha256-0AsBuKssJMbcRcw4HJQwJsUHhZxR5+gaf6xPQayhR44=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc", + "rev": "5c8c151bdd639074a0051325c16df1a64ee23497", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2ac76488..6dcaf6cc 100644 --- a/flake.nix +++ b/flake.nix @@ -123,8 +123,6 @@ then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment'' else "$RUSTFLAGS"; in { - # by default NCI adds rust-analyzer component, but helix toolchain doesn't have rust-analyzer - nci.toolchains.shell.components = ["rust-src" "rustfmt" "clippy"]; nci.projects."helix-project".relPath = ""; nci.crates."helix-term" = { overrides = { diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 9dfef9ae..e5c5f8f1 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -32,6 +32,7 @@ regex = "1" bitflags = "2.0" ahash = "0.8.3" hashbrown = { version = "0.13.2", features = ["raw"] } +dunce = "1.0" log = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index e3f862a6..b67e2c8a 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -36,55 +36,12 @@ pub mod unicode { pub use unicode_width as width; } +pub use helix_loader::find_workspace; + pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option { 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 tendril::StrTendril as Tendril; @@ -98,7 +55,7 @@ pub use {regex, tree_sitter}; pub use graphemes::RopeGraphemes; pub use position::{ 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)] pub use position::{pos_at_visual_coords, visual_coords_at_pos}; diff --git a/helix-core/src/path.rs b/helix-core/src/path.rs index d59a6baa..efa46c46 100644 --- a/helix-core/src/path.rs +++ b/helix-core/src/path.rs @@ -40,6 +40,21 @@ pub fn expand_tilde(path: &Path) -> PathBuf { /// needs to improve on. /// Copied from cargo: 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 ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { 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. @@ -82,13 +97,19 @@ pub fn get_canonicalized_path(path: &Path) -> std::io::Result { } pub fn get_relative_path(path: &Path) -> PathBuf { + let path = PathBuf::from(path); let path = if path.is_absolute() { - let cwdir = std::env::current_dir().expect("couldn't determine current directory"); - path.strip_prefix(cwdir).unwrap_or(path) + let cwdir = std::env::current_dir() + .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 { path }; - fold_home_dir(path) + fold_home_dir(&path) } /// Returns a truncated filepath where the basepart of the path is reduced to the first diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 7b8dc326..ee764bc6 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -109,7 +109,7 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po /// softwrapping positions are estimated with an O(1) algorithm /// to ensure consistent performance for large lines (currently unimplemented) /// -/// Usualy you want to use `visual_offset_from_anchor` instead but this function +/// Usually you want to use `visual_offset_from_anchor` instead but this function /// can be useful (and faster) if /// * You already know the visual position of the block /// * You only care about the horizontal offset (column) and not the vertical offset (row) @@ -137,6 +137,12 @@ pub fn visual_offset_from_block( (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 /// that contains anchor. pub fn visual_offset_from_anchor( @@ -146,28 +152,46 @@ pub fn visual_offset_from_anchor( text_fmt: &TextFormat, annotations: &TextAnnotations, max_rows: usize, -) -> Option<(Position, usize)> { +) -> Result<(Position, usize), VisualOffsetError> { let (formatter, block_start) = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); let mut char_pos = block_start; let mut anchor_line = None; + let mut found_pos = None; let mut last_pos = Position::default(); + if pos < block_start { + return Err(VisualOffsetError::PosBeforeAnchorRow); + } + for (grapheme, vpos) in formatter { last_pos = vpos; char_pos += grapheme.doc_chars(); - if char_pos > anchor && anchor_line.is_none() { - 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 { + 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 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); + } } if let Some(anchor_line) = anchor_line { 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); last_pos.row -= anchor_line; - Some((last_pos, block_start)) + Ok((last_pos, block_start)) } /// 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 /// start of the grapheme to the left is returned. If there is no grapheme -/// to the left (for example if the line starts with virtual text) then the positiong +/// to the left (for example if the line starts with virtual text) then the positioning /// of the next grapheme to the right is returned. /// /// If the `line` coordinate is beyond the end of the file, the EOF @@ -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: /// 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`) -pub fn char_idx_at_visual_offset<'a>( - text: RopeSlice<'a>, +pub fn char_idx_at_visual_offset( + text: RopeSlice, mut anchor: usize, mut row_offset: isize, column: usize, text_fmt: &TextFormat, annotations: &TextAnnotations, ) -> (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) loop { 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; anchor = block_char_offset; if row_offset >= 0 { @@ -308,10 +333,10 @@ pub fn char_idx_at_visual_offset<'a>( break; } // 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 - // this char index is also always a line earlier so increase the row_offset by 1 + // set the anchor to the last char before the current block so that we can compute + // the distance of this block from the start of the previous block + pos = anchor; anchor -= 1; - row_offset += 1; } char_idx_at_visual_block_offset( diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 8e93c633..259b131a 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -38,7 +38,7 @@ use std::borrow::Cow; /// Ranges are considered to be inclusive on the left and /// exclusive on the right, regardless of anchor-head ordering. /// This means, for example, that non-zero-width ranges that -/// are directly adjecent, sharing an edge, do not overlap. +/// are directly adjacent, sharing an edge, do not overlap. /// However, a zero-width range will overlap with the shared /// left-edge of another range. /// diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 0883eb91..9d873c36 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -294,14 +294,14 @@ mod test { #[test] fn test_lists() { let input = - r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#; + r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "quotes"]'"#; let shellwords = Shellwords::from(input); let result = shellwords.words().to_vec(); let expected = vec![ Cow::from(":set"), Cow::from("statusline.center"), Cow::from(r#"["file-type","file-encoding"]"#), - Cow::from(r#"["list", "in", "qoutes"]"#), + Cow::from(r#"["list", "in", "quotes"]"#), ]; assert_eq!(expected, result); } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index e494ee9b..c34ea81a 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -20,7 +20,7 @@ use std::{ fmt, hash::{Hash, Hasher}, mem::{replace, transmute}, - path::Path, + path::{Path, PathBuf}, str::FromStr, sync::Arc, }; @@ -127,6 +127,10 @@ pub struct LanguageConfiguration { pub auto_pairs: Option, pub rulers: Option>, // 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>, } #[derive(Debug, PartialEq, Eq, Hash)] @@ -551,6 +555,8 @@ impl LanguageConfiguration { #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct SoftWrap { /// Soft wrap lines that exceed viewport width. Default to off + // NOTE: Option on purpose because the struct is shared between language config and global config. + // By default the option is None so that the language config falls back to the global config unless explicitly set. pub enable: Option, /// 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 diff --git a/helix-core/src/text_annotations.rs b/helix-core/src/text_annotations.rs index 3e48de4d..11d19d48 100644 --- a/helix-core/src/text_annotations.rs +++ b/helix-core/src/text_annotations.rs @@ -1,5 +1,4 @@ use std::cell::Cell; -use std::convert::identity; use std::ops::Range; use std::rc::Rc; @@ -113,9 +112,7 @@ impl Layer { pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) { let new_index = self .annotations - .binary_search_by_key(&char_idx, get_char_idx) - .unwrap_or_else(identity); - + .partition_point(|annot| get_char_idx(annot) < char_idx); self.current_index.set(new_index); } @@ -175,7 +172,7 @@ impl TextAnnotations { for char_idx in char_range { if let Some((_, Some(highlight))) = self.overlay_at(char_idx) { // we don't know the number of chars the original grapheme takes - // however it doesn't matter as highlight bounderies are automatically + // however it doesn't matter as highlight boundaries are automatically // aligned to grapheme boundaries in the rendering code highlights.push((highlight.0, char_idx..char_idx + 1)) } @@ -206,7 +203,7 @@ impl TextAnnotations { /// Add new grapheme overlays. /// - /// The overlayed grapheme will be rendered with `highlight` + /// The overlaid grapheme will be rendered with `highlight` /// patched on top of `ui.text`. /// /// The overlays **must be sorted** by their `char_idx`. diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index ff727d00..7efb72d8 100644 --- a/helix-dap/src/client.rs +++ b/helix-dap/src/client.rs @@ -512,4 +512,10 @@ impl Client { self.call::(args) } + + pub fn current_stack_frame(&self) -> Option<&StackFrame> { + self.stack_frames + .get(&self.thread_id?)? + .get(self.active_frame?) + } } diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index 0f329d21..d092d20f 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -9,37 +9,38 @@ pub fn default_lang_config() -> toml::Value { /// User configured languages.toml file, merged with the default config. pub fn user_lang_config() -> Result { - let config = crate::local_config_dirs() - .into_iter() - .chain([crate::config_dir()].into_iter()) - .map(|path| path.join("languages.toml")) - .filter_map(|file| { - std::fs::read_to_string(file) - .map(|config| toml::from_str(&config)) - .ok() - }) - .collect::, _>>()? - .into_iter() - .chain([default_lang_config()].into_iter()) - .fold(toml::Value::Table(toml::value::Table::default()), |a, b| { - // combines for example - // b: - // [[language]] - // name = "toml" - // language-server = { command = "taplo", args = ["lsp", "stdio"] } - // - // a: - // [[language]] - // language-server = { command = "/usr/bin/taplo" } - // - // into: - // [[language]] - // name = "toml" - // 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 - crate::merge_toml_values(b, a, 3) - }); + let config = [ + crate::config_dir(), + crate::find_workspace().0.join(".helix"), + ] + .into_iter() + .map(|path| path.join("languages.toml")) + .filter_map(|file| { + std::fs::read_to_string(file) + .map(|config| toml::from_str(&config)) + .ok() + }) + .collect::, _>>()? + .into_iter() + .fold(default_lang_config(), |a, b| { + // combines for example + // b: + // [[language]] + // name = "toml" + // language-server = { command = "taplo", args = ["lsp", "stdio"] } + // + // a: + // [[language]] + // language-server = { command = "/usr/bin/taplo" } + // + // into: + // [[language]] + // name = "toml" + // 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 + crate::merge_toml_values(a, b, 3) + }); Ok(config) } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 04b44b5a..62d55f14 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -1,8 +1,14 @@ pub mod config; pub mod grammar; +use anyhow::{anyhow, Result}; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; -use std::path::{Path, PathBuf}; +use once_cell::sync::Lazy; +use std::{ + collections::{HashMap, HashSet}, + path::{Path, PathBuf}, +}; +use toml::Value; pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); @@ -42,7 +48,7 @@ fn prioritize_runtime_dirs() -> Vec { let mut rt_dirs = Vec::new(); 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 - 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()); rt_dirs.push(path); } @@ -113,15 +119,6 @@ pub fn config_dir() -> PathBuf { path } -pub fn local_config_dirs() -> Vec { - 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 { // TODO: allow env var override 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")) } +pub fn workspace_config_file() -> PathBuf { + find_workspace().0.join(".helix").join("config.toml") +} + pub fn lang_config_file() -> PathBuf { config_dir().join("languages.toml") } @@ -145,22 +146,6 @@ pub fn log_file() -> PathBuf { cache_dir().join("helix.log") } -pub fn find_local_config_dirs() -> Vec { - 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` /// /// When an array exists in both `left` and `right`, `right`'s array is @@ -175,8 +160,6 @@ pub fn find_local_config_dirs() -> Vec { /// where one usually wants to override or add to the array instead of /// replacing it altogether. pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value { - use toml::Value; - fn get_name(v: &Value) -> Option<&str> { v.get("name").and_then(Value::as_str) } @@ -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, + default_toml_data: &HashMap<&str, &Lazy>, + merge_toml_docs: fn(Value, Value) -> Value, +) -> Result { + 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, +) -> Result { + 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 { + 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 { + std::fs::read_dir(path) + .map(|entries| { + entries + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + (path.extension()? == "toml") + .then(|| path.file_stem().unwrap().to_string_lossy().into_owned()) + }) + .collect() + }) + .unwrap_or_default() +} + #[cfg(test)] mod merge_toml_tests { use std::str; @@ -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) +} diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 9d76822d..f8526515 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -24,6 +24,7 @@ lsp-types = { version = "0.94" } serde = { version = "1.0", features = ["derive"] } serde_json = "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" which = "4.4" +parking_lot = "0.12.1" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index f93e5826..93e822c4 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1,22 +1,26 @@ use crate::{ - jsonrpc, + find_lsp_workspace, jsonrpc, transport::{Payload, Transport}, 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 lsp::PositionEncodingKind; +use lsp::{ + notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf, + PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent, +}; use lsp_types as lsp; +use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; -use std::collections::HashMap; use std::future::Future; use std::process::Stdio; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, }; +use std::{collections::HashMap, path::PathBuf}; use tokio::{ io::{BufReader, BufWriter}, process::{Child, Command}, @@ -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)] pub struct Client { id: usize, @@ -36,11 +51,121 @@ pub struct Client { config: Option, root_path: std::path::PathBuf, root_uri: Option, - workspace_folders: Vec, + workspace_folders: Mutex>, + initialize_notify: Arc, + /// workspace folders added while the server is still initializing req_timeout: u64, } impl Client { + pub fn try_add_doc( + self: &Arc, + 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, + change_notifications: &Option>, + ) { + // 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::too_many_arguments)] pub fn start( @@ -49,6 +174,7 @@ impl Client { config: Option, server_environment: HashMap, root_markers: &[String], + manual_roots: &[PathBuf], id: usize, req_timeout: u64, doc_path: Option<&std::path::PathBuf>, @@ -75,27 +201,26 @@ impl Client { let (server_rx, server_tx, initialize_notify) = Transport::start(reader, writer, stderr, id); - - let root_path = find_root( - doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())), + 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 = 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 .clone() - .map(|root| { - vec![lsp::WorkspaceFolder { - name: root - .path_segments() - .and_then(|segments| segments.last()) - .map(|basename| basename.to_string()) - .unwrap_or_default(), - uri: root, - }] - }) + .map(|root| vec![workspace_for_uri(root)]) .unwrap_or_default(); let client = Self { @@ -106,10 +231,10 @@ impl Client { capabilities: OnceCell::new(), config, req_timeout, - root_path, root_uri, - workspace_folders, + workspace_folders: Mutex::new(workspace_folders), + initialize_notify: initialize_notify.clone(), }; Ok((client, server_rx, initialize_notify)) @@ -154,7 +279,7 @@ impl Client { "utf-16" => Some(OffsetEncoding::Utf16), "utf-32" => Some(OffsetEncoding::Utf32), encoding => { - log::error!("Server provided invalid position encording {encoding}, defaulting to utf-16"); + log::error!("Server provided invalid position encoding {encoding}, defaulting to utf-16"); None }, }) @@ -165,8 +290,10 @@ impl Client { self.config.as_ref() } - pub fn workspace_folders(&self) -> &[lsp::WorkspaceFolder] { - &self.workspace_folders + pub async fn workspace_folders( + &self, + ) -> parking_lot::MutexGuard<'_, Vec> { + self.workspace_folders.lock() } /// Execute a RPC request on the language server. @@ -286,7 +413,7 @@ impl Client { // General messages // ------------------------------------------------------------------------------------------- - pub(crate) async fn initialize(&self) -> Result { + pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result { if let Some(config) = &self.config { log::info!("Using custom LSP config: {}", config); } @@ -294,7 +421,7 @@ impl Client { #[allow(deprecated)] let params = lsp::InitializeParams { 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. // clients will prefer _uri if possible root_path: self.root_path.to_str().map(|path| path.to_owned()), @@ -334,7 +461,7 @@ impl Client { text_document: Some(lsp::TextDocumentClientCapabilities { completion: Some(lsp::CompletionClientCapabilities { completion_item: Some(lsp::CompletionItemCapability { - snippet_support: Some(true), + snippet_support: Some(enable_snippets), resolve_support: Some(lsp::CompletionItemCapabilityResolveSupport { properties: vec![ String::from("documentation"), @@ -413,8 +540,8 @@ impl Client { }), general: Some(lsp::GeneralClientCapabilities { position_encodings: Some(vec![ - PositionEncodingKind::UTF32, PositionEncodingKind::UTF8, + PositionEncodingKind::UTF32, PositionEncodingKind::UTF16, ]), ..Default::default() @@ -465,6 +592,16 @@ impl Client { ) } + pub fn did_change_workspace( + &self, + added: Vec, + removed: Vec, + ) -> impl Future> { + self.notify::(DidChangeWorkspaceFoldersParams { + event: WorkspaceFoldersChangeEvent { added, removed }, + }) + } + // ------------------------------------------------------------------------------------------- // Text document // ------------------------------------------------------------------------------------------- diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 5609a624..31ee1d75 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -10,11 +10,15 @@ pub use lsp::{Position, Url}; pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; -use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration}; +use helix_core::{ + path, + syntax::{LanguageConfiguration, LanguageServerConfiguration}, +}; use tokio::sync::mpsc::UnboundedReceiver; use std::{ collections::{hash_map::Entry, HashMap}, + path::{Path, PathBuf}, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -128,7 +132,11 @@ pub mod util { ) -> Option { let pos_line = pos.line as usize; if pos_line > doc.len_lines() - 1 { - return None; + // If it extends past the end, truncate it to the end. This is because the + // way the LSP describes the range including the last newline is by + // specifying a line number after what we would call the last line. + log::warn!("LSP position {pos:?} out of range assuming EOF"); + return Some(doc.len_chars()); } // We need to be careful here to fully comply ith the LSP spec. @@ -144,10 +152,10 @@ pub mod util { // > ‘\n’, ‘\r\n’ and ‘\r’. Positions are line end character agnostic. // > So you can not specify a position that denotes \r|\n or \n| where | represents the character offset. // - // This means that while the line must be in bounds the `charater` + // This means that while the line must be in bounds the `character` // must be capped to the end of the line. // Note that the end of the line here is **before** the line terminator - // so we must use `line_end_char_index` istead of `doc.line_to_char(pos_line + 1)` + // so we must use `line_end_char_index` instead of `doc.line_to_char(pos_line + 1)` // // FIXME: Helix does not fully comply with the LSP spec for line terminators. // The LSP standard requires that line terminators are ['\n', '\r\n', '\r']. @@ -238,9 +246,20 @@ pub mod util { pub fn lsp_range_to_range( doc: &Rope, - range: lsp::Range, + mut range: lsp::Range, offset_encoding: OffsetEncoding, ) -> Option { + // This is sort of an edgecase. It's not clear from the spec how to deal with + // ranges where end < start. They don't make much sense but vscode simply caps start to end + // and because it's not specified quite a few LS rely on this as a result (for example the TS server) + if range.start > range.end { + log::error!( + "Invalid LSP range start {:?} > end {:?}, using an empty range at the end instead", + range.start, + range.end + ); + range.start = range.end; + } let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?; let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?; @@ -605,7 +624,7 @@ impl Notification { #[derive(Debug)] pub struct Registry { - inner: HashMap)>, + inner: HashMap)>>, counter: AtomicUsize, pub incoming: SelectAll>, @@ -629,18 +648,24 @@ impl Registry { pub fn get_by_id(&self, id: usize) -> Option<&Client> { self.inner .values() + .flatten() .find(|(client_id, _)| client_id == &id) .map(|(_, client)| client.as_ref()) } 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( &mut self, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -655,15 +680,23 @@ impl Registry { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path)?; + let NewClientResult(client, incoming) = start_client( + id, + language_config, + config, + doc_path, + root_dirs, + enable_snippets, + )?; self.incoming.push(UnboundedReceiverStream::new(incoming)); - let (_, old_client) = entry.insert((id, client.clone())); + let old_clients = entry.insert(vec![(id, client.clone())]); - tokio::spawn(async move { - let _ = old_client.force_shutdown().await; - }); + for (_, old_client) in old_clients { + tokio::spawn(async move { + let _ = old_client.force_shutdown().await; + }); + } Ok(Some(client)) } @@ -673,10 +706,12 @@ impl Registry { pub fn stop(&mut self, language_config: &LanguageConfiguration) { let scope = language_config.scope.clone(); - if let Some((_, client)) = self.inner.remove(&scope) { - tokio::spawn(async move { - let _ = client.force_shutdown().await; - }); + if let Some(clients) = self.inner.remove(&scope) { + for (_, client) in clients { + tokio::spawn(async move { + let _ = client.force_shutdown().await; + }); + } } } @@ -684,30 +719,39 @@ impl Registry { &mut self, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, None => return Ok(None), }; - match self.inner.entry(language_config.scope.clone()) { - Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())), - Entry::Vacant(entry) => { - // initialize a new client - let id = self.counter.fetch_add(1, Ordering::Relaxed); - - let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path)?; - self.incoming.push(UnboundedReceiverStream::new(incoming)); - - entry.insert((id, client.clone())); - Ok(Some(client)) - } + let clients = self.inner.entry(language_config.scope.clone()).or_default(); + // check if we already have a client for this documents root that we can reuse + 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 + let id = self.counter.fetch_add(1, Ordering::Relaxed); + + let NewClientResult(client, incoming) = start_client( + id, + language_config, + config, + doc_path, + root_dirs, + enable_snippets, + )?; + clients.push((id, client.clone())); + self.incoming.push(UnboundedReceiverStream::new(incoming)); + Ok(Some(client)) } pub fn iter_clients(&self) -> impl Iterator> { - self.inner.values().map(|(_, client)| client) + self.inner.values().flatten().map(|(_, client)| client) } } @@ -798,6 +842,8 @@ fn start_client( config: &LanguageConfiguration, ls_config: &LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result { let (client, incoming, initialize_notify) = Client::start( &ls_config.command, @@ -805,6 +851,7 @@ fn start_client( config.config.clone(), ls_config.environment.clone(), &config.roots, + config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), id, ls_config.timeout, doc_path, @@ -820,7 +867,7 @@ fn start_client( .capabilities .get_or_try_init(|| { _client - .initialize() + .initialize(enable_snippets) .map_ok(|response| response.capabilities) }) .await; @@ -842,6 +889,65 @@ fn start_client( 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 { + 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 "); + None +} + #[cfg(test)] mod tests { use super::{lsp, util::*, OffsetEncoding}; @@ -860,16 +966,16 @@ mod tests { test_case!("", (0, 0) => Some(0)); test_case!("", (0, 1) => Some(0)); - test_case!("", (1, 0) => None); + test_case!("", (1, 0) => Some(0)); test_case!("\n\n", (0, 0) => Some(0)); test_case!("\n\n", (1, 0) => Some(1)); test_case!("\n\n", (1, 1) => Some(1)); test_case!("\n\n", (2, 0) => Some(2)); - test_case!("\n\n", (3, 0) => None); + test_case!("\n\n", (3, 0) => Some(2)); test_case!("test\n\n\n\ncase", (4, 3) => Some(11)); test_case!("test\n\n\n\ncase", (4, 4) => Some(12)); test_case!("test\n\n\n\ncase", (4, 5) => Some(12)); - test_case!("", (u32::MAX, u32::MAX) => None); + test_case!("", (u32::MAX, u32::MAX) => Some(0)); } #[test] diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index a4f049e8..ebf3da24 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -61,7 +61,7 @@ fn render_elements( offset: &mut usize, tabstops: &mut Vec<(usize, (usize, usize))>, newline_with_offset: &str, - include_placeholer: bool, + include_placeholder: bool, ) { use SnippetElement::*; @@ -89,7 +89,7 @@ fn render_elements( offset, tabstops, newline_with_offset, - include_placeholer, + include_placeholder, ); } &Tabstop { tabstop } => { @@ -100,14 +100,14 @@ fn render_elements( value: inner_snippet_elements, } => { let start_offset = *offset; - if include_placeholer { + if include_placeholder { render_elements( inner_snippet_elements, insert, offset, tabstops, newline_with_offset, - include_placeholer, + include_placeholder, ); } tabstops.push((*tabstop, (start_offset, *offset))); @@ -127,7 +127,7 @@ fn render_elements( pub fn render( snippet: &Snippet<'_>, newline_with_offset: &str, - include_placeholer: bool, + include_placeholder: bool, ) -> (Tendril, Vec>) { let mut insert = Tendril::new(); let mut tabstops = Vec::new(); @@ -139,7 +139,7 @@ pub fn render( &mut offset, &mut tabstops, newline_with_offset, - include_placeholer, + include_placeholder, ); // sort in ascending order (except for 0, which should always be the last one (per lsp doc)) diff --git a/helix-term/.gitignore b/helix-term/.gitignore index ea8c4bf7..3c99a66e 100644 --- a/helix-term/.gitignore +++ b/helix-term/.gitignore @@ -1 +1,4 @@ /target + +# This folder is used by `test_explorer` to create temporary folders needed for testing +test_explorer diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 95faa01b..91bbc7a6 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -11,7 +11,7 @@ use helix_view::{ document::DocumentSavedEventResult, editor::{ConfigEvent, EditorEvent}, graphics::Rect, - theme, + icons, theme, tree::Layout, Align, Editor, }; @@ -21,11 +21,11 @@ use tui::backend::Backend; use crate::{ args::Args, commands::apply_workspace_edit, - compositor::{Compositor, Event}, + compositor::{self, Compositor, Event}, config::Config, job::Jobs, keymap::Keymaps, - ui::{self, overlay::overlayed}, + ui::{self, overlay::overlaid as overlayed, Explorer}, }; use log::{debug, error, warn}; @@ -69,6 +69,7 @@ pub struct Application { #[allow(dead_code)] theme_loader: Arc, + icons_loader: Arc, #[allow(dead_code)] syn_loader: Arc, @@ -111,9 +112,9 @@ impl Application { use helix_view::editor::Action; - let mut theme_parent_dirs = vec![helix_loader::config_dir()]; - theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); - let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs)); + let mut theme_and_icons_parent_dirs = vec![helix_loader::config_dir()]; + theme_and_icons_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); + let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_and_icons_parent_dirs)); let true_color = config.editor.true_color || crate::true_color(); let theme = config @@ -131,6 +132,21 @@ impl Application { }) .unwrap_or_else(|| theme_loader.default_theme(true_color)); + let icons_loader = std::sync::Arc::new(icons::Loader::new(&theme_and_icons_parent_dirs)); + let icons = config + .icons + .as_ref() + .and_then(|icons| { + icons_loader + .load(icons, &theme, true_color) + .map_err(|e| { + log::warn!("failed to load icons `{}` - {}", icons, e); + e + }) + .ok() + }) + .unwrap_or_else(|| icons_loader.default(&theme)); + let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); #[cfg(not(feature = "integration"))] @@ -146,16 +162,34 @@ impl Application { let mut editor = Editor::new( area, theme_loader.clone(), + icons_loader.clone(), syn_loader.clone(), Arc::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor })), ); + editor.set_theme(theme); + editor.set_icons(icons); + let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.keys })); - let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + let mut editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + + let mut jobs = Jobs::new(); + + if args.show_explorer { + let mut context = compositor::Context { + editor: &mut editor, + scroll: None, + jobs: &mut jobs, + }; + let mut explorer = Explorer::new(&mut context)?; + explorer.unfocus(); + editor_view.explorer = Some(explorer); + } + compositor.push(editor_view); if args.load_tutor { @@ -168,7 +202,7 @@ impl Application { if first.is_dir() { std::env::set_current_dir(first).context("set current dir")?; editor.new_file(Action::VerticalSplit); - let picker = ui::file_picker(".".into(), &config.load().editor); + let picker = ui::file_picker(".".into(), &config.load().editor, &editor.icons); compositor.push(Box::new(overlayed(picker))); } else { let nr_of_files = args.files.len(); @@ -225,8 +259,6 @@ impl Application { .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); } - editor.set_theme(theme); - #[cfg(windows)] let signals = futures_util::stream::empty(); #[cfg(not(windows))] @@ -241,10 +273,11 @@ impl Application { config, theme_loader, + icons_loader, syn_loader, signals, - jobs: Jobs::new(), + jobs, lsp_progress: LspProgressMap::new(), last_render: Instant::now(), }; @@ -393,18 +426,35 @@ impl Application { /// Refresh theme after config change fn refresh_theme(&mut self, config: &Config) -> Result<(), Error> { - if let Some(theme) = config.theme.clone() { - let true_color = self.true_color(); - let theme = self - .theme_loader - .load(&theme) - .map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; - - if true_color || theme.is_16_color() { - self.editor.set_theme(theme); - } else { - anyhow::bail!("theme requires truecolor support, which is not available") - } + let true_color = config.editor.true_color || crate::true_color(); + let theme = config + .theme + .as_ref() + .and_then(|theme| { + self.theme_loader + .load(theme) + .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); + Ok(()) + } + + /// Refresh icons after config change + fn refresh_icons(&mut self, config: &Config) -> Result<(), Error> { + if let Some(icons) = config.icons.clone() { + let true_color = config.editor.true_color || crate::true_color(); + let icons = self + .icons_loader + .load(&icons, &self.editor.theme, true_color) + .map_err(|err| anyhow::anyhow!("Failed to load icons `{}`: {}", icons, err))?; + self.editor.set_icons(icons); } Ok(()) @@ -416,6 +466,7 @@ impl Application { .map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?; self.refresh_language_config()?; self.refresh_theme(&default_config)?; + self.refresh_icons(&default_config)?; // Store new config self.config.store(Arc::new(default_config)); Ok(()) @@ -431,10 +482,6 @@ impl Application { } } - fn true_color(&self) -> bool { - self.config.load().editor.true_color || crate::true_color() - } - #[cfg(windows)] // no signal handling available on windows pub async fn handle_signals(&mut self, _signal: ()) {} @@ -1018,7 +1065,7 @@ impl Application { let language_server = 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)) => { let result: Vec<_> = params diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index dd787f1f..53920e29 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -10,6 +10,7 @@ pub struct Args { pub health: bool, pub health_arg: Option, pub load_tutor: bool, + pub show_explorer: bool, pub fetch_grammars: bool, pub build_grammars: bool, pub split: Option, @@ -32,6 +33,7 @@ impl Args { "--version" => args.display_version = true, "--help" => args.display_help = true, "--tutor" => args.load_tutor = true, + "--show-explorer" => args.show_explorer = true, "--vsplit" => match args.split { Some(_) => anyhow::bail!("can only set a split once of a specific type"), None => args.split = Some(Layout::Vertical), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1c1edece..e1dd0226 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -6,13 +6,13 @@ pub use dap::*; use helix_vcs::Hunk; pub use lsp::*; use tokio::sync::oneshot; -use tui::widgets::Row; +use tui::{text::Span, widgets::Row}; pub use typed::*; use helix_core::{ char_idx_at_visual_offset, comment, doc_formatter::TextFormat, - encoding, find_first_non_whitespace_char, find_root, graphemes, + encoding, find_first_non_whitespace_char, find_workspace, graphemes, history::UndoKind, increment, indent, indent::IndentStyle, @@ -34,6 +34,7 @@ use helix_view::{ clipboard::ClipboardType, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, editor::{Action, Motion}, + icons::Icons, info::Info, input::KeyEvent, keyboard::KeyCode, @@ -54,8 +55,8 @@ use crate::{ job::Callback, keymap::ReverseKeymap, ui::{ - self, editor::InsertEvent, overlay::overlayed, FilePicker, Picker, Popup, Prompt, - PromptEvent, + self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker, + Popup, Prompt, PromptEvent, }, }; @@ -472,6 +473,8 @@ impl MappableCommand { record_macro, "Record macro", replay_macro, "Replay macro", command_palette, "Open command palette", + open_or_focus_explorer, "Open or focus explorer", + reveal_current_file, "Reveal current file in explorer", ); } @@ -501,7 +504,7 @@ impl std::str::FromStr for MappableCommand { fn from_str(s: &str) -> Result { 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 .next() .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 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 { Forward => offset as isize, Backward => -(offset as isize), @@ -1489,18 +1492,19 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { &annotations, ); - let head; + let mut head; match direction { Forward => { - head = char_idx_at_visual_offset( + let off; + (head, off) = char_idx_at_visual_offset( doc_text, view.offset.anchor, (view.offset.vertical_offset + scrolloff) as isize, 0, &text_fmt, &annotations, - ) - .0; + ); + head += (off != 0) as usize; if head <= cursor { return; } @@ -1509,7 +1513,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { head = char_idx_at_visual_offset( doc_text, view.offset.anchor, - (view.offset.vertical_offset + height - scrolloff) as isize, + (view.offset.vertical_offset + height - scrolloff - 1) as isize, 0, &text_fmt, &annotations, @@ -1560,7 +1564,7 @@ fn half_page_down(cx: &mut Context) { } #[allow(deprecated)] -// currently uses the deprected `visual_coords_at_pos`/`pos_at_visual_coords` functions +// currently uses the deprecated `visual_coords_at_pos`/`pos_at_visual_coords` functions // as this function ignores softwrapping (and virtual text) and instead only cares // about "text visual position" // @@ -1988,11 +1992,12 @@ fn global_search(cx: &mut Context) { impl ui::menu::Item for FileResult { type Data = Option; - fn format(&self, current_path: &Self::Data) -> Row { + fn format<'a>(&self, current_path: &Self::Data, icons: Option<&'a Icons>) -> Row { + let icon = icons.and_then(|icons| icons.icon_from_path(Some(&self.path))); let relative_path = helix_core::path::get_relative_path(&self.path) .to_string_lossy() .into_owned(); - if current_path + let path_span: Span = if current_path .as_ref() .map(|p| p == &self.path) .unwrap_or(false) @@ -2000,6 +2005,12 @@ fn global_search(cx: &mut Context) { format!("{} (*)", relative_path).into() } else { relative_path.into() + }; + + if let Some(icon) = icon { + Row::new([icon.into(), path_span]) + } else { + path_span.into() } } } @@ -2116,6 +2127,7 @@ fn global_search(cx: &mut Context) { let picker = FilePicker::new( all_matches, current_path, + editor.config().icons.picker.then_some(&editor.icons), move |cx, FileResult { path, line_num }, action| { match cx.editor.open(path, action) { Ok(_) => {} @@ -2146,7 +2158,7 @@ fn global_search(cx: &mut Context) { Some((path.clone().into(), Some((*line_num, *line_num)))) }, ); - compositor.push(Box::new(overlayed(picker))); + compositor.push(Box::new(overlaid(picker))); }, )); Ok(call) @@ -2418,11 +2430,9 @@ fn append_mode(cx: &mut Context) { } fn file_picker(cx: &mut Context) { - // We don't specify language markers, root will be the root of the current - // git repo or the current dir if we're not in a repo - let root = find_root(None, &[]); - let picker = ui::file_picker(root, &cx.editor.config()); - cx.push_layer(Box::new(overlayed(picker))); + let root = find_workspace().0; + let picker = ui::file_picker(root, &cx.editor.config(), &cx.editor.icons); + cx.push_layer(Box::new(overlaid(picker))); } fn file_picker_in_current_buffer_directory(cx: &mut Context) { @@ -2438,13 +2448,56 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) { } }; - let picker = ui::file_picker(path, &cx.editor.config()); - cx.push_layer(Box::new(overlayed(picker))); + let picker = ui::file_picker(path, &cx.editor.config(), &cx.editor.icons); + cx.push_layer(Box::new(overlaid(picker))); } fn file_picker_in_current_directory(cx: &mut Context) { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./")); - let picker = ui::file_picker(cwd, &cx.editor.config()); - cx.push_layer(Box::new(overlayed(picker))); + let picker = ui::file_picker(cwd, &cx.editor.config(), &cx.editor.icons); + cx.push_layer(Box::new(overlaid(picker))); +} + +fn open_or_focus_explorer(cx: &mut Context) { + cx.callback = Some(Box::new( + |compositor: &mut Compositor, cx: &mut compositor::Context| { + if let Some(editor) = compositor.find::() { + 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) { + cx.callback = Some(Box::new( + |compositor: &mut Compositor, cx: &mut compositor::Context| { + if let Some(editor) = compositor.find::() { + (|| 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) { @@ -2460,7 +2513,7 @@ fn buffer_picker(cx: &mut Context) { impl ui::menu::Item for BufferMeta { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row { let path = self .path .as_deref() @@ -2470,6 +2523,9 @@ fn buffer_picker(cx: &mut Context) { None => SCRATCH_BUFFER_NAME, }; + // Get the filetype icon, or a "file" icon for scratch buffers + let icon = icons.and_then(|icons| icons.icon_from_path(self.path.as_ref())); + let mut flags = String::new(); if self.is_modified { flags.push('+'); @@ -2478,7 +2534,17 @@ fn buffer_picker(cx: &mut Context) { flags.push('*'); } - Row::new([self.id.to_string(), flags, path.to_string()]) + if let Some(icon) = icon { + let icon_span = Span::from(icon); + Row::new(vec![ + icon_span, + self.id.to_string().into(), + flags.into(), + path.to_string().into(), + ]) + } else { + Row::new([self.id.to_string(), flags, path.to_string()]) + } } } @@ -2496,6 +2562,7 @@ fn buffer_picker(cx: &mut Context) { .map(|doc| new_meta(doc)) .collect(), (), + cx.editor.config().icons.picker.then_some(&cx.editor.icons), |cx, meta, action| { cx.editor.switch(meta.id, action); }, @@ -2509,7 +2576,7 @@ fn buffer_picker(cx: &mut Context) { Some((meta.id.into(), Some((line, line)))) }, ); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } fn jumplist_picker(cx: &mut Context) { @@ -2524,7 +2591,10 @@ fn jumplist_picker(cx: &mut Context) { impl ui::menu::Item for JumpMeta { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row { + // Get the filetype icon, or a "file" icon for scratch buffers + let icon = icons.and_then(|icons| icons.icon_from_path(self.path.as_ref())); + let path = self .path .as_deref() @@ -2544,7 +2614,13 @@ fn jumplist_picker(cx: &mut Context) { } else { format!(" ({})", flags.join("")) }; - format!("{} {}{} {}", self.id, path, flag, self.text).into() + + let path_span: Span = format!("{} {}{} {}", self.id, path, flag, self.text).into(); + if let Some(icon) = icon { + Row::new(vec![icon.into(), path_span]) + } else { + path_span.into() + } } } @@ -2578,6 +2654,7 @@ fn jumplist_picker(cx: &mut Context) { }) .collect(), (), + cx.editor.config().icons.picker.then_some(&cx.editor.icons), |cx, meta, action| { cx.editor.switch(meta.id, action); let config = cx.editor.config(); @@ -2591,13 +2668,13 @@ fn jumplist_picker(cx: &mut Context) { Some((meta.path.clone()?.into(), Some((line, line)))) }, ); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } impl ui::menu::Item for MappableCommand { type Data = ReverseKeymap; - fn format(&self, keymap: &Self::Data) -> Row { + fn format<'a>(&self, keymap: &Self::Data, _icons: Option<&'a Icons>) -> Row { let fmt_binding = |bindings: &Vec>| -> String { bindings.iter().fold(String::new(), |mut acc, bind| { 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 { register: None, 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, }; - // setup a chanel that allows the request to be canceled + // setup a channel that allows the request to be canceled let (tx, rx) = oneshot::channel(); // set completion_request so that this request can be canceled // by setting completion_request, the old channel stored there is dropped @@ -4239,7 +4316,7 @@ pub fn completion(cx: &mut Context) { let (view, doc) = current_ref!(editor); // check if the completion request is stale. // - // Completions are completed asynchrounsly and therefore the user could + // Completions are completed asynchronously and therefore the user could //switch document/view or leave insert mode. In all of thoise cases the // completion should be discarded if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc { @@ -4262,7 +4339,7 @@ pub fn completion(cx: &mut Context) { } let size = compositor.size(); let ui = compositor.find::().unwrap(); - ui.set_completion( + let completion_area = ui.set_completion( editor, savepoint, items, @@ -4271,6 +4348,15 @@ pub fn completion(cx: &mut Context) { trigger_offset, size, ); + let size = compositor.size(); + let signature_help_area = compositor + .find_id::>(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); + } }, ); } diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index dac1e9d5..3c0214b0 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -2,13 +2,13 @@ use super::{Context, Editor}; use crate::{ compositor::{self, Compositor}, job::{Callback, Jobs}, - ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, + ui::{self, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, }; use dap::{StackFrame, Thread, ThreadStates}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_dap::{self as dap, Client}; use helix_lsp::block_on; -use helix_view::editor::Breakpoint; +use helix_view::{editor::Breakpoint, icons::Icons}; use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -25,7 +25,7 @@ use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select impl ui::menu::Item for StackFrame { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row { self.name.as_str().into() // TODO: include thread_states in the label } } @@ -33,7 +33,7 @@ impl ui::menu::Item for StackFrame { impl ui::menu::Item for DebugTemplate { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row { self.name.as_str().into() } } @@ -41,7 +41,7 @@ impl ui::menu::Item for DebugTemplate { impl ui::menu::Item for Thread { type Data = ThreadStates; - fn format(&self, thread_states: &Self::Data) -> Row { + fn format<'a>(&self, thread_states: &Self::Data, _icons: Option<&'a Icons>) -> Row { format!( "{} ({})", self.name, @@ -76,6 +76,7 @@ fn thread_picker( let picker = FilePicker::new( threads, thread_states, + None, move |cx, thread, _action| callback_fn(cx.editor, thread), move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; @@ -270,9 +271,10 @@ pub fn dap_launch(cx: &mut Context) { let templates = config.templates.clone(); - cx.push_layer(Box::new(overlayed(Picker::new( + cx.push_layer(Box::new(overlaid(Picker::new( templates, (), + None, |cx, template, _action| { let completions = template.completion.clone(); let name = template.name.clone(); @@ -731,6 +733,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let picker = FilePicker::new( frames, (), + None, move |cx, frame, _action| { let debugger = debugger!(cx.editor); // TODO: this should be simpler to find diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 0b0d1db4..7b6499d4 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -3,15 +3,12 @@ use helix_lsp::{ block_on, lsp::{ self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity, - NumberOrString, + NumberOrString, SymbolKind, }, util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, OffsetEncoding, }; -use tui::{ - text::{Span, Spans}, - widgets::Row, -}; +use tui::{text::Span, widgets::Row}; use super::{align_view, push_jump, Align, Context, Editor, Open}; @@ -19,6 +16,7 @@ use helix_core::{path, text_annotations::InlineAnnotation, Selection}; use helix_view::{ document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, editor::Action, + icons::{self, Icon, Icons}, theme::Style, Document, View, }; @@ -26,7 +24,7 @@ use helix_view::{ use crate::{ compositor::{self, Compositor}, ui::{ - self, lsp::SignatureHelp, overlay::overlayed, DynamicPicker, FileLocation, FilePicker, + self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker, Popup, PromptEvent, }, }; @@ -57,7 +55,7 @@ impl ui::menu::Item for lsp::Location { /// Current working directory. type Data = PathBuf; - fn format(&self, cwdir: &Self::Data) -> Row { + fn format<'a>(&self, cwdir: &Self::Data, _icons: Option<&'a Icons>) -> Row { // The preallocation here will overallocate a few characters since it will account for the // URL's scheme, which is not used most of the time since that scheme will be "file://". // Those extra chars will be used to avoid allocating when writing the line number (in the @@ -81,7 +79,7 @@ impl ui::menu::Item for lsp::Location { // Most commonly, this will not allocate, especially on Unix systems where the root prefix // is a simple `/` and not `C:\` (with whatever drive letter) - write!(&mut res, ":{}", self.range.start.line) + write!(&mut res, ":{}", self.range.start.line + 1) .expect("Will only failed if allocating fail"); res.into() } @@ -91,16 +89,58 @@ impl ui::menu::Item for lsp::SymbolInformation { /// Path to currently focussed document type Data = Option; - fn format(&self, current_doc_path: &Self::Data) -> Row { + fn format<'a>(&self, current_doc_path: &Self::Data, icons: Option<&'a Icons>) -> Row { + let icon = + icons + .and_then(|icons| icons.symbol_kind.as_ref()) + .and_then(|symbol_kind_icons| match self.kind { + SymbolKind::FILE => symbol_kind_icons.get("file"), + SymbolKind::MODULE => symbol_kind_icons.get("module"), + SymbolKind::NAMESPACE => symbol_kind_icons.get("namespace"), + SymbolKind::PACKAGE => symbol_kind_icons.get("package"), + SymbolKind::CLASS => symbol_kind_icons.get("class"), + SymbolKind::METHOD => symbol_kind_icons.get("method"), + SymbolKind::PROPERTY => symbol_kind_icons.get("property"), + SymbolKind::FIELD => symbol_kind_icons.get("field"), + SymbolKind::CONSTRUCTOR => symbol_kind_icons.get("constructor"), + SymbolKind::ENUM => symbol_kind_icons.get("enumeration"), + SymbolKind::INTERFACE => symbol_kind_icons.get("interface"), + SymbolKind::FUNCTION => symbol_kind_icons.get("function"), + SymbolKind::VARIABLE => symbol_kind_icons.get("variable"), + SymbolKind::CONSTANT => symbol_kind_icons.get("constant"), + SymbolKind::STRING => symbol_kind_icons.get("string"), + SymbolKind::NUMBER => symbol_kind_icons.get("number"), + SymbolKind::BOOLEAN => symbol_kind_icons.get("boolean"), + SymbolKind::ARRAY => symbol_kind_icons.get("array"), + SymbolKind::OBJECT => symbol_kind_icons.get("object"), + SymbolKind::KEY => symbol_kind_icons.get("key"), + SymbolKind::NULL => symbol_kind_icons.get("null"), + SymbolKind::ENUM_MEMBER => symbol_kind_icons.get("enum-member"), + SymbolKind::STRUCT => symbol_kind_icons.get("structure"), + SymbolKind::EVENT => symbol_kind_icons.get("event"), + SymbolKind::OPERATOR => symbol_kind_icons.get("operator"), + SymbolKind::TYPE_PARAMETER => symbol_kind_icons.get("type-parameter"), + _ => Some(&icons::BLANK_ICON), + }); + if current_doc_path.as_ref() == Some(&self.location.uri) { - self.name.as_str().into() + if let Some(icon) = icon { + Row::new([Span::from(icon), self.name.as_str().into()]) + } else { + self.name.as_str().into() + } } else { - match self.location.uri.to_file_path() { + let symbol_span: Span = match self.location.uri.to_file_path() { Ok(path) => { let get_relative_path = path::get_relative_path(path.as_path()); format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into() } Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(), + }; + if let Some(icon) = icon { + Row::new([Span::from(icon), symbol_span]) + } else { + Row::from(symbol_span) } } } @@ -121,7 +161,18 @@ struct PickerDiagnostic { impl ui::menu::Item for PickerDiagnostic { type Data = (DiagnosticStyles, DiagnosticsFormat); - fn format(&self, (styles, format): &Self::Data) -> Row { + fn format<'a>(&self, (styles, format): &Self::Data, icons: Option<&'a Icons>) -> Row { + let icon: Option<&'a Icon> = + icons + .zip(self.diag.severity) + .map(|(icons, severity)| match severity { + DiagnosticSeverity::ERROR => &icons.diagnostic.error, + DiagnosticSeverity::WARNING => &icons.diagnostic.warning, + DiagnosticSeverity::HINT => &icons.diagnostic.hint, + DiagnosticSeverity::INFORMATION => &icons.diagnostic.info, + _ => &icons::BLANK_ICON, + }); + let mut style = self .diag .severity @@ -152,12 +203,20 @@ impl ui::menu::Item for PickerDiagnostic { } }; - Spans::from(vec![ - Span::raw(path), - Span::styled(&self.diag.message, style), - Span::styled(code, style), - ]) - .into() + if let Some(icon) = icon { + Row::new(vec![ + icon.into(), + Span::raw(path), + Span::styled(&self.diag.message, style), + Span::styled(code, style), + ]) + } else { + Row::new(vec![ + Span::raw(path), + Span::styled(&self.diag.message, style), + Span::styled(code, style), + ]) + } } } @@ -213,11 +272,13 @@ fn sym_picker( symbols: Vec, current_path: Option, offset_encoding: OffsetEncoding, + editor: &Editor, ) -> FilePicker { // TODO: drop current_path comparison and instead use workspace: bool flag? FilePicker::new( symbols, current_path.clone(), + editor.config().icons.picker.then_some(&editor.icons), move |cx, symbol, action| { let (view, doc) = current!(cx.editor); push_jump(view, doc); @@ -293,6 +354,7 @@ fn diag_picker( FilePicker::new( flat_diag, (styles, format), + cx.editor.config().icons.picker.then_some(&cx.editor.icons), move |cx, PickerDiagnostic { url, diag }, action| { if current_path.as_ref() == Some(url) { let (view, doc) = current!(cx.editor); @@ -371,8 +433,8 @@ pub fn symbol_picker(cx: &mut Context) { } }; - let picker = sym_picker(symbols, current_url, offset_encoding); - compositor.push(Box::new(overlayed(picker))) + let picker = sym_picker(symbols, current_url, offset_encoding, editor); + compositor.push(Box::new(overlaid(picker))) } }, ) @@ -394,9 +456,9 @@ pub fn workspace_symbol_picker(cx: &mut Context) { cx.callback( future, - move |_editor, compositor, response: Option>| { + move |editor, compositor, response: Option>| { let symbols = response.unwrap_or_default(); - let picker = sym_picker(symbols, current_url, offset_encoding); + let picker = sym_picker(symbols, current_url, offset_encoding, editor); let get_symbols = |query: String, editor: &mut Editor| { let doc = doc!(editor); let language_server = match doc.language_server() { @@ -431,7 +493,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { future.boxed() }; let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols)); - compositor.push(Box::new(overlayed(dyn_picker))) + compositor.push(Box::new(overlaid(dyn_picker))) }, ) } @@ -454,7 +516,7 @@ pub fn diagnostics_picker(cx: &mut Context) { DiagnosticsFormat::HideSourcePath, offset_encoding, ); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } } @@ -471,12 +533,12 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { DiagnosticsFormat::ShowSourcePath, offset_encoding, ); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } impl ui::menu::Item for lsp::CodeActionOrCommand { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row { match self { lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(), lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), @@ -491,7 +553,7 @@ impl ui::menu::Item for lsp::CodeActionOrCommand { /// /// While the `kind` field is defined as open ended in the LSP spec (any value may be used) /// in practice a closed set of common values (mostly suggested in the LSP spec) are used. -/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker) +/// VSCode displays each of these categories separately (separated by a heading in the codeactions picker) /// to make them easier to navigate. Helix does not display these headings to the user. /// However it does sort code actions by their categories to achieve the same order as the VScode picker, /// just without the headings. @@ -521,7 +583,7 @@ fn action_category(action: &CodeActionOrCommand) -> u32 { } } -fn action_prefered(action: &CodeActionOrCommand) -> bool { +fn action_preferred(action: &CodeActionOrCommand) -> bool { matches!( action, CodeActionOrCommand::CodeAction(CodeAction { @@ -600,12 +662,12 @@ pub fn code_action(cx: &mut Context) { } // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec. - // Many details are modeled after vscode because langauge servers are usually tested against it. + // Many details are modeled after vscode because language servers are usually tested against it. // VScode sorts the codeaction two times: // // First the codeactions that fix some diagnostics are moved to the front. // If both codeactions fix some diagnostics (or both fix none) the codeaction - // that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate + // that is marked with `is_preferred` is shown first. The codeactions are then shown in separate // submenus that only contain a certain category (see `action_category`) of actions. // // Below this done in in a single sorting step @@ -627,10 +689,10 @@ pub fn code_action(cx: &mut Context) { return order; } - // if one of the codeactions is marked as prefered show it first + // if one of the codeactions is marked as preferred show it first // otherwise keep the original LSP sorting - action_prefered(action1) - .cmp(&action_prefered(action2)) + action_preferred(action1) + .cmp(&action_preferred(action2)) .reverse() }); @@ -672,7 +734,7 @@ pub fn code_action(cx: &mut Context) { impl ui::menu::Item for lsp::Command { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row { self.title.as_str().into() } } @@ -950,12 +1012,13 @@ fn goto_impl( let picker = FilePicker::new( locations, cwdir, + None, move |cx, location, action| { jump_to_location(cx.editor, location, offset_encoding, action) }, move |_editor, location| Some(location_to_file_location(location)), ); - compositor.push(Box::new(overlayed(picker))); + compositor.push(Box::new(overlaid(picker))); } } } @@ -1221,10 +1284,25 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { contents.set_active_param_range(active_param_range()); let old_popup = compositor.find_id::>(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_bias(Open::Above) .ignore_escape_key(true); + + // Don't create a popup if it intersects the auto-complete menu. + let size = compositor.size(); + if compositor + .find::() + .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); }, ); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 2c72686d..e6af58a8 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -115,8 +115,8 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> let callback = async move { let call: job::Callback = job::Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { - let picker = ui::file_picker(path, &editor.config()); - compositor.push(Box::new(overlayed(picker))); + let picker = ui::file_picker(path, &editor.config(), &editor.icons); + compositor.push(Box::new(overlaid(picker))); }, )); Ok(call) @@ -298,6 +298,30 @@ fn force_buffer_close_all( buffer_close_by_ids_impl(cx, &document_ids, true) } +fn delete( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + cx.block_try_flush_writes()?; + let doc = doc_mut!(cx.editor); + + if doc.path().is_none() { + bail!("cannot delete a buffer with no associated file on the disk"); + } + + let doc_id = view!(cx.editor).doc; + + let future = doc.delete(); + cx.jobs.add(Job::new(future)); + + buffer_close_by_ids_impl(cx, &[doc_id], true) +} + fn buffer_next( cx: &mut compositor::Context, _args: &[Cow], @@ -853,6 +877,30 @@ fn theme( Ok(()) } +fn icons( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + let true_color = cx.editor.config.load().true_color || crate::true_color(); + if let PromptEvent::Validate = event { + if let Some(flavor_name) = args.first() { + let icons = cx + .editor + .icons_loader + .load(flavor_name, &cx.editor.theme, true_color) + .map_err(|err| anyhow!("Could not load icon flavor: {}", err))?; + cx.editor.set_icons(icons); + } else { + let name = cx.editor.icons.name().to_string(); + + cx.editor.set_status(name); + } + }; + + Ok(()) +} + fn yank_main_selection_to_clipboard( cx: &mut compositor::Context, _args: &[Cow], @@ -1332,10 +1380,10 @@ fn lsp_workspace_command( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = ui::Picker::new(commands, (), |cx, command, _action| { + let picker = ui::Picker::new(commands, (), None, |cx, command, _action| { execute_lsp_command(cx.editor, command.clone()); }); - compositor.push(Box::new(overlayed(picker))) + compositor.push(Box::new(overlaid(picker))) }, )); Ok(call) @@ -1371,13 +1419,19 @@ fn lsp_restart( return Ok(()); } + let editor_config = cx.editor.config.load(); let (_view, doc) = current!(cx.editor); let config = doc .language_config() .context("LSP not defined for the current document")?; 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. let document_ids_to_refresh: Vec = cx @@ -1970,6 +2024,20 @@ fn open_config( Ok(()) } +fn open_workspace_config( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + cx.editor + .open(&helix_loader::workspace_config_file(), Action::Replace)?; + Ok(()) +} + fn open_log( cx: &mut compositor::Context, _args: &[Cow], @@ -2105,20 +2173,16 @@ fn reset_diff_change( let scrolloff = editor.config().scrolloff; let (view, doc) = current!(editor); - // TODO refactor to use let..else once MSRV is raised to 1.65 - let handle = match doc.diff_handle() { - Some(handle) => handle, - None => bail!("Diff is not available in the current buffer"), + let Some(handle) = doc.diff_handle() else { + bail!("Diff is not available in the current buffer") }; let diff = handle.load(); let doc_text = doc.text().slice(..); let line = doc.selection(view.id).primary().cursor_line(doc_text); - // TODO refactor to use let..else once MSRV is raised to 1.65 - let hunk_idx = match diff.hunk_at(line as u32, true) { - Some(hunk_idx) => hunk_idx, - None => bail!("There is no change at the cursor"), + let Some(hunk_idx) = diff.hunk_at(line as u32, true) else { + bail!("There is no change at the cursor") }; let hunk = diff.nth_hunk(hunk_idx); let diff_base = diff.diff_base(); @@ -2213,6 +2277,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: force_buffer_close_all, signature: CommandSignature::none(), }, + TypableCommand { + name: "delete", + aliases: &["remove", "rm", "del"], + doc: "Deletes the file associated with the current buffer", + fun: delete, + signature: CommandSignature::none(), + }, TypableCommand { name: "buffer-next", aliases: &["bn", "bnext"], @@ -2358,6 +2429,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: theme, signature: CommandSignature::positional(&[completers::theme]), }, + TypableCommand { + name: "icons", + aliases: &[], + doc: "Change the editor icon flavor (show current flavor if no name specified).", + fun: icons, + signature: CommandSignature::positional(&[completers::icons]), + }, TypableCommand { name: "clipboard-yank", aliases: &[], @@ -2646,6 +2724,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: open_config, signature: CommandSignature::none(), }, + TypableCommand { + name: "config-open-workspace", + aliases: &[], + doc: "Open the workspace config.toml file.", + fun: open_workspace_config, + signature: CommandSignature::none(), + }, TypableCommand { name: "log-open", aliases: &[], diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index bcb3e449..ede4ff94 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -34,6 +34,48 @@ impl<'a> Context<'a> { tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?; Ok(()) } + + /// Purpose: to test `handle_event` without escalating the test case to integration test + /// Usage: + /// ``` + /// let mut editor = Context::dummy_editor(); + /// let mut jobs = Context::dummy_jobs(); + /// let mut cx = Context::dummy(&mut jobs, &mut editor); + /// ``` + #[cfg(test)] + pub fn dummy(jobs: &'a mut Jobs, editor: &'a mut helix_view::Editor) -> Context<'a> { + Context { + jobs, + scroll: None, + editor, + } + } + + #[cfg(test)] + pub fn dummy_jobs() -> Jobs { + Jobs::new() + } + + #[cfg(test)] + pub fn dummy_editor() -> Editor { + use crate::config::Config; + use arc_swap::{access::Map, ArcSwap}; + use helix_core::syntax::{self, Configuration}; + use helix_view::{icons, theme}; + use std::sync::Arc; + + let config = Arc::new(ArcSwap::from_pointee(Config::default())); + Editor::new( + Rect::new(0, 0, 60, 120), + Arc::new(theme::Loader::new(&[])), + Arc::new(icons::Loader::new(&[])), + Arc::new(syntax::Loader::new(Configuration { language: vec![] })), + Arc::new(Arc::new(Map::new( + Arc::clone(&config), + |config: &Config| &config.editor, + ))), + ) + } } pub trait Component: Any + AnyComponent { @@ -72,6 +114,21 @@ pub trait Component: Any + AnyComponent { fn id(&self) -> Option<&'static str> { None } + + #[cfg(test)] + /// Utility method for testing `handle_event` without using integration test. + /// Especially useful for testing helper components such as `Prompt`, `TreeView` etc + fn handle_events(&mut self, events: &str) -> anyhow::Result<()> { + use helix_view::input::parse_macro; + + let mut editor = Context::dummy_editor(); + let mut jobs = Context::dummy_jobs(); + let mut cx = Context::dummy(&mut jobs, &mut editor); + for event in parse_macro(events)? { + self.handle_event(&Event::Key(event), &mut cx); + } + Ok(()) + } } pub struct Compositor { diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882..e36b4851 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -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 serde::Deserialize; use std::collections::HashMap; use std::fmt::Display; +use std::fs; use std::io::Error as IOError; -use std::path::PathBuf; use toml::de::Error as TomlError; -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Clone, PartialEq)] pub struct Config { pub theme: Option, - #[serde(default = "default")] + pub icons: Option, pub keys: HashMap, - #[serde(default)] pub editor: helix_view::editor::Config, } +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigRaw { + pub theme: Option, + pub icons: Option, + pub keys: Option>, + pub editor: Option, +} + impl Default for Config { fn default() -> Config { Config { theme: None, - keys: default(), + icons: None, + keys: keymap::default(), editor: helix_view::editor::Config::default(), } } @@ -33,6 +43,12 @@ pub enum ConfigLoadError { Error(IOError), } +impl Default for ConfigLoadError { + fn default() -> Self { + ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder")) + } +} + impl Display for ConfigLoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -43,17 +59,74 @@ impl Display for ConfigLoadError { } impl Config { - pub fn load(config_path: PathBuf) -> Result { - match std::fs::read_to_string(config_path) { - Ok(config) => toml::from_str(&config) - .map(merge_keys) - .map_err(ConfigLoadError::BadConfig), - Err(err) => Err(ConfigLoadError::Error(err)), - } + pub fn load( + global: Result, + local: Result, + ) -> Result { + let global_config: Result = + global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); + let local_config: Result = + 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::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 { use super::*; + impl Config { + fn load_test(config: &str) -> Config { + Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap() + } + } + #[test] fn parsing_keymaps_config_file() { use crate::keymap; @@ -77,18 +156,24 @@ mod tests { A-F12 = "move_next_word_end" "#; + let mut keys = keymap::default(); + merge_keys( + &mut keys, + hashmap! { + Mode::Insert => Keymap::new(keymap!({ "Insert mode" + "y" => move_line_down, + "S-C-a" => delete_selection, + })), + Mode::Normal => Keymap::new(keymap!({ "Normal mode" + "A-F12" => move_next_word_end, + })), + }, + ); + assert_eq!( - toml::from_str::(sample_keymaps).unwrap(), + Config::load_test(sample_keymaps), Config { - keys: hashmap! { - Mode::Insert => Keymap::new(keymap!({ "Insert mode" - "y" => move_line_down, - "S-C-a" => delete_selection, - })), - Mode::Normal => Keymap::new(keymap!({ "Normal mode" - "A-F12" => move_next_word_end, - })), - }, + keys, ..Default::default() } ); @@ -97,11 +182,11 @@ mod tests { #[test] fn keys_resolve_to_correct_defaults() { // From serde default - let default_keys = toml::from_str::("").unwrap().keys; - assert_eq!(default_keys, default()); + let default_keys = Config::load_test("").keys; + assert_eq!(default_keys, keymap::default()); // From the Default trait let default_keys = Config::default().keys; - assert_eq!(default_keys, default()); + assert_eq!(default_keys, keymap::default()); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index e94a5f66..3033c6a4 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -2,7 +2,6 @@ pub mod default; pub mod macros; pub use crate::commands::MappableCommand; -use crate::config::Config; use arc_swap::{ access::{DynAccess, DynGuard}, ArcSwap, @@ -16,7 +15,7 @@ use std::{ sync::Arc, }; -use default::default; +pub use default::default; use macros::key; #[derive(Debug, Clone)] @@ -417,12 +416,10 @@ impl Default for Keymaps { } /// Merge default config keys with user overwritten keys for custom user config. -pub fn merge_keys(mut config: Config) -> Config { - let mut delta = std::mem::replace(&mut config.keys, default()); - for (mode, keys) in &mut config.keys { +pub fn merge_keys(dst: &mut HashMap, mut delta: HashMap) { + for (mode, keys) in dst { keys.merge(delta.remove(mode).unwrap_or_default()) } - config } #[cfg(test)] @@ -449,26 +446,24 @@ mod tests { #[test] fn merge_partial_keys() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "i" => normal_mode, - "无" => insert_mode, - "z" => jump_backward, - "g" => { "Merge into goto mode" - "$" => goto_line_end, - "g" => delete_char_forward, - }, - }) - ) - }, - ..Default::default() + let keymap = hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "i" => normal_mode, + "无" => insert_mode, + "z" => jump_backward, + "g" => { "Merge into goto mode" + "$" => goto_line_end, + "g" => delete_char_forward, + }, + }) + ) }; - let mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); + let mut merged_keyamp = default(); + 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!( keymap.get(Mode::Normal, key!('i')), KeymapResult::Matched(MappableCommand::normal_mode), @@ -486,7 +481,7 @@ mod tests { "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 assert_eq!( keymap.root().search(&[key!('g'), key!('$')]).unwrap(), @@ -506,30 +501,28 @@ mod tests { "Old leaves in subnode should be present in merged node" ); - assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); + assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1); + assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0); } #[test] fn order_should_be_set() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "space" => { "" - "s" => { "" - "v" => vsplit, - "c" => hsplit, - }, + let keymap = hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "space" => { "" + "s" => { "" + "v" => vsplit, + "c" => hsplit, }, - }) - ) - }, - ..Default::default() + }, + }) + ) }; - let mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + let mut merged_keyamp = default(); + merge_keys(&mut merged_keyamp, keymap.clone()); + assert_ne!(keymap, merged_keyamp); + let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); // Make sure mapping works assert_eq!( keymap diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 9bd00280..c8a0aa68 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -274,6 +274,7 @@ pub fn default() -> HashMap { "r" => rename_symbol, "h" => select_references_to_symbol_under_cursor, "?" => command_palette, + "e" => reveal_current_file, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c537..e0c3f6e7 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -3,7 +3,7 @@ use crossterm::event::EventStream; use helix_loader::VERSION_AND_GIT_HASH; use helix_term::application::Application; use helix_term::args::Args; -use helix_term::config::Config; +use helix_term::config::{Config, ConfigLoadError}; use std::path::PathBuf; fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { @@ -126,18 +126,19 @@ FLAGS: helix_loader::initialize_config_file(args.config_file.clone()); - let config = match std::fs::read_to_string(helix_loader::config_file()) { - Ok(config) => toml::from_str(&config) - .map(helix_term::keymap::merge_keys) - .unwrap_or_else(|err| { - eprintln!("Bad config: {}", err); - eprintln!("Press to continue with default config"); - use std::io::Read; - let _ = std::io::stdin().read(&mut []); - Config::default() - }), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), - Err(err) => return Err(Error::new(err)), + let config = match Config::load_default() { + Ok(config) => config, + Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => { + Config::default() + } + Err(ConfigLoadError::Error(err)) => return Err(Error::new(err)), + Err(ConfigLoadError::BadConfig(err)) => { + eprintln!("Bad config: {}", err); + eprintln!("Press to continue with default config"); + use std::io::Read; + let _ = std::io::stdin().read(&mut []); + Config::default() + } }; let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| { diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index da6b5ddc..efd41ef1 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult}; use helix_view::{ document::SavePoint, editor::CompleteAction, + icons::Icons, theme::{Modifier, Style}, ViewId, }; @@ -33,7 +34,8 @@ impl menu::Item for CompletionItem { .into() } - fn format(&self, _data: &Self::Data) -> menu::Row { + // Before implementing icons for the `CompletionItemKind`s, something must be done to `Menu::required_size` and `Menu::recalculate_size` in order to have correct sizes even with icons. + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> menu::Row { let deprecated = self.deprecated.unwrap_or_default() || self.tags.as_ref().map_or(false, |tags| { tags.contains(&lsp::CompletionItemTag::DEPRECATED) @@ -141,16 +143,12 @@ impl Completion { } }; - let start_offset = - match util::lsp_pos_to_pos(doc.text(), edit.range.start, offset_encoding) { - Some(start) => start as i128 - primary_cursor as i128, - None => return Transaction::new(doc.text()), - }; - let end_offset = - match util::lsp_pos_to_pos(doc.text(), edit.range.end, offset_encoding) { - Some(end) => end as i128 - primary_cursor as i128, - None => return Transaction::new(doc.text()), - }; + let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{ + return Transaction::new(doc.text()); + }; + + let start_offset = range.anchor as i128 - primary_cursor as i128; + let end_offset = range.head as i128 - primary_cursor as i128; (Some((start_offset, end_offset)), edit.new_text) } else { @@ -414,6 +412,10 @@ impl Completion { true } + + pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { + self.popup.area(viewport, editor) + } } impl Component for Completion { @@ -481,7 +483,7 @@ impl Component for Completion { }; 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(); Rect::new(popup_x, popup_y, popup_width, popup_height) }; diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index d4176264..80da1c54 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -118,7 +118,7 @@ pub fn render_document( fn translate_positions( char_pos: usize, - first_visisble_char_idx: usize, + first_visible_char_idx: usize, translated_positions: &mut [TranslatedPosition], text_fmt: &TextFormat, renderer: &mut TextRenderer, @@ -126,7 +126,7 @@ fn translate_positions( ) { // check if any positions translated on the fly (like cursor) has been reached for (char_idx, callback) in &mut *translated_positions { - if *char_idx < char_pos && *char_idx >= first_visisble_char_idx { + if *char_idx < char_pos && *char_idx >= first_visible_char_idx { // by replacing the char_index with usize::MAX large number we ensure // that the same position is only translated once // text will never reach usize::MAX as rust memory allocations are limited @@ -175,7 +175,6 @@ pub fn render_text<'t>( text_annotations, ); row_off += offset.vertical_offset; - assert_eq!(0, offset.vertical_offset); let (mut formatter, mut first_visible_char_idx) = 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 { 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 is_whitespace = grapheme.is_whitespace(); - // TODO is it correct to apply the whitspace style to all unicode white spaces? + // TODO is it correct to apply the whitespace style to all unicode white spaces? if is_whitespace { style = style.patch(self.whitespace_style); } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 0b6ab046..c6c6db4b 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -6,7 +6,7 @@ use crate::{ keymap::{KeymapResult, Keymaps}, ui::{ document::{render_document, LinePos, TextRenderer, TranslatedPosition}, - Completion, ProgressSpinners, + Completion, Explorer, ProgressSpinners, }, }; @@ -23,7 +23,7 @@ use helix_core::{ }; use helix_view::{ document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, - editor::{CompleteAction, CursorShapeConfig}, + editor::{CompleteAction, CursorShapeConfig, ExplorerPosition}, graphics::{Color, CursorKind, Modifier, Rect, Style}, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, keyboard::{KeyCode, KeyModifiers}, @@ -43,6 +43,7 @@ pub struct EditorView { pub(crate) last_insert: (commands::MappableCommand, Vec), pub(crate) completion: Option, spinners: ProgressSpinners, + pub(crate) explorer: Option, } #[derive(Debug, Clone)] @@ -68,6 +69,7 @@ impl EditorView { last_insert: (commands::MappableCommand::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), + explorer: None, } } @@ -93,40 +95,6 @@ impl EditorView { let mut line_decorations: Vec> = Vec::new(); let mut translated_positions: Vec = Vec::new(); - // DAP: Highlight current stack frame position - let stack_frame = editor.debugger.as_ref().and_then(|debugger| { - 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 doc.path().is_some() - && frame - .source - .as_ref() - .and_then(|source| source.path.as_ref()) - == doc.path() - { - let line = frame.line - 1; // convert to 0-indexing - let style = theme.get("ui.highlight"); - let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { - if pos.doc_line != line { - return; - } - renderer - .surface - .set_style(Rect::new(area.x, pos.visual_line, area.width, 1), style); - }; - - line_decorations.push(Box::new(line_decoration)); - } - } - if is_focused && config.cursorline { line_decorations.push(Self::cursorline_decorator(doc, view, theme)) } @@ -135,6 +103,23 @@ impl EditorView { Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations); } + // Set DAP highlights, if needed. + if let Some(frame) = editor.current_stack_frame() { + let dap_line = frame.line.saturating_sub(1) as usize; + let style = theme.get("ui.highlight.frameline"); + let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { + if pos.doc_line != dap_line { + return; + } + renderer.surface.set_style( + Rect::new(inner.x, inner.y + pos.visual_line, inner.width, 1), + style, + ); + }; + + line_decorations.push(Box::new(line_decoration)); + } + let mut highlights = Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme); let overlay_highlights = Self::overlay_syntax_highlights( @@ -422,6 +407,7 @@ impl EditorView { let primary_selection_scope = theme .find_scope_index_exact("ui.selection.primary") .unwrap_or(selection_scope); + let base_cursor_scope = theme .find_scope_index_exact("ui.cursor") .unwrap_or(selection_scope); @@ -547,8 +533,24 @@ impl EditorView { let mut x = viewport.x; let current_doc = view!(editor).doc; + let config = editor.config(); + let icons_enabled = config.icons.bufferline; for doc in editor.documents() { + let filetype_icon = doc + .language_config() + .and_then(|config| { + config + .file_types + .iter() + .map(|filetype| match filetype { + helix_core::syntax::FileType::Extension(s) => s, + helix_core::syntax::FileType::Suffix(s) => s, + }) + .find_map(|filetype| editor.icons.icon_from_filetype(filetype)) + }) + .or_else(|| editor.icons.icon_from_path(doc.path())); + let fname = doc .path() .unwrap_or(&scratch) @@ -567,6 +569,22 @@ impl EditorView { let used_width = viewport.x.saturating_sub(x); let rem_width = surface.area.width.saturating_sub(used_width); + if icons_enabled { + if let Some(icon) = filetype_icon { + x = surface + .set_stringn( + x, + viewport.y, + format!(" {}", icon.icon_char), + rem_width as usize, + match icon.style { + Some(s) => style.patch(s.into()), + None => style, + }, + ) + .0; + } + } x = surface .set_stringn(x, viewport.y, text, rem_width as usize, style) .0; @@ -968,7 +986,7 @@ impl EditorView { start_offset: usize, trigger_offset: usize, size: Rect, - ) { + ) -> Option { let mut completion = Completion::new( editor, savepoint, @@ -980,15 +998,17 @@ impl EditorView { if completion.is_empty() { // skip if we got no completion results - return; + return None; } + let area = completion.area(size, editor); editor.last_completion = None; self.last_insert.1.push(InsertEvent::TriggerCompletion); // TODO : propagate required size on resize to completion too completion.required_size((size.width, size.height)); self.completion = Some(completion); + Some(area) } pub fn clear_completion(&mut self, editor: &mut Editor) { @@ -1038,10 +1058,15 @@ impl EditorView { .. } = *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)| { - view.pos_at_screen_coords(&editor.documents[&view.doc], row, column, true) - .map(|pos| (pos, view.id)) + view.pos_at_screen_coords( + &editor.documents[&view.doc], + row, + column, + ignore_virtual_text, + ) + .map(|pos| (pos, view.id)) }) }; @@ -1056,7 +1081,7 @@ impl EditorView { MouseEventKind::Down(MouseButton::Left) => { 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); if modifiers == KeyModifiers::ALT { @@ -1120,7 +1145,7 @@ impl EditorView { _ => 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, None => return EventResult::Ignored(None), } @@ -1191,7 +1216,7 @@ impl EditorView { 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); doc.set_selection(view_id, Selection::point(pos)); cxt.editor.focus(view_id); @@ -1214,6 +1239,11 @@ impl Component for EditorView { event: &Event, context: &mut crate::compositor::Context, ) -> EventResult { + if let Some(explore) = self.explorer.as_mut() { + if let EventResult::Consumed(callback) = explore.handle_event(event, context) { + return EventResult::Consumed(callback); + } + } let mut cx = commands::Context { editor: context.editor, count: None, @@ -1267,13 +1297,15 @@ impl Component for EditorView { // let completion swallow the event if necessary let mut consumed = false; if let Some(completion) = &mut self.completion { - // use a fake context here - let mut cx = Context { - editor: cx.editor, - jobs: cx.jobs, - scroll: None, + let res = { + // use a fake context here + let mut cx = Context { + editor: cx.editor, + jobs: cx.jobs, + scroll: None, + }; + completion.handle_event(event, &mut cx) }; - let res = completion.handle_event(event, &mut cx); if let EventResult::Consumed(callback) = res { consumed = true; @@ -1281,6 +1313,12 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn 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")); let config = cx.editor.config(); + let editor_area = area.clip_bottom(1); + // check if bufferline should be rendered use helix_view::editor::BufferLine; let use_bufferline = match config.bufferline { @@ -1370,15 +1410,43 @@ impl Component for EditorView { _ => false, }; - // -1 for commandline and -1 for bufferline - let mut editor_area = area.clip_bottom(1); - if use_bufferline { - editor_area = editor_area.clip_top(1); - } + let editor_area = if use_bufferline { + editor_area.clip_top(1) + } else { + editor_area + }; + + let editor_area = if let Some(explorer) = &self.explorer { + let explorer_column_width = if explorer.is_opened() { + explorer.column_width().saturating_add(2) + } else { + 0 + }; + // For future developer: + // We should have a Dock trait that allows a component to dock to the top/left/bottom/right + // of another component. + match config.explorer.position { + ExplorerPosition::Left => editor_area.clip_left(explorer_column_width), + ExplorerPosition::Right => editor_area.clip_right(explorer_column_width), + } + } else { + editor_area + }; // if the terminal size suddenly changed, we need to trigger a resize cx.editor.resize(editor_area); + if let Some(explorer) = self.explorer.as_mut() { + if !explorer.is_focus() { + let area = if use_bufferline { + area.clip_top(1) + } else { + area + }; + explorer.render(area, surface, cx); + } + } + if use_bufferline { Self::render_bufferline(cx.editor, area.with_height(1), surface); } @@ -1457,9 +1525,47 @@ impl Component for EditorView { if let Some(completion) = self.completion.as_mut() { completion.render(area, surface, cx); } + + if let Some(explore) = self.explorer.as_mut() { + let needs_update = explore.is_focus() || { + if let Some(current_document_path) = doc!(cx.editor).path().cloned() { + if let Some(current_explore_path) = explore.current_file() { + if *current_explore_path != current_document_path { + let _ = explore.reveal_file(current_document_path); + + true + } else { + false + } + } else { + let _ = explore.reveal_file(current_document_path); + true + } + } else { + false + } + }; + + if needs_update { + let area = if use_bufferline { + area.clip_top(1) + } else { + area + }; + explore.render(area, surface, cx); + } + } } fn cursor(&self, _area: Rect, editor: &Editor) -> (Option, CursorKind) { + if let Some(explore) = &self.explorer { + if explore.is_focus() { + let cursor = explore.cursor(_area, editor); + if cursor.0.is_some() { + return cursor; + } + } + } match editor.cursor() { // All block cursors are drawn manually (pos, CursorKind::Block) => (pos, CursorKind::Hidden), diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs new file mode 100644 index 00000000..f4953966 --- /dev/null +++ b/helix-term/src/ui/explorer.rs @@ -0,0 +1,1442 @@ +use super::{tree::TreeIcons, Prompt, TreeOp, TreeView, TreeViewItem}; +use crate::{ + compositor::{Component, Context, EventResult}, + ctrl, key, shift, ui, +}; +use anyhow::{bail, ensure, Result}; +use helix_core::Position; + +use helix_view::{ + editor::{Action, ExplorerPosition}, + graphics::{CursorKind, Rect}, + info::Info, + input::{Event, KeyEvent}, + theme::Modifier, + Editor, +}; +use std::path::{Path, PathBuf}; +use std::{borrow::Cow, fs::DirEntry}; +use std::{cmp::Ordering, sync::Arc}; +use tui::{ + buffer::Buffer as Surface, + widgets::{Block, Borders, Widget}, +}; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)] +enum FileType { + File, + Folder, + Root, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +struct FileInfo { + file_type: FileType, + path: PathBuf, +} + +impl FileInfo { + fn root(path: PathBuf) -> Self { + Self { + file_type: FileType::Root, + path, + } + } + + fn get_text(&self) -> Cow<'static, str> { + let text = match self.file_type { + FileType::Root => format!("{}", self.path.display()), + FileType::File | FileType::Folder => self + .path + .file_name() + .map_or("/".into(), |p| p.to_string_lossy().into_owned()), + }; + + #[cfg(test)] + let text = text.replace(std::path::MAIN_SEPARATOR, "/"); + + text.into() + } +} + +impl PartialOrd for FileInfo { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for FileInfo { + fn cmp(&self, other: &Self) -> Ordering { + use FileType::*; + match (self.file_type, other.file_type) { + (Root, _) => return Ordering::Less, + (_, Root) => return Ordering::Greater, + _ => {} + }; + + if let (Some(p1), Some(p2)) = (self.path.parent(), other.path.parent()) { + if p1 == p2 { + match (self.file_type, other.file_type) { + (Folder, File) => return Ordering::Less, + (File, Folder) => return Ordering::Greater, + _ => {} + }; + } + } + self.path.cmp(&other.path) + } +} + +impl TreeViewItem for FileInfo { + type Params = State; + + fn get_children(&self) -> Result> { + match self.file_type { + FileType::Root | FileType::Folder => {} + _ => return Ok(vec![]), + }; + let ret: Vec<_> = std::fs::read_dir(&self.path)? + .filter_map(|entry| entry.ok()) + .filter_map(|entry| dir_entry_to_file_info(entry, &self.path)) + .collect(); + Ok(ret) + } + + fn name(&self) -> String { + self.get_text().to_string() + } + + fn is_parent(&self) -> bool { + matches!(self.file_type, FileType::Folder | FileType::Root) + } +} + +fn dir_entry_to_file_info(entry: DirEntry, path: &Path) -> Option { + entry.metadata().ok().map(|meta| { + let file_type = match meta.is_dir() { + true => FileType::Folder, + false => FileType::File, + }; + FileInfo { + file_type, + path: path.join(entry.file_name()), + } + }) +} + +#[derive(Clone, Debug)] +enum PromptAction { + CreateFileOrFolder, + RemoveFolder, + RemoveFile, + RenameFile, +} + +#[derive(Clone, Debug, Default)] +struct State { + focus: bool, + open: bool, + current_root: PathBuf, + area_width: u16, +} + +impl State { + fn new(focus: bool, current_root: PathBuf) -> Self { + Self { + focus, + current_root, + open: true, + area_width: 0, + } + } +} + +struct ExplorerHistory { + tree: TreeView, + current_root: PathBuf, +} + +pub struct Explorer { + tree: TreeView, + history: Vec, + show_help: bool, + state: State, + prompt: Option<(PromptAction, Prompt)>, + #[allow(clippy::type_complexity)] + on_next_key: Option EventResult>>, + column_width: u16, +} + +impl Explorer { + pub fn new(cx: &mut Context) -> Result { + let current_root = std::env::current_dir() + .unwrap_or_else(|_| "./".into()) + .canonicalize()?; + Ok(Self { + tree: Self::new_tree_view(current_root.clone(), Self::get_tree_icons(cx))?, + history: vec![], + show_help: false, + state: State::new(true, current_root), + prompt: None, + on_next_key: None, + column_width: cx.editor.config().explorer.column_width as u16, + }) + } + + fn get_tree_icons(cx: &Context) -> TreeIcons { + let icons = cx.editor.icons.clone(); + let defaults = TreeIcons::default(); + + let file_icon = icons + .ui + .as_ref() + .and_then(|s| s.get("file").cloned()) + .unwrap_or(defaults.item); + let item = file_icon.clone(); + let tree_closed = icons + .ui + .as_ref() + .and_then(|s| s.get("folder").cloned()) + .unwrap_or(defaults.tree_closed); + let tree_opened = icons + .ui + .as_ref() + .and_then(|s| s.get("folder_opened").cloned()) + .unwrap_or(defaults.tree_opened); + + TreeIcons { + tree_closed, + tree_opened, + item, + icon_fn: Some(Arc::new(move |item| { + icons + .icon_from_path(Some(&PathBuf::from(item))) + .cloned() + .unwrap_or_else(|| file_icon.to_owned()) + })), + } + } + + #[cfg(test)] + fn from_path(root: PathBuf, column_width: u16) -> Result { + Ok(Self { + tree: Self::new_tree_view(root.clone(), TreeIcons::default())?, + history: vec![], + show_help: false, + state: State::new(true, root), + prompt: None, + on_next_key: None, + column_width, + }) + } + + fn new_tree_view(root: PathBuf, icons: TreeIcons) -> Result> { + let root = FileInfo::root(root); + Ok(TreeView::build_tree(root)? + .with_enter_fn(Self::toggle_current) + .icons(icons)) + } + + fn push_history(&mut self, tree_view: TreeView, current_root: PathBuf) { + self.history.push(ExplorerHistory { + tree: tree_view, + current_root, + }); + const MAX_HISTORY_SIZE: usize = 20; + Vec::truncate(&mut self.history, MAX_HISTORY_SIZE) + } + + fn change_root(&mut self, root: PathBuf) -> Result<()> { + if self.state.current_root.eq(&root) { + return Ok(()); + } + let tree = Self::new_tree_view(root.clone(), self.tree.icons.clone())?; + let old_tree = std::mem::replace(&mut self.tree, tree); + self.push_history(old_tree, self.state.current_root.clone()); + self.state.current_root = root; + Ok(()) + } + + pub fn reveal_file(&mut self, path: PathBuf) -> Result<()> { + let current_root = &self.state.current_root.canonicalize()?; + let current_path = &path.canonicalize()?; + let segments = { + let stripped = match current_path.strip_prefix(current_root) { + Ok(stripped) => Ok(stripped), + Err(_) => { + let parent = path.parent().ok_or_else(|| { + anyhow::anyhow!("Failed get parent of '{}'", current_path.to_string_lossy()) + })?; + self.change_root(parent.into())?; + current_path + .strip_prefix(parent.canonicalize()?) + .map_err(|_| { + anyhow::anyhow!( + "Failed to strip prefix (parent) '{}' from '{}'", + parent.to_string_lossy(), + current_path.to_string_lossy() + ) + }) + } + }?; + + stripped + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect::>() + }; + self.tree.reveal_item(segments)?; + Ok(()) + } + + pub fn reveal_current_file(&mut self, cx: &mut Context) -> Result<()> { + self.focus(); + let current_document_path = doc!(cx.editor).path().cloned(); + match current_document_path { + None => Ok(()), + Some(current_path) => self.reveal_file(current_path), + } + } + + pub fn focus(&mut self) { + self.state.focus = true; + self.state.open = true; + } + + pub fn unfocus(&mut self) { + self.state.focus = false; + } + + fn close(&mut self) { + self.state.focus = false; + self.state.open = false; + } + + pub fn is_focus(&self) -> bool { + self.state.focus + } + + fn new_create_file_or_folder_prompt(&mut self, cx: &mut Context) -> Result<()> { + let folder_path = self.nearest_folder()?; + self.prompt = Some(( + PromptAction::CreateFileOrFolder, + Prompt::new( + format!( + " New file or folder (ends with '{}'): ", + std::path::MAIN_SEPARATOR + ) + .into(), + None, + ui::completers::none, + |_, _, _| {}, + ) + .with_line(format!("{}/", folder_path.to_string_lossy()), cx.editor), + )); + Ok(()) + } + + fn nearest_folder(&self) -> Result { + let current = self.tree.current()?.item(); + if current.is_parent() { + Ok(current.path.to_path_buf()) + } else { + let parent_path = current.path.parent().ok_or_else(|| { + anyhow::anyhow!(format!( + "Unable to get parent path of '{}'", + current.path.to_string_lossy() + )) + })?; + Ok(parent_path.to_path_buf()) + } + } + + fn new_remove_prompt(&mut self) -> Result<()> { + let item = self.tree.current()?.item(); + match item.file_type { + FileType::Folder => self.new_remove_folder_prompt(), + FileType::File => self.new_remove_file_prompt(), + FileType::Root => bail!("Root is not removable"), + } + } + + fn new_rename_prompt(&mut self, cx: &mut Context) -> Result<()> { + let path = self.tree.current_item()?.path.clone(); + self.prompt = Some(( + PromptAction::RenameFile, + Prompt::new( + " Rename to ".into(), + None, + ui::completers::none, + |_, _, _| {}, + ) + .with_line(path.to_string_lossy().to_string(), cx.editor), + )); + Ok(()) + } + + fn new_remove_file_prompt(&mut self) -> Result<()> { + let item = self.tree.current_item()?; + ensure!( + item.path.is_file(), + "The path '{}' is not a file", + item.path.to_string_lossy() + ); + self.prompt = Some(( + PromptAction::RemoveFile, + Prompt::new( + format!(" Delete file: '{}'? y/N: ", item.path.display()).into(), + None, + ui::completers::none, + |_, _, _| {}, + ), + )); + Ok(()) + } + + fn new_remove_folder_prompt(&mut self) -> Result<()> { + let item = self.tree.current_item()?; + ensure!( + item.path.is_dir(), + "The path '{}' is not a folder", + item.path.to_string_lossy() + ); + + self.prompt = Some(( + PromptAction::RemoveFolder, + Prompt::new( + format!(" Delete folder: '{}'? y/N: ", item.path.display()).into(), + None, + ui::completers::none, + |_, _, _| {}, + ), + )); + Ok(()) + } + + fn toggle_current(item: &mut FileInfo, cx: &mut Context, state: &mut State) -> TreeOp { + (|| -> Result { + if item.path == Path::new("") { + return Ok(TreeOp::Noop); + } + let meta = std::fs::metadata(&item.path)?; + if meta.is_file() { + cx.editor.open(&item.path, Action::Replace)?; + state.focus = false; + return Ok(TreeOp::Noop); + } + + if item.path.is_dir() { + return Ok(TreeOp::GetChildsAndInsert); + } + + Err(anyhow::anyhow!("Unknown file type: {:?}", meta.file_type())) + })() + .unwrap_or_else(|err| { + cx.editor.set_error(format!("{err}")); + TreeOp::Noop + }) + } + + fn render_tree( + &mut self, + area: Rect, + prompt_area: Rect, + surface: &mut Surface, + cx: &mut Context, + ) { + self.tree.render(area, prompt_area, surface, cx); + } + + fn render_embed( + &mut self, + area: Rect, + surface: &mut Surface, + cx: &mut Context, + position: &ExplorerPosition, + ) { + if !self.state.open { + return; + } + let width = area.width.min(self.column_width + 2); + + self.state.area_width = area.width; + + let side_area = match position { + ExplorerPosition::Left => Rect { width, ..area }, + ExplorerPosition::Right => Rect { + x: area.width - width, + width, + ..area + }, + } + .clip_bottom(1); + let background = cx.editor.theme.get("ui.background"); + surface.clear_with(side_area, background); + + let prompt_area = area.clip_top(side_area.height); + + let list_area = match position { + ExplorerPosition::Left => { + render_block(side_area.clip_left(1), surface, Borders::RIGHT).clip_bottom(1) + } + ExplorerPosition::Right => { + render_block(side_area.clip_right(1), surface, Borders::LEFT).clip_bottom(1) + } + }; + self.render_tree(list_area, prompt_area, surface, cx); + + { + let statusline = if self.is_focus() { + cx.editor.theme.get("ui.statusline") + } else { + cx.editor.theme.get("ui.statusline.inactive") + }; + let area = side_area.clip_top(list_area.height); + let area = match position { + ExplorerPosition::Left => area.clip_right(1), + ExplorerPosition::Right => area.clip_left(1), + }; + surface.clear_with(area, statusline); + + let title_style = cx.editor.theme.get("ui.text"); + let title_style = if self.is_focus() { + title_style.add_modifier(Modifier::BOLD) + } else { + title_style + }; + surface.set_stringn( + area.x, + area.y, + if self.is_focus() { + " EXPLORER: press ? for help" + } else { + " EXPLORER" + }, + area.width.into(), + title_style, + ); + } + + if self.is_focus() && self.show_help { + let help_area = match position { + ExplorerPosition::Left => area, + ExplorerPosition::Right => area.clip_right(list_area.width.saturating_add(2)), + }; + self.render_help(help_area, surface, cx); + } + + if let Some((_, prompt)) = self.prompt.as_mut() { + prompt.render_prompt(prompt_area, surface, cx) + } + } + + fn render_help(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + Info::new( + "Explorer", + &[ + ("?", "Toggle help"), + ("a", "Add file/folder"), + ("r", "Rename file/folder"), + ("d", "Delete file"), + ("B", "Change root to parent folder"), + ("]", "Change root to current folder"), + ("[", "Go to previous root"), + ("+, =", "Increase size"), + ("-, _", "Decrease size"), + ("q", "Close"), + ] + .into_iter() + .chain(ui::tree::tree_view_help().into_iter()) + .collect::>(), + ) + .render(area, surface, cx) + } + + fn handle_prompt_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { + let result = (|| -> Result { + let (action, mut prompt) = match self.prompt.take() { + Some((action, p)) => (action, p), + _ => return Ok(EventResult::Ignored(None)), + }; + let line = prompt.line(); + + let current_item_path = self.tree.current_item()?.path.clone(); + match (&action, event) { + (PromptAction::CreateFileOrFolder, key!(Enter)) => { + if line.ends_with(std::path::MAIN_SEPARATOR) { + self.new_folder(line)? + } else { + self.new_file(line)? + } + } + (PromptAction::RemoveFolder, key) => { + if let key!('y') = key { + close_documents(current_item_path, cx)?; + self.remove_folder()?; + } + } + (PromptAction::RemoveFile, key) => { + if let key!('y') = key { + close_documents(current_item_path, cx)?; + self.remove_file()?; + } + } + (PromptAction::RenameFile, key!(Enter)) => { + close_documents(current_item_path, cx)?; + self.rename_current(line)?; + } + (_, key!(Esc) | ctrl!('c')) => {} + _ => { + prompt.handle_event(&Event::Key(*event), cx); + self.prompt = Some((action, prompt)); + } + } + Ok(EventResult::Consumed(None)) + })(); + match result { + Ok(event_result) => event_result, + Err(err) => { + cx.editor.set_error(err.to_string()); + EventResult::Consumed(None) + } + } + } + + fn new_file(&mut self, path: &str) -> Result<()> { + let path = helix_core::path::get_normalized_path(&PathBuf::from(path)); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let mut fd = std::fs::OpenOptions::new(); + fd.create_new(true).write(true).open(&path)?; + self.reveal_file(path) + } + + fn new_folder(&mut self, path: &str) -> Result<()> { + let path = helix_core::path::get_normalized_path(&PathBuf::from(path)); + std::fs::create_dir_all(&path)?; + self.reveal_file(path) + } + + fn toggle_help(&mut self) { + self.show_help = !self.show_help + } + + fn go_to_previous_root(&mut self) { + if let Some(history) = self.history.pop() { + self.tree = history.tree; + self.state.current_root = history.current_root + } + } + + fn change_root_to_current_folder(&mut self) -> Result<()> { + self.change_root(self.tree.current_item()?.path.clone()) + } + + fn change_root_parent_folder(&mut self) -> Result<()> { + if let Some(parent) = self.state.current_root.parent() { + let path = parent.to_path_buf(); + self.change_root(path) + } else { + Ok(()) + } + } + + /// Returns the current file in the tree view + pub fn current_file(&self) -> Option<&PathBuf> { + self.tree.current_item().ok().map(|c| &c.path) + } + + pub fn is_opened(&self) -> bool { + self.state.open + } + + pub fn column_width(&self) -> u16 { + self.column_width + } + + fn increase_size(&mut self) { + const EDITOR_MIN_WIDTH: u16 = 10; + self.column_width = std::cmp::min( + self.state.area_width.saturating_sub(EDITOR_MIN_WIDTH), + self.column_width.saturating_add(1), + ) + } + + fn decrease_size(&mut self) { + self.column_width = self.column_width.saturating_sub(1) + } + + fn rename_current(&mut self, line: &String) -> Result<()> { + let item = self.tree.current_item()?; + let path = PathBuf::from(line); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(&item.path, &path)?; + self.tree.refresh()?; + self.reveal_file(path) + } + + fn remove_folder(&mut self) -> Result<()> { + let item = self.tree.current_item()?; + std::fs::remove_dir_all(&item.path)?; + self.tree.refresh() + } + + fn remove_file(&mut self) -> Result<()> { + let item = self.tree.current_item()?; + std::fs::remove_file(&item.path)?; + self.tree.refresh() + } +} + +fn close_documents(current_item_path: PathBuf, cx: &mut Context) -> Result<()> { + let ids = cx + .editor + .documents + .iter() + .filter_map(|(id, doc)| { + if doc.path()?.starts_with(¤t_item_path) { + Some(*id) + } else { + None + } + }) + .collect::>(); + + for id in ids { + cx.editor.close_document(id, true)?; + } + Ok(()) +} + +impl Component for Explorer { + /// Process input events, return true if handled. + fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { + if self.tree.prompting() { + return self.tree.handle_event(event, cx, &mut self.state); + } + let key_event = match event { + Event::Key(event) => event, + Event::Resize(..) => return EventResult::Consumed(None), + _ => return EventResult::Ignored(None), + }; + if !self.is_focus() { + return EventResult::Ignored(None); + } + if let Some(mut on_next_key) = self.on_next_key.take() { + return on_next_key(cx, self, key_event); + } + + if let EventResult::Consumed(c) = self.handle_prompt_event(key_event, cx) { + return EventResult::Consumed(c); + } + + (|| -> Result<()> { + match key_event { + key!(Esc) => self.unfocus(), + key!('q') => self.close(), + key!('?') => self.toggle_help(), + key!('a') => self.new_create_file_or_folder_prompt(cx)?, + shift!('B') => self.change_root_parent_folder()?, + key!(']') => self.change_root_to_current_folder()?, + key!('[') => self.go_to_previous_root(), + key!('d') => self.new_remove_prompt()?, + key!('r') => self.new_rename_prompt(cx)?, + key!('-') | key!('_') => self.decrease_size(), + key!('+') | key!('=') => self.increase_size(), + _ => { + self.tree + .handle_event(&Event::Key(*key_event), cx, &mut self.state); + } + }; + Ok(()) + })() + .unwrap_or_else(|err| cx.editor.set_error(format!("{err}"))); + + EventResult::Consumed(None) + } + + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + if area.width < 10 || area.height < 5 { + cx.editor.set_error("explorer render area is too small"); + return; + } + let config = &cx.editor.config().explorer; + let position = config.position; + self.render_embed(area, surface, cx, &position); + } + + fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { + if let Some(prompt) = self + .prompt + .as_ref() + .map(|(_, prompt)| prompt) + .or_else(|| self.tree.prompt()) + { + let (x, y) = (area.x, area.y + area.height.saturating_sub(1)); + prompt.cursor(Rect::new(x, y, area.width, 1), editor) + } else { + (None, CursorKind::Hidden) + } + } +} + +fn render_block(area: Rect, surface: &mut Surface, borders: Borders) -> Rect { + let block = Block::default().borders(borders); + let inner = block.inner(area); + block.render(area, surface); + inner +} + +#[cfg(test)] +mod test_explorer { + use crate::compositor::Component; + + use super::Explorer; + use helix_view::graphics::Rect; + use std::{fs, path::PathBuf}; + + /// This code should create the following file tree: + /// + /// test_explorer/ + /// ├── index.html + /// ├── .gitignore + /// ├── scripts + /// │ └── main.js + /// └── styles + /// ├── style.css + /// └── public + /// └── file + /// + fn dummy_file_tree(name: &str) -> PathBuf { + let path: PathBuf = format!("test_explorer{}{}", std::path::MAIN_SEPARATOR, name).into(); + if path.exists() { + fs::remove_dir_all(path.clone()).unwrap(); + } + fs::create_dir_all(path.clone()).unwrap(); + fs::write(path.join("index.html"), "").unwrap(); + fs::write(path.join(".gitignore"), "").unwrap(); + + fs::create_dir_all(path.join("scripts")).unwrap(); + fs::write(path.join("scripts").join("main.js"), "").unwrap(); + + fs::create_dir_all(path.join("styles")).unwrap(); + fs::write(path.join("styles").join("style.css"), "").unwrap(); + + fs::create_dir_all(path.join("styles").join("public")).unwrap(); + fs::write(path.join("styles").join("public").join("file"), "").unwrap(); + + path + } + + fn render(explorer: &mut Explorer) -> String { + explorer.tree.render_to_string(Rect::new(0, 0, 50, 10)) + } + + fn new_explorer(name: &str) -> (PathBuf, Explorer) { + let path = dummy_file_tree(name); + (path.clone(), Explorer::from_path(path, 30).unwrap()) + } + + #[test] + fn test_reveal_file() { + let (path, mut explorer) = new_explorer("reveal_file"); + + // 0a. Expect the "scripts" folder is not opened + assert_eq!( + render(&mut explorer), + " +(test_explorer/reveal_file) +⏵ scripts +⏵ styles + .gitignore + index.html +" + .trim() + ); + + // 1. Reveal "scripts/main.js" + explorer.reveal_file(path.join("scripts/main.js")).unwrap(); + + // 1a. Expect the "scripts" folder is opened, and "main.js" is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/reveal_file] +⏷ [scripts] + (main.js) +⏵ styles + .gitignore + index.html +" + .trim() + ); + + // 2. Change root to "scripts" + explorer.tree.move_up(1); + explorer.change_root_to_current_folder().unwrap(); + + // 2a. Expect the current root is "scripts" + assert_eq!( + render(&mut explorer), + " +(test_explorer/reveal_file/scripts) + main.js +" + .trim() + ); + + // 3. Reveal "styles/public/file", which is outside of the current root + explorer + .reveal_file(path.join("styles/public/file")) + .unwrap(); + + // 3a. Expect the current root is "public", and "file" is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/reveal_file/styles/public] + (file) +" + .trim() + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_rename() { + let (path, mut explorer) = new_explorer("rename"); + + explorer.handle_events("jjj").unwrap(); + assert_eq!( + render(&mut explorer), + " +[test_explorer/rename] +⏵ scripts +⏵ styles + (.gitignore) + index.html +" + .trim() + ); + + // 0. Open the rename file prompt + explorer.handle_events("r").unwrap(); + + // 0.1 Expect the current prompt to be related to file renaming + let prompt = &explorer.prompt.as_ref().unwrap().1; + assert_eq!(prompt.prompt(), " Rename to "); + assert_eq!( + prompt.line().replace(std::path::MAIN_SEPARATOR, "/"), + "test_explorer/rename/.gitignore" + ); + + // 1. Rename the current file to a name that is lexicographically greater than "index.html" + explorer.handle_events("who.is").unwrap(); + + // 1a. Expect the file is renamed, and is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/rename] +⏵ scripts +⏵ styles + index.html + (who.is) +" + .trim() + ); + + assert!(path.join("who.is").exists()); + + // 2. Rename the current file into an existing folder + explorer + .handle_events(&format!( + "rstyles{}lol", + std::path::MAIN_SEPARATOR + )) + .unwrap(); + + // 2a. Expect the file is moved to the folder, and is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/rename] +⏵ scripts +⏷ [styles] + ⏵ public + (lol) + style.css + index.html +" + .trim() + ); + + assert!(path.join("styles/lol").exists()); + + // 3. Rename the current file into a non-existent folder + explorer + .handle_events(&format!( + "r{}", + path.join("new_folder/sponge/bob").display() + )) + .unwrap(); + + // 3a. Expect the non-existent folder to be created, + // and the file is moved into it, + // and the renamed file is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/rename] +⏷ [new_folder] + ⏷ [sponge] + (bob) +⏵ scripts +⏷ styles + ⏵ public + style.css + index.html +" + .trim() + ); + + assert!(path.join("new_folder/sponge/bob").exists()); + + // 4. Change current root to "new_folder/sponge" + explorer.handle_events("k]").unwrap(); + + // 4a. Expect the current root to be "sponge" + assert_eq!( + render(&mut explorer), + " +(test_explorer/rename/new_folder/sponge) + bob +" + .trim() + ); + + // 5. Move cursor to "bob", and rename it outside of the current root + explorer.handle_events("j").unwrap(); + explorer + .handle_events(&format!( + "r{}", + path.join("scripts/bob").display() + )) + .unwrap(); + + // 5a. Expect the current root to be "scripts" + assert_eq!( + render(&mut explorer), + " +[test_explorer/rename/scripts] + (bob) + main.js +" + .trim() + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_new_folder() { + let (path, mut explorer) = new_explorer("new_folder"); + + // 0. Open the add file/folder prompt + explorer.handle_events("a").unwrap(); + let prompt = &explorer.prompt.as_ref().unwrap().1; + fn to_forward_slash(s: &str) -> String { + s.replace(std::path::MAIN_SEPARATOR, "/") + } + fn to_os_main_separator(s: &str) -> String { + s.replace('/', format!("{}", std::path::MAIN_SEPARATOR).as_str()) + } + assert_eq!( + to_forward_slash(prompt.prompt()), + " New file or folder (ends with '/'): " + ); + assert_eq!(to_forward_slash(prompt.line()), "test_explorer/new_folder/"); + + // 1. Add a new folder at the root + explorer + .handle_events(&to_os_main_separator("yoyo/")) + .unwrap(); + + // 1a. Expect the new folder is added, and is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/new_folder] +⏵ scripts +⏵ styles +⏷ (yoyo) + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("yoyo")).is_ok()); + + // 2. Move up to "styles" + explorer.handle_events("k").unwrap(); + + // 3. Add a new folder + explorer + .handle_events(&to_os_main_separator("asus.sass/")) + .unwrap(); + + // 3a. Expect the new folder is added under "styles", although "styles" is not opened + assert_eq!( + render(&mut explorer), + " +[test_explorer/new_folder] +⏵ scripts +⏷ [styles] + ⏵ public + ⏷ (sus.sass) + style.css +⏷ yoyo + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles/sus.sass")).is_ok()); + + // 4. Add a new folder with non-existent parents + explorer + .handle_events(&to_os_main_separator("aa/b/c/")) + .unwrap(); + + // 4a. Expect the non-existent parents are created, + // and the new folder is created, + // and is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/new_folder] +⏷ [styles] + ⏷ [sus.sass] + ⏷ [a] + ⏷ [b] + ⏷ (c) + style.css +⏷ yoyo + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles/sus.sass/a/b/c")).is_ok()); + + // 5. Move to "style.css" + explorer.handle_events("j").unwrap(); + + // 6. Add a new folder here + explorer + .handle_events(&to_os_main_separator("afoobar/")) + .unwrap(); + + // 6a. Expect the folder is added under "styles", + // because the folder of the current item, "style.css" is "styles/" + assert_eq!( + render(&mut explorer), + " +[test_explorer/new_folder] +⏵ scripts +⏷ [styles] + ⏷ (foobar) + ⏵ public + ⏷ sus.sass + ⏷ a + ⏷ b + ⏷ c + style.css +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles/foobar")).is_ok()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_new_file() { + let (path, mut explorer) = new_explorer("new_file"); + + // 1. Add a new file at the root + explorer.handle_events("ayoyo").unwrap(); + + // 1a. Expect the new file is added, and is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/new_file] +⏵ scripts +⏵ styles + .gitignore + index.html + (yoyo) +" + .trim() + ); + + assert!(fs::read_to_string(path.join("yoyo")).is_ok()); + + // 2. Move up to "styles" + explorer.tree.move_up(3); + + // 3. Add a new file + explorer.handle_events("asus.sass").unwrap(); + + // 3a. Expect the new file is added under "styles", although "styles" is not opened + assert_eq!( + render(&mut explorer), + " +[test_explorer/new_file] +⏵ scripts +⏷ [styles] + ⏵ public + style.css + (sus.sass) + .gitignore + index.html + yoyo +" + .trim() + ); + + assert!(fs::read_to_string(path.join("styles/sus.sass")).is_ok()); + + // 4. Add a new file with non-existent parents + explorer.handle_events("aa/b/c").unwrap(); + + // 4a. Expect the non-existent parents are created, + // and the new file is created, + // and is focused + assert_eq!( + render(&mut explorer), + " +[test_explorer/new_file] +⏵ scripts +⏷ [styles] + ⏷ [a] + ⏷ [b] + (c) + ⏵ public + style.css + sus.sass + .gitignore +" + .trim() + ); + + assert!(fs::read_to_string(path.join("styles/a/b/c")).is_ok()); + + // 5. Move to "style.css" + explorer.handle_events("jj").unwrap(); + + // 6. Add a new file here + explorer.handle_events("afoobar").unwrap(); + + // 6a. Expect the file is added under "styles", + // because the folder of the current item, "style.css" is "styles/" + assert_eq!( + render(&mut explorer), + " +[test_explorer/new_file] +⏷ [styles] + ⏷ b + c + ⏵ public + (foobar) + style.css + sus.sass + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_to_string(path.join("styles/foobar")).is_ok()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_remove_file() { + let (path, mut explorer) = new_explorer("remove_file"); + + // 1. Move to ".gitignore" + explorer.handle_events("/.gitignore").unwrap(); + + // 1a. Expect the cursor is at ".gitignore" + assert_eq!( + render(&mut explorer), + " +[test_explorer/remove_file] +⏵ scripts +⏵ styles + (.gitignore) + index.html +" + .trim() + ); + + assert!(fs::read_to_string(path.join(".gitignore")).is_ok()); + + // 2. Remove the current file + explorer.handle_events("dy").unwrap(); + + // 3. Expect ".gitignore" is deleted, and the cursor moved down + assert_eq!( + render(&mut explorer), + " +[test_explorer/remove_file] +⏵ scripts +⏵ styles + (index.html) +" + .trim() + ); + + assert!(fs::read_to_string(path.join(".gitignore")).is_err()); + + // 3a. Expect "index.html" exists + assert!(fs::read_to_string(path.join("index.html")).is_ok()); + + // 4. Remove the current file + explorer.handle_events("dy").unwrap(); + + // 4a. Expect "index.html" is deleted, at the cursor moved up + assert_eq!( + render(&mut explorer), + " +[test_explorer/remove_file] +⏵ scripts +⏵ (styles) +" + .trim() + ); + + assert!(fs::read_to_string(path.join("index.html")).is_err()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_remove_folder() { + let (path, mut explorer) = new_explorer("remove_folder"); + + // 1. Move to "styles/" + explorer.handle_events("/styleso").unwrap(); + + // 1a. Expect the cursor is at "styles" + assert_eq!( + render(&mut explorer), + " +[test_explorer/remove_folder] +⏵ scripts +⏷ (styles) + ⏵ public + style.css + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles")).is_ok()); + + // 2. Remove the current folder + explorer.handle_events("dy").unwrap(); + + // 3. Expect "styles" is deleted, and the cursor moved down + assert_eq!( + render(&mut explorer), + " +[test_explorer/remove_folder] +⏵ scripts + (.gitignore) + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles")).is_err()); + } + + #[test] + fn test_change_root() { + let (path, mut explorer) = new_explorer("change_root"); + + // 1. Move cursor to "styles" + explorer.reveal_file(path.join("styles")).unwrap(); + + // 2. Change root to current folder, and move cursor down + explorer.change_root_to_current_folder().unwrap(); + explorer.tree.move_down(1); + + // 2a. Expect the current root to be "styles", and the cursor is at "public" + assert_eq!( + render(&mut explorer), + " +[test_explorer/change_root/styles] +⏵ (public) + style.css +" + .trim() + ); + + let current_root = explorer.state.current_root.clone(); + + // 3. Change root to the parent of current folder + explorer.change_root_parent_folder().unwrap(); + + // 3a. Expect the current root to be "change_root" + assert_eq!( + render(&mut explorer), + " +(test_explorer/change_root) +⏵ scripts +⏵ styles + .gitignore + index.html +" + .trim() + ); + + // 4. Go back to previous root + explorer.go_to_previous_root(); + + // 4a. Expect the root te become "styles", and the cursor position is not forgotten + assert_eq!( + render(&mut explorer), + " +[test_explorer/change_root/styles] +⏵ (public) + style.css +" + .trim() + ); + assert_eq!(explorer.state.current_root, current_root); + + // 5. Go back to previous root again + explorer.go_to_previous_root(); + + // 5a. Expect the current root to be "change_root" again, + // but this time the "styles" folder is opened, + // because it was opened before any change of root + assert_eq!( + render(&mut explorer), + " +[test_explorer/change_root] +⏵ scripts +⏷ (styles) + ⏵ public + style.css + .gitignore + index.html +" + .trim() + ); + } +} diff --git a/helix-term/src/ui/fuzzy_match.rs b/helix-term/src/ui/fuzzy_match.rs index b406702f..22dc3a7f 100644 --- a/helix-term/src/ui/fuzzy_match.rs +++ b/helix-term/src/ui/fuzzy_match.rs @@ -54,7 +54,7 @@ impl QueryAtom { } fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec) -> bool { - // for inverse there are no indicies to return + // for inverse there are no indices to return // just return whether we matched if self.inverse { return self.matches(matcher, item); @@ -120,7 +120,7 @@ enum QueryAtomKind { /// /// Usage: `foo` Fuzzy, - /// Item contains query atom as a continous substring + /// Item contains query atom as a continuous substring /// /// Usage `'foo` Substring, @@ -213,7 +213,7 @@ impl FuzzyQuery { Some(score) } - pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec)> { + pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec)> { let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else( || Some((0, Vec::new())), |atom| matcher.fuzzy_indices(item, atom), diff --git a/helix-term/src/ui/fuzzy_match/test.rs b/helix-term/src/ui/fuzzy_match/test.rs index 3f90ef68..5df79eeb 100644 --- a/helix-term/src/ui/fuzzy_match/test.rs +++ b/helix-term/src/ui/fuzzy_match/test.rs @@ -7,8 +7,8 @@ fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec { items .iter() .filter_map(|item| { - let (_, indicies) = query.fuzzy_indicies(item, &matcher)?; - let matched_string = indicies + let (_, indices) = query.fuzzy_indices(item, &matcher)?; + let matched_string = indices .iter() .map(|&pos| item.chars().nth(pos).unwrap()) .collect(); diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 30625ace..be94eeee 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -4,29 +4,29 @@ use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; -use tui::{buffer::Buffer as Surface, widgets::Table}; +use tui::{buffer::Buffer as Surface, text::Span, widgets::Table}; pub use tui::widgets::{Cell, Row}; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; -use helix_view::{graphics::Rect, Editor}; +use helix_view::{graphics::Rect, icons::Icons, Editor}; use tui::layout::Constraint; pub trait Item { /// Additional editor state that is used for label calculation. type Data; - fn format(&self, data: &Self::Data) -> Row; + fn format<'a>(&self, data: &Self::Data, icons: Option<&'a Icons>) -> Row; fn sort_text(&self, data: &Self::Data) -> Cow { - let label: String = self.format(data).cell_text().collect(); + let label: String = self.format(data, None).cell_text().collect(); label.into() } fn filter_text(&self, data: &Self::Data) -> Cow { - let label: String = self.format(data).cell_text().collect(); + let label: String = self.format(data, None).cell_text().collect(); label.into() } } @@ -35,11 +35,15 @@ impl Item for PathBuf { /// Root prefix to strip. type Data = PathBuf; - fn format(&self, root_path: &Self::Data) -> Row { - self.strip_prefix(root_path) + fn format<'a>(&self, root_path: &Self::Data, icons: Option<&'a Icons>) -> Row { + let path_str = self + .strip_prefix(root_path) .unwrap_or(self) - .to_string_lossy() - .into() + .to_string_lossy(); + match icons.and_then(|icons| icons.icon_from_path(Some(self))) { + Some(icon) => Row::new([icon.into(), Span::raw(path_str)]), + None => path_str.into(), + } } } @@ -142,10 +146,10 @@ impl Menu { let n = self .options .first() - .map(|option| option.format(&self.editor_data).cells.len()) + .map(|option| option.format(&self.editor_data, None).cells.len()) .unwrap_or_default(); let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.format(&self.editor_data); + let row = option.format(&self.editor_data, None); // maintain max for each column for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { let width = cell.content.width(); @@ -331,7 +335,7 @@ impl Component for Menu { let rows = options .iter() - .map(|option| option.format(&self.editor_data)); + .map(|option| option.format(&self.editor_data, None)); let table = Table::new(rows) .style(style) .highlight_style(selected) @@ -347,6 +351,7 @@ impl Component for Menu { offset: scroll, selected: self.cursor, }, + false, ); if let Some(cursor) = self.cursor { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3e9a14b0..a0433d7d 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,6 +1,7 @@ mod completion; mod document; pub(crate) mod editor; +mod explorer; mod fuzzy_match; mod info; pub mod lsp; @@ -13,12 +14,14 @@ mod prompt; mod spinner; mod statusline; mod text; +mod tree; use crate::compositor::{Component, Compositor}; use crate::filter_picker_entry; use crate::job::{self, Callback}; pub use completion::Completion; pub use editor::EditorView; +use helix_view::icons::Icons; pub use markdown::Markdown; pub use menu::Menu; pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; @@ -26,7 +29,9 @@ pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; +pub use tree::{TreeOp, TreeView, TreeViewItem}; +pub use explorer::Explorer; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; use helix_view::Editor; @@ -158,7 +163,11 @@ pub fn regex_prompt( cx.push_layer(Box::new(prompt)); } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { +pub fn file_picker( + root: PathBuf, + config: &helix_view::editor::Config, + icons: &Icons, +) -> FilePicker { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; @@ -220,6 +229,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi FilePicker::new( files, root, + config.icons.picker.then_some(icons), move |cx, path: &PathBuf, action| { if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { @@ -239,7 +249,7 @@ pub mod completers { use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; use helix_view::document::SCRATCH_BUFFER_NAME; - use helix_view::theme; + use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; @@ -280,9 +290,9 @@ pub mod completers { } pub fn theme(_editor: &Editor, input: &str) -> Vec { - let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); + let mut names = helix_loader::read_toml_names(&helix_loader::config_dir().join("themes")); for rt_dir in helix_loader::runtime_dirs() { - names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); + names.extend(helix_loader::read_toml_names(&rt_dir.join("themes"))); } names.push("default".into()); names.push("base16_default".into()); @@ -311,6 +321,37 @@ pub mod completers { names } + pub fn icons(_editor: &Editor, input: &str) -> Vec { + let mut names = helix_loader::read_toml_names(&helix_loader::config_dir().join("icons")); + for rt_dir in helix_loader::runtime_dirs() { + names.extend(helix_loader::read_toml_names(&rt_dir.join("icons"))); + } + names.push("default".into()); + names.sort(); + names.dedup(); + + let mut names: Vec<_> = names + .into_iter() + .map(|name| ((0..), Cow::from(name))) + .collect(); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = names + .into_iter() + .filter_map(|(_range, name)| { + matcher.fuzzy_match(&name, input).map(|score| (name, score)) + }) + .collect(); + + matches.sort_unstable_by(|(name1, score1), (name2, score2)| { + (Reverse(*score1), name1).cmp(&(Reverse(*score2), name2)) + }); + names = matches.into_iter().map(|(name, _)| ((0..), name)).collect(); + + names + } + /// Recursive function to get all keys from this value and add them to vec fn get_keys(value: &serde_json::Value, vec: &mut Vec, scope: Option<&str>) { if let Some(map) = value.as_object() { diff --git a/helix-term/src/ui/overlay.rs b/helix-term/src/ui/overlay.rs index 5b2bc806..56252c71 100644 --- a/helix-term/src/ui/overlay.rs +++ b/helix-term/src/ui/overlay.rs @@ -16,29 +16,10 @@ pub struct Overlay { } /// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom -pub fn overlayed(content: T) -> Overlay { +pub fn overlaid(content: T) -> Overlay { Overlay { content, - calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)), - } -} - -fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect { - fn mul_and_cast(size: u16, factor: u8) -> u16 { - ((size as u32) * (factor as u32) / 100).try_into().unwrap() - } - - let inner_w = mul_and_cast(rect.width, percent_horizontal); - let inner_h = mul_and_cast(rect.height, percent_vertical); - - let offset_x = rect.width.saturating_sub(inner_w) / 2; - let offset_y = rect.height.saturating_sub(inner_h) / 2; - - Rect { - x: rect.x + offset_x, - y: rect.y + offset_y, - width: inner_w, - height: inner_h, + calc_child_size: Box::new(|rect: Rect| rect.overlayed()), } } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 3294a2a1..18ff7313 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -31,6 +31,7 @@ use helix_core::{ use helix_view::{ editor::Action, graphics::{CursorKind, Margin, Modifier, Rect}, + icons::Icons, theme::Style, view::ViewPosition, Document, DocumentId, Editor, @@ -126,11 +127,12 @@ impl FilePicker { pub fn new( options: Vec, editor_data: T::Data, + icons: Option<&'_ Icons>, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { let truncate_start = true; - let mut picker = Picker::new(options, editor_data, callback_fn); + let mut picker = Picker::new(options, editor_data, icons, callback_fn); picker.truncate_start = truncate_start; Self { @@ -424,12 +426,14 @@ pub struct Picker { widths: Vec, callback_fn: PickerCallback, + has_icons: bool, } impl Picker { pub fn new( options: Vec, editor_data: T::Data, + icons: Option<&'_ Icons>, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { let prompt = Prompt::new( @@ -452,9 +456,10 @@ impl Picker { callback_fn: Box::new(callback_fn), completion_height: 0, widths: Vec::new(), + has_icons: icons.is_some(), }; - picker.calculate_column_widths(); + picker.calculate_column_widths(icons); // scoring on empty input // TODO: just reuse score() @@ -472,23 +477,23 @@ impl Picker { picker } - pub fn set_options(&mut self, new_options: Vec) { + pub fn set_options(&mut self, new_options: Vec, icons: &'_ Icons) { self.options = new_options; self.cursor = 0; self.force_score(); - self.calculate_column_widths(); + self.calculate_column_widths(self.has_icons.then_some(icons)); } /// Calculate the width constraints using the maximum widths of each column /// for the current options. - fn calculate_column_widths(&mut self) { + fn calculate_column_widths(&mut self, icons: Option<&'_ Icons>) { let n = self .options .first() - .map(|option| option.format(&self.editor_data).cells.len()) + .map(|option| option.format(&self.editor_data, icons).cells.len()) .unwrap_or_default(); let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.format(&self.editor_data); + let row = option.format(&self.editor_data, icons); // maintain max for each column for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { let width = cell.content.width(); @@ -779,7 +784,12 @@ impl Component for Picker { .skip(offset) .take(rows as usize) .map(|pmatch| &self.options[pmatch.index]) - .map(|option| option.format(&self.editor_data)) + .map(|option| { + option.format( + &self.editor_data, + cx.editor.config().icons.picker.then_some(&cx.editor.icons), + ) + }) .map(|mut row| { const TEMP_CELL_SEP: &str = " "; @@ -794,7 +804,7 @@ impl Component for Picker { // might be inconsistencies. This is the best we can do since only the // text in Row is displayed to the end user. let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) - .fuzzy_indicies(&line, &self.matcher) + .fuzzy_indices(&line, &self.matcher) .unwrap_or_default(); let highlight_byte_ranges: Vec<_> = line @@ -885,6 +895,7 @@ impl Component for Picker { offset: 0, selected: Some(cursor), }, + self.truncate_start, ); } @@ -952,7 +963,7 @@ impl Component for DynamicPicker { Some(overlay) => &mut overlay.content.file_picker.picker, None => return, }; - picker.set_options(new_options); + picker.set_options(new_options, &editor.icons); editor.reset_idle_timer(); })); anyhow::Ok(callback) diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 5a95c1bb..dff7b231 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -6,7 +6,10 @@ use crate::{ use tui::buffer::Buffer as Surface; 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 // a width/height hint. maybe Popup(Box) @@ -88,10 +91,10 @@ impl Popup { /// Calculate the position where the popup should be rendered and return the coordinates of the /// 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 .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; @@ -155,6 +158,16 @@ impl Popup { pub fn contents_mut(&mut self) -> &mut T { &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 Component for Popup { @@ -232,16 +245,9 @@ impl Component for Popup { } fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { - // trigger required_size so we recalculate if the child changed - self.required_size((viewport.width, viewport.height)); - + let area = self.area(viewport, cx.editor); 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 let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 35ae8c2a..3be74ebf 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -94,6 +94,10 @@ impl Prompt { self } + pub fn prompt(&self) -> &str { + self.prompt.as_ref() + } + pub fn line(&self) -> &String { &self.line } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 88786351..72f716dc 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -4,6 +4,7 @@ use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, graphics::Rect, + icons::Icon, theme::Style, Document, Editor, View, }; @@ -21,6 +22,7 @@ pub struct RenderContext<'a> { pub focused: bool, pub spinners: &'a ProgressSpinners, pub parts: RenderBuffer<'a>, + pub icons: RenderContextIcons<'a>, } impl<'a> RenderContext<'a> { @@ -31,6 +33,25 @@ impl<'a> RenderContext<'a> { focused: bool, spinners: &'a ProgressSpinners, ) -> Self { + // Determine icon based on language name if possible + let mut filetype_icon = None; + if let Some(language_config) = doc.language_config() { + for filetype in &language_config.file_types { + let filetype_str = match filetype { + helix_core::syntax::FileType::Extension(s) => s, + helix_core::syntax::FileType::Suffix(s) => s, + }; + filetype_icon = editor.icons.icon_from_filetype(filetype_str); + if filetype_icon.is_some() { + break; + } + } + } + // Otherwise based on filetype + if filetype_icon.is_none() { + filetype_icon = editor.icons.icon_from_path(doc.path()) + } + RenderContext { editor, doc, @@ -38,10 +59,21 @@ impl<'a> RenderContext<'a> { focused, spinners, parts: RenderBuffer::default(), + icons: RenderContextIcons { + enabled: editor.config().icons.statusline, + filetype_icon, + vcs_icon: editor.icons.ui.as_ref().and_then(|ui| ui.get("vcs_branch")), + }, } } } +pub struct RenderContextIcons<'a> { + pub enabled: bool, + pub filetype_icon: Option<&'a Icon>, + pub vcs_icon: Option<&'a Icon>, +} + #[derive(Default)] pub struct RenderBuffer<'a> { pub left: Spans<'a>, @@ -148,6 +180,7 @@ where helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding, helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending, helix_view::editor::StatusLineElement::FileType => render_file_type, + helix_view::editor::StatusLineElement::FileTypeIcon => render_file_type_icon, helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics, helix_view::editor::StatusLineElement::WorkspaceDiagnostics => render_workspace_diagnostics, helix_view::editor::StatusLineElement::Selections => render_selections, @@ -240,7 +273,13 @@ where if warnings > 0 { write( context, - "●".to_string(), + context + .editor + .icons + .diagnostic + .warning + .icon_char + .to_string(), Some(context.editor.theme.get("warning")), ); write(context, format!(" {} ", warnings), None); @@ -249,7 +288,7 @@ where if errors > 0 { write( context, - "●".to_string(), + context.editor.icons.diagnostic.error.icon_char.to_string(), Some(context.editor.theme.get("error")), ); write(context, format!(" {} ", errors), None); @@ -282,7 +321,13 @@ where if warnings > 0 { write( context, - "●".to_string(), + context + .editor + .icons + .diagnostic + .warning + .icon_char + .to_string(), Some(context.editor.theme.get("warning")), ); write(context, format!(" {} ", warnings), None); @@ -291,7 +336,7 @@ where if errors > 0 { write( context, - "●".to_string(), + context.editor.icons.diagnostic.error.icon_char.to_string(), Some(context.editor.theme.get("error")), ); write(context, format!(" {} ", errors), None); @@ -412,6 +457,21 @@ where write(context, format!(" {} ", file_type), None); } +fn render_file_type_icon(context: &mut RenderContext, write: F) +where + F: Fn(&mut RenderContext, String, Option