Compare commits

...

50 Commits

Author SHA1 Message Date
trivernis eda2ae2f18
Add WIP change detection 2 years ago
trivernis 817c2a7e6d
Add blocking on file deletion
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 092163bd8f
Add command to delete the file of the current buffer 2 years ago
trivernis 0725aa1046
Add flag to open the file explorer on startup
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis a864b63b0f
Add space to open files in the explorer as an alternative to enter 2 years ago
trivernis 423435d12a
Add auto-select of tree explorer nodes when jumping between buffers 2 years ago
trivernis dbebd26661
Update README 2 years ago
trivernis c7492aea1f
Merge branch 'completion' 2 years ago
trivernis 1e1967308d
Merge branch 'master' 2 years ago
trivernis c77120433f
Modify workflows 2 years ago
trivernis 32079b7ed9
Change some default config values and default theme
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 4487f144bd
Swap screenshot in README
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 83f81c730a
Fix README indentation
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 9d1d6a74ec
Merge remote-tracking branch 'tree-explorer-icons/tree_explorer_icons' 2 years ago
Roland Singer d839c2a2f5 Merge branch 'tree_explorer' into tree_explorer_icons 2 years ago
Roland Singer 3d987c0703 removed match 2 years ago
Roland Singer 92ebab74b5 adjusted explorer to new helix code 2 years ago
Roland Singer eec845cc34 adjusted explorer to new helix code 2 years ago
Roland Singer 8f43b1f6e9 Merge branch 'tree_explore' of https://github.com/cossonfork/helix into tree_explorer 2 years ago
trivernis 63a879d156
Add hint to README 2 years ago
trivernis e0b30e0911
Merge branch 'master' 2 years ago
trivernis beeca2d823
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
trivernis 6d245dd3d6
Update README
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 233aa1c023
Change keybinds of workspace command picker 2 years ago
trivernis 59c0c41a74
Merge branch 'lsp-execute-command' 2 years ago
trivernis 9816d82e48
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
trivernis 3cec10e1b1
Fix missing error variant 2 years ago
trivernis 2da81760f5
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
trivernis 388181bab7
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
cossonleo aec001ad84 completion fix 2 years ago
trivernis c24c30584b
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
trivernis 18b30200cb
Merge branch 'tree_explore' 2 years ago
trivernis e5ed461ca7
Merge branch 'tree_explore' of github.com:cossonfork/helix into tree_explore 2 years ago
trivernis de3b54563b
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
cossonleo 0e04c4c93c tree helper and file explorer 2 years ago
trivernis 117e3b3b55
Update tree explore with new helix event types
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 151ac52741
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
MDeiml 90d40b70e8 Add workspace command picker 2 years ago
trivernis c3c50517ee
Merge branch 'lsp-restart'
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 6f01208a73
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
trivernis a6e16df9a3
Merge branch 'master' of github.com:helix-editor/helix 2 years ago
trivernis 7cb8330751
Merge remote-tracking branch 'pr-tree/tree_explore'
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Igor Cohanovschi 9c15d4fade Removed unused fields. Renamed style names to be in line with other ui.editor styles 2 years ago
Igor Cohanovschi 9bd9e4cc42 Added icons and light styling to tree_explorer 2 years ago
Igor Cohanovschi e1bf96f25f fixed compilation issue 2 years ago
cossonleo 0f4cfa0fe7 optimize tree explore render 2 years ago
cossonleo 10cd4c56c9 tree helper and file explorer 2 years ago
cossonleo 06a5bca79b optimize tree explore render 2 years ago
cossonleo 21d4bf859b tree helper and file explorer 2 years ago
Carter Green 46ff498766 Add command to restart LSP server
Useful if LSP configuration changes or crashes
2 years ago

@ -3,7 +3,7 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- master - main
schedule: schedule:
- cron: '00 01 * * *' - cron: '00 01 * * *'

@ -1,26 +0,0 @@
# Publish the Nix flake outputs to Cachix
name: Cachix
on:
push:
branches:
- master
jobs:
publish:
name: Publish Flake
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install nix
uses: cachix/install-nix-action@v17
- name: Authenticate with Cachix
uses: cachix/cachix-action@v10
with:
name: helix
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Build nix flake
run: nix build -L

@ -1,41 +0,0 @@
name: Github Pages
on:
push:
branches:
- master
tags:
- '*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
with:
mdbook-version: 'latest'
# mdbook-version: '0.4.8'
- run: mdbook build book
- name: Set output directory
run: |
OUTDIR=$(basename ${{ github.ref }})
echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book
destination_dir: ./${{ env.OUTDIR }}
- name: Deploy stable
uses: peaceiris/actions-gh-pages@v3
if: startswith(github.ref, 'refs/tags/')
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book

90
Cargo.lock generated

@ -140,6 +140,16 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "crossbeam-channel"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.11" version = "0.8.11"
@ -260,6 +270,18 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "filetime"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -275,6 +297,15 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.24" version = "0.3.24"
@ -483,6 +514,7 @@ dependencies = [
"ignore", "ignore",
"indoc", "indoc",
"log", "log",
"notify",
"once_cell", "once_cell",
"pulldown-cmark", "pulldown-cmark",
"serde", "serde",
@ -594,6 +626,26 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@ -618,6 +670,26 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "kqueue"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587"
dependencies = [
"bitflags",
"libc",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -699,6 +771,24 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "notify"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a"
dependencies = [
"bitflags",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"mio",
"walkdir",
"winapi",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"

@ -1,4 +1,26 @@
# Helix # Helix Plus
<h1 style="color:red">This is an unstable fork of helix with some PRs merged and some merge conflicts resolved </h1>
# Merged PRs
- [File explorer and tree helper](https://github.com/helix-editor/helix/pull/2377)
- [with Icons](https://github.com/r0l1/helix/tree/tree_explorer_icons)
- [Add LSP workspace command picker](https://github.com/helix-editor/helix/pull/3140)
- [completion fix](https://github.com/helix-editor/helix/pull/1819)
And others I forgot about...
# Applied Changes
- Changed opening the window popup from `ctrl + w` to `ctrl + v`
- Added an auto highlight for files in the tree explorer when jumping through opened buffers
- Changed some default settings (enabling bufferline, indent guides, the embedded explorer, cursor modes etc.)
- Added a `--show-explorer` cli flag to open the file explorer on startup (useful for embedded explorer mode)
- Added a `delete` (aliases `rm`, `del`) command to delete the file associated with the current buffer
- - -
[![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions) [![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions)

@ -148,6 +148,8 @@ auto-pairs = false # defaults to `true`
The default pairs are <code>(){}[]''""``</code>, but these can be customized by The default pairs are <code>(){}[]''""``</code>, but these can be customized by
setting `auto-pairs` to a TOML table: setting `auto-pairs` to a TOML table:
Example
```toml ```toml
[editor.auto-pairs] [editor.auto-pairs]
'(' = ')' '(' = ')'
@ -213,6 +215,18 @@ tab = "→"
newline = "⏎" newline = "⏎"
tabpad = "·" # Tabs will look like "→···" (depending on tab width) tabpad = "·" # Tabs will look like "→···" (depending on tab width)
``` ```
<<<<<<< HEAD
### `[editor.explorer]` Section
Sets explorer side width and style.
| Key | Description | Default |
| --- | ----------- | ------- |
| `column-width` | explorer side width | 30 |
| `style` | explorer item style, tree or list | tree |
| `position` | explorer widget position, embed or overlay | overlay |
||||||| 43027d91
=======
### `[editor.indent-guides]` Section ### `[editor.indent-guides]` Section
@ -230,3 +244,13 @@ Example:
render = true render = true
character = "╎" character = "╎"
``` ```
### `[editor.explorer]` Section
Sets explorer side width and style.
| Key | Description | Default |
| --- | ----------- | ------- |
| `column-width` | explorer side width | 30 |
| `style` | explorer item style, tree or list | tree |
| `position` | explorer widget position, embed or overlay | overlay |
>>>>>>> 0e04c4c93caadb704c11a72bcf626b1f10ff2d98

@ -68,4 +68,9 @@
| `:insert-output` | Run shell command, inserting output after each selection. | | `:insert-output` | Run shell command, inserting output after each selection. |
| `:append-output` | Run shell command, appending output after each selection. | | `:append-output` | Run shell command, appending output after each selection. |
| `:pipe` | Pipe each selection to the shell command. | | `:pipe` | Pipe each selection to the shell command. |
<<<<<<< HEAD
| `:run-shell-command`, `:sh` | Run a shell command | | `:run-shell-command`, `:sh` | Run a shell command |
||||||| 4b1fe367
=======
| `:lsp-restart` | Restarts the LSP server of the current buffer |
>>>>>>> lsp-restart

@ -0,0 +1,75 @@
| Name | Description |
| --- | --- |
| `:quit`, `:q` | Close the current view. |
| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). |
| `:open`, `:o` | Open a file from disk into the current view. |
| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). |
| `:buffer-close-others`, `:bco`, `:bcloseother` | Close all buffers but the currently focused one. |
| `:buffer-close-others!`, `:bco!`, `:bcloseother!` | Close all buffers but the currently focused one. |
| `:buffer-close-all`, `:bca`, `:bcloseall` | Close all buffers, without quitting. |
| `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Close all buffers forcefully (ignoring unsaved changes), without quitting. |
| `:buffer-next`, `:bn`, `:bnext` | Go to next buffer. |
| `:buffer-previous`, `:bp`, `:bprev` | Go to previous buffer. |
| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
| `:write!`, `:w!` | Write changes to disk forcefully (creating necessary subdirectories). Accepts an optional path (:write some/path.txt) |
| `:new`, `:n` | Create a new scratch buffer. |
| `:format`, `:fmt` | Format the file using the LSP formatter. |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
| `:line-ending` | Set the document's default line ending. Options: crlf, lf. |
| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
| `:write-all`, `:wa` | Write changes from all buffers to disk. |
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. |
| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). |
| `:quit-all`, `:qa` | Close all views. |
| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). |
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:cquit!`, `:cq!` | Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme. |
| `: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. |
| `:primary-clipboard-yank-join` | Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. |
| `:clipboard-paste-after` | Paste system clipboard after selections. |
| `:clipboard-paste-before` | Paste system clipboard before selections. |
| `:clipboard-paste-replace` | Replace selections with content of system clipboard. |
| `:primary-clipboard-paste-after` | Paste primary clipboard after selections. |
| `:primary-clipboard-paste-before` | Paste primary clipboard before selections. |
| `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. |
| `:show-clipboard-provider` | Show clipboard provider name in status bar. |
| `:change-current-directory`, `:cd` | Change the current working directory. |
| `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
| `:reload` | Discard changes and reload from the source file. |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
| `:debug-eval` | Evaluate expression in current debug context. |
| `:vsplit`, `:vs` | Open the file in a vertical split. |
| `:vsplit-new`, `:vnew` | Open a scratch buffer in a vertical split. |
| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
| `:hsplit-new`, `:hnew` | Open a scratch buffer in a horizontal split. |
| `:tutor` | Open the tutorial. |
| `:goto`, `:g` | Go to line number. |
| `:set-language`, `:lang` | Set the language of current buffer. |
| `:set-option`, `:set` | Set a config option at runtime.<br>For example to disable smart case search, use `:set search.smart-case false`. |
| `:get-option`, `:get` | Get the current value of a config option. |
| `:sort` | Sort ranges in selection. |
| `:rsort` | Sort ranges in selection in reverse order. |
| `:reflow` | Hard-wrap the current selection of lines to a given width. |
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
| `:config-reload` | Refreshes helix's config. |
| `:config-open` | Open the helix config.toml file. |
| `:log-open` | Open the helix log file. |
| `:insert-output` | Run shell command, inserting output after each selection. |
| `:append-output` | Run shell command, appending output after each selection. |
| `:pipe` | Pipe each selection to the shell command. |
<<<<<<< HEAD
| `:run-shell-command`, `:sh` | Run a shell command |
||||||| 4b1fe367
=======
| `:lsp-restart` | Restarts the LSP server of the current buffer |
>>>>>>> lsp-restart

@ -276,6 +276,8 @@ This layer is a kludge of mappings, mostly pickers.
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` | | `/` | Global search in workspace folder | `global_search` |
| `?` | Open command palette | `command_palette` | | `?` | Open command palette | `command_palette` |
| `e` | Open or focus explorer | `toggle_or_focus_explorer` |
| `E` | open explorer recursion | `open_explorer_recursion` |
> TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file. > TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
@ -420,3 +422,35 @@ Keys to use within prompt, Remapping currently not supported.
| `Tab` | Select next completion item | | `Tab` | Select next completion item |
| `BackTab` | Select previous completion item | | `BackTab` | Select previous completion item |
| `Enter` | Open selected | | `Enter` | Open selected |
# File explorer
Keys to use within explorer, Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Escape` | Back to editor |
| `Ctrl-c` | Close explorer |
| `Enter` | Open file or toggle dir selected |
| `b` | Back to current root's parent |
| `f` | Filter items |
| `z` | Fold currrent level |
| `k`, `Shift-Tab`, `Up` | select previous item |
| `j`, `Tab`, `Down` | select next item |
| `h` | Scroll left |
| `l` | Scroll right |
| `G` | Move to last item |
| `Ctrl-d` | Move down half page |
| `Ctrl-u` | Move up half page |
| `Shift-d` | Move down a page |
| `Shift-u` | Move up a page |
| `/` | Search item |
| `?` | Search item reverse |
| `n` | Repeat last search |
| `Shift-n` | Repeat last search reverse |
| `gg` | Move to first item |
| `ge` | Move to last item |
| `gc` | Make current dir as root dir |
| `mf` | Create new file under current item's parent |
| `md` | Create new dir under current item's parent |
| `rf` | Remove file selected |
| `rd` | Remove dir selected |

@ -1 +0,0 @@
../runtime/themes

@ -1 +0,0 @@
../../../src/indent.rs

@ -294,6 +294,9 @@ impl Client {
dynamic_registration: Some(false), dynamic_registration: Some(false),
..Default::default() ..Default::default()
}), }),
execute_command: Some(lsp::DynamicRegistrationClientCapabilities {
dynamic_registration: Some(false),
}),
..Default::default() ..Default::default()
}), }),
text_document: Some(lsp::TextDocumentClientCapabilities { text_document: Some(lsp::TextDocumentClientCapabilities {

@ -39,6 +39,8 @@ pub enum Error {
Timeout, Timeout,
#[error("server closed the stream")] #[error("server closed the stream")]
StreamClosed, StreamClosed,
#[error("LPS not defined")]
LspNotDefined,
#[error("Unhandled")] #[error("Unhandled")]
Unhandled, Unhandled,
#[error(transparent)] #[error(transparent)]
@ -336,20 +338,15 @@ impl Registry {
.map(|(_, client)| client.as_ref()) .map(|(_, client)| client.as_ref())
} }
pub fn restart( pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Option<Arc<Client>>> {
&mut self,
language_config: &LanguageConfiguration,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server { let config = match &language_config.language_server {
Some(config) => config, Some(config) => config,
None => return Ok(None), None => return Ok(None),
}; };
let scope = language_config.scope.clone(); match self.inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())),
match self.inner.entry(scope) { Entry::Vacant(entry) => {
Entry::Vacant(_) => Ok(None),
Entry::Occupied(mut entry) => {
// initialize a new client // initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed); let id = self.counter.fetch_add(1, Ordering::Relaxed);
@ -357,31 +354,76 @@ impl Registry {
self.incoming.push(UnboundedReceiverStream::new(incoming)); self.incoming.push(UnboundedReceiverStream::new(incoming));
entry.insert((id, client.clone())); entry.insert((id, client.clone()));
Ok(Some(client)) Ok(Some(client))
} }
} }
} }
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Option<Arc<Client>>> { pub fn restart(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
let config = match &language_config.language_server { let config = language_config
Some(config) => config, .language_server
None => return Ok(None), .as_ref()
}; .ok_or(Error::LspNotDefined)?;
let id = self
.inner
.get(&language_config.scope)
.ok_or(Error::LspNotDefined)?
.0;
let new_client = self.initialize_client(language_config, config, id)?;
let (_, client) = self
.inner
.get_mut(&language_config.scope)
.ok_or(Error::LspNotDefined)?;
*client = new_client;
Ok(client.clone())
}
match self.inner.entry(language_config.scope.clone()) { fn initialize_client(
Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())), &mut self,
Entry::Vacant(entry) => { language_config: &LanguageConfiguration,
// initialize a new client config: &helix_core::syntax::LanguageServerConfiguration,
let id = self.counter.fetch_add(1, Ordering::Relaxed); id: usize,
) -> Result<Arc<Client>> {
let (client, incoming, initialize_notify) = Client::start(
&config.command,
&config.args,
language_config.config.clone(),
&language_config.roots,
id,
config.timeout,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client);
// Initialize the client asynchronously
let _client = client.clone();
tokio::spawn(async move {
use futures_util::TryFutureExt;
let value = _client
.capabilities
.get_or_try_init(|| {
_client
.initialize()
.map_ok(|response| response.capabilities)
})
.await;
if let Err(e) = value {
log::error!("failed to initialize language server: {}", e);
return;
}
let NewClientResult(client, incoming) = start_client(id, language_config, config)?; // next up, notify<initialized>
self.incoming.push(UnboundedReceiverStream::new(incoming)); _client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await
.unwrap();
entry.insert((id, client.clone())); initialize_notify.notify_one();
Ok(Some(client)) });
}
} Ok(client)
} }
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> { pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {

@ -66,6 +66,7 @@ serde = { version = "1.0", features = ["derive"] }
# ripgrep for global search # ripgrep for global search
grep-regex = "0.1.10" grep-regex = "0.1.10"
grep-searcher = "0.1.10" grep-searcher = "0.1.10"
notify = "5.0.0"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

@ -12,11 +12,11 @@ use serde_json::json;
use crate::{ use crate::{
args::Args, args::Args,
commands::apply_workspace_edit, commands::apply_workspace_edit,
compositor::{Compositor, Event}, compositor::{self, Compositor, Event},
config::Config, config::Config,
job::Jobs, job::Jobs,
keymap::Keymaps, keymap::Keymaps,
ui::{self, overlay::overlayed}, ui::{self, overlay::overlayed, Explorer},
}; };
use log::{error, warn}; use log::{error, warn};
@ -153,7 +153,19 @@ impl Application {
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.keys &config.keys
})); }));
let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); let mut editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
if args.show_explorer {
let mut jobs = Jobs::new();
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(overlayed(explorer));
}
compositor.push(editor_view); compositor.push(editor_view);
if args.load_tutor { if args.load_tutor {

@ -17,6 +17,7 @@ pub struct Args {
pub log_file: Option<PathBuf>, pub log_file: Option<PathBuf>,
pub config_file: Option<PathBuf>, pub config_file: Option<PathBuf>,
pub files: Vec<(PathBuf, Position)>, pub files: Vec<(PathBuf, Position)>,
pub show_explorer: bool,
} }
impl Args { impl Args {
@ -32,6 +33,7 @@ impl Args {
"--version" => args.display_version = true, "--version" => args.display_version = true,
"--help" => args.display_help = true, "--help" => args.display_help = true,
"--tutor" => args.load_tutor = true, "--tutor" => args.load_tutor = true,
"--show-explorer" => args.show_explorer = true,
"--vsplit" => args.split = Some(Layout::Vertical), "--vsplit" => args.split = Some(Layout::Vertical),
"--hsplit" => args.split = Some(Layout::Horizontal), "--hsplit" => args.split = Some(Layout::Horizontal),
"--health" => { "--health" => {

@ -265,6 +265,7 @@ impl MappableCommand {
file_picker, "Open file picker", file_picker, "Open file picker",
file_picker_in_current_directory, "Open file picker at current working directory", file_picker_in_current_directory, "Open file picker at current working directory",
code_action, "Perform code action", code_action, "Perform code action",
workspace_command_picker, "Open workspace command picker",
buffer_picker, "Open buffer picker", buffer_picker, "Open buffer picker",
jumplist_picker, "Open jumplist picker", jumplist_picker, "Open jumplist picker",
symbol_picker, "Open symbol picker", symbol_picker, "Open symbol picker",
@ -433,7 +434,10 @@ impl MappableCommand {
decrement, "Decrement item under cursor", decrement, "Decrement item under cursor",
record_macro, "Record macro", record_macro, "Record macro",
replay_macro, "Replay macro", replay_macro, "Replay macro",
command_palette, "Open command palette", command_palette, "Open command pallete",
toggle_or_focus_explorer, "toggle or focus explorer",
open_explorer_recursion, "open explorer recursion",
close_explorer, "close explorer",
); );
} }
@ -2238,6 +2242,43 @@ fn file_picker_in_current_directory(cx: &mut Context) {
cx.push_layer(Box::new(overlayed(picker))); cx.push_layer(Box::new(overlayed(picker)));
} }
fn toggle_or_focus_explorer(cx: &mut Context) {
cx.callback = Some(Box::new(
|compositor: &mut Compositor, cx: &mut compositor::Context| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
match editor.explorer.as_mut() {
Some(explore) => explore.content.focus(),
None => match ui::Explorer::new(cx) {
Ok(explore) => editor.explorer = Some(overlayed(explore)),
Err(err) => cx.editor.set_error(format!("{}", err)),
},
}
}
},
));
}
fn open_explorer_recursion(cx: &mut Context) {
cx.callback = Some(Box::new(
|compositor: &mut Compositor, cx: &mut compositor::Context| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
match ui::Explorer::new_explorer_recursion() {
Ok(explore) => editor.explorer = Some(overlayed(explore)),
Err(err) => cx.editor.set_error(format!("{}", err)),
}
}
},
));
}
fn close_explorer(cx: &mut Context) {
cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
editor.explorer.take();
}
}));
}
fn buffer_picker(cx: &mut Context) { fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc; let current = view!(cx.editor).doc;
@ -2890,13 +2931,11 @@ pub mod insert {
super::completion(cx); super::completion(cx);
} }
fn language_server_completion(cx: &mut Context, ch: char) { pub fn is_server_trigger_char(doc: &Document, ch: char) -> bool {
use helix_lsp::lsp; use helix_lsp::lsp;
// if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor);
let language_server = match doc.language_server() { let language_server = match doc.language_server() {
Some(language_server) => language_server, Some(language_server) => language_server,
None => return, None => return false,
}; };
let capabilities = language_server.capabilities(); let capabilities = language_server.capabilities();
@ -2906,11 +2945,35 @@ pub mod insert {
.. ..
}) = &capabilities.completion_provider }) = &capabilities.completion_provider
{ {
// TODO: what if trigger is multiple chars long triggers.iter().any(|trigger| trigger.contains(ch))
if triggers.iter().any(|trigger| trigger.contains(ch)) { } else {
cx.editor.clear_idle_timer(); false
super::completion(cx); }
}
fn language_server_completion(cx: &mut Context, ch: char) {
use helix_core::chars::char_is_word;
let config = cx.editor.config();
if !config.auto_completion {
return;
}
let (view, doc) = current_ref!(cx.editor);
if char_is_word(ch) && doc.savepoint.is_none() {
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let mut iter = text.chars_at(cursor);
iter.reverse();
for _ in 0..config.completion_trigger_len {
if iter.next().map_or(true, |c| !char_is_word(c)) {
return;
}
} }
cx.editor.reset_idle_timer();
return;
}
if is_server_trigger_char(doc, ch) {
cx.editor.reset_idle_timer_zero();
} }
} }
@ -3822,6 +3885,13 @@ pub fn completion(cx: &mut Context) {
let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
let future = language_server.completion(doc.identifier(), pos, None); let future = language_server.completion(doc.identifier(), pos, None);
let future = async move {
match future.await {
Ok(v) => Ok(v),
Err(helix_lsp::Error::Timeout) => Ok(serde_json::Value::Null),
Err(e) => Err(e),
}
};
let trigger_offset = cursor; let trigger_offset = cursor;
@ -3835,11 +3905,21 @@ pub fn completion(cx: &mut Context) {
let start_offset = cursor.saturating_sub(offset); let start_offset = cursor.saturating_sub(offset);
let prefix = text.slice(start_offset..cursor).to_string(); let prefix = text.slice(start_offset..cursor).to_string();
doc.savepoint();
let trigger_version = doc.version();
cx.callback( cx.callback(
future, future,
move |editor, compositor, response: Option<lsp::CompletionResponse>| { move |editor, compositor, response: Option<lsp::CompletionResponse>| {
let doc = doc_mut!(editor);
let savepoint = match doc.savepoint.take() {
Some(s) => s,
None => return,
};
if editor.mode != Mode::Insert { if editor.mode != Mode::Insert {
// we're not in insert mode anymore return;
}
if savepoint.0 != trigger_version {
doc.savepoint = Some(savepoint);
return; return;
} }
@ -3850,25 +3930,27 @@ pub fn completion(cx: &mut Context) {
is_incomplete: _is_incomplete, is_incomplete: _is_incomplete,
items, items,
})) => items, })) => items,
None => Vec::new(), None => {
editor.set_status(
"The completion response is none and will request server again",
);
editor.reset_idle_timer();
return;
}
}; };
if !prefix.is_empty() { if !prefix.is_empty() {
items = items items.retain(|item| match &item.filter_text {
.into_iter() Some(t) => t.starts_with(&prefix),
.filter(|item| { None => item.label.starts_with(&prefix),
item.filter_text });
.as_ref() };
.unwrap_or(&item.label)
.starts_with(&prefix)
})
.collect();
}
if items.is_empty() { if items.is_empty() {
// editor.set_error("No completion available"); // editor.set_error("No completion available".to_string());
return; return;
} }
doc.savepoint = Some(savepoint);
let size = compositor.size(); let size = compositor.size();
let ui = compositor.find::<ui::EditorView>().unwrap(); let ui = compositor.find::<ui::EditorView>().unwrap();
ui.set_completion( ui.set_completion(

@ -496,6 +496,42 @@ pub fn code_action(cx: &mut Context) {
}, },
) )
} }
impl ui::menu::Item for lsp::Command {
type Data = ();
fn label(&self, _data: &Self::Data) -> Spans {
self.title.as_str().into()
}
}
pub fn workspace_command_picker(cx: &mut Context) {
let (_, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let execute_command_provider = match &language_server.capabilities().execute_command_provider {
Some(p) => p,
None => return,
};
let commands = execute_command_provider
.commands
.iter()
.map(|command| lsp::Command {
title: command.clone(),
command: command.clone(),
arguments: None,
})
.collect::<Vec<_>>();
cx.callback = Some(Box::new(
move |compositor: &mut Compositor, _cx: &mut compositor::Context| {
let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
execute_lsp_command(cx.editor, command.clone());
});
compositor.push(Box::new(overlayed(picker)))
},
));
}
pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) { pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
let doc = doc!(editor); let doc = doc!(editor);
let language_server = language_server!(editor, doc); let language_server = language_server!(editor, doc);

@ -232,6 +232,31 @@ fn buffer_previous(
Ok(()) Ok(())
} }
fn delete(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let doc = doc_mut!(cx.editor);
if doc.path().is_none() {
bail!("cannot delete a buffer with no associated file on the disk");
}
let future = doc.delete();
cx.jobs.add(Job::new(future));
helix_lsp::block_on(cx.jobs.finish())?;
let doc_id = view!(cx.editor).doc;
cx.editor.close_document(doc_id, true)?;
Ok(())
}
fn write_impl( fn write_impl(
cx: &mut compositor::Context, cx: &mut compositor::Context,
path: Option<&Cow<str>>, path: Option<&Cow<str>>,
@ -1658,6 +1683,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: new_file, fun: new_file,
completer: Some(completers::filename), completer: Some(completers::filename),
}, },
TypableCommand {
name: "delete",
aliases: &["remove", "rm", "del"],
doc: "Deletes the file associated with the current buffer",
fun: delete,
completer: None,
},
TypableCommand { TypableCommand {
name: "format", name: "format",
aliases: &["fmt"], aliases: &["fmt"],
@ -2046,6 +2078,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: run_shell_command, fun: run_shell_command,
completer: Some(completers::directory), completer: Some(completers::directory),
}, },
TypableCommand {
name: "lsp-restart",
aliases: &[],
doc: "Restarts the LSP server of the current buffer",
fun: lsp_restart,
completer: None,
},
]; ];
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> = pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =

@ -10,6 +10,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"j" | "down" => move_line_down, "j" | "down" => move_line_down,
"k" | "up" => move_line_up, "k" | "up" => move_line_up,
"l" | "right" => move_char_right, "l" | "right" => move_char_right,
"C-j" => half_page_down,
"C-k" => half_page_up,
"t" => find_till_char, "t" => find_till_char,
"f" => find_next_char, "f" => find_next_char,
@ -169,7 +171,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-u" => half_page_up, "C-u" => half_page_up,
"C-d" => half_page_down, "C-d" => half_page_down,
"C-w" => { "Window" "C-v" => { "View"
"C-w" | "w" => rotate_view, "C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit, "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit, "C-v" | "v" => vsplit,
@ -232,7 +234,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"e" => dap_enable_exceptions, "e" => dap_enable_exceptions,
"E" => dap_disable_exceptions, "E" => dap_disable_exceptions,
}, },
"w" => { "Window" "w" => workspace_command_picker,
"v" => { "View"
"C-w" | "w" => rotate_view, "C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit, "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit, "C-v" | "v" => vsplit,
@ -264,6 +267,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"r" => rename_symbol, "r" => rename_symbol,
"h" => select_references_to_symbol_under_cursor, "h" => select_references_to_symbol_under_cursor,
"?" => command_palette, "?" => command_palette,
"e" => toggle_or_focus_explorer,
"E" => open_explorer_recursion,
}, },
"z" => { "View" "z" => { "View"
"z" | "c" => align_view_center, "z" | "c" => align_view_center,

@ -72,6 +72,7 @@ FLAGS:
-V, --version Prints version information -V, --version Prints version information
--vsplit Splits all given files vertically into different windows --vsplit Splits all given files vertically into different windows
--hsplit Splits all given files horizontally into different windows --hsplit Splits all given files horizontally into different windows
--show-explorer Opens the explorer on startup
", ",
env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"),
env!("VERSION_AND_GIT_HASH"), env!("VERSION_AND_GIT_HASH"),

@ -394,6 +394,16 @@ impl Component for Completion {
height = rel_height.min(height); height = rel_height.min(height);
} }
Rect::new(x, y, width, height) Rect::new(x, y, width, height)
} else if popup_x > 30 {
let mut height = area.height.saturating_sub(popup_y);
let mut width = popup_x;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
width = rel_width.min(width);
height = rel_height.min(height);
}
let x = popup_x - width;
let y = popup_y;
Rect::new(x, y, width, height)
} else { } else {
let half = area.height / 2; let half = area.height / 2;
let height = 15.min(half); let height = 15.min(half);

@ -3,7 +3,7 @@ use crate::{
compositor::{Component, Context, Event, EventResult}, compositor::{Component, Context, Event, EventResult},
job, key, job, key,
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, Keymaps},
ui::{Completion, ProgressSpinners}, ui::{overlay::Overlay, Completion, Explorer, ProgressSpinners},
}; };
use helix_core::{ use helix_core::{
@ -36,6 +36,7 @@ pub struct EditorView {
last_insert: (commands::MappableCommand, Vec<InsertEvent>), last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>, pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
pub(crate) explorer: Option<Overlay<Explorer>>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -59,6 +60,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()), last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
explorer: None,
} }
} }
@ -851,7 +853,7 @@ impl EditorView {
} }
(Mode::Insert, Mode::Normal) => { (Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion // if exiting insert mode, remove completion
self.completion = None; self.clear_completion(cxt.editor);
// TODO: Use an on_mode_change hook to remove signature help // TODO: Use an on_mode_change hook to remove signature help
cxt.jobs.callback(async { cxt.jobs.callback(async {
@ -985,9 +987,6 @@ impl EditorView {
return; return;
} }
// Immediately initialize a savepoint
doc_mut!(editor).savepoint();
editor.last_completion = None; editor.last_completion = None;
self.last_insert.1.push(InsertEvent::TriggerCompletion); self.last_insert.1.push(InsertEvent::TriggerCompletion);
@ -1006,23 +1005,20 @@ impl EditorView {
} }
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult { pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
if self.completion.is_some() let config = cx.editor.config();
|| cx.editor.mode != Mode::Insert if cx.editor.mode != Mode::Insert || !config.auto_completion {
|| !cx.editor.config().auto_completion
{
return EventResult::Ignored(None); return EventResult::Ignored(None);
} }
let mut cx = commands::Context { self.clear_completion(cx.editor);
commands::completion(&mut commands::Context {
register: None, register: None,
editor: cx.editor, editor: cx.editor,
jobs: cx.jobs, jobs: cx.jobs,
count: None, count: None,
callback: None, callback: None,
on_next_key_callback: None, on_next_key_callback: None,
}; });
crate::commands::insert::idle_completion(&mut cx);
EventResult::Consumed(None) EventResult::Consumed(None)
} }
} }
@ -1212,6 +1208,11 @@ impl Component for EditorView {
event: &Event, event: &Event,
context: &mut crate::compositor::Context, context: &mut crate::compositor::Context,
) -> EventResult { ) -> EventResult {
if let Some(explore) = self.explorer.as_mut() {
if let EventResult::Consumed(callback) = explore.handle_event(event, context) {
return EventResult::Consumed(callback);
}
}
let mut cx = commands::Context { let mut cx = commands::Context {
editor: context.editor, editor: context.editor,
count: None, count: None,
@ -1246,7 +1247,7 @@ impl Component for EditorView {
EventResult::Consumed(None) EventResult::Consumed(None)
} }
Event::Key(mut key) => { Event::Key(mut key) => {
cx.editor.reset_idle_timer(); cx.editor.clear_idle_timer();
canonicalize_key(&mut key); canonicalize_key(&mut key);
// clear status // clear status
@ -1298,7 +1299,8 @@ impl Component for EditorView {
if let Some(completion) = &mut self.completion { if let Some(completion) = &mut self.completion {
completion.update(&mut cx); completion.update(&mut cx);
if completion.is_empty() { if completion.is_empty() {
self.clear_completion(cx.editor); self.completion = None;
doc_mut!(cx.editor).savepoint = None;
} }
} }
} }
@ -1361,6 +1363,22 @@ impl Component for EditorView {
} }
// if the terminal size suddenly changed, we need to trigger a resize // if the terminal size suddenly changed, we need to trigger a resize
if self.explorer.is_some() && (config.explorer.is_embed()) {
editor_area = editor_area.clip_left(config.explorer.column_width as u16 + 2);
}
cx.editor.resize(editor_area); // -1 from bottom for commandline
if let Some(explore) = self.explorer.as_mut() {
if !explore.content.is_focus() && config.explorer.is_embed() {
explore.content.handle_changes(cx).unwrap();
let current_doc = view!(cx.editor).doc;
let current_doc = cx.editor.document(current_doc).unwrap();
if let Some(path) = current_doc.path() {
explore.content.set_selection(&path);
}
explore.content.render(area, surface, cx);
}
}
cx.editor.resize(editor_area); cx.editor.resize(editor_area);
if use_bufferline { if use_bufferline {
@ -1441,9 +1459,30 @@ impl Component for EditorView {
if let Some(completion) = self.completion.as_mut() { if let Some(completion) = self.completion.as_mut() {
completion.render(area, surface, cx); completion.render(area, surface, cx);
} }
if let Some(explore) = self.explorer.as_mut() {
if explore.content.is_focus() {
if config.explorer.is_embed() {
explore.content.render(area, surface, cx);
} else {
explore.render(area, surface, cx);
}
}
}
} }
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) { fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
if let Some(explore) = &self.explorer {
if explore.content.is_focus() {
if editor.config().explorer.is_overlay() {
return explore.cursor(_area, editor);
}
let cursor = explore.content.cursor(_area, editor);
if cursor.0.is_some() {
return cursor;
}
}
}
match editor.cursor() { match editor.cursor() {
// All block cursors are drawn manually // All block cursors are drawn manually
(pos, CursorKind::Block) => (pos, CursorKind::Hidden), (pos, CursorKind::Block) => (pos, CursorKind::Hidden),

@ -0,0 +1,986 @@
use super::{Prompt, Tree, TreeItem, TreeOp};
use crate::{
compositor::{Component, Compositor, Context, EventResult},
ctrl, key, shift, ui,
};
use anyhow::{bail, ensure, Result};
use helix_core::Position;
use helix_view::{
editor::Action,
graphics::{CursorKind, Modifier, Rect},
input::{Event, KeyEvent},
Editor,
};
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::{Path, PathBuf};
use std::{borrow::Cow, sync::mpsc::channel};
use std::{cmp::Ordering, sync::mpsc::Receiver};
use tui::{
buffer::Buffer as Surface,
text::{Span, Spans},
widgets::{Block, Borders, Widget},
};
macro_rules! get_theme {
($theme: expr, $s1: expr, $s2: expr) => {
$theme.try_get($s1).unwrap_or_else(|| $theme.get($s2))
};
}
const ICONS: &'static [&'static str] =
&["", "", "", "", "", "ﰟ", "", "", "", "ﯤ", "", "ﬥ"];
const ICONS_EXT: &'static [&'static str] = &[
".rs", ".md", ".js", ".c", ".png", ".svg", ".css", ".html", ".lua", ".ts", ".py", ".json",
];
const ICONS_COLORS: &'static [helix_view::theme::Color] = &[
helix_view::theme::Color::Rgb(227, 134, 84),
helix_view::theme::Color::LightCyan,
helix_view::theme::Color::Yellow,
helix_view::theme::Color::Blue,
helix_view::theme::Color::Yellow,
helix_view::theme::Color::Yellow,
helix_view::theme::Color::Green,
helix_view::theme::Color::Blue,
helix_view::theme::Color::Red,
helix_view::theme::Color::Blue,
helix_view::theme::Color::Red,
];
#[derive(Debug, Clone, Copy, PartialEq)]
enum FileType {
File,
Dir,
Exe,
Placeholder,
Parent,
Root,
}
#[derive(Debug, Clone)]
struct FileInfo {
file_type: FileType,
expanded: bool,
path: PathBuf,
}
impl FileInfo {
fn new(path: PathBuf, file_type: FileType) -> Self {
Self {
path,
file_type,
expanded: false,
}
}
fn root(path: PathBuf) -> Self {
Self {
file_type: FileType::Root,
path,
expanded: true,
}
}
fn parent(path: &Path) -> Self {
let p = path.parent().unwrap_or_else(|| Path::new(""));
Self {
file_type: FileType::Parent,
path: p.to_path_buf(),
expanded: false,
}
}
fn get_text(&self) -> Cow<'static, str> {
match self.file_type {
FileType::Parent => "..".into(),
FileType::Placeholder => "---".into(),
FileType::Root => return format!("{}", self.path.display()).into(),
FileType::File | FileType::Exe | FileType::Dir => self
.path
.file_name()
.map_or("/".into(), |p| p.to_string_lossy().into_owned().into()),
}
}
}
impl TreeItem for FileInfo {
type Params = State;
fn text(&self, cx: &mut Context, selected: bool, state: &mut State) -> Spans {
let text = self.get_text();
let theme = &cx.editor.theme;
let style = match self.file_type {
FileType::Parent | FileType::Dir | FileType::Root => "ui.explorer.dir",
FileType::File | FileType::Exe | FileType::Placeholder => "ui.explorer.file",
};
let mut style = theme.try_get(style).unwrap_or_else(|| theme.get("ui.text"));
if selected {
let patch = match state.focus {
true => "ui.explorer.focus",
false => "ui.explorer.unfocus",
};
if let Some(patch) = theme.try_get(patch) {
style = style.patch(patch);
} else {
style = style.add_modifier(Modifier::REVERSED);
}
}
Spans::from(Span::styled(text, style))
}
fn is_child(&self, other: &Self) -> bool {
if let FileType::Parent = other.file_type {
return false;
}
if let FileType::Placeholder = self.file_type {
self.path == other.path
} else {
self.path.parent().map_or(false, |p| p == other.path)
}
}
fn cmp(&self, other: &Self) -> Ordering {
use FileType::*;
match (self.file_type, other.file_type) {
(Parent, _) => return Ordering::Less,
(_, Parent) => return Ordering::Greater,
(Root, _) => return Ordering::Less,
(_, Root) => return Ordering::Greater,
_ => {}
};
if self.path == other.path {
match (self.file_type, other.file_type) {
(_, Placeholder) => return Ordering::Less,
(Placeholder, _) => 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) {
(Dir, File | Exe) => return Ordering::Less,
(File | Exe, Dir) => return Ordering::Greater,
_ => {}
};
}
}
self.path.cmp(&other.path)
}
fn get_childs(&self) -> Result<Vec<Self>> {
match self.file_type {
FileType::Root | FileType::Dir => {}
_ => return Ok(vec![]),
};
let mut ret: Vec<_> = std::fs::read_dir(&self.path)?
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
entry.metadata().ok().map(|meta| {
let is_exe = false;
let file_type = match (meta.is_dir(), is_exe) {
(true, _) => FileType::Dir,
(_, false) => FileType::File,
(_, true) => FileType::Exe,
};
Self {
file_type,
path: self.path.join(entry.file_name()),
expanded: false,
}
})
})
.collect();
if ret.is_empty() {
ret.push(Self {
path: self.path.clone(),
file_type: FileType::Placeholder,
expanded: false,
})
}
Ok(ret)
}
fn filter(&self, _cx: &mut Context, s: &str, _params: &mut Self::Params) -> bool {
if s.is_empty() {
false
} else {
self.get_text().contains(s)
}
}
fn icon(&self) -> Option<(&'static str, &'static helix_view::theme::Color)> {
return match self.file_type {
FileType::Dir => {
if self.expanded {
//Some(("", &helix_view::theme::Color::Yellow))
Some(("", &helix_view::theme::Color::Yellow))
} else {
// Some(("", &helix_view::theme::Color::Yellow))
Some(("", &helix_view::theme::Color::Yellow))
}
}
FileType::File => {
for (i, ext) in ICONS_EXT.iter().enumerate() {
if self.get_text().ends_with(ext) {
let color = ICONS_COLORS
.iter()
.nth(i)
.unwrap_or(&helix_view::theme::Color::Blue);
return ICONS.iter().nth(i).map(|c| (*c, color));
}
}
return Some(("", &helix_view::theme::Color::LightBlue));
}
_ => None,
};
}
}
#[derive(Clone, Copy, Debug)]
enum PromptAction {
Search(bool), // search next/search pre
Mkdir,
CreateFile,
RemoveDir,
RemoveFile,
Filter,
}
#[derive(Clone, Debug)]
struct State {
focus: bool,
current_root: PathBuf,
}
impl State {
fn new(focus: bool, current_root: PathBuf) -> Self {
Self {
focus,
current_root,
}
}
}
pub struct Explorer {
tree: Tree<FileInfo>,
state: State,
prompt: Option<(PromptAction, Prompt)>,
#[allow(clippy::type_complexity)]
on_next_key: Option<Box<dyn FnMut(&mut Context, &mut Self, &KeyEvent) -> EventResult>>,
#[allow(clippy::type_complexity)]
repeat_motion: Option<Box<dyn FnMut(&mut Self, PromptAction, &mut Context) + 'static>>,
watcher: RecommendedWatcher,
io_events: Receiver<Result<notify::Event, notify::Error>>,
}
impl Explorer {
pub fn new(cx: &mut Context) -> Result<Self> {
let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into());
let items = Self::get_items(current_root.clone(), cx)?;
let (mut watcher, receiver) = Self::create_watcher()?;
watcher.watch(&current_root, RecursiveMode::Recursive)?;
Ok(Self {
tree: Tree::build_tree(items)
.with_enter_fn(Self::toggle_current)
.with_folded_fn(Self::fold_current),
state: State::new(true, current_root),
repeat_motion: None,
prompt: None,
on_next_key: None,
watcher,
io_events: receiver,
})
}
pub fn set_selection(&mut self, path: &Path) {
let info = if path.is_file() {
FileInfo::new(path.into(), FileType::File)
} else {
FileInfo::new(path.into(), FileType::Dir)
};
self.tree.select(&info);
self.tree.save_view();
}
pub fn new_explorer_recursion() -> Result<Self> {
let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into());
let parent = FileInfo::parent(&current_root);
let root = FileInfo::root(current_root.clone());
let mut tree = Tree::build_from_root(root, usize::MAX / 2)?
.with_enter_fn(Self::toggle_current)
.with_folded_fn(Self::fold_current);
tree.insert_current_level(parent);
let (mut watcher, receiver) = Self::create_watcher()?;
watcher.watch(&current_root, RecursiveMode::Recursive)?;
Ok(Self {
tree,
state: State::new(true, current_root),
repeat_motion: None,
prompt: None,
on_next_key: None,
watcher,
io_events: receiver,
})
}
pub fn focus(&mut self) {
self.state.focus = true
}
pub fn unfocus(&mut self) {
self.state.focus = false;
}
pub fn is_focus(&self) -> bool {
self.state.focus
}
fn get_items(p: PathBuf, cx: &mut Context) -> Result<Vec<FileInfo>> {
let mut items = vec![FileInfo::parent(p.as_path())];
let root = FileInfo::root(p);
let childs = root.get_childs()?;
if cx.editor.config().explorer.is_tree() {
items.push(root)
}
items.extend(childs);
Ok(items)
}
pub fn handle_changes(&mut self, cx: &mut Context) -> Result<()> {
if let Ok(o) = self.io_events.try_recv() {
match o {
Ok(_) => self.refresh(cx),
Err(e) => Err(e.into()),
}
} else {
Ok(())
}
}
fn refresh(&mut self, cx: &mut Context) -> Result<()> {
let items = Self::get_items(self.state.current_root.clone(), cx)?;
self.tree.replace_with_new_items(items);
Ok(())
}
fn render_preview(&mut self, area: Rect, surface: &mut Surface, editor: &Editor) {
if area.height <= 2 || area.width < 60 {
return;
}
let item = self.tree.current().item();
if item.file_type == FileType::Placeholder {
return;
}
let head_area = render_block(
area.clip_bottom(area.height - 2),
surface,
Borders::BOTTOM,
None,
);
let path_str = format!("{}", item.path.display());
surface.set_stringn(
head_area.x,
head_area.y,
path_str,
head_area.width as usize,
get_theme!(editor.theme, "ui.explorer.dir", "ui.text"),
);
let body_area = area.clip_top(2);
let style = editor.theme.get("ui.text");
if let Ok(preview_content) = get_preview(&item.path, body_area.height as usize) {
preview_content
.into_iter()
.enumerate()
.for_each(|(row, line)| {
surface.set_stringn(
body_area.x,
body_area.y + row as u16,
line,
body_area.width as usize,
style,
);
})
}
}
fn watch(&mut self, path: &Path) -> Result<()> {
self.watcher.watch(path, RecursiveMode::Recursive)?;
Ok(())
}
fn unwatch(&mut self, path: &Path) -> Result<()> {
self.watcher.unwatch(path)?;
Ok(())
}
fn create_watcher() -> Result<(
RecommendedWatcher,
Receiver<Result<notify::Event, notify::Error>>,
)> {
let (tx, rx) = channel();
let watcher = RecommendedWatcher::new(tx, Config::default())?;
Ok((watcher, rx))
}
fn new_search_prompt(&mut self, search_next: bool) {
self.tree.save_view();
self.prompt = Some((
PromptAction::Search(search_next),
Prompt::new("search: ".into(), None, ui::completers::none, |_, _, _| {}),
))
}
fn new_filter_prompt(&mut self) {
self.tree.save_view();
self.prompt = Some((
PromptAction::Filter,
Prompt::new("filter: ".into(), None, ui::completers::none, |_, _, _| {}),
))
}
fn new_mkdir_prompt(&mut self) {
self.prompt = Some((
PromptAction::Mkdir,
Prompt::new("mkdir: ".into(), None, ui::completers::none, |_, _, _| {}),
));
}
fn new_create_file_prompt(&mut self) {
self.prompt = Some((
PromptAction::CreateFile,
Prompt::new(
"create file: ".into(),
None,
ui::completers::none,
|_, _, _| {},
),
));
}
fn new_remove_file_prompt(&mut self, cx: &mut Context) {
let item = self.tree.current_item();
let check = || {
ensure!(item.file_type != FileType::Placeholder, "The path is empty");
ensure!(
item.file_type != FileType::Parent,
"can not remove parent dir"
);
ensure!(item.path.is_file(), "The path is not a file");
let doc = cx.editor.document_by_path(&item.path);
ensure!(doc.is_none(), "The file is opened");
Ok(())
};
if let Err(e) = check() {
cx.editor.set_error(format!("{e}"));
return;
}
let p = format!("remove file: {}, YES? ", item.path.display());
self.prompt = Some((
PromptAction::RemoveFile,
Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}),
));
}
fn new_remove_dir_prompt(&mut self, cx: &mut Context) {
let item = self.tree.current_item();
let check = || {
ensure!(item.file_type != FileType::Placeholder, "The path is empty");
ensure!(
item.file_type != FileType::Parent,
"can not remove parent dir"
);
ensure!(item.path.is_dir(), "The path is not a dir");
let doc = cx.editor.documents().find(|doc| {
doc.path()
.map(|p| p.starts_with(&item.path))
.unwrap_or(false)
});
ensure!(doc.is_none(), "There are files opened under the dir");
Ok(())
};
if let Err(e) = check() {
cx.editor.set_error(format!("{e}"));
return;
}
let p = format!("remove dir: {}, YES? ", item.path.display());
self.prompt = Some((
PromptAction::RemoveDir,
Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}),
));
}
fn fold_current(item: &mut FileInfo, _cx: &mut Context, _state: &mut State) {
if item.path.is_dir() {
item.expanded = false;
}
}
fn toggle_current(
item: &mut FileInfo,
cx: &mut Context,
state: &mut State,
) -> TreeOp<FileInfo> {
if item.file_type == FileType::Placeholder {
return TreeOp::Noop;
}
if item.path == Path::new("") {
return TreeOp::Noop;
}
let meta = match std::fs::metadata(&item.path) {
Ok(meta) => meta,
Err(e) => {
cx.editor.set_error(format!("{e}"));
return TreeOp::Noop;
}
};
if meta.is_file() {
if let Err(e) = cx.editor.open(&item.path.clone(), Action::Replace) {
cx.editor.set_error(format!("{e}"));
}
state.focus = false;
return TreeOp::Noop;
}
if item.path.is_dir() {
item.expanded = true;
if cx.editor.config().explorer.is_list() || item.file_type == FileType::Parent {
match Self::get_items(item.path.clone(), cx) {
Ok(items) => {
state.current_root = item.path.clone();
return TreeOp::ReplaceTree(items);
}
Err(e) => cx.editor.set_error(format!("{e}")),
}
} else {
return TreeOp::GetChildsAndInsert;
}
}
cx.editor.set_error("unkonw file type");
TreeOp::Noop
}
fn render_float(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let background = cx.editor.theme.get("ui.background");
let column_width = cx.editor.config().explorer.column_width as u16;
surface.clear_with(area, background);
let area = render_block(area, surface, Borders::ALL, None);
let mut preview_area = area.clip_left(column_width + 1);
if let Some((_, prompt)) = self.prompt.as_mut() {
let area = preview_area.clip_bottom(2);
let promp_area = render_block(
preview_area.clip_top(area.height),
surface,
Borders::TOP,
None,
);
prompt.render(promp_area, surface, cx);
preview_area = area;
}
self.render_preview(preview_area, surface, cx.editor);
let list_area = render_block(
area.clip_right(preview_area.width),
surface,
Borders::RIGHT,
None,
);
self.tree.render(list_area, surface, cx, &mut self.state);
}
fn render_embed(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let config = &cx.editor.config().explorer;
let side_area = area
.with_width(area.width.min(config.column_width as u16 + 2))
.clip_bottom(1);
let background = cx.editor.theme.get("ui.statusline");
surface.clear_with(side_area, background);
let preview_area = area.clip_left(side_area.width).clip_bottom(2);
let prompt_area = area.clip_top(side_area.height);
let border_style = cx.editor.theme.get("ui.explorer.border");
let list_area = render_block(
side_area.clip_left(1),
surface,
Borders::RIGHT,
Some(border_style),
)
.clip_bottom(1);
self.tree.render(list_area, surface, cx, &mut self.state);
{
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).clip_right(1);
surface.clear_with(area, statusline);
// surface.set_string_truncated(
// area.x,
// area.y,
// &self.path_state.root.to_string_lossy(),
// area.width as usize,
// |_| statusline,
// true,
// true,
// );
}
if self.is_focus() {
if preview_area.width < 30 || preview_area.height < 3 {
return;
}
let width = preview_area.width.min(90);
let mut y = self.tree.row().saturating_sub(1) as u16;
let height = (preview_area.height).min(25);
if (height + y) > preview_area.height {
y = preview_area.height - height;
}
let area = Rect::new(preview_area.x, y, width, height);
surface.clear_with(area, background);
let area = render_block(area, surface, Borders::all(), None);
self.render_preview(area, surface, cx.editor);
}
if let Some((_, prompt)) = self.prompt.as_mut() {
prompt.render_prompt(prompt_area, surface, cx)
}
}
fn handle_filter_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
let (action, mut prompt) = self.prompt.take().unwrap();
match event {
key!(Tab) | key!(Down) | ctrl!('j') => {
self.tree.clean_recycle();
return self
.tree
.handle_event(Event::Key(event.clone()), cx, &mut self.state);
}
key!(Enter) => {
self.tree.clean_recycle();
return self
.tree
.handle_event(Event::Key(event.clone()), cx, &mut self.state);
}
key!(Esc) | ctrl!('c') => self.tree.restore_recycle(),
_ => {
if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) {
self.tree.filter(prompt.line(), cx, &mut self.state);
}
self.prompt = Some((action, prompt));
}
};
EventResult::Consumed(None)
}
fn handle_search_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
let (action, mut prompt) = self.prompt.take().unwrap();
let search_next = match action {
PromptAction::Search(search_next) => search_next,
_ => return EventResult::Ignored(None),
};
match event {
key!(Tab) | key!(Down) | ctrl!('j') => {
return self
.tree
.handle_event(Event::Key(event.clone()), cx, &mut self.state)
}
key!(Enter) => {
let search_str = prompt.line().clone();
if !search_str.is_empty() {
self.repeat_motion = Some(Box::new(move |explorer, action, cx| {
if let PromptAction::Search(is_next) = action {
explorer.tree.save_view();
if is_next == search_next {
explorer
.tree
.search_next(cx, &search_str, &mut explorer.state);
} else {
explorer
.tree
.search_pre(cx, &search_str, &mut explorer.state);
}
}
}))
} else {
self.repeat_motion = None;
}
return self
.tree
.handle_event(Event::Key(event.clone()), cx, &mut self.state);
}
key!(Esc) | ctrl!('c') => self.tree.restore_view(),
_ => {
if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) {
if search_next {
self.tree.search_next(cx, prompt.line(), &mut self.state);
} else {
self.tree.search_pre(cx, prompt.line(), &mut self.state);
}
}
self.prompt = Some((action, prompt));
}
};
EventResult::Consumed(None)
}
fn handle_prompt_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
match &self.prompt {
Some((PromptAction::Search(_), _)) => return self.handle_search_event(event, cx),
Some((PromptAction::Filter, _)) => return self.handle_filter_event(event, cx),
_ => {}
};
let (action, mut prompt) = match self.prompt.take() {
Some((action, p)) => (action, p),
_ => return EventResult::Ignored(None),
};
let line = prompt.line();
match (action, event) {
(PromptAction::Mkdir, key!(Enter)) => {
if let Err(e) = self.new_path(line, true) {
cx.editor.set_error(format!("{e}"))
}
}
(PromptAction::CreateFile, key!(Enter)) => {
if let Err(e) = self.new_path(line, false) {
cx.editor.set_error(format!("{e}"))
}
}
(PromptAction::RemoveDir, key!(Enter)) => {
let item = self.tree.current_item();
if let Err(e) = std::fs::remove_dir_all(&item.path) {
cx.editor.set_error(format!("{e}"));
} else {
self.tree.fold_current_child();
self.tree.remove_current();
}
}
(PromptAction::RemoveFile, key!(Enter)) => {
if line == "YES" {
let item = self.tree.current_item();
if let Err(e) = std::fs::remove_file(&item.path) {
cx.editor.set_error(format!("{e}"));
} else {
self.tree.remove_current();
}
}
}
(_, key!(Esc) | ctrl!('c')) => {}
_ => {
prompt.handle_event(&Event::Key(*event), cx);
self.prompt = Some((action, prompt));
}
}
EventResult::Consumed(None)
}
fn new_path(&mut self, file_name: &str, is_dir: bool) -> Result<()> {
let current = self.tree.current_item();
let current_parent = if current.file_type == FileType::Placeholder {
&current.path
} else {
current
.path
.parent()
.ok_or_else(|| anyhow::anyhow!("can not get parent dir"))?
};
let p = helix_core::path::get_normalized_path(&current_parent.join(file_name));
match p.parent() {
Some(p) if p == current_parent => {}
_ => bail!("The file name is not illegal"),
};
let f = if is_dir {
std::fs::create_dir(&p)?;
FileInfo::new(p, FileType::Dir)
} else {
let mut fd = std::fs::OpenOptions::new();
fd.create_new(true).write(true).open(&p)?;
FileInfo::new(p, FileType::File)
};
if current.file_type == FileType::Placeholder {
self.tree.replace_current(f);
} else {
self.tree.insert_current_level(f);
}
Ok(())
}
}
impl Component for Explorer {
/// Process input events, return true if handled.
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
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);
}
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
editor.explorer = None;
}
})));
match key_event {
key!(Esc) => self.unfocus(),
ctrl!('c') => return close_fn,
key!('n') => {
if let Some(mut repeat_motion) = self.repeat_motion.take() {
repeat_motion(self, PromptAction::Search(true), cx);
self.repeat_motion = Some(repeat_motion);
}
}
shift!('N') => {
if let Some(mut repeat_motion) = self.repeat_motion.take() {
repeat_motion(self, PromptAction::Search(false), cx);
self.repeat_motion = Some(repeat_motion);
}
}
key!('b') => {
if let Some(p) = self.state.current_root.parent() {
match Self::get_items(p.to_path_buf(), cx) {
Ok(items) => {
self.state.current_root = p.to_path_buf();
self.tree = Tree::build_tree(items)
.with_enter_fn(Self::toggle_current)
.with_folded_fn(Self::fold_current);
}
Err(e) => cx.editor.set_error(format!("{e}")),
}
}
}
key!('f') => self.new_filter_prompt(),
key!('/') => self.new_search_prompt(true),
key!('?') => self.new_search_prompt(false),
key!('m') => {
self.on_next_key = Some(Box::new(|_, explorer, event| {
match event {
key!('d') => explorer.new_mkdir_prompt(),
key!('f') => explorer.new_create_file_prompt(),
_ => return EventResult::Ignored(None),
};
EventResult::Consumed(None)
}));
}
key!('r') => {
self.on_next_key = Some(Box::new(|cx, explorer, event| {
match event {
key!('d') => explorer.new_remove_dir_prompt(cx),
key!('f') => explorer.new_remove_file_prompt(cx),
_ => return EventResult::Ignored(None),
};
EventResult::Consumed(None)
}));
}
_ => {
self.tree
.handle_event(Event::Key(key_event.clone()), cx, &mut self.state);
}
}
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;
if config.is_embed() {
self.render_embed(area, surface, cx);
} else {
self.render_float(area, surface, cx);
}
}
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
let prompt = match self.prompt.as_ref() {
Some((_, prompt)) => prompt,
None => return (None, CursorKind::Hidden),
};
let config = &editor.config().explorer;
let (x, y) = if config.is_overlay() {
let colw = config.column_width as u16;
if area.width > colw {
(area.x + colw + 2, area.y + area.height - 2)
} else {
return (None, CursorKind::Hidden);
}
} else {
(area.x, area.y + area.height - 1)
};
prompt.cursor(Rect::new(x, y, area.width, 1), editor)
}
}
fn get_preview(p: impl AsRef<Path>, max_line: usize) -> Result<Vec<String>> {
let p = p.as_ref();
if p.is_dir() {
return Ok(p
.read_dir()?
.filter_map(|entry| entry.ok())
.take(max_line)
.map(|entry| {
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
format!("{}/", entry.file_name().to_string_lossy())
} else {
format!("{}", entry.file_name().to_string_lossy())
}
})
.collect());
}
ensure!(p.is_file(), "path: {} is not file or dir", p.display());
use std::fs::OpenOptions;
use std::io::BufRead;
let mut fd = OpenOptions::new();
fd.read(true);
let fd = fd.open(p)?;
Ok(std::io::BufReader::new(fd)
.lines()
.take(max_line)
.filter_map(|line| line.ok())
.map(|line| line.replace('\t', " "))
.collect())
}
fn render_block(
area: Rect,
surface: &mut Surface,
borders: Borders,
border_style: Option<helix_view::theme::Style>,
) -> Rect {
let mut block = Block::default().borders(borders);
if let Some(style) = border_style {
block = block.border_style(style);
}
//let block = Block::default();
let inner = block.inner(area);
block.render(area, surface);
inner
}

@ -1,5 +1,6 @@
mod completion; mod completion;
pub(crate) mod editor; pub(crate) mod editor;
mod explore;
mod info; mod info;
pub mod lsp; pub mod lsp;
mod markdown; mod markdown;
@ -11,9 +12,11 @@ mod prompt;
mod spinner; mod spinner;
mod statusline; mod statusline;
mod text; mod text;
mod tree;
pub use completion::Completion; pub use completion::Completion;
pub use editor::EditorView; pub use editor::EditorView;
pub use explore::Explorer;
pub use markdown::Markdown; pub use markdown::Markdown;
pub use menu::Menu; pub use menu::Menu;
pub use picker::{FileLocation, FilePicker, Picker}; pub use picker::{FileLocation, FilePicker, Picker};
@ -21,6 +24,7 @@ pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent}; pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner}; pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text; pub use text::Text;
pub use tree::{Tree, TreeItem, TreeOp};
use helix_core::regex::Regex; use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder; use helix_core::regex::RegexBuilder;

@ -0,0 +1,707 @@
use std::cmp::Ordering;
use std::iter::Peekable;
use anyhow::Result;
use crate::{
compositor::{Context, EventResult},
ctrl, key, shift,
};
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::{
graphics::Rect,
input::{Event, KeyEvent},
};
use tui::{buffer::Buffer as Surface, text::Spans};
pub trait TreeItem: Sized {
type Params;
fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans;
fn is_child(&self, other: &Self) -> bool;
fn cmp(&self, other: &Self) -> Ordering;
fn icon(&self) -> Option<(&'static str, &'static helix_view::theme::Color)>;
fn filter(&self, cx: &mut Context, s: &str, params: &mut Self::Params) -> bool {
self.text(cx, false, params)
.0
.into_iter()
.map(|s| s.content)
.collect::<Vec<_>>()
.concat()
.contains(s)
}
fn get_childs(&self) -> Result<Vec<Self>> {
Ok(vec![])
}
}
fn tree_item_cmp<T: TreeItem>(item1: &T, item2: &T) -> Ordering {
if item1.is_child(item2) {
return Ordering::Greater;
}
if item2.is_child(item1) {
return Ordering::Less;
}
T::cmp(item1, item2)
}
fn vec_to_tree<T: TreeItem>(mut items: Vec<T>, level: usize) -> Vec<Elem<T>> {
fn get_childs<T, Iter>(iter: &mut Peekable<Iter>, elem: &mut Elem<T>)
where
T: TreeItem,
Iter: Iterator<Item = T>,
{
let level = elem.level + 1;
loop {
if !iter.peek().map_or(false, |next| next.is_child(&elem.item)) {
break;
}
let mut child = Elem::new(iter.next().unwrap(), level);
if iter.peek().map_or(false, |nc| nc.is_child(&child.item)) {
get_childs(iter, &mut child);
}
elem.folded.push(child);
}
}
items.sort_by(tree_item_cmp);
let mut elems = Vec::with_capacity(items.len());
let mut iter = items.into_iter().peekable();
while let Some(item) = iter.next() {
let mut elem = Elem::new(item, level);
if iter.peek().map_or(false, |next| next.is_child(&elem.item)) {
get_childs(&mut iter, &mut elem);
}
expand_elems(&mut elems, elem);
}
elems
}
// return total elems's count contain self
fn get_elems_recursion<T: TreeItem>(t: &mut Elem<T>, depth: usize) -> Result<usize> {
let mut childs = t.item.get_childs()?;
childs.sort_by(tree_item_cmp);
let mut elems = Vec::with_capacity(childs.len());
let level = t.level + 1;
let mut total = 1;
for child in childs {
let mut elem = Elem::new(child, level);
let count = if depth > 0 {
get_elems_recursion(&mut elem, depth - 1)?
} else {
1
};
elems.push(elem);
total += count;
}
t.folded = elems;
Ok(total)
}
fn expand_elems<T: TreeItem>(dist: &mut Vec<Elem<T>>, mut t: Elem<T>) {
let childs = std::mem::take(&mut t.folded);
dist.push(t);
for child in childs {
expand_elems(dist, child)
}
}
pub enum TreeOp<T> {
Noop,
Restore,
InsertChild(Vec<T>),
GetChildsAndInsert,
ReplaceTree(Vec<T>),
}
pub struct Elem<T> {
item: T,
level: usize,
folded: Vec<Self>,
}
impl<T: Clone> Clone for Elem<T> {
fn clone(&self) -> Self {
Self {
item: self.item.clone(),
level: self.level,
folded: self.folded.clone(),
}
}
}
impl<T> Elem<T> {
pub fn new(item: T, level: usize) -> Self {
Self {
item,
level,
folded: vec![],
}
}
pub fn item(&self) -> &T {
&self.item
}
}
pub struct Tree<T: TreeItem> {
items: Vec<Elem<T>>,
recycle: Option<(String, Vec<Elem<T>>)>,
selected: usize,
save_view: (usize, usize), // (selected, row)
row: usize,
col: usize,
max_len: usize,
count: usize,
tree_symbol_style: String,
#[allow(clippy::type_complexity)]
pre_render: Option<Box<dyn Fn(&mut Self, Rect) + 'static>>,
#[allow(clippy::type_complexity)]
on_opened_fn:
Option<Box<dyn FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp<T> + 'static>>,
#[allow(clippy::type_complexity)]
on_folded_fn: Option<Box<dyn FnMut(&mut T, &mut Context, &mut T::Params) + 'static>>,
#[allow(clippy::type_complexity)]
on_next_key: Option<Box<dyn FnMut(&mut Context, &mut Self, KeyEvent)>>,
}
impl<T: TreeItem> Tree<T> {
pub fn new(items: Vec<Elem<T>>) -> Self {
Self {
items,
recycle: None,
selected: 0,
save_view: (0, 0),
row: 0,
col: 0,
max_len: 0,
count: 0,
tree_symbol_style: "ui.explorer.guide".into(),
pre_render: None,
on_opened_fn: None,
on_folded_fn: None,
on_next_key: None,
}
}
pub fn replace_with_new_items(&mut self, items: Vec<T>) {
let old = std::mem::replace(self, Self::new(vec_to_tree(items, 0)));
self.on_opened_fn = old.on_opened_fn;
self.on_folded_fn = old.on_folded_fn;
self.tree_symbol_style = old.tree_symbol_style;
}
pub fn build_tree(items: Vec<T>) -> Self {
Self::new(vec_to_tree(items, 0))
}
pub fn build_from_root(t: T, depth: usize) -> Result<Self> {
let mut elem = Elem::new(t, 0);
let count = get_elems_recursion(&mut elem, depth)?;
let mut elems = Vec::with_capacity(count);
expand_elems(&mut elems, elem);
Ok(Self::new(elems))
}
pub fn with_enter_fn<F>(mut self, f: F) -> Self
where
F: FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp<T> + 'static,
{
self.on_opened_fn = Some(Box::new(f));
self
}
pub fn with_folded_fn<F>(mut self, f: F) -> Self
where
F: FnMut(&mut T, &mut Context, &mut T::Params) + 'static,
{
self.on_folded_fn = Some(Box::new(f));
self
}
pub fn tree_symbol_style(mut self, style: String) -> Self {
self.tree_symbol_style = style;
self
}
fn next_item(&self) -> Option<&Elem<T>> {
self.items.get(self.selected + 1)
}
fn next_not_descendant_pos(&self, index: usize) -> usize {
let item = &self.items[index];
self.find(index + 1, false, |n| n.level <= item.level)
.unwrap_or(self.items.len())
}
fn find_parent(&self, index: usize) -> Option<usize> {
let item = &self.items[index];
self.find(index, true, |p| p.level < item.level)
}
// rev start: start - 1
fn find<F>(&self, start: usize, rev: bool, f: F) -> Option<usize>
where
F: FnMut(&Elem<T>) -> bool,
{
let iter = self.items.iter();
if rev {
iter.take(start).rposition(f)
} else {
iter.skip(start).position(f).map(|p| p + start)
}
}
}
impl<T: TreeItem> Tree<T> {
pub fn on_enter(&mut self, cx: &mut Context, params: &mut T::Params) {
if self.items.is_empty() {
return;
}
if let Some(next_level) = self.next_item().map(|elem| elem.level) {
let current = &mut self.items[self.selected];
let current_level = current.level;
if next_level > current_level {
if let Some(mut on_folded_fn) = self.on_folded_fn.take() {
on_folded_fn(&mut current.item, cx, params);
self.on_folded_fn = Some(on_folded_fn);
}
self.fold_current_child();
return;
}
}
if let Some(mut on_open_fn) = self.on_opened_fn.take() {
let mut f = || {
let current = &mut self.items[self.selected];
let items = match on_open_fn(&mut current.item, cx, params) {
TreeOp::Restore => {
let inserts = std::mem::take(&mut current.folded);
let _: Vec<_> = self
.items
.splice(self.selected + 1..self.selected + 1, inserts)
.collect();
return;
}
TreeOp::InsertChild(items) => items,
TreeOp::GetChildsAndInsert => match current.item.get_childs() {
Ok(items) => items,
Err(e) => return cx.editor.set_error(format!("{e}")),
},
TreeOp::ReplaceTree(items) => return self.replace_with_new_items(items),
TreeOp::Noop => return,
};
current.folded = vec![];
let inserts = vec_to_tree(items, current.level + 1);
let _: Vec<_> = self
.items
.splice(self.selected + 1..self.selected + 1, inserts)
.collect();
};
f();
self.on_opened_fn = Some(on_open_fn)
} else {
let current = &mut self.items[self.selected];
let inserts = std::mem::take(&mut current.folded);
let _: Vec<_> = self
.items
.splice(self.selected + 1..self.selected + 1, inserts)
.collect();
}
}
pub fn fold_current_level(&mut self) {
let start = match self.find_parent(self.selected) {
Some(start) => start,
None => return,
};
self.selected = start;
self.fold_current_child();
}
pub fn fold_current_child(&mut self) {
if self.selected + 1 >= self.items.len() {
return;
}
let pos = self.next_not_descendant_pos(self.selected);
if self.selected < pos {
self.items[self.selected].folded = self.items.drain(self.selected + 1..pos).collect();
}
}
pub fn search_next(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) {
let skip = self.save_view.0 + 1;
self.selected = self
.find(skip, false, |e| e.item.filter(cx, s, params))
.unwrap_or(self.save_view.0);
self.row = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0);
}
pub fn search_pre(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) {
let take = self.save_view.0;
self.selected = self
.find(take, true, |e| e.item.filter(cx, s, params))
.unwrap_or(self.save_view.0);
self.row = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0);
}
pub fn move_down(&mut self, rows: usize) {
let len = self.items.len();
if len > 0 {
self.selected = std::cmp::min(self.selected + rows, len.saturating_sub(1));
self.row = std::cmp::min(self.selected, self.row + rows);
}
}
pub fn move_up(&mut self, rows: usize) {
let len = self.items.len();
if len > 0 {
self.selected = self.selected.saturating_sub(rows);
self.row = std::cmp::min(self.selected, self.row.saturating_sub(rows));
}
}
pub fn move_left(&mut self, cols: usize) {
self.col = self.col.saturating_sub(cols);
}
pub fn move_right(&mut self, cols: usize) {
self.pre_render = Some(Box::new(move |tree: &mut Self, area: Rect| {
let max_scroll = tree.max_len.saturating_sub(area.width as usize);
tree.col = max_scroll.min(tree.col + cols);
}));
}
pub fn move_down_half_page(&mut self) {
self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| {
tree.move_down((area.height / 2) as usize);
}));
}
pub fn move_up_half_page(&mut self) {
self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| {
tree.move_up((area.height / 2) as usize);
}));
}
pub fn move_down_page(&mut self) {
self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| {
tree.move_down((area.height) as usize);
}));
}
pub fn move_up_page(&mut self) {
self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| {
tree.move_up((area.height) as usize);
}));
}
pub fn save_view(&mut self) {
self.save_view = (self.selected, self.row);
}
pub fn restore_view(&mut self) {
(self.selected, self.row) = self.save_view;
}
pub fn current(&self) -> &Elem<T> {
&self.items[self.selected]
}
pub fn current_item(&self) -> &T {
&self.items[self.selected].item
}
pub fn row(&self) -> usize {
self.row
}
pub fn remove_current(&mut self) -> T {
let elem = self.items.remove(self.selected);
self.selected = self.selected.saturating_sub(1);
elem.item
}
pub fn replace_current(&mut self, item: T) {
self.items[self.selected].item = item;
}
pub fn select(&mut self, select_item: &T) {
let selected = self
.items
.iter()
.enumerate()
.filter(|(_, i)| i.item.cmp(select_item) == Ordering::Equal)
.next();
if let Some((idx, _)) = selected {
self.selected = idx;
self.row = idx;
}
}
pub fn insert_current_level(&mut self, item: T) {
let current = self.current();
let level = current.level;
let pos = match current.item.cmp(&item) {
Ordering::Less => self
.find(self.selected + 1, false, |e| {
e.level < level || (e.level == level && e.item.cmp(&item) != Ordering::Less)
})
.unwrap_or(self.items.len()),
Ordering::Greater => {
match self.find(self.selected, true, |elem| {
elem.level < level
|| (elem.level == level && elem.item.cmp(&item) != Ordering::Greater)
}) {
Some(p) if self.items[p].level == level => self.next_not_descendant_pos(p),
Some(p) => p + 1,
None => 0,
}
}
Ordering::Equal => self.selected + 1,
};
self.items.insert(pos, Elem::new(item, level));
}
}
impl<T: TreeItem> Tree<T> {
pub fn render(
&mut self,
area: Rect,
surface: &mut Surface,
cx: &mut Context,
params: &mut T::Params,
) {
if let Some(pre_render) = self.pre_render.take() {
pre_render(self, area);
}
self.max_len = 0;
self.row = std::cmp::min(self.row, area.height.saturating_sub(1) as usize);
let style = cx.editor.theme.get(&self.tree_symbol_style);
let folder_style = cx.editor.theme.get("special");
let last_item_index = self.items.len().saturating_sub(1);
let skip = self.selected.saturating_sub(self.row);
let iter = self
.items
.iter()
.skip(skip)
.take(area.height as usize)
.enumerate();
for (index, elem) in iter {
let row = index as u16;
let mut area = Rect::new(area.x, area.y + row, area.width, 1);
let indent = if elem.level > 0 {
if index + skip != last_item_index {
format!("{}", "│ ".repeat(elem.level - 1))
} else {
format!("{}", "".repeat(elem.level - 1))
}
} else {
"".to_string()
};
let indent_len = indent.chars().count();
if indent_len > self.col {
let indent: String = indent.chars().skip(self.col).collect();
if !indent.is_empty() {
surface.set_stringn(area.x, area.y, &indent, area.width as usize, style);
area = area.clip_left(indent.width() as u16);
}
};
let mut start_index = self.col.saturating_sub(indent_len);
let mut text = elem.item.text(cx, skip + index == self.selected, params);
self.max_len = self.max_len.max(text.width() + indent.len() - 2);
for span in text.0.iter_mut() {
if area.width == 0 {
return;
}
if start_index == 0 {
let mut icon_offset = 0;
if let Some((icon, color)) = elem.item.icon() {
let style = folder_style.fg(*color);
surface.set_string(area.x, area.y, icon, style);
icon_offset = 2;
}
surface.set_span(area.x + icon_offset, area.y, span, area.width - icon_offset);
area = area.clip_left((span.width() - icon_offset as usize) as u16);
} else {
let span_width = span.width();
if start_index > span_width {
start_index -= span_width;
} else {
let content: String = span
.content
.chars()
.filter(|c| {
if start_index > 0 {
start_index = start_index.saturating_sub(c.to_string().width());
false
} else {
true
}
})
.collect();
let mut cont = String::new();
cont.push_str("");
cont.push_str(&content);
surface.set_string_truncated(
area.x,
area.y,
&cont,
area.width as usize,
|_| span.style,
false,
false,
);
start_index = 0
}
}
}
}
}
pub fn handle_event(
&mut self,
event: Event,
cx: &mut Context,
params: &mut T::Params,
) -> EventResult {
let key_event = match event {
Event::Key(event) => event,
Event::Resize(..) => return EventResult::Consumed(None),
_ => return EventResult::Ignored(None),
};
if let Some(mut on_next_key) = self.on_next_key.take() {
on_next_key(cx, self, key_event);
return EventResult::Consumed(None);
}
let count = std::mem::replace(&mut self.count, 0);
match key_event.into() {
key!(i @ '0'..='9') => self.count = i.to_digit(10).unwrap() as usize + count * 10,
key!('k') | shift!(Tab) | key!(Up) | ctrl!('k') => self.move_up(1.max(count)),
key!('j') | key!(Tab) | key!(Down) | ctrl!('j') => self.move_down(1.max(count)),
key!('z') => self.fold_current_level(),
key!('h') => self.move_left(1.max(count)),
key!('l') => self.move_right(1.max(count)),
shift!('G') => self.move_down(usize::MAX / 2),
key!(Enter) => self.on_enter(cx, params),
key!(' ') => self.on_enter(cx, params),
ctrl!('d') => self.move_down_half_page(),
ctrl!('u') => self.move_up_half_page(),
shift!('D') => self.move_down_page(),
shift!('U') => self.move_up_page(),
key!('g') => {
self.on_next_key = Some(Box::new(|_, tree, event| match event.into() {
key!('g') => tree.move_up(usize::MAX / 2),
key!('e') => tree.move_down(usize::MAX / 2),
_ => {}
}));
}
_ => return EventResult::Ignored(None),
}
EventResult::Consumed(None)
}
}
impl<T: TreeItem + Clone> Tree<T> {
pub fn filter(&mut self, s: &str, cx: &mut Context, params: &mut T::Params) {
fn filter_recursion<T>(
elems: &Vec<Elem<T>>,
mut index: usize,
s: &str,
cx: &mut Context,
params: &mut T::Params,
) -> (Vec<Elem<T>>, usize)
where
T: TreeItem + Clone,
{
let mut retain = vec![];
let elem = &elems[index];
loop {
let child = match elems.get(index + 1) {
Some(child) if child.item.is_child(&elem.item) => child,
_ => break,
};
index += 1;
let next = elems.get(index + 1);
if next.map_or(false, |n| n.item.is_child(&child.item)) {
let (sub_retain, current_index) = filter_recursion(elems, index, s, cx, params);
retain.extend(sub_retain);
index = current_index;
} else if child.item.filter(cx, s, params) {
retain.push(child.clone());
}
}
if !retain.is_empty() || elem.item.filter(cx, s, params) {
retain.insert(0, elem.clone());
}
(retain, index)
}
if s.is_empty() {
if let Some((_, recycle)) = self.recycle.take() {
self.items = recycle;
self.restore_view();
return;
}
}
let mut retain = vec![];
let mut index = 0;
let items = match &self.recycle {
Some((pre, _)) if pre == s => return,
Some((pre, recycle)) if pre.contains(s) => recycle,
_ => &self.items,
};
while let Some(elem) = items.get(index) {
let next = items.get(index + 1);
if next.map_or(false, |n| n.item.is_child(&elem.item)) {
let (sub_items, current_index) = filter_recursion(items, index, s, cx, params);
index = current_index;
retain.extend(sub_items);
} else if elem.item.filter(cx, s, params) {
retain.push(elem.clone())
}
index += 1;
}
if retain.is_empty() {
if let Some((_, recycle)) = self.recycle.take() {
self.items = recycle;
self.restore_view();
}
return;
}
let recycle = std::mem::replace(&mut self.items, retain);
if let Some(r) = self.recycle.as_mut() {
r.0 = s.into()
} else {
self.recycle = Some((s.into(), recycle));
self.save_view();
}
self.selected = self
.find(0, false, |elem| elem.item.filter(cx, s, params))
.unwrap_or(0);
self.row = self.selected;
}
pub fn clean_recycle(&mut self) {
self.recycle = None;
}
pub fn restore_recycle(&mut self) {
if let Some((_, recycle)) = self.recycle.take() {
self.items = recycle;
}
}
}

@ -112,7 +112,7 @@ pub struct Document {
// be more troublesome. // be more troublesome.
pub history: Cell<History>, pub history: Cell<History>,
pub savepoint: Option<Transaction>, pub savepoint: Option<(i32, Transaction)>,
last_saved_revision: usize, last_saved_revision: usize,
version: i32, // should be usize? version: i32, // should be usize?
@ -491,6 +491,21 @@ impl Document {
Some(fut.boxed()) Some(fut.boxed())
} }
/// Deletes the file associated with this document
pub fn delete(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
let path = self
.path()
.expect("Cannot delete with no path set!")
.clone();
async move {
use tokio::fs;
fs::remove_file(path).await?;
Ok(())
}
}
pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> { pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
self.save_impl::<futures_util::future::Ready<_>>(None, force) self.save_impl::<futures_util::future::Ready<_>>(None, force)
} }
@ -768,7 +783,8 @@ impl Document {
if self.savepoint.is_some() { if self.savepoint.is_some() {
take_with(&mut self.savepoint, |prev_revert| { take_with(&mut self.savepoint, |prev_revert| {
let revert = transaction.invert(&old_doc); let revert = transaction.invert(&old_doc);
Some(revert.compose(prev_revert.unwrap())) let (version, prev_revert) = prev_revert.unwrap();
Some((version, revert.compose(prev_revert)))
}); });
} }
@ -858,11 +874,11 @@ impl Document {
} }
pub fn savepoint(&mut self) { pub fn savepoint(&mut self) {
self.savepoint = Some(Transaction::new(self.text())); self.savepoint = Some((self.version, Transaction::new(self.text())));
} }
pub fn restore(&mut self, view_id: ViewId) { pub fn restore(&mut self, view_id: ViewId) {
if let Some(revert) = self.savepoint.take() { if let Some((_, revert)) = self.savepoint.take() {
self.apply(&revert, view_id); self.apply(&revert, view_id);
} }
} }

@ -109,6 +109,57 @@ impl Default for FilePickerConfig {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExplorerStyle {
Tree,
List,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExplorerPosition {
Embed,
Overlay,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct ExplorerConfig {
pub style: ExplorerStyle,
pub position: ExplorerPosition,
/// explorer column width
pub column_width: usize,
}
impl ExplorerConfig {
pub fn is_embed(&self) -> bool {
return self.position == ExplorerPosition::Embed;
}
pub fn is_overlay(&self) -> bool {
return self.position == ExplorerPosition::Overlay;
}
pub fn is_list(&self) -> bool {
return self.style == ExplorerStyle::List;
}
pub fn is_tree(&self) -> bool {
return self.style == ExplorerStyle::Tree;
}
}
impl Default for ExplorerConfig {
fn default() -> Self {
Self {
style: ExplorerStyle::Tree,
position: ExplorerPosition::Embed,
column_width: 30,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config { pub struct Config {
@ -168,6 +219,8 @@ pub struct Config {
pub indent_guides: IndentGuidesConfig, pub indent_guides: IndentGuidesConfig,
/// Whether to color modes with different colors. Defaults to `false`. /// Whether to color modes with different colors. Defaults to `false`.
pub color_modes: bool, pub color_modes: bool,
/// explore config
pub explorer: ExplorerConfig,
} }
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
@ -269,7 +322,16 @@ impl Default for StatusLineConfig {
Self { Self {
left: vec![E::Mode, E::Spinner, E::FileName], left: vec![E::Mode, E::Spinner, E::FileName],
center: vec![], center: vec![],
right: vec![E::Diagnostics, E::Selections, E::Position, E::FileEncoding], right: vec![
E::Diagnostics,
E::Selections,
E::Position,
E::PositionPercentage,
E::Separator,
E::FileEncoding,
E::FileLineEnding,
E::FileType,
],
separator: String::from("│"), separator: String::from("│"),
} }
} }
@ -365,7 +427,7 @@ impl std::ops::Deref for CursorShapeConfig {
impl Default for CursorShapeConfig { impl Default for CursorShapeConfig {
fn default() -> Self { fn default() -> Self {
Self([CursorKind::Block; 3]) Self([CursorKind::Block, CursorKind::Underline, CursorKind::Bar])
} }
} }
@ -538,7 +600,7 @@ pub struct IndentGuidesConfig {
impl Default for IndentGuidesConfig { impl Default for IndentGuidesConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
render: false, render: true,
character: '', character: '',
} }
} }
@ -556,7 +618,7 @@ impl Default for Config {
vec!["sh".to_owned(), "-c".to_owned()] vec!["sh".to_owned(), "-c".to_owned()]
}, },
line_number: LineNumber::Absolute, line_number: LineNumber::Absolute,
cursorline: false, cursorline: true,
gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers], gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
middle_click_paste: true, middle_click_paste: true,
auto_pairs: AutoPairConfig::default(), auto_pairs: AutoPairConfig::default(),
@ -572,11 +634,12 @@ impl Default for Config {
search: SearchConfig::default(), search: SearchConfig::default(),
lsp: LspConfig::default(), lsp: LspConfig::default(),
terminal: get_terminal_provider(), terminal: get_terminal_provider(),
rulers: Vec::new(), rulers: vec![120],
whitespace: WhitespaceConfig::default(), whitespace: WhitespaceConfig::default(),
bufferline: BufferLine::default(), bufferline: BufferLine::default(),
indent_guides: IndentGuidesConfig::default(), indent_guides: IndentGuidesConfig::default(),
color_modes: false, color_modes: true,
explorer: ExplorerConfig::default(),
} }
} }
} }
@ -763,6 +826,10 @@ impl Editor {
.reset(Instant::now() + config.idle_timeout); .reset(Instant::now() + config.idle_timeout);
} }
pub fn reset_idle_timer_zero(&mut self) {
self.idle_timer.as_mut().reset(Instant::now());
}
pub fn clear_status(&mut self) { pub fn clear_status(&mut self) {
self.status_msg = None; self.status_msg = None;
} }
@ -837,6 +904,23 @@ impl Editor {
Self::launch_language_server(&mut self.language_servers, doc) Self::launch_language_server(&mut self.language_servers, doc)
} }
/// Restarts a language server for a given document
pub fn restart_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let doc = self.documents.get_mut(&doc_id)?;
if let Some(language) = doc.language.as_ref() {
if let Ok(client) = self.language_servers.restart(&*language).map_err(|e| {
log::error!(
"Failed to restart the LSP for `{}` {{ {} }}",
language.scope(),
e
)
}) {
doc.set_language_server(Some(client));
}
};
Some(())
}
/// Launch a language server for a given document /// Launch a language server for a given document
fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> { fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
// if doc doesn't have a URL it's a scratch buffer, ignore it // if doc doesn't have a URL it's a scratch buffer, ignore it

@ -0,0 +1,11 @@
(attribute_name) @attribute
(attribute_value) @string
(comment) @comment
(cdata_sect) @special
(tag_name) @tag
[
"version"
"encoding"
"standalone"
] @attribute
(xml_decl) @constant

@ -69,6 +69,12 @@
"warning" = "my_yellow2" "warning" = "my_yellow2"
"error" = "my_red" "error" = "my_red"
"ui.explorer.file" = { fg = "my_white" }
"ui.explorer.dir" = { fg = "my_yellow1" }
"ui.explorer.exe" = { fg = "my_green" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { modifiers = ["reversed"] }
[palette] [palette]
my_black = "#212121" # Cursorline my_black = "#212121" # Cursorline
my_gray0 = "#262626" # Default Background my_gray0 = "#262626" # Default Background

@ -54,6 +54,12 @@
"warning" = "base09" "warning" = "base09"
"error" = "base08" "error" = "base08"
"ui.explorer.file" = { fg = "base05" }
"ui.explorer.dir" = { fg = "base0D" }
"ui.explorer.exe" = { fg = "base05" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base02" }
"ui.bufferline" = { fg = "base04", bg = "base00" } "ui.bufferline" = { fg = "base04", bg = "base00" }
"ui.bufferline.active" = { fg = "base06", bg = "base01" } "ui.bufferline.active" = { fg = "base06", bg = "base01" }

@ -54,6 +54,12 @@
"warning" = "base09" "warning" = "base09"
"error" = "base08" "error" = "base08"
"ui.explorer.file" = { fg = "base05" }
"ui.explorer.dir" = { fg = "base0D" }
"ui.explorer.exe" = { fg = "base05" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base02" }
"ui.bufferline" = { fg = "base04", bg = "base01" } "ui.bufferline" = { fg = "base04", bg = "base01" }
"ui.bufferline.active" = { fg = "base07", bg = "base00" } "ui.bufferline.active" = { fg = "base07", bg = "base00" }

@ -50,3 +50,9 @@
"debug" = "gray" "debug" = "gray"
"warning" = "yellow" "warning" = "yellow"
"error" = "light-red" "error" = "light-red"
# "ui.explorer.file" = { fg = "base05" }
"ui.explorer.dir" = { fg = "light-blue" }
# "ui.explorer.exe" = { fg = "base05" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "light-gray" }

@ -74,3 +74,9 @@
# make diagnostic underlined, to distinguish with selection text. # make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] } diagnostic = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "#e5ded6" }
"ui.explorer.dir" = { fg = "#59dcd8" }
"ui.explorer.exe" = { fg = "#e5ded6" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "#313f4e" }

@ -62,6 +62,12 @@
"hint" = { fg = "lilac" } "hint" = { fg = "lilac" }
"diagnostic" = { modifiers = ["underlined"] } "diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "lilac" }
"ui.explorer.dir" = { fg = "mint" }
"ui.explorer.exe" = { fg = "lilac" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "berry_saturated" }
[palette] [palette]
berry = "#3A2A4D" berry = "#3A2A4D"
berry_fade = "#5A3D6E" berry_fade = "#5A3D6E"

@ -94,6 +94,12 @@
diagnostic = { modifiers = ["underlined"] } diagnostic = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "text" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "dark_blue2" }
[palette] [palette]
white = "#ffffff" white = "#ffffff"
orange = "#ce9178" orange = "#ce9178"

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

@ -55,6 +55,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "foreground" } "markup.raw" = { fg = "foreground" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "cyan" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "secondary_highlight" }
[palette] [palette]
background = "#282a36" background = "#282a36"
background_dark = "#21222c" background_dark = "#21222c"

@ -55,6 +55,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "foreground" } "markup.raw" = { fg = "foreground" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "cyan" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "comment" }
[palette] [palette]
background = "#0e1419" background = "#0e1419"
background_dark = "#21222c" background_dark = "#21222c"

@ -80,6 +80,11 @@
"error" = "red" "error" = "red"
"diagnostic" = { modifiers = ["underlined"] } "diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "grey1" }
[palette] [palette]

@ -80,6 +80,11 @@
"error" = "red" "error" = "red"
"diagnostic" = { modifiers = ["underlined"] } "diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "grey1" }
[palette] [palette]

@ -69,6 +69,12 @@
"markup.link.text" = "red1" "markup.link.text" = "red1"
"markup.raw" = "red1" "markup.raw" = "red1"
"ui.explorer.file" = { fg = "fg1" }
"ui.explorer.dir" = { fg = "blue0" }
"ui.explorer.exe" = { fg = "fg1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette] [palette]
bg0 = "#282828" # main background bg0 = "#282828" # main background
bg1 = "#3c3836" bg1 = "#3c3836"

@ -70,6 +70,12 @@
"markup.link.text" = "red1" "markup.link.text" = "red1"
"markup.raw" = "red1" "markup.raw" = "red1"
"ui.explorer.file" = { fg = "fg1" }
"ui.explorer.dir" = { fg = "blue0" }
"ui.explorer.exe" = { fg = "fg1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette] [palette]
bg0 = "#fbf1c7" # main background bg0 = "#fbf1c7" # main background
bg1 = "#ebdbb2" bg1 = "#ebdbb2"

@ -67,3 +67,9 @@
"error" = "#D74E50" "error" = "#D74E50"
"info" = "#839A53" "info" = "#839A53"
"hint" = "#A6B6CE" "hint" = "#A6B6CE"
"ui.explorer.file" = { fg = "#7B91B3" }
"ui.explorer.dir" = { fg = "#89BEB7" }
"ui.explorer.exe" = { fg = "#7B91B3" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "#F3EAE9" }

@ -87,6 +87,12 @@
diagnostic = { modifiers = ["underlined"] } diagnostic = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "fn_declaration" }
"ui.explorer.exe" = { fg = "text" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "#75715e" }
[palette] [palette]
type = "#A6E22E" type = "#A6E22E"
keyword = "#F92672" keyword = "#F92672"

@ -97,6 +97,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow" "markup.link.text" = "yellow"
"markup.quote" = "green" "markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette] [palette]
# primary colors # primary colors
"red" = "#ff6188" "red" = "#ff6188"

@ -97,6 +97,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow" "markup.link.text" = "yellow"
"markup.quote" = "green" "markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette] [palette]
# primary colors # primary colors
"red" = "#ff6d7e" "red" = "#ff6d7e"

@ -97,6 +97,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow" "markup.link.text" = "yellow"
"markup.quote" = "green" "markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette] [palette]
# primary colors # primary colors
"red" = "#ff657a" "red" = "#ff657a"

@ -97,6 +97,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow" "markup.link.text" = "yellow"
"markup.quote" = "green" "markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette] [palette]
# primary colors # primary colors
"red" = "#fd6883" "red" = "#fd6883"

@ -97,6 +97,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow" "markup.link.text" = "yellow"
"markup.quote" = "green" "markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette] [palette]
# primary colors # primary colors
"red" = "#fc618d" "red" = "#fc618d"

@ -87,6 +87,12 @@
'diff.delta' = { fg = 'blue' } 'diff.delta' = { fg = 'blue' }
'diff.delta.moved' = { fg = 'blue', modifiers = ['italic'] } 'diff.delta.moved' = { fg = 'blue', modifiers = ['italic'] }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "selection" }
[palette] [palette]
background = '#011627' background = '#011627'
background2 = '#112630' background2 = '#112630'

@ -103,6 +103,12 @@
"diff.delta" = "nord12" "diff.delta" = "nord12"
"diff.minus" = "nord11" "diff.minus" = "nord11"
"ui.explorer.file" = { fg = "nord6" }
"ui.explorer.dir" = { fg = "nord8" }
"ui.explorer.exe" = { fg = "nord6" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "nord2" }
[palette] [palette]
nord0 = "#2e3440" nord0 = "#2e3440"
nord1 = "#3b4252" nord1 = "#3b4252"

@ -78,6 +78,18 @@ diagnostic = { modifiers = ["underlined"] }
"ui.menu.selected" = { fg = "black", bg = "blue" } "ui.menu.selected" = { fg = "black", bg = "blue" }
"ui.menu.scroll" = { fg = "white", bg = "light-gray" } "ui.menu.scroll" = { fg = "white", bg = "light-gray" }
"ui.explorer.file" = { fg = "white" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "white" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "light-gray" }
"ui.explorer.file" = { fg = "white" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "white" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "light-gray" }
[palette] [palette]
yellow = "#E5C07B" yellow = "#E5C07B"

@ -106,6 +106,12 @@
"ui.menu" = { fg = "black", bg = "light-white" } "ui.menu" = { fg = "black", bg = "light-white" }
"ui.menu.selected" = { fg = "white", bg = "light-blue" } "ui.menu.selected" = { fg = "white", bg = "light-blue" }
"ui.explorer.file" = { fg = "black" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "black" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "light-white" }
[palette] [palette]
white = "#FAFAFA" white = "#FAFAFA"
yellow = "#A06600" yellow = "#A06600"

@ -121,6 +121,12 @@ namespace = { fg = 'orangeL' }
'diff.delta' = { fg = '#4d4ddd' } 'diff.delta' = { fg = '#4d4ddd' }
'diff.delta.moved' = { fg = '#dd4ddd' } 'diff.delta.moved' = { fg = '#dd4ddd' }
"ui.explorer.file" = { fg = "greyT" }
"ui.explorer.dir" = { fg = "blueL" }
"ui.explorer.exe" = { fg = "greyT" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "greyL" }
[palette] [palette]
white = '#FFFFFF' white = '#FFFFFF'
greyH = '#CFCFCF' greyH = '#CFCFCF'

@ -66,6 +66,12 @@
"markup.quote" = { fg = "rose" } "markup.quote" = { fg = "rose" }
"markup.raw" = { fg = "foam" } "markup.raw" = { fg = "foam" }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "rose" }
"ui.explorer.exe" = { fg = "text" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "highlight" }
[palette] [palette]
base = "#191724" base = "#191724"
surface = "#1f1d2e" surface = "#1f1d2e"

@ -63,6 +63,12 @@
"markup.quote" = { fg = "rose" } "markup.quote" = { fg = "rose" }
"markup.raw" = { fg = "foam" } "markup.raw" = { fg = "foam" }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "rose" }
"ui.explorer.exe" = { fg = "text" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "highlight" }
[palette] [palette]
base = "#faf4ed" base = "#faf4ed"
surface = "#fffaf3" surface = "#fffaf3"

@ -72,6 +72,18 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "fg" } "markup.raw" = { fg = "fg" }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette] [palette]
bg0 = "#323437" bg0 = "#323437"

@ -72,6 +72,11 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "fg" } "markup.raw" = { fg = "fg" }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette] [palette]

@ -103,6 +103,12 @@
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] } "hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { modifiers = ["underlined"] } "diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "base1" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base0175" }
[palette] [palette]
# 深色 越来越深 # 深色 越来越深
base03 = "#002b36" base03 = "#002b36"

@ -120,6 +120,12 @@
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] } "hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { modifiers = ["underlined"] } "diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "base1" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base0175" }
[palette] [palette]
red = '#dc322f' red = '#dc322f'
green = '#859900' green = '#859900'

@ -74,6 +74,12 @@
"diagnostic" = { modifiers = ["underlined"] } "diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "fg1" }
"ui.explorer.dir" = { fg = "#715ab1" }
"ui.explorer.exe" = { fg = "fg1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette] [palette]
base = "#655370" base = "#655370"
base-dim = "#a094a2" base-dim = "#a094a2"

@ -63,6 +63,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "cyan" } "markup.raw" = { fg = "cyan" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "background_highlight" }
[palette] [palette]
red = "#f7768e" red = "#f7768e"
orange = "#ff9e64" orange = "#ff9e64"

@ -63,6 +63,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "cyan" } "markup.raw" = { fg = "cyan" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "background_highlight" }
[palette] [palette]
red = "#f7768e" red = "#f7768e"
orange = "#ff9e64" orange = "#ff9e64"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 271 KiB

@ -1,104 +1,80 @@
attribute = "lilac" # Author : Sam Sartor <me@samsartor.com>, Trivernis <trivernis@pm.me>
keyword = "almond" # A port of https://github.com/bceskavich/dracula-at-night
"keyword.directive" = "lilac" # -- preprocessor comments (#if in C) "comment" = { fg = "comment" }
namespace = "lilac" "constant" = { fg = "purple" }
punctuation = "lavender" "constant.character.escape" = { fg = "pink" }
"punctuation.delimiter" = "lavender" "function" = { fg = "green" }
operator = "lilac" "keyword" = { fg = "pink" }
special = "honey" "operator" = { fg = "pink" }
"variable.other.member" = "white" "special" = { fg = "yellow" }
variable = "lavender" "punctuation" = { fg = "foreground" }
# variable = "almond" # TODO: metavariables only "string" = { fg = "yellow" }
# "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] } "string.regexp" = { fg = "red" }
"variable.parameter" = { fg = "lavender" } "tag" = { fg = "pink" }
"variable.builtin" = "mint" "attribute" = { fg = "cyan" }
type = "white" "type" = { fg = "cyan", modifiers = ["italic"] }
"type.builtin" = "white" # TODO: distinguish? "type.enum.variant" = { fg = "foreground", modifiers = ["italic"] }
constructor = "lilac" "variable" = { fg = "foreground" }
function = "white" "variable.builtin" = { fg = "cyan", modifiers = ["italic"] }
"function.macro" = "lilac" "variable.parameter" = { fg ="orange", modifiers = ["italic"] }
"function.builtin" = "white"
tag = "almond"
comment = "sirocco"
constant = "white"
"constant.builtin" = "white"
string = "silver"
"constant.numeric" = "chamois"
"constant.character.escape" = "honey"
# used for lifetimes
label = "honey"
"markup.heading" = "lilac" "diff.plus" = { fg = "green" }
"markup.bold" = { modifiers = ["bold"] } "diff.delta" = { fg = "orange" }
"markup.italic" = { modifiers = ["italic"] } "diff.minus" = { fg = "red" }
"markup.link.url" = { fg = "silver", modifiers = ["underlined"] }
"markup.link.text" = "almond"
"markup.raw" = "almond"
"diff.plus" = "#35bf86" "ui.background" = { fg = "foreground", bg = "background" }
"diff.minus" = "#f22c86" "ui.cursor" = { fg = "background", bg = "orange", modifiers = ["dim"] }
"diff.delta" = "#6f44f0" "ui.cursor.match" = { fg = "green", modifiers = ["underlined"] }
"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] }
"ui.cursorline" = {bg = "background_dark"}
"ui.help" = { fg = "foreground", bg = "background_dark" }
"ui.linenr" = { fg = "comment" }
"ui.linenr.selected" = { fg = "foreground" }
"ui.menu" = { fg = "foreground", bg = "background_dark" }
"ui.menu.selected" = { fg = "cyan", bg = "background_dark" }
"ui.popup" = { fg = "foreground", bg = "background_dark" }
"ui.selection" = { fg = "background", bg = "purple", modifiers = ["dim"] }
"ui.selection.primary" = { fg = "background", bg = "pink" }
"ui.text" = { fg = "foreground" }
"ui.text.focus" = { fg = "cyan" }
"ui.window" = { fg = "foreground" }
"ui.virtual.ruler" = { bg = "ruler" }
"ui.virtual.indent-guide" = { fg = "ruler" }
# TODO: diferentiate doc comment "ui.statusline" = { fg = "foreground", bg = "background_dark" }
# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors "ui.statusline.inactive" = { fg = "comment", bg = "background_dark" }
"ui.statusline.normal" = { fg = "background_dark", bg = "purple"}
"ui.statusline.insert" = { fg = "background_dark", bg = "pink"}
"ui.statusline.select" = { fg = "background_dark", bg = "cyan"}
"ui.background" = { bg = "midnight" } "error" = { fg = "red" }
"ui.background.separator" = { fg = "comet" } "warning" = { fg = "cyan" }
"ui.linenr" = { fg = "comet" }
"ui.linenr.selected" = { fg = "lilac" }
"ui.statusline" = { fg = "lilac", bg = "revolver" }
"ui.statusline.inactive" = { fg = "lavender", bg = "revolver" }
"ui.popup" = { bg = "revolver" }
"ui.window" = { fg = "bossanova" }
"ui.help" = { bg = "#7958DC", fg = "#171452" }
"ui.text" = { fg = "lavender" } "markup.heading" = { fg = "purple", modifiers = ["bold"] }
"ui.text.focus" = { fg = "white" } "markup.list" = "cyan"
"ui.virtual" = { fg = "comet" } "markup.bold" = { fg = "orange", modifiers = ["bold"] }
"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
"markup.link.url" = "cyan"
"markup.link.text" = "pink"
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "foreground" }
"ui.virtual.indent-guide" = { fg = "comet" } "ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "cyan" }
"ui.selection" = { bg = "#540099" } "ui.explorer.exe" = { fg = "foreground" }
"ui.selection.primary" = { bg = "#540099" } "ui.explorer.focus" = { modifiers = ["reversed"] }
# TODO: namespace ui.cursor as ui.selection.cursor? "ui.explorer.unfocus" = { bg = "comment" }
"ui.cursor.select" = { bg = "delta" }
"ui.cursor.insert" = { bg = "white" }
"ui.cursor.match" = { fg = "#212121", bg = "#6C6999" }
"ui.cursor" = { modifiers = ["reversed"] }
"ui.cursorline.primary" = { bg = "bossanova" }
"ui.highlight" = { bg = "bossanova" }
"ui.menu" = { fg = "lavender", bg = "revolver" }
"ui.menu.selected" = { fg = "revolver", bg = "white" }
"ui.menu.scroll" = { fg = "lavender", bg = "comet" }
diagnostic = { modifiers = ["underlined"] }
# "diagnostic.hint" = { fg = "revolver", bg = "lilac" }
# "diagnostic.info" = { fg = "revolver", bg = "lavender" }
# "diagnostic.warning" = { fg = "revolver", bg = "honey" }
# "diagnostic.error" = { fg = "revolver", bg = "apricot" }
warning = "lightning"
error = "apricot"
info = "delta"
hint = "silver"
[palette] [palette]
white = "#ffffff" background = "#3A2A4D"
lilac = "#dbbfef" background_dark = "#2B1C3D"
lavender = "#a4a0e8" foreground = "#f8f8f2"
comet = "#5a5977" ruler = "#453254"
bossanova = "#452859" comment = "#886C9C"
midnight = "#3b224c" red = "#ff5555"
revolver = "#281733" orange = "#ffb86c"
yellow = "#f1fa8c"
silver = "#cccccc" green = "#50fa7b"
sirocco = "#697C81" purple = "#bd93f9"
mint = "#9ff28f" cyan = "#8be9fd"
almond = "#eccdba" pink = "#ff79c6"
chamois = "#E8DCA0"
honey = "#efba5d"
apricot = "#f47868"
lightning = "#ffcd1c"
delta = "#6F44F0"
Loading…
Cancel
Save