Merge branch 'master'

pull/6/head
trivernis 1 year ago
commit e20886dd60
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -0,0 +1,139 @@
name: Build
on:
pull_request:
push:
branches:
- master
schedule:
- cron: '00 01 * * *'
jobs:
check:
name: Check
runs-on: ubuntu-latest
strategy:
matrix:
rust: [stable, msrv]
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Use MSRV rust toolchain
if: matrix.rust == 'msrv'
run: cp .github/workflows/msrv-rust-toolchain.toml rust-toolchain.toml
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2
- name: Run cargo check
run: cargo check
test:
name: Test Suite
runs-on: ${{ matrix.os }}
env:
RUST_BACKTRACE: 1
HELIX_LOG_LEVEL: info
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2
- name: Cache test tree-sitter grammar
uses: actions/cache@v3
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-
- name: Run cargo test
run: cargo test --workspace
- name: Run cargo integration-test
run: cargo integration-test
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
lints:
name: Lints
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
run: cargo fmt --all -- --check
- name: Run cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Run cargo doc
run: cargo doc --no-deps --workspace --document-private-items
env:
RUSTDOCFLAGS: -D warnings
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2
- name: Generate docs
run: cargo xtask docgen
- name: Check uncommitted documentation changes
run: |
git diff
git diff-files --quiet \
|| (echo "Run 'cargo xtask docgen', commit the changes and push again" \
&& exit 1)
queries:
name: Tree-sitter queries
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2
- name: Generate docs
run: cargo xtask query-check

@ -0,0 +1,139 @@
name: Build
on:
pull_request:
push:
branches:
- master
schedule:
- cron: '00 01 * * *'
jobs:
check:
name: Check
runs-on: ubuntu-latest
strategy:
matrix:
rust: [stable, msrv]
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Use MSRV rust toolchain
if: matrix.rust == 'msrv'
run: cp .github/workflows/msrv-rust-toolchain.toml rust-toolchain.toml
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2
- name: Run cargo check
run: cargo check
test:
name: Test Suite
runs-on: ${{ matrix.os }}
env:
RUST_BACKTRACE: 1
HELIX_LOG_LEVEL: info
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2
- name: Cache test tree-sitter grammar
uses: actions/cache@v3
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-
- name: Run cargo test
run: cargo test --workspace
- name: Run cargo integration-test
run: cargo integration-test
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
lints:
name: Lints
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
run: cargo fmt --all -- --check
- name: Run cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Run cargo doc
run: cargo doc --no-deps --workspace --document-private-items
env:
RUSTDOCFLAGS: -D warnings
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2
- name: Generate docs
run: cargo xtask docgen
- name: Check uncommitted documentation changes
run: |
git diff
git diff-files --quiet \
|| (echo "Run 'cargo xtask docgen', commit the changes and push again" \
&& exit 1)
queries:
name: Tree-sitter queries
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2
- name: Generate docs
run: cargo xtask query-check

@ -11,7 +11,7 @@ indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "rust"
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "a360da0a29a19c281d08295a35ecd0544d2da211" }
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
[[language]]
name = "nix"

@ -24,13 +24,10 @@ jobs:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v1
- uses: Swatinem/rust-cache@v2
- name: Fetch tree-sitter grammars
uses: actions-rs/cargo@v1
with:
command: run
args: --package=helix-loader --bin=hx-loader
run: cargo run --package=helix-loader --bin=hx-loader
- name: Bundle grammars
run: tar cJf grammars.tar.xz -C runtime/grammars/sources .
@ -198,16 +195,6 @@ jobs:
- uses: actions/download-artifact@v3
- name: Calculate tag name
run: |
name=dev
if [[ $GITHUB_REF == refs/tags/* ]]; then
name=${GITHUB_REF:10}
fi
echo ::set-output name=val::$name
echo TAG=$name >> $GITHUB_ENV
id: tagname
- name: Build archive
shell: bash
run: |
@ -227,7 +214,7 @@ jobs:
if [[ $platform =~ "windows" ]]; then
exe=".exe"
fi
pkgname=helix-$TAG-$platform
pkgname=helix-$GITHUB_REF_NAME-$platform
mkdir $pkgname
cp $source/LICENSE $source/README.md $pkgname
mkdir $pkgname/contrib
@ -247,7 +234,7 @@ jobs:
fi
done
tar cJf dist/helix-$TAG-source.tar.xz -C $source .
tar cJf dist/helix-$GITHUB_REF_NAME-source.tar.xz -C $source .
mv dist $source/
- name: Upload binaries to release
@ -257,7 +244,7 @@ jobs:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/*
file_glob: true
tag: ${{ steps.tagname.outputs.val }}
tag: ${{ github.ref_name }}
overwrite: true
- name: Upload binaries as artifact

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/helix.iml" filepath="$PROJECT_DIR$/.idea/helix.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

55
Cargo.lock generated

@ -13,6 +13,18 @@ dependencies = [
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.18"
@ -92,9 +104,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.74"
version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
[[package]]
name = "cfg-if"
@ -115,9 +127,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.22"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"iana-time-zone",
"num-integer",
@ -400,18 +412,29 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
"ahash 0.7.6",
]
[[package]]
name = "hashbrown"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038"
dependencies = [
"ahash 0.8.2",
]
[[package]]
name = "helix-core"
version = "0.6.0"
dependencies = [
"ahash 0.8.2",
"arc-swap",
"bitflags",
"chrono",
"encoding_rs",
"etcetera",
"hashbrown 0.13.1",
"helix-loader",
"log",
"once_cell",
@ -655,9 +678,9 @@ checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
[[package]]
name = "libloading"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
@ -876,9 +899,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [
"aho-corasick",
"memchr",
@ -959,9 +982,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.87"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7"
dependencies = [
"itoa",
"ryu",
@ -1023,9 +1046,9 @@ dependencies = [
[[package]]
name = "similar"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
[[package]]
name = "slab"
@ -1196,9 +1219,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.21.2"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
dependencies = [
"autocfg",
"bytes",
@ -1288,7 +1311,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"hashbrown 0.12.3",
"regex",
]

@ -76,10 +76,10 @@ config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%App
| -------------------- | ------------------------------------------------ |
| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| Linux / MacOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
| Linux / macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires
elevated priviliges - i.e. PowerShell or Cmd must be run as administrator.
elevated privileges - i.e. PowerShell or Cmd must be run as administrator.
**PowerShell:**
@ -135,9 +135,9 @@ sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desk
Please note: there is no icon for Helix yet, so the system default will be used.
## MacOS
## macOS
Helix can be installed on MacOS through homebrew:
Helix can be installed on macOS through homebrew:
```
brew install helix

@ -46,7 +46,7 @@ on unix operating systems.
| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
| `cursorline` | Highlight all lines with a cursor. | `false` |
| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "line-numbers"]` |
| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers"]` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `true` |
| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` |
@ -103,7 +103,9 @@ The following statusline elements can be configured:
| `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections |
| `primary-selection-length` | The number of characters currently in primary selection |
| `position` | The cursor position |
| `position-percentage` | The cursor position as a percentage of the total number of lines |
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |

@ -5,6 +5,7 @@
| bash | ✓ | | | `bash-language-server` |
| bass | ✓ | | | `bass` |
| beancount | ✓ | | | |
| bicep | ✓ | | | `bicep-langserver` |
| c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` |
| cairo | ✓ | | | |
@ -24,7 +25,7 @@
| edoc | ✓ | | | |
| eex | ✓ | | | |
| ejs | ✓ | | | |
| elixir | ✓ | ✓ | | `elixir-ls` |
| elixir | ✓ | ✓ | | `elixir-ls` |
| elm | ✓ | | | `elm-language-server` |
| elvish | ✓ | | | `elvish` |
| env | ✓ | | | |
@ -50,12 +51,12 @@
| hare | ✓ | | | |
| haskell | ✓ | | | `haskell-language-server-wrapper` |
| hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | |
| heex | ✓ | ✓ | | `elixir-ls` |
| html | ✓ | | | `vscode-html-language-server` |
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
| java | ✓ | | | `jdtls` |
| java | ✓ | | | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` |
@ -77,7 +78,7 @@
| meson | ✓ | | ✓ | |
| mint | | | | `mint` |
| nickel | ✓ | | ✓ | `nls` |
| nix | ✓ | | | `rnix-lsp` |
| nix | ✓ | | | `nil` |
| nu | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` |
@ -92,6 +93,7 @@
| protobuf | ✓ | | ✓ | |
| purescript | ✓ | | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `pylsp` |
| qml | ✓ | | ✓ | `qmlls` |
| r | ✓ | | | `R` |
| racket | | | | `racket` |
| regex | ✓ | | | |

@ -44,7 +44,9 @@
| `: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. |
| `:reload-all` | Discard changes and reload all documents from the source files. |
| `:update` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
| `: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. |

@ -103,10 +103,10 @@ via the `HELIX_RUNTIME` environment variable.
| -------------------- | ------------------------------------------------ |
| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| Linux / MacOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
| Linux / macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires
elevated priviliges - i.e. PowerShell or Cmd must be run as administrator.
elevated privileges - i.e. PowerShell or Cmd must be run as administrator.
**PowerShell:**

@ -19,5 +19,5 @@ _hx() {
COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- $2))
;;
esac
} && complete -F _hx hx
} && complete -o filenames -F _hx hx

@ -1,22 +1,5 @@
{
"nodes": {
"all-cabal-json": {
"flake": false,
"locked": {
"lastModified": 1665552503,
"narHash": "sha256-r14RmRSwzv5c+bWKUDaze6pXM7nOsiz1H8nvFHJvufc=",
"owner": "nix-community",
"repo": "all-cabal-json",
"rev": "d7c0434eebffb305071404edcf9d5cd99703878e",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "hackage",
"repo": "all-cabal-json",
"type": "github"
}
},
"crane": {
"flake": false,
"locked": {
@ -52,47 +35,45 @@
"dream2nix": {
"inputs": {
"alejandra": [
"nci",
"nixpkgs"
"nci"
],
"all-cabal-json": [
"nci"
],
"all-cabal-json": "all-cabal-json",
"crane": "crane",
"devshell": [
"nci",
"devshell"
],
"flake-utils-pre-commit": [
"nci",
"nixpkgs"
"nci"
],
"ghc-utils": [
"nci"
],
"ghc-utils": "ghc-utils",
"gomod2nix": [
"nci",
"nixpkgs"
"nci"
],
"mach-nix": [
"nci",
"nixpkgs"
"nci"
],
"nixpkgs": [
"nci",
"nixpkgs"
],
"poetry2nix": [
"nci",
"nixpkgs"
"nci"
],
"pre-commit-hooks": [
"nci",
"nixpkgs"
"nci"
]
},
"locked": {
"lastModified": 1667429039,
"narHash": "sha256-Lu6da25JioHzerkLHAHSO9suCQFzJ/XBjkcGCIbasLM=",
"lastModified": 1668851003,
"narHash": "sha256-X7RCQQynbxStZR2m7HW38r/msMQwVl3afD6UXOCtvx4=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "5252794e58eedb02d607fa3187ffead7becc81b0",
"rev": "c77e8379d8fe01213ba072e40946cbfb7b58e628",
"type": "github"
},
"original": {
@ -116,22 +97,6 @@
"type": "github"
}
},
"ghc-utils": {
"flake": false,
"locked": {
"lastModified": 1662774800,
"narHash": "sha256-1Rd2eohGUw/s1tfvkepeYpg8kCEXiIot0RijapUjAkE=",
"ref": "refs/heads/master",
"rev": "bb3a2d3dc52ff0253fb9c2812bd7aa2da03e0fea",
"revCount": 1072,
"type": "git",
"url": "https://gitlab.haskell.org/bgamari/ghc-utils"
},
"original": {
"type": "git",
"url": "https://gitlab.haskell.org/bgamari/ghc-utils"
}
},
"nci": {
"inputs": {
"devshell": "devshell",
@ -144,11 +109,11 @@
]
},
"locked": {
"lastModified": 1667542401,
"narHash": "sha256-mdWjP5tjSf8n6FAtpSgL23kX4+eWBwLrSYo9iY3mA8Q=",
"lastModified": 1669011203,
"narHash": "sha256-Lymj4HktNEFmVXtwI0Os7srDXHZbZW0Nzw3/+5Hf8ko=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "cd5e5cbd81c80dc219455dd3b1e0ddb55fae51ec",
"rev": "c5133b91fc1d549087c91228bd213f2518728a4b",
"type": "github"
},
"original": {
@ -159,11 +124,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1667482890,
"narHash": "sha256-pua0jp87iwN7NBY5/ypx0s9L9CG49Ju/NI4wGwurHc4=",
"lastModified": 1668905981,
"narHash": "sha256-RBQa/+9Uk1eFTqIOXBSBezlEbA3v5OkgP+qptQs1OxY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a2a777538d971c6b01c6e54af89ddd6567c055e8",
"rev": "690ffff026b4e635b46f69002c0f4e81c65dfc2e",
"type": "github"
},
"original": {
@ -188,11 +153,11 @@
]
},
"locked": {
"lastModified": 1667487142,
"narHash": "sha256-bVuzLs1ZVggJAbJmEDVO9G6p8BH3HRaolK70KXvnWnU=",
"lastModified": 1668998422,
"narHash": "sha256-G/BklIplCHZEeDIabaaxqgITdIXtMolRGlwxn9jG2/Q=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "cf668f737ac986c0a89e83b6b2e3c5ddbd8cf33b",
"rev": "68ab029c93f8f8eed4cf3ce9a89a9fd4504b2d6e",
"type": "github"
},
"original": {

@ -150,6 +150,7 @@
["languages.toml" "theme.toml" "base16_theme.toml"]
}
'';
checkPhase = ":";
meta.mainProgram = "hx";
};
@ -166,7 +167,7 @@
packages
// {
helix-unwrapped = packages.helix.passthru.unwrapped;
helix-unwrapped-debug = packages.helix-debug.passthru.unwrapped;
helix-unwrapped-dev = packages.helix-dev.passthru.unwrapped;
}
)
outputs.packages;

@ -30,6 +30,8 @@ once_cell = "1.16"
arc-swap = "1"
regex = "1"
bitflags = "1.3"
ahash = "0.8.2"
hashbrown = { version = "0.13.1", features = ["raw"] }
log = "0.4"
serde = { version = "1.0", features = ["derive"] }

@ -100,43 +100,41 @@ mod test {
#[test]
fn test_find_line_comment() {
use crate::State;
// four lines, two space indented, except for line 1 which is blank.
let doc = Rope::from(" 1\n\n 2\n 3");
let mut state = State::new(doc);
let mut doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
state.selection = Selection::single(0, state.doc.len_chars() - 1);
let mut selection = Selection::single(0, doc.len_chars() - 1);
let text = state.doc.slice(..);
let text = doc.slice(..);
let res = find_line_comment("//", text, 0..3);
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1)
assert_eq!(res, (false, vec![0, 2], 2, 1));
// comment
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.map(transaction.changes());
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(state.doc, " // 1\n\n // 2\n // 3");
assert_eq!(doc, " // 1\n\n // 2\n // 3");
// uncomment
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3");
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
// 0 margin comments
state.doc = Rope::from(" //1\n\n //2\n //3");
doc = Rope::from(" //1\n\n //2\n //3");
// reset the selection.
state.selection = Selection::single(0, state.doc.len_chars() - 1);
selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3");
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
// TODO: account for uncommenting with uneven comment indentation
}

@ -1,9 +1,15 @@
use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction};
use crate::{Assoc, ChangeSet, Range, Rope, Selection, Transaction};
use once_cell::sync::Lazy;
use regex::Regex;
use std::num::NonZeroUsize;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct State {
pub doc: Rope,
pub selection: Selection,
}
/// Stores the history of changes to a buffer.
///
/// Currently the history is represented as a vector of revisions. The vector
@ -48,7 +54,7 @@ pub struct History {
}
/// A single point in history. See [History] for more information.
#[derive(Debug)]
#[derive(Debug, Clone)]
struct Revision {
parent: usize,
last_child: Option<NonZeroUsize>,
@ -113,6 +119,37 @@ impl History {
self.current == 0
}
/// Returns the changes since the given revision composed into a transaction.
/// Returns None if there are no changes between the current and given revisions.
pub fn changes_since(&self, revision: usize) -> Option<Transaction> {
use std::cmp::Ordering::*;
match revision.cmp(&self.current) {
Equal => None,
Less => {
let mut child = self.revisions[revision].last_child?.get();
let mut transaction = self.revisions[child].transaction.clone();
while child != self.current {
child = self.revisions[child].last_child?.get();
transaction = transaction.compose(self.revisions[child].transaction.clone());
}
Some(transaction)
}
Greater => {
let mut inversion = self.revisions[revision].inversion.clone();
let mut parent = self.revisions[revision].parent;
while parent != self.current {
parent = self.revisions[parent].parent;
if parent == 0 {
return None;
}
inversion = inversion.compose(self.revisions[parent].inversion.clone());
}
Some(inversion)
}
}
}
/// Undo the last edit.
pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() {
@ -366,12 +403,16 @@ impl std::str::FromStr for UndoKind {
#[cfg(test)]
mod test {
use super::*;
use crate::Selection;
#[test]
fn test_undo_redo() {
let mut history = History::default();
let doc = Rope::from("hello");
let mut state = State::new(doc);
let mut state = State {
doc,
selection: Selection::point(0),
};
let transaction1 =
Transaction::change(&state.doc, vec![(5, 5, Some(" world!".into()))].into_iter());
@ -420,7 +461,10 @@ mod test {
fn test_earlier_later() {
let mut history = History::default();
let doc = Rope::from("a\n");
let mut state = State::new(doc);
let mut state = State {
doc,
selection: Selection::point(0),
};
fn undo(history: &mut History, state: &mut State) {
if let Some(transaction) = history.undo() {

@ -74,12 +74,12 @@ impl DateTimeIncrementor {
(true, false) => {
let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
date.and_hms(0, 0, 0)
date.and_hms_opt(0, 0, 0).unwrap()
}
(false, true) => {
let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
NaiveDate::from_ymd(0, 1, 1).and_time(time)
NaiveDate::from_ymd_opt(0, 1, 1).unwrap().and_time(time)
}
(false, false) => return None,
};
@ -312,10 +312,10 @@ fn ndays_in_month(year: i32, month: u32) -> u32 {
} else {
(year, month + 1)
};
let d = NaiveDate::from_ymd(y, m, 1);
let d = NaiveDate::from_ymd_opt(y, m, 1).unwrap();
// ...is preceded by the last day of the original month.
d.pred().day()
d.pred_opt().unwrap().day()
}
fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
@ -334,7 +334,7 @@ fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
let day = cmp::min(date_time.day(), ndays_in_month(year, month));
Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time()))
NaiveDate::from_ymd_opt(year, month, day).map(|date| date.and_time(date_time.time()))
}
fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
@ -342,8 +342,8 @@ fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
let ndays = ndays_in_month(year, date_time.month());
if date_time.day() > ndays {
let d = NaiveDate::from_ymd(year, date_time.month(), ndays);
Some(d.succ().and_time(date_time.time()))
NaiveDate::from_ymd_opt(year, date_time.month(), ndays)
.and_then(|date| date.succ_opt().map(|date| date.and_time(date_time.time())))
} else {
date_time.with_year(year)
}

@ -461,59 +461,61 @@ fn query_indents(
/// so that the indent computation starts with the correct syntax node.
fn extend_nodes<'a>(
node: &mut Node<'a>,
deepest_preceding: Option<Node<'a>>,
mut deepest_preceding: Node<'a>,
extend_captures: &HashMap<usize, Vec<ExtendCapture>>,
text: RopeSlice,
line: usize,
tab_width: usize,
) {
if let Some(mut deepest_preceding) = deepest_preceding {
let mut stop_extend = false;
while deepest_preceding != *node {
let mut extend_node = false;
// This will be set to true if this node is captured, regardless of whether
// it actually will be extended (e.g. because the cursor isn't indented
// more than the node).
let mut node_captured = false;
if let Some(captures) = extend_captures.get(&deepest_preceding.id()) {
for capture in captures {
match capture {
ExtendCapture::PreventOnce => {
stop_extend = true;
}
ExtendCapture::Extend => {
node_captured = true;
// We extend the node if
// - the cursor is on the same line as the end of the node OR
// - the line that the cursor is on is more indented than the
// first line of the node
if deepest_preceding.end_position().row == line {
let mut stop_extend = false;
while deepest_preceding != *node {
let mut extend_node = false;
// This will be set to true if this node is captured, regardless of whether
// it actually will be extended (e.g. because the cursor isn't indented
// more than the node).
let mut node_captured = false;
if let Some(captures) = extend_captures.get(&deepest_preceding.id()) {
for capture in captures {
match capture {
ExtendCapture::PreventOnce => {
stop_extend = true;
}
ExtendCapture::Extend => {
node_captured = true;
// We extend the node if
// - the cursor is on the same line as the end of the node OR
// - the line that the cursor is on is more indented than the
// first line of the node
if deepest_preceding.end_position().row == line {
extend_node = true;
} else {
let cursor_indent = indent_level_for_line(text.line(line), tab_width);
let node_indent = indent_level_for_line(
text.line(deepest_preceding.start_position().row),
tab_width,
);
if cursor_indent > node_indent {
extend_node = true;
} else {
let cursor_indent =
indent_level_for_line(text.line(line), tab_width);
let node_indent = indent_level_for_line(
text.line(deepest_preceding.start_position().row),
tab_width,
);
if cursor_indent > node_indent {
extend_node = true;
}
}
}
}
}
}
// If we encountered some `StopExtend` capture before, we don't
// extend the node even if we otherwise would
if node_captured && stop_extend {
stop_extend = false;
} else if extend_node && !stop_extend {
*node = deepest_preceding;
break;
}
// This parent always exists since node is an ancestor of deepest_preceding
deepest_preceding = deepest_preceding.parent().unwrap();
}
// If we encountered some `StopExtend` capture before, we don't
// extend the node even if we otherwise would
if node_captured && stop_extend {
stop_extend = false;
} else if extend_node && !stop_extend {
*node = deepest_preceding;
break;
}
// If the tree contains a syntax error, `deepest_preceding` may not
// have a parent despite being a descendant of `node`.
deepest_preceding = match deepest_preceding.parent() {
Some(parent) => parent,
None => return,
}
}
}
@ -612,14 +614,16 @@ pub fn treesitter_indent_for_pos(
let extend_captures = query_result.extend_captures;
// Check for extend captures, potentially changing the node that the indent calculation starts with
extend_nodes(
&mut node,
deepest_preceding,
&extend_captures,
text,
line,
tab_width,
);
if let Some(deepest_preceding) = deepest_preceding {
extend_nodes(
&mut node,
deepest_preceding,
&extend_captures,
text,
line,
tab_width,
);
}
let mut first_in_line = get_first_in_line(node, new_line.then(|| byte_pos));
let mut result = Indentation::default();

@ -21,7 +21,6 @@ pub mod register;
pub mod search;
pub mod selection;
pub mod shellwords;
mod state;
pub mod surround;
pub mod syntax;
pub mod test;
@ -103,7 +102,6 @@ pub use smallvec::{smallvec, SmallVec};
pub use syntax::Syntax;
pub use diagnostic::Diagnostic;
pub use state::State;
pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};

@ -15,11 +15,7 @@ impl Register {
}
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
if name == '_' {
Self::new(name)
} else {
Self { name, values }
}
Self { name, values }
}
pub const fn name(&self) -> char {
@ -31,15 +27,11 @@ impl Register {
}
pub fn write(&mut self, values: Vec<String>) {
if self.name != '_' {
self.values = values;
}
self.values = values;
}
pub fn push(&mut self, value: String) {
if self.name != '_' {
self.values.push(value);
}
self.values.push(value);
}
}
@ -54,19 +46,25 @@ impl Registers {
self.inner.get(&name)
}
pub fn get_mut(&mut self, name: char) -> &mut Register {
self.inner
.entry(name)
.or_insert_with(|| Register::new(name))
pub fn read(&self, name: char) -> Option<&[String]> {
self.get(name).map(|reg| reg.read())
}
pub fn write(&mut self, name: char, values: Vec<String>) {
self.inner
.insert(name, Register::new_with_values(name, values));
if name != '_' {
self.inner
.insert(name, Register::new_with_values(name, values));
}
}
pub fn read(&self, name: char) -> Option<&[String]> {
self.get(name).map(|reg| reg.read())
pub fn push(&mut self, name: char, value: String) {
if name != '_' {
if let Some(r) = self.inner.get_mut(&name) {
r.push(value);
} else {
self.write(name, vec![value]);
}
}
}
pub fn first(&self, name: char) -> Option<&String> {

@ -1,9 +1,9 @@
use std::borrow::Cow;
/// Auto escape for shellwords usage.
pub fn escape(input: &str) -> Cow<'_, str> {
pub fn escape(input: Cow<str>) -> Cow<str> {
if !input.chars().any(|x| x.is_ascii_whitespace()) {
Cow::Borrowed(input)
input
} else if cfg!(unix) {
Cow::Owned(input.chars().fold(String::new(), |mut buf, c| {
if c.is_ascii_whitespace() {
@ -17,127 +17,182 @@ pub fn escape(input: &str) -> Cow<'_, str> {
}
}
/// Get the vec of escaped / quoted / doublequoted filenames from the input str
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
enum State {
OnWhitespace,
Unquoted,
UnquotedEscaped,
Quoted,
QuoteEscaped,
Dquoted,
DquoteEscaped,
}
enum State {
OnWhitespace,
Unquoted,
UnquotedEscaped,
Quoted,
QuoteEscaped,
Dquoted,
DquoteEscaped,
}
use State::*;
pub struct Shellwords<'a> {
state: State,
/// Shellwords where whitespace and escapes has been resolved.
words: Vec<Cow<'a, str>>,
/// The parts of the input that are divided into shellwords. This can be
/// used to retrieve the original text for a given word by looking up the
/// same index in the Vec as the word in `words`.
parts: Vec<&'a str>,
}
let mut state = Unquoted;
let mut args: Vec<Cow<str>> = Vec::new();
let mut escaped = String::with_capacity(input.len());
impl<'a> From<&'a str> for Shellwords<'a> {
fn from(input: &'a str) -> Self {
use State::*;
let mut start = 0;
let mut end = 0;
let mut state = Unquoted;
let mut words = Vec::new();
let mut parts = Vec::new();
let mut escaped = String::with_capacity(input.len());
for (i, c) in input.char_indices() {
state = match state {
OnWhitespace => match c {
'"' => {
end = i;
Dquoted
}
'\'' => {
end = i;
Quoted
}
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
UnquotedEscaped
} else {
let mut part_start = 0;
let mut unescaped_start = 0;
let mut end = 0;
for (i, c) in input.char_indices() {
state = match state {
OnWhitespace => match c {
'"' => {
end = i;
Dquoted
}
'\'' => {
end = i;
Quoted
}
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
OnWhitespace
}
}
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
}
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
_ => Unquoted,
},
Unquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
UnquotedEscaped
} else {
Unquoted
_ => Unquoted,
},
Unquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
Unquoted
}
}
}
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
_ => Unquoted,
},
UnquotedEscaped => Unquoted,
Quoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
QuoteEscaped
} else {
Quoted
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
}
'\'' => {
end = i;
OnWhitespace
}
_ => Quoted,
},
QuoteEscaped => Quoted,
Dquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
DquoteEscaped
} else {
Dquoted
_ => Unquoted,
},
UnquotedEscaped => Unquoted,
Quoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
QuoteEscaped
} else {
Quoted
}
}
}
'"' => {
end = i;
OnWhitespace
}
_ => Dquoted,
},
DquoteEscaped => Dquoted,
};
'\'' => {
end = i;
OnWhitespace
}
_ => Quoted,
},
QuoteEscaped => Quoted,
Dquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
DquoteEscaped
} else {
Dquoted
}
}
'"' => {
end = i;
OnWhitespace
}
_ => Dquoted,
},
DquoteEscaped => Dquoted,
};
if i >= input.len() - 1 && end == 0 {
end = i + 1;
}
if i >= input.len() - 1 && end == 0 {
end = i + 1;
}
if end > 0 {
let esc_trim = escaped.trim();
let inp = &input[start..end];
if end > 0 {
let esc_trim = escaped.trim();
let inp = &input[unescaped_start..end];
if !(esc_trim.is_empty() && inp.trim().is_empty()) {
if esc_trim.is_empty() {
args.push(inp.into());
} else {
args.push([escaped, inp.into()].concat().into());
escaped = "".to_string();
if !(esc_trim.is_empty() && inp.trim().is_empty()) {
if esc_trim.is_empty() {
words.push(inp.into());
parts.push(inp);
} else {
words.push([escaped, inp.into()].concat().into());
parts.push(&input[part_start..end]);
escaped = "".to_string();
}
}
unescaped_start = i + 1;
part_start = i + 1;
end = 0;
}
start = i + 1;
end = 0;
}
debug_assert!(words.len() == parts.len());
Self {
state,
words,
parts,
}
}
}
impl<'a> Shellwords<'a> {
/// Checks that the input ends with a whitespace character which is not escaped.
///
/// # Examples
///
/// ```rust
/// use helix_core::shellwords::Shellwords;
/// assert_eq!(Shellwords::from(" ").ends_with_whitespace(), true);
/// assert_eq!(Shellwords::from(":open ").ends_with_whitespace(), true);
/// assert_eq!(Shellwords::from(":open foo.txt ").ends_with_whitespace(), true);
/// assert_eq!(Shellwords::from(":open").ends_with_whitespace(), false);
/// #[cfg(unix)]
/// assert_eq!(Shellwords::from(":open a\\ ").ends_with_whitespace(), false);
/// #[cfg(unix)]
/// assert_eq!(Shellwords::from(":open a\\ b.txt").ends_with_whitespace(), false);
/// ```
pub fn ends_with_whitespace(&self) -> bool {
matches!(self.state, State::OnWhitespace)
}
/// Returns the list of shellwords calculated from the input string.
pub fn words(&self) -> &[Cow<'a, str>] {
&self.words
}
/// Returns a list of strings which correspond to [`Self::words`] but represent the original
/// text in the input string - including escape characters - without separating whitespace.
pub fn parts(&self) -> &[&'a str] {
&self.parts
}
args
}
#[cfg(test)]
@ -148,7 +203,8 @@ mod test {
#[cfg(windows)]
fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
let result = shellwords(input);
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
@ -166,7 +222,8 @@ mod test {
#[cfg(unix)]
fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
let result = shellwords(input);
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
@ -183,7 +240,8 @@ mod test {
fn test_quoted() {
let quoted =
r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
let result = shellwords(quoted);
let shellwords = Shellwords::from(quoted);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
@ -198,7 +256,8 @@ mod test {
#[cfg(unix)]
fn test_dquoted() {
let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
let result = shellwords(dquoted);
let shellwords = Shellwords::from(dquoted);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
@ -213,7 +272,8 @@ mod test {
#[cfg(unix)]
fn test_mixed() {
let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
let result = shellwords(dquoted);
let shellwords = Shellwords::from(dquoted);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
@ -234,7 +294,8 @@ mod test {
fn test_lists() {
let input =
r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#;
let result = shellwords(input);
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":set"),
Cow::from("statusline.center"),
@ -247,15 +308,29 @@ mod test {
#[test]
#[cfg(unix)]
fn test_escaping_unix() {
assert_eq!(escape("foobar"), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar"), Cow::Borrowed("foo\\ bar"));
assert_eq!(escape("foo\tbar"), Cow::Borrowed("foo\\\tbar"));
assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar".into()), Cow::Borrowed("foo\\ bar"));
assert_eq!(escape("foo\tbar".into()), Cow::Borrowed("foo\\\tbar"));
}
#[test]
#[cfg(windows)]
fn test_escaping_windows() {
assert_eq!(escape("foobar"), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar"), Cow::Borrowed("\"foo bar\""));
assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar".into()), Cow::Borrowed("\"foo bar\""));
}
#[test]
#[cfg(unix)]
fn test_parts() {
assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]);
assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\ "]);
}
#[test]
#[cfg(windows)]
fn test_parts() {
assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]);
assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\"]);
}
}

@ -1,17 +0,0 @@
use crate::{Rope, Selection};
#[derive(Debug, Clone)]
pub struct State {
pub doc: Rope,
pub selection: Selection,
}
impl State {
#[must_use]
pub fn new(doc: Rope) -> Self {
Self {
doc,
selection: Selection::point(0),
}
}
}

@ -7,8 +7,10 @@ use crate::{
Rope, RopeSlice, Tendril,
};
use ahash::RandomState;
use arc_swap::{ArcSwap, Guard};
use bitflags::bitflags;
use hashbrown::raw::RawTable;
use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{
@ -16,7 +18,8 @@ use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
fmt,
mem::replace,
hash::{Hash, Hasher},
mem::{replace, transmute},
path::Path,
str::FromStr,
sync::Arc,
@ -354,6 +357,26 @@ impl<'a> CapturedNode<'a> {
}
}
/// The maximum number of in-progress matches a TS cursor can consider at once.
/// This is set to a constant in order to avoid performance problems for medium to large files. Set with `set_match_limit`.
/// Using such a limit means that we lose valid captures, so there is fundamentally a tradeoff here.
///
///
/// Old tree sitter versions used a limit of 32 by default until this limit was removed in version `0.19.5` (must now be set manually).
/// However, this causes performance issues for medium to large files.
/// In helix, this problem caused treesitter motions to take multiple seconds to complete in medium-sized rust files (3k loc).
///
///
/// Neovim also encountered this problem and reintroduced this limit after it was removed upstream
/// (see <https://github.com/neovim/neovim/issues/14897> and <https://github.com/neovim/neovim/pull/14915>).
/// The number used here is fundamentally a tradeoff between breaking some obscure edge cases and performance.
///
///
/// Neovim chose 64 for this value somewhat arbitrarily (<https://github.com/neovim/neovim/pull/18397>).
/// 64 is too low for some languages though. In particular, it breaks some highlighting for record fields in Erlang record definitions.
/// This number can be increased if new syntax highlight breakages are found, as long as the performance penalty is not too high.
const TREE_SITTER_MATCH_LIMIT: u32 = 256;
impl TextObjectQuery {
/// Run the query on the given node and return sub nodes which match given
/// capture ("function.inside", "class.around", etc).
@ -394,6 +417,8 @@ impl TextObjectQuery {
.iter()
.find_map(|cap| self.query.capture_index_for_name(cap))?;
cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
let nodes = cursor
.captures(&self.query, node, RopeProvider(slice))
.filter_map(move |(mat, _)| {
@ -748,30 +773,38 @@ impl Syntax {
// Convert the changeset into tree sitter edits.
let edits = generate_edits(old_source, changeset);
// This table allows inverse indexing of `layers`.
// That is by hashing a `Layer` you can find
// the `LayerId` of an existing equivalent `Layer` in `layers`.
//
// It is used to determine if a new layer exists for an injection
// or if an existing layer needs to be updated.
let mut layers_table = RawTable::with_capacity(self.layers.len());
let layers_hasher = RandomState::new();
// Use the edits to update all layers markers
if !edits.is_empty() {
fn point_add(a: Point, b: Point) -> Point {
if b.row > 0 {
Point::new(a.row.saturating_add(b.row), b.column)
} else {
Point::new(0, a.column.saturating_add(b.column))
}
fn point_add(a: Point, b: Point) -> Point {
if b.row > 0 {
Point::new(a.row.saturating_add(b.row), b.column)
} else {
Point::new(0, a.column.saturating_add(b.column))
}
fn point_sub(a: Point, b: Point) -> Point {
if a.row > b.row {
Point::new(a.row.saturating_sub(b.row), a.column)
} else {
Point::new(0, a.column.saturating_sub(b.column))
}
}
fn point_sub(a: Point, b: Point) -> Point {
if a.row > b.row {
Point::new(a.row.saturating_sub(b.row), a.column)
} else {
Point::new(0, a.column.saturating_sub(b.column))
}
}
for layer in self.layers.values_mut() {
// The root layer always covers the whole range (0..usize::MAX)
if layer.depth == 0 {
layer.flags = LayerUpdateFlags::MODIFIED;
continue;
}
for (layer_id, layer) in self.layers.iter_mut() {
// The root layer always covers the whole range (0..usize::MAX)
if layer.depth == 0 {
layer.flags = LayerUpdateFlags::MODIFIED;
continue;
}
if !edits.is_empty() {
for range in &mut layer.ranges {
// Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720
for edit in edits.iter().rev() {
@ -836,6 +869,12 @@ impl Syntax {
}
}
}
let hash = layers_hasher.hash_one(layer);
// Safety: insert_no_grow is unsafe because it assumes that the table
// has enough capacity to hold additional elements.
// This is always the case as we reserved enough capacity above.
unsafe { layers_table.insert_no_grow(hash, layer_id) };
}
PARSER.with(|ts_parser| {
@ -843,6 +882,7 @@ impl Syntax {
let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
// TODO: might need to set cursor range
cursor.set_byte_range(0..usize::MAX);
cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
let source_slice = source.slice(..);
@ -959,27 +999,23 @@ impl Syntax {
let depth = layer.depth + 1;
// TODO: can't inline this since matches borrows self.layers
for (config, ranges) in injections {
// Find an existing layer
let layer = self
.layers
.iter_mut()
.find(|(_, layer)| {
layer.depth == depth && // TODO: track parent id instead
layer.config.language == config.language && layer.ranges == ranges
let new_layer = LanguageLayer {
tree: None,
config,
depth,
ranges,
flags: LayerUpdateFlags::empty(),
};
// Find an identical existing layer
let layer = layers_table
.get(layers_hasher.hash_one(&new_layer), |&it| {
self.layers[it] == new_layer
})
.map(|(id, _layer)| id);
.copied();
// ...or insert a new one.
let layer_id = layer.unwrap_or_else(|| {
self.layers.insert(LanguageLayer {
tree: None,
config,
depth,
ranges,
// set the modified flag to ensure the layer is parsed
flags: LayerUpdateFlags::empty(),
})
});
let layer_id = layer.unwrap_or_else(|| self.layers.insert(new_layer));
queue.push_back(layer_id);
}
@ -1032,6 +1068,7 @@ impl Syntax {
// if reusing cursors & no range this resets to whole range
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT);
let mut captures = cursor_ref
.captures(
@ -1115,6 +1152,34 @@ pub struct LanguageLayer {
flags: LayerUpdateFlags,
}
/// This PartialEq implementation only checks if that
/// two layers are theoretically identical (meaning they highlight the same text range with the same language).
/// It does not check whether the layers have the same internal treesitter
/// state.
impl PartialEq for LanguageLayer {
fn eq(&self, other: &Self) -> bool {
self.depth == other.depth
&& self.config.language == other.config.language
&& self.ranges == other.ranges
}
}
/// Hash implementation belongs to PartialEq implementation above.
/// See its documentation for details.
impl Hash for LanguageLayer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.depth.hash(state);
// The transmute is necessary here because tree_sitter::Language does not derive Hash at the moment.
// However it does use #[repr] transparent so the transmute here is safe
// as `Language` (which `Grammar` is an alias for) is just a newtype wrapper around a (thin) pointer.
// This is also compatible with the PartialEq implementation of language
// as that is just a pointer comparison.
let language: *const () = unsafe { transmute(self.config.language) };
language.hash(state);
self.ranges.hash(state);
}
}
impl LanguageLayer {
pub fn tree(&self) -> &Tree {
// TODO: no unwrap
@ -1260,7 +1325,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize};
use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
QueryMatch, Range, TextProvider, Tree,
QueryMatch, Range, TextProvider, Tree, TreeCursor,
};
const CANCELLATION_CHECK_INTERVAL: usize = 100;
@ -2130,57 +2195,68 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
}
}
fn node_is_visible(node: &Node) -> bool {
node.is_missing() || (node.is_named() && node.language().node_kind_is_visible(node.kind_id()))
}
pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result {
pretty_print_tree_impl(fmt, node, true, None, 0)
if node.child_count() == 0 {
if node_is_visible(&node) {
write!(fmt, "({})", node.kind())
} else {
write!(fmt, "\"{}\"", node.kind())
}
} else {
pretty_print_tree_impl(fmt, &mut node.walk(), 0)
}
}
fn pretty_print_tree_impl<W: fmt::Write>(
fmt: &mut W,
node: Node,
is_root: bool,
field_name: Option<&str>,
cursor: &mut TreeCursor,
depth: usize,
) -> fmt::Result {
fn is_visible(node: Node) -> bool {
node.is_missing()
|| (node.is_named() && node.language().node_kind_is_visible(node.kind_id()))
}
let node = cursor.node();
let visible = node_is_visible(&node);
if is_visible(node) {
if visible {
let indentation_columns = depth * 2;
write!(fmt, "{:indentation_columns$}", "")?;
if let Some(field_name) = field_name {
if let Some(field_name) = cursor.field_name() {
write!(fmt, "{}: ", field_name)?;
}
write!(fmt, "({}", node.kind())?;
} else if is_root {
write!(fmt, "(\"{}\")", node.kind())?;
}
for child_idx in 0..node.child_count() {
if let Some(child) = node.child(child_idx) {
if is_visible(child) {
// Handle children.
if cursor.goto_first_child() {
loop {
if node_is_visible(&cursor.node()) {
fmt.write_char('\n')?;
}
pretty_print_tree_impl(
fmt,
child,
false,
node.field_name_for_child(child_idx as u32),
depth + 1,
)?;
pretty_print_tree_impl(fmt, cursor, depth + 1)?;
if !cursor.goto_next_sibling() {
break;
}
}
let moved = cursor.goto_parent();
// The parent of the first child must exist, and must be `node`.
debug_assert!(moved);
debug_assert!(cursor.node() == node);
}
if is_visible(node) {
write!(fmt, ")")?;
if visible {
fmt.write_char(')')?;
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
@ -2353,11 +2429,17 @@ mod test {
}
#[track_caller]
fn assert_pretty_print(source: &str, expected: &str, start: usize, end: usize) {
fn assert_pretty_print(
language_name: &str,
source: &str,
expected: &str,
start: usize,
end: usize,
) {
let source = Rope::from_str(source);
let loader = Loader::new(Configuration { language: vec![] });
let language = get_language("rust").unwrap();
let language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
@ -2377,13 +2459,14 @@ mod test {
#[test]
fn test_pretty_print() {
let source = r#"/// Hello"#;
assert_pretty_print(source, "(line_comment)", 0, source.len());
assert_pretty_print("rust", source, "(line_comment)", 0, source.len());
// A large tree should be indented with fields:
let source = r#"fn main() {
println!("Hello, World!");
}"#;
assert_pretty_print(
"rust",
source,
concat!(
"(function_item\n",
@ -2402,11 +2485,34 @@ mod test {
// Selecting a token should print just that token:
let source = r#"fn main() {}"#;
assert_pretty_print(source, r#"("fn")"#, 0, 1);
assert_pretty_print("rust", source, r#""fn""#, 0, 1);
// Error nodes are printed as errors:
let source = r#"}{"#;
assert_pretty_print(source, "(ERROR)", 0, source.len());
assert_pretty_print("rust", source, "(ERROR)", 0, source.len());
// Fields broken under unnamed nodes are determined correctly.
// In the following source, `object` belongs to the `singleton_method`
// rule but `name` and `body` belong to an unnamed helper `_method_rest`.
// This can cause a bug with a pretty-printing implementation that
// uses `Node::field_name_for_child` to determine field names but is
// fixed when using `TreeCursor::field_name`.
let source = "def self.method_name
true
end";
assert_pretty_print(
"ruby",
source,
concat!(
"(singleton_method\n",
" object: (self)\n",
" name: (identifier)\n",
" body: (body_statement\n",
" (true)))"
),
0,
source.len(),
);
}
#[test]

@ -148,6 +148,7 @@ pub fn plain(s: &str, selection: Selection) -> String {
}
#[cfg(test)]
#[allow(clippy::module_inception)]
mod test {
use super::*;

@ -579,7 +579,7 @@ impl<'a> Iterator for ChangeIterator<'a> {
#[cfg(test)]
mod test {
use super::*;
use crate::State;
use crate::history::State;
#[test]
fn composition() {
@ -706,7 +706,10 @@ mod test {
#[test]
fn optimized_composition() {
let mut state = State::new("".into());
let mut state = State {
doc: "".into(),
selection: Selection::point(0),
};
let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from("h"));
t1.apply(&mut state.doc);
state.selection = state.selection.clone().map(t1.changes());

@ -10,4 +10,4 @@ indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "rust"
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "a360da0a29a19c281d08295a35ecd0544d2da211" }
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }

@ -22,6 +22,6 @@ lsp-types = { version = "0.93", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.21", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio = { version = "1.22", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.11"
which = "4.2"

@ -4,7 +4,6 @@ use crate::{
Call, Error, OffsetEncoding, Result,
};
use anyhow::anyhow;
use helix_core::{find_root, ChangeSet, Rope};
use lsp_types as lsp;
use serde::Deserialize;
@ -314,6 +313,7 @@ impl Client {
String::from("additionalTextEdits"),
],
}),
insert_replace_support: Some(true),
..Default::default()
}),
completion_item_kind: Some(lsp::CompletionItemKindCapability {
@ -545,16 +545,17 @@ impl Client {
new_text: &Rope,
changes: &ChangeSet,
) -> Option<impl Future<Output = Result<()>>> {
// figure out what kind of sync the server supports
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document sync.
let sync_capabilities = match capabilities.text_document_sync {
Some(lsp::TextDocumentSyncCapability::Kind(kind))
| Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
change: Some(kind),
..
})) => kind,
Some(
lsp::TextDocumentSyncCapability::Kind(kind)
| lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
change: Some(kind),
..
}),
) => kind,
// None | SyncOptions { changes: None }
_ => return None,
};
@ -630,8 +631,12 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
// ) -> Result<Vec<lsp::CompletionItem>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support completion.
capabilities.completion_provider.as_ref()?;
let params = lsp::CompletionParams {
text_document_position: lsp::TextDocumentPositionParams {
text_document,
@ -646,15 +651,25 @@ impl Client {
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), }
};
self.call::<lsp::request::Completion>(params)
Some(self.call::<lsp::request::Completion>(params))
}
pub async fn resolve_completion_item(
pub fn resolve_completion_item(
&self,
completion_item: lsp::CompletionItem,
) -> Result<lsp::CompletionItem> {
self.request::<lsp::request::ResolveCompletionItem>(completion_item)
.await
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support resolving completion items.
match capabilities.completion_provider {
Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..
}) => (),
_ => return None,
}
Some(self.call::<lsp::request::ResolveCompletionItem>(completion_item))
}
pub fn text_document_signature_help(
@ -665,7 +680,7 @@ impl Client {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if signature help is not supported
// Return early if the server does not support signature help.
capabilities.signature_help_provider.as_ref()?;
let params = lsp::SignatureHelpParams {
@ -686,7 +701,18 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support hover.
match capabilities.hover_provider {
Some(
lsp::HoverProviderCapability::Simple(true)
| lsp::HoverProviderCapability::Options(_),
) => (),
_ => return None,
}
let params = lsp::HoverParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
@ -696,7 +722,7 @@ impl Client {
// lsp::SignatureHelpContext
};
self.call::<lsp::request::HoverRequest>(params)
Some(self.call::<lsp::request::HoverRequest>(params))
}
// formatting
@ -709,13 +735,11 @@ impl Client {
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
let capabilities = self.capabilities.get().unwrap();
// check if we're able to format
// Return early if the server does not support formatting.
match capabilities.document_formatting_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
// None | Some(false)
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
_ => return None,
};
// TODO: return err::unavailable so we can fall back to tree sitter formatting
// merge FormattingOptions with 'config.format'
let config_format = self
@ -750,22 +774,20 @@ impl Client {
})
}
pub async fn text_document_range_formatting(
pub fn text_document_range_formatting(
&self,
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> anyhow::Result<Vec<lsp::TextEdit>> {
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
let capabilities = self.capabilities.get().unwrap();
// check if we're able to format
// Return early if the server does not support range formatting.
match capabilities.document_range_formatting_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
// None | Some(false)
_ => return Ok(Vec::new()),
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
_ => return None,
};
// TODO: return err::unavailable so we can fall back to tree sitter formatting
let params = lsp::DocumentRangeFormattingParams {
text_document,
@ -774,11 +796,13 @@ impl Client {
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let response = self
.request::<lsp::request::RangeFormatting>(params)
.await?;
let request = self.call::<lsp::request::RangeFormatting>(params);
Ok(response.unwrap_or_default())
Some(async move {
let json = request.await?;
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
}
pub fn text_document_document_highlight(
@ -786,7 +810,15 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document highlight.
match capabilities.document_highlight_provider {
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
_ => return None,
}
let params = lsp::DocumentHighlightParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
@ -798,7 +830,7 @@ impl Client {
},
};
self.call::<lsp::request::DocumentHighlightRequest>(params)
Some(self.call::<lsp::request::DocumentHighlightRequest>(params))
}
fn goto_request<
@ -831,8 +863,20 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoDefinition>(text_document, position, work_done_token)
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-definition.
match capabilities.definition_provider {
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
_ => return None,
}
Some(self.goto_request::<lsp::request::GotoDefinition>(
text_document,
position,
work_done_token,
))
}
pub fn goto_type_definition(
@ -840,12 +884,23 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoTypeDefinition>(
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-type-definition.
match capabilities.type_definition_provider {
Some(
lsp::TypeDefinitionProviderCapability::Simple(true)
| lsp::TypeDefinitionProviderCapability::Options(_),
) => (),
_ => return None,
}
Some(self.goto_request::<lsp::request::GotoTypeDefinition>(
text_document,
position,
work_done_token,
)
))
}
pub fn goto_implementation(
@ -853,12 +908,23 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoImplementation>(
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-definition.
match capabilities.implementation_provider {
Some(
lsp::ImplementationProviderCapability::Simple(true)
| lsp::ImplementationProviderCapability::Options(_),
) => (),
_ => return None,
}
Some(self.goto_request::<lsp::request::GotoImplementation>(
text_document,
position,
work_done_token,
)
))
}
pub fn goto_reference(
@ -866,7 +932,15 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-reference.
match capabilities.references_provider {
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
_ => return None,
}
let params = lsp::ReferenceParams {
text_document_position: lsp::TextDocumentPositionParams {
text_document,
@ -881,31 +955,47 @@ impl Client {
},
};
self.call::<lsp::request::References>(params)
Some(self.call::<lsp::request::References>(params))
}
pub fn document_symbols(
&self,
text_document: lsp::TextDocumentIdentifier,
) -> impl Future<Output = Result<Value>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document symbols.
match capabilities.document_symbol_provider {
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
_ => return None,
}
let params = lsp::DocumentSymbolParams {
text_document,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};
self.call::<lsp::request::DocumentSymbolRequest>(params)
Some(self.call::<lsp::request::DocumentSymbolRequest>(params))
}
// empty string to get all symbols
pub fn workspace_symbols(&self, query: String) -> impl Future<Output = Result<Value>> {
pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support workspace symbols.
match capabilities.workspace_symbol_provider {
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
_ => return None,
}
let params = lsp::WorkspaceSymbolParams {
query,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};
self.call::<lsp::request::WorkspaceSymbol>(params)
Some(self.call::<lsp::request::WorkspaceSymbol>(params))
}
pub fn code_actions(
@ -913,7 +1003,18 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
context: lsp::CodeActionContext,
) -> impl Future<Output = Result<Value>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support code actions.
match capabilities.code_action_provider {
Some(
lsp::CodeActionProviderCapability::Simple(true)
| lsp::CodeActionProviderCapability::Options(_),
) => (),
_ => return None,
}
let params = lsp::CodeActionParams {
text_document,
range,
@ -922,26 +1023,22 @@ impl Client {
partial_result_params: lsp::PartialResultParams::default(),
};
self.call::<lsp::request::CodeActionRequest>(params)
Some(self.call::<lsp::request::CodeActionRequest>(params))
}
pub async fn rename_symbol(
pub fn rename_symbol(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
new_name: String,
) -> anyhow::Result<lsp::WorkspaceEdit> {
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
let capabilities = self.capabilities.get().unwrap();
// check if we're able to rename
// Return early if the language server does not support renaming.
match capabilities.rename_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
// None | Some(false)
_ => {
log::warn!("rename_symbol failed: The server does not support rename");
let err = "The server does not support rename";
return Err(anyhow!(err));
}
_ => return None,
};
let params = lsp::RenameParams {
@ -955,11 +1052,21 @@ impl Client {
},
};
let response = self.request::<lsp::request::Rename>(params).await?;
Ok(response.unwrap_or_default())
let request = self.call::<lsp::request::Rename>(params);
Some(async move {
let json = request.await?;
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
}
pub fn command(&self, command: lsp::Command) -> impl Future<Output = Result<Value>> {
pub fn command(&self, command: lsp::Command) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the language server does not support executing commands.
capabilities.execute_command_provider.as_ref()?;
let params = lsp::ExecuteCommandParams {
command: command.command,
arguments: command.arguments.unwrap_or_default(),
@ -968,6 +1075,6 @@ impl Client {
},
};
self.call::<lsp::request::ExecuteCommand>(params)
Some(self.call::<lsp::request::ExecuteCommand>(params))
}
}

@ -285,6 +285,8 @@ impl MethodCall {
pub enum Notification {
// we inject this notification to signal the LSP is ready
Initialized,
// and this notification to signal that the LSP exited
Exit,
PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams),
@ -297,6 +299,7 @@ impl Notification {
let notification = match method {
lsp::notification::Initialized::METHOD => Self::Initialized,
lsp::notification::Exit::METHOD => Self::Exit,
lsp::notification::PublishDiagnostics::METHOD => {
let params: lsp::PublishDiagnosticsParams = params.parse()?;
Self::PublishDiagnostics(params)
@ -353,7 +356,11 @@ impl Registry {
.map(|(_, client)| client.as_ref())
}
pub fn get(
pub fn remove_by_id(&mut self, id: usize) {
self.inner.retain(|_, (client_id, _)| client_id != &id)
}
pub fn restart(
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
@ -363,9 +370,11 @@ impl Registry {
None => return Ok(None),
};
match self.inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())),
Entry::Vacant(entry) => {
let scope = language_config.scope.clone();
match self.inner.entry(scope) {
Entry::Vacant(_) => Ok(None),
Entry::Occupied(mut entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
@ -373,83 +382,41 @@ impl Registry {
start_client(id, language_config, config, doc_path)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
entry.insert((id, client.clone()));
let (_, old_client) = entry.insert((id, client.clone()));
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
Ok(Some(client))
}
}
}
pub fn restart(
&mut self,
language_config: &LanguageConfiguration,
path: Option<&PathBuf>,
) -> Result<Arc<Client>> {
let config = language_config
.language_server
.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, path)?;
let (_, client) = self
.inner
.get_mut(&language_config.scope)
.ok_or(Error::LspNotDefined)?;
*client = new_client;
Ok(client.clone())
}
fn initialize_client(
pub fn get(
&mut self,
language_config: &LanguageConfiguration,
config: &helix_core::syntax::LanguageServerConfiguration,
id: usize,
path: Option<&PathBuf>,
) -> Result<Arc<Client>> {
let (client, incoming, initialize_notify) = Client::start(
&config.command,
&config.args,
language_config.config.clone(),
&language_config.roots,
id,
config.timeout,
path,
)?;
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;
}
doc_path: Option<&std::path::PathBuf>,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
None => return Ok(None),
};
// next up, notify<initialized>
_client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await
.unwrap();
match self.inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())),
Entry::Vacant(entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
initialize_notify.notify_one();
});
let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(client)
entry.insert((id, client.clone()));
Ok(Some(client))
}
}
}
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {

@ -0,0 +1,696 @@
mod client;
pub mod jsonrpc;
mod transport;
pub use client::Client;
pub use futures_executor::block_on;
pub use jsonrpc::Call;
pub use lsp::{Position, Url};
pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration};
use tokio::sync::mpsc::UnboundedReceiver;
use std::{
collections::{hash_map::Entry, HashMap},
path::PathBuf,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream;
pub type Result<T> = core::result::Result<T, Error>;
type LanguageId = String;
#[derive(Error, Debug)]
pub enum Error {
#[error("protocol error: {0}")]
Rpc(#[from] jsonrpc::Error),
#[error("failed to parse: {0}")]
Parse(#[from] serde_json::Error),
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
#[error("request timed out")]
Timeout,
#[error("server closed the stream")]
StreamClosed,
#[error("LPS not defined")]
LspNotDefined,
#[error("Unhandled")]
Unhandled,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum OffsetEncoding {
/// UTF-8 code units aka bytes
#[serde(rename = "utf-8")]
Utf8,
/// UTF-16 code units
#[serde(rename = "utf-16")]
Utf16,
}
pub mod util {
use super::*;
use helix_core::{diagnostic::NumberOrString, Range, Rope, Transaction};
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
///
/// Panics when [`pos_to_lsp_pos`] would for an invalid range on the diagnostic.
pub fn diagnostic_to_lsp_diagnostic(
doc: &Rope,
diag: &helix_core::diagnostic::Diagnostic,
offset_encoding: OffsetEncoding,
) -> lsp::Diagnostic {
use helix_core::diagnostic::Severity::*;
let range = Range::new(diag.range.start, diag.range.end);
let severity = diag.severity.map(|s| match s {
Hint => lsp::DiagnosticSeverity::HINT,
Info => lsp::DiagnosticSeverity::INFORMATION,
Warning => lsp::DiagnosticSeverity::WARNING,
Error => lsp::DiagnosticSeverity::ERROR,
});
let code = match diag.code.clone() {
Some(x) => match x {
NumberOrString::Number(x) => Some(lsp::NumberOrString::Number(x)),
NumberOrString::String(x) => Some(lsp::NumberOrString::String(x)),
},
None => None,
};
let new_tags: Vec<_> = diag
.tags
.iter()
.map(|tag| match tag {
helix_core::diagnostic::DiagnosticTag::Unnecessary => {
lsp::DiagnosticTag::UNNECESSARY
}
helix_core::diagnostic::DiagnosticTag::Deprecated => lsp::DiagnosticTag::DEPRECATED,
})
.collect();
let tags = if !new_tags.is_empty() {
Some(new_tags)
} else {
None
};
// TODO: add support for Diagnostic.data
lsp::Diagnostic::new(
range_to_lsp_range(doc, range, offset_encoding),
severity,
code,
diag.source.clone(),
diag.message.to_owned(),
None,
tags,
)
}
/// Converts [`lsp::Position`] to a position in the document.
///
/// Returns `None` if position exceeds document length or an operation overflows.
pub fn lsp_pos_to_pos(
doc: &Rope,
pos: lsp::Position,
offset_encoding: OffsetEncoding,
) -> Option<usize> {
let pos_line = pos.line as usize;
if pos_line > doc.len_lines() - 1 {
return None;
}
match offset_encoding {
OffsetEncoding::Utf8 => {
let line = doc.line_to_char(pos_line);
let pos = line.checked_add(pos.character as usize)?;
if pos <= doc.len_chars() {
Some(pos)
} else {
None
}
}
OffsetEncoding::Utf16 => {
let line = doc.line_to_char(pos_line);
let line_start = doc.char_to_utf16_cu(line);
let pos = line_start.checked_add(pos.character as usize)?;
doc.try_utf16_cu_to_char(pos).ok()
}
}
}
/// Converts position in the document to [`lsp::Position`].
///
/// Panics when `pos` is out of `doc` bounds or operation overflows.
pub fn pos_to_lsp_pos(
doc: &Rope,
pos: usize,
offset_encoding: OffsetEncoding,
) -> lsp::Position {
match offset_encoding {
OffsetEncoding::Utf8 => {
let line = doc.char_to_line(pos);
let line_start = doc.line_to_char(line);
let col = pos - line_start;
lsp::Position::new(line as u32, col as u32)
}
OffsetEncoding::Utf16 => {
let line = doc.char_to_line(pos);
let line_start = doc.char_to_utf16_cu(doc.line_to_char(line));
let col = doc.char_to_utf16_cu(pos) - line_start;
lsp::Position::new(line as u32, col as u32)
}
}
}
/// Converts a range in the document to [`lsp::Range`].
pub fn range_to_lsp_range(
doc: &Rope,
range: Range,
offset_encoding: OffsetEncoding,
) -> lsp::Range {
let start = pos_to_lsp_pos(doc, range.from(), offset_encoding);
let end = pos_to_lsp_pos(doc, range.to(), offset_encoding);
lsp::Range::new(start, end)
}
pub fn lsp_range_to_range(
doc: &Rope,
range: lsp::Range,
offset_encoding: OffsetEncoding,
) -> Option<Range> {
let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?;
let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?;
Some(Range::new(start, end))
}
pub fn generate_transaction_from_edits(
doc: &Rope,
mut edits: Vec<lsp::TextEdit>,
offset_encoding: OffsetEncoding,
) -> Transaction {
// Sort edits by start range, since some LSPs (Omnisharp) send them
// in reverse order.
edits.sort_unstable_by_key(|edit| edit.range.start);
// Generate a diff if the edit is a full document replacement.
#[allow(clippy::collapsible_if)]
if edits.len() == 1 {
let is_document_replacement = edits.first().and_then(|edit| {
let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding)?;
let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding)?;
Some(start..end)
}) == Some(0..doc.len_chars());
if is_document_replacement {
let new_text = Rope::from(edits.pop().unwrap().new_text);
return helix_core::diff::compare_ropes(doc, &new_text);
}
}
Transaction::change(
doc,
edits.into_iter().map(|edit| {
// simplify "" into None for cleaner changesets
let replacement = if !edit.new_text.is_empty() {
Some(edit.new_text.into())
} else {
None
};
let start =
if let Some(start) = lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
start
} else {
return (0, 0, None);
};
let end = if let Some(end) = lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
end
} else {
return (0, 0, None);
};
(start, end, replacement)
}),
)
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum MethodCall {
WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams),
ApplyWorkspaceEdit(lsp::ApplyWorkspaceEditParams),
WorkspaceFolders,
WorkspaceConfiguration(lsp::ConfigurationParams),
}
impl MethodCall {
pub fn parse(method: &str, params: jsonrpc::Params) -> Result<MethodCall> {
use lsp::request::Request;
let request = match method {
lsp::request::WorkDoneProgressCreate::METHOD => {
let params: lsp::WorkDoneProgressCreateParams = params.parse()?;
Self::WorkDoneProgressCreate(params)
}
lsp::request::ApplyWorkspaceEdit::METHOD => {
let params: lsp::ApplyWorkspaceEditParams = params.parse()?;
Self::ApplyWorkspaceEdit(params)
}
lsp::request::WorkspaceFoldersRequest::METHOD => Self::WorkspaceFolders,
lsp::request::WorkspaceConfiguration::METHOD => {
let params: lsp::ConfigurationParams = params.parse()?;
Self::WorkspaceConfiguration(params)
}
_ => {
return Err(Error::Unhandled);
}
};
Ok(request)
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum Notification {
// we inject this notification to signal the LSP is ready
Initialized,
// and this notification to signal that the LSP exited
Exit,
PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams),
ProgressMessage(lsp::ProgressParams),
}
impl Notification {
pub fn parse(method: &str, params: jsonrpc::Params) -> Result<Notification> {
use lsp::notification::Notification as _;
let notification = match method {
lsp::notification::Initialized::METHOD => Self::Initialized,
lsp::notification::Exit::METHOD => Self::Exit,
lsp::notification::PublishDiagnostics::METHOD => {
let params: lsp::PublishDiagnosticsParams = params.parse()?;
Self::PublishDiagnostics(params)
}
lsp::notification::ShowMessage::METHOD => {
let params: lsp::ShowMessageParams = params.parse()?;
Self::ShowMessage(params)
}
lsp::notification::LogMessage::METHOD => {
let params: lsp::LogMessageParams = params.parse()?;
Self::LogMessage(params)
}
lsp::notification::Progress::METHOD => {
let params: lsp::ProgressParams = params.parse()?;
Self::ProgressMessage(params)
}
_ => {
return Err(Error::Unhandled);
}
};
Ok(notification)
}
}
#[derive(Debug)]
pub struct Registry {
inner: HashMap<LanguageId, (usize, Arc<Client>)>,
counter: AtomicUsize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
impl Registry {
pub fn new() -> Self {
Self {
inner: HashMap::new(),
counter: AtomicUsize::new(0),
incoming: SelectAll::new(),
}
}
pub fn get_by_id(&self, id: usize) -> Option<&Client> {
self.inner
.values()
.find(|(client_id, _)| client_id == &id)
.map(|(_, client)| client.as_ref())
}
<<<<<<< HEAD
||||||| 4ec2a21c
pub fn restart(
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
None => return Ok(None),
};
let scope = language_config.scope.clone();
match self.inner.entry(scope) {
Entry::Vacant(_) => Ok(None),
Entry::Occupied(mut entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
let (_, old_client) = entry.insert((id, client.clone()));
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
Ok(Some(client))
}
}
}
=======
pub fn remove_by_id(&mut self, id: usize) {
self.inner.retain(|_, (client_id, _)| client_id != &id)
}
pub fn restart(
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
None => return Ok(None),
};
let scope = language_config.scope.clone();
match self.inner.entry(scope) {
Entry::Vacant(_) => Ok(None),
Entry::Occupied(mut entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
let (_, old_client) = entry.insert((id, client.clone()));
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
Ok(Some(client))
}
}
}
>>>>>>> master
pub fn get(
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
None => return Ok(None),
};
match self.inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())),
Entry::Vacant(entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
entry.insert((id, client.clone()));
Ok(Some(client))
}
}
}
pub fn restart(
&mut self,
language_config: &LanguageConfiguration,
path: Option<&PathBuf>,
) -> Result<Arc<Client>> {
let config = language_config
.language_server
.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, path)?;
let (_, client) = self
.inner
.get_mut(&language_config.scope)
.ok_or(Error::LspNotDefined)?;
*client = new_client;
Ok(client.clone())
}
fn initialize_client(
&mut self,
language_config: &LanguageConfiguration,
config: &helix_core::syntax::LanguageServerConfiguration,
id: usize,
path: Option<&PathBuf>,
) -> Result<Arc<Client>> {
let (client, incoming, initialize_notify) = Client::start(
&config.command,
&config.args,
language_config.config.clone(),
&language_config.roots,
id,
config.timeout,
path,
)?;
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;
}
// next up, notify<initialized>
_client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await
.unwrap();
initialize_notify.notify_one();
});
Ok(client)
}
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().map(|(_, client)| client)
}
}
#[derive(Debug)]
pub enum ProgressStatus {
Created,
Started(lsp::WorkDoneProgress),
}
impl ProgressStatus {
pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
match &self {
ProgressStatus::Created => None,
ProgressStatus::Started(progress) => Some(progress),
}
}
}
#[derive(Default, Debug)]
/// Acts as a container for progress reported by language servers. Each server
/// has a unique id assigned at creation through [`Registry`]. This id is then used
/// to store the progress in this map.
pub struct LspProgressMap(HashMap<usize, HashMap<lsp::ProgressToken, ProgressStatus>>);
impl LspProgressMap {
pub fn new() -> Self {
Self::default()
}
/// Returns a map of all tokens corresponding to the language server with `id`.
pub fn progress_map(&self, id: usize) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
self.0.get(&id)
}
pub fn is_progressing(&self, id: usize) -> bool {
self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default()
}
/// Returns last progress status for a given server with `id` and `token`.
pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> {
self.0.get(&id).and_then(|values| values.get(token))
}
/// Checks if progress `token` for server with `id` is created.
pub fn is_created(&mut self, id: usize, token: &lsp::ProgressToken) -> bool {
self.0
.get(&id)
.map(|values| values.get(token).is_some())
.unwrap_or_default()
}
pub fn create(&mut self, id: usize, token: lsp::ProgressToken) {
self.0
.entry(id)
.or_default()
.insert(token, ProgressStatus::Created);
}
/// Ends the progress by removing the `token` from server with `id`, if removed returns the value.
pub fn end_progress(
&mut self,
id: usize,
token: &lsp::ProgressToken,
) -> Option<ProgressStatus> {
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
}
/// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`.
pub fn update(
&mut self,
id: usize,
token: lsp::ProgressToken,
status: lsp::WorkDoneProgress,
) -> Option<ProgressStatus> {
self.0
.entry(id)
.or_default()
.insert(token, ProgressStatus::Started(status))
}
}
struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>);
/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
/// it is only called when it makes sense.
fn start_client(
id: usize,
config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>,
) -> Result<NewClientResult> {
let (client, incoming, initialize_notify) = Client::start(
&ls_config.command,
&ls_config.args,
config.config.clone(),
&config.roots,
id,
ls_config.timeout,
doc_path,
)?;
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;
}
// next up, notify<initialized>
_client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await
.unwrap();
initialize_notify.notify_one();
});
Ok(NewClientResult(client, incoming))
}
#[cfg(test)]
mod tests {
use super::{lsp, util::*, OffsetEncoding};
use helix_core::Rope;
#[test]
fn converts_lsp_pos_to_pos() {
macro_rules! test_case {
($doc:expr, ($x:expr, $y:expr) => $want:expr) => {
let doc = Rope::from($doc);
let pos = lsp::Position::new($x, $y);
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf16));
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf8))
};
}
test_case!("", (0, 0) => Some(0));
test_case!("", (0, 1) => None);
test_case!("", (1, 0) => None);
test_case!("\n\n", (0, 0) => Some(0));
test_case!("\n\n", (1, 0) => Some(1));
test_case!("\n\n", (1, 1) => Some(2));
test_case!("\n\n", (2, 0) => Some(2));
test_case!("\n\n", (3, 0) => None);
test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
test_case!("test\n\n\n\ncase", (4, 5) => None);
test_case!("", (u32::MAX, u32::MAX) => None);
}
}

@ -250,6 +250,36 @@ impl Transport {
}
};
}
Err(Error::StreamClosed) => {
// Close any outstanding requests.
for (id, tx) in transport.pending_requests.lock().await.drain() {
match tx.send(Err(Error::StreamClosed)).await {
Ok(_) => (),
Err(_) => {
error!("Could not close request on a closed channel (id={:?})", id)
}
}
}
// Hack: inject a terminated notification so we trigger code that needs to happen after exit
use lsp_types::notification::Notification as _;
let notification =
ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification {
jsonrpc: None,
method: lsp_types::notification::Exit::METHOD.to_string(),
params: jsonrpc::Params::None,
}));
match transport
.process_server_message(&client_tx, notification)
.await
{
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
}
}
break;
}
Err(err) => {
error!("err: <- {:?}", err);
break;

@ -10,11 +10,13 @@ use helix_view::{
align_view,
document::DocumentSavedEventResult,
editor::{ConfigEvent, EditorEvent},
graphics::Rect,
theme,
tree::Layout,
Align, Editor,
};
use serde_json::json;
use tui::backend::Backend;
use crate::{
args::Args,
@ -53,8 +55,21 @@ type Signals = futures_util::stream::Empty<()>;
const LSP_DEADLINE: Duration = Duration::from_millis(16);
#[cfg(not(feature = "integration"))]
use tui::backend::CrosstermBackend;
#[cfg(feature = "integration")]
use tui::backend::TestBackend;
#[cfg(not(feature = "integration"))]
type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
#[cfg(feature = "integration")]
type Terminal = tui::terminal::Terminal<TestBackend>;
pub struct Application {
compositor: Compositor,
terminal: Terminal,
pub editor: Editor,
config: Arc<ArcSwap<Config>>,
@ -143,10 +158,18 @@ impl Application {
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut compositor = Compositor::new().context("build compositor")?;
#[cfg(not(feature = "integration"))]
let backend = CrosstermBackend::new(stdout());
#[cfg(feature = "integration")]
let backend = TestBackend::new(120, 150);
let terminal = Terminal::new(backend)?;
let area = terminal.size().expect("couldn't get terminal size");
let mut compositor = Compositor::new(area);
let config = Arc::new(ArcSwap::from_pointee(config));
let mut editor = Editor::new(
compositor.size(),
area,
theme_loader.clone(),
syn_loader.clone(),
Box::new(Map::new(Arc::clone(&config), |config: &Config| {
@ -245,6 +268,7 @@ impl Application {
let app = Self {
compositor,
terminal,
editor,
config,
@ -266,15 +290,26 @@ impl Application {
#[cfg(not(feature = "integration"))]
fn render(&mut self) {
let compositor = &mut self.compositor;
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
scroll: None,
};
compositor.render(&mut cx);
let area = self
.terminal
.autoresize()
.expect("Unable to determine terminal size");
// TODO: need to recalculate view tree if necessary
let surface = self.terminal.current_buffer_mut();
self.compositor.render(area, surface, &mut cx);
let (pos, kind) = self.compositor.cursor(area, &self.editor);
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos, kind).unwrap();
}
pub async fn event_loop<S>(&mut self, input_stream: &mut S)
@ -404,19 +439,24 @@ impl Application {
#[cfg(not(windows))]
pub async fn handle_signals(&mut self, signal: i32) {
use helix_view::graphics::Rect;
match signal {
signal::SIGTSTP => {
self.compositor.save_cursor();
// restore cursor
use helix_view::graphics::CursorKind;
self.terminal
.backend_mut()
.show_cursor(CursorKind::Block)
.ok();
restore_term().unwrap();
low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
}
signal::SIGCONT => {
self.claim_term().await.unwrap();
// redraw the terminal
let Rect { width, height, .. } = self.compositor.size();
self.compositor.resize(width, height);
self.compositor.load_cursor();
let area = self.terminal.size().expect("couldn't get terminal size");
self.compositor.resize(area);
self.terminal.clear().expect("couldn't clear terminal");
self.render();
}
signal::SIGUSR1 => {
@ -553,7 +593,14 @@ impl Application {
// Handle key events
let should_redraw = match event.unwrap() {
CrosstermEvent::Resize(width, height) => {
self.compositor.resize(width, height);
self.terminal
.resize(Rect::new(0, 0, width, height))
.expect("Unable to resize terminal");
let area = self.terminal.size().expect("couldn't get terminal size");
self.compositor.resize(area);
self.compositor
.handle_event(&Event::Resize(width, height), &mut cx)
}
@ -836,6 +883,32 @@ impl Application {
Notification::ProgressMessage(_params) => {
// do nothing
}
Notification::Exit => {
self.editor.set_status("Language server exited");
// Clear any diagnostics for documents with this server open.
let urls: Vec<_> = self
.editor
.documents_mut()
.filter_map(|doc| {
if doc.language_server().map(|server| server.id())
== Some(server_id)
{
doc.set_diagnostics(Vec::new());
doc.url()
} else {
None
}
})
.collect();
for url in urls {
self.editor.diagnostics.remove(&url);
}
// Remove the language server from the registry.
self.editor.language_servers.remove_by_id(server_id);
}
}
}
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
@ -936,7 +1009,11 @@ impl Application {
}
async fn claim_term(&mut self) -> Result<(), Error> {
use helix_view::graphics::CursorKind;
terminal::enable_raw_mode()?;
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal.backend_mut().hide_cursor().ok();
}
let mut stdout = stdout();
execute!(
stdout,
@ -970,6 +1047,13 @@ impl Application {
self.event_loop(input_stream).await;
let close_errs = self.close().await;
// restore cursor
use helix_view::graphics::CursorKind;
self.terminal
.backend_mut()
.show_cursor(CursorKind::Block)
.ok();
restore_term()?;
for err in close_errs {

@ -54,7 +54,7 @@ use crate::{
use crate::job::{self, Jobs};
use futures_util::StreamExt;
use std::{collections::HashMap, fmt, fmt::Write, future::Future};
use std::{collections::HashMap, fmt, future::Future};
use std::{collections::HashSet, num::NonZeroUsize};
use std::{
@ -210,17 +210,18 @@ impl MappableCommand {
copy_selection_on_prev_line, "Copy selection on previous line",
move_next_word_start, "Move to start of next word",
move_prev_word_start, "Move to start of previous word",
move_prev_word_end, "Move to end of previous word",
move_next_word_end, "Move to end of next word",
move_prev_word_end, "Move to end of previous word",
move_next_long_word_start, "Move to start of next long word",
move_prev_long_word_start, "Move to start of previous long word",
move_next_long_word_end, "Move to end of next long word",
extend_next_word_start, "Extend to start of next word",
extend_prev_word_start, "Extend to start of previous word",
extend_next_word_end, "Extend to end of next word",
extend_prev_word_end, "Extend to end of previous word",
extend_next_long_word_start, "Extend to start of next long word",
extend_prev_long_word_start, "Extend to start of previous long word",
extend_next_long_word_end, "Extend to end of next long word",
extend_next_word_end, "Extend to end of next word",
find_till_char, "Move till next occurrence of char",
find_next_char, "Move to next occurrence of char",
extend_till_char, "Extend till next occurrence of char",
@ -249,6 +250,7 @@ impl MappableCommand {
extend_search_next, "Add next search match to selection",
extend_search_prev, "Add previous search match to selection",
search_selection, "Use current selection as search pattern",
make_search_word_bounded, "Modify current search to make it word bounded",
global_search, "Global search in workspace folder",
extend_line, "Select current line, if already selected, extend to another line based on the anchor",
extend_line_below, "Select current line, if already selected, extend to next line",
@ -311,8 +313,7 @@ impl MappableCommand {
goto_line_end, "Goto line end",
goto_next_buffer, "Goto next buffer",
goto_previous_buffer, "Goto previous buffer",
// TODO: different description ?
goto_line_end_newline, "Goto line end",
goto_line_end_newline, "Goto newline at line end",
goto_first_nonwhitespace, "Goto first non-blank in line",
trim_selections, "Trim whitespace from selections",
extend_to_line_start, "Extend to line start",
@ -783,11 +784,7 @@ fn trim_selections(cx: &mut Context) {
let mut end = range.to();
start = movement::skip_while(text, start, |x| x.is_whitespace()).unwrap_or(start);
end = movement::backwards_skip_while(text, end, |x| x.is_whitespace()).unwrap_or(end);
if range.anchor < range.head {
Some(Range::new(start, end))
} else {
Some(Range::new(end, start))
}
Some(Range::new(start, end).with_direction(range.direction()))
})
.collect();
@ -874,7 +871,7 @@ fn goto_window(cx: &mut Context, align: Align) {
let config = cx.editor.config();
let (view, doc) = current!(cx.editor);
let height = view.inner_area().height as usize;
let height = view.inner_height();
// respect user given count if any
// - 1 so we have at least one gap in the middle.
@ -1097,6 +1094,10 @@ fn extend_next_word_end(cx: &mut Context) {
extend_word_impl(cx, movement::move_next_word_end)
}
fn extend_prev_word_end(cx: &mut Context) {
extend_word_impl(cx, movement::move_prev_word_end)
}
fn extend_next_long_word_start(cx: &mut Context) {
extend_word_impl(cx, movement::move_next_long_word_start)
}
@ -1134,6 +1135,10 @@ where
doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
}
KeyEvent {
code: KeyCode::Tab, ..
} => '\t',
KeyEvent {
code: KeyCode::Char(ch),
..
@ -1280,6 +1285,9 @@ fn replace(cx: &mut Context) {
code: KeyCode::Enter,
..
} => Some(doc.line_ending.as_str()),
KeyEvent {
code: KeyCode::Tab, ..
} => Some("\t"),
_ => None,
};
@ -1376,9 +1384,9 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
return;
}
let height = view.inner_area().height;
let height = view.inner_height();
let scrolloff = config.scrolloff.min(height as usize / 2);
let scrolloff = config.scrolloff.min(height / 2);
view.offset.row = match direction {
Forward => view.offset.row + offset,
@ -1416,25 +1424,25 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
fn page_up(cx: &mut Context) {
let view = view!(cx.editor);
let offset = view.inner_area().height as usize;
let offset = view.inner_height();
scroll(cx, offset, Direction::Backward);
}
fn page_down(cx: &mut Context) {
let view = view!(cx.editor);
let offset = view.inner_area().height as usize;
let offset = view.inner_height();
scroll(cx, offset, Direction::Forward);
}
fn half_page_up(cx: &mut Context) {
let view = view!(cx.editor);
let offset = view.inner_area().height as usize / 2;
let offset = view.inner_height() / 2;
scroll(cx, offset, Direction::Backward);
}
fn half_page_down(cx: &mut Context) {
let view = view!(cx.editor);
let offset = view.inner_area().height as usize / 2;
let offset = view.inner_height() / 2;
scroll(cx, offset, Direction::Forward);
}
@ -1655,11 +1663,7 @@ fn search_impl(
// Determine range direction based on the primary range
let primary = selection.primary();
let range = if primary.head < primary.anchor {
Range::new(end, start)
} else {
Range::new(start, end)
};
let range = Range::new(start, end).with_direction(primary.direction());
let selection = match movement {
Movement::Extend => selection.clone().push(range),
@ -1805,7 +1809,36 @@ fn search_selection(cx: &mut Context) {
.join("|");
let msg = format!("register '{}' set to '{}'", '/', &regex);
cx.editor.registers.get_mut('/').push(regex);
cx.editor.registers.push('/', regex);
cx.editor.set_status(msg);
}
fn make_search_word_bounded(cx: &mut Context) {
let regex = match cx.editor.registers.last('/') {
Some(regex) => regex,
None => return,
};
let start_anchored = regex.starts_with("\\b");
let end_anchored = regex.ends_with("\\b");
if start_anchored && end_anchored {
return;
}
let mut new_regex = String::with_capacity(
regex.len() + if start_anchored { 0 } else { 2 } + if end_anchored { 0 } else { 2 },
);
if !start_anchored {
new_regex.push_str("\\b");
}
new_regex.push_str(regex);
if !end_anchored {
new_regex.push_str("\\b");
}
let msg = format!("register '{}' set to '{}'", '/', &new_regex);
cx.editor.registers.push('/', new_regex);
cx.editor.set_status(msg);
}
@ -1976,7 +2009,7 @@ fn global_search(cx: &mut Context) {
align_view(doc, view, Align::Center);
},
|_editor, FileResult { path, line_num }| {
Some((path.clone(), Some((*line_num, *line_num))))
Some((path.clone().into(), Some((*line_num, *line_num))))
},
);
compositor.push(Box::new(overlayed(picker)));
@ -2063,11 +2096,7 @@ fn extend_to_line_bounds(cx: &mut Context) {
let start = text.line_to_char(start_line);
let end = text.line_to_char((end_line + 1).min(text.len_lines()));
if range.anchor <= range.head {
Range::new(start, end)
} else {
Range::new(end, start)
}
Range::new(start, end).with_direction(range.direction())
}),
);
}
@ -2104,11 +2133,7 @@ fn shrink_to_line_bounds(cx: &mut Context) {
end = text.line_to_char(end_line);
}
if range.anchor <= range.head {
Range::new(start, end)
} else {
Range::new(end, start)
}
Range::new(start, end).with_direction(range.direction())
}),
);
}
@ -2121,16 +2146,14 @@ enum Operation {
fn delete_selection_impl(cx: &mut Context, op: Operation) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
if cx.register != Some('_') {
// first yank the selection
let text = doc.text().slice(..);
let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
let reg_name = cx.register.unwrap_or('"');
let registers = &mut cx.editor.registers;
let reg = registers.get_mut(reg_name);
reg.write(values);
cx.editor.registers.write(reg_name, values);
};
// then delete
@ -2378,7 +2401,7 @@ fn buffer_picker(cx: &mut Context) {
.selection(view_id)
.primary()
.cursor_line(doc.text().slice(..));
Some((meta.path.clone()?, Some((line, line))))
Some((meta.id.into(), Some((line, line))))
},
);
cx.push_layer(Box::new(overlayed(picker)));
@ -2445,7 +2468,6 @@ fn jumplist_picker(cx: &mut Context) {
.views()
.flat_map(|(view, _)| {
view.jumps
.get()
.iter()
.map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone()))
})
@ -2459,7 +2481,7 @@ fn jumplist_picker(cx: &mut Context) {
|editor, meta| {
let doc = &editor.documents.get(&meta.id)?;
let line = meta.selection.primary().cursor_line(doc.text().slice(..));
Some((meta.path.clone()?, Some((line, line))))
Some((meta.path.clone()?.into(), Some((line, line))))
},
);
cx.push_layer(Box::new(overlayed(picker)));
@ -2472,13 +2494,11 @@ impl ui::menu::Item for MappableCommand {
let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
bindings.iter().fold(String::new(), |mut acc, bind| {
if !acc.is_empty() {
acc.push_str(", ");
acc.push(' ');
}
for key in bind {
acc.push_str(&key.key_sequence_format());
}
bind.iter().fold(false, |needs_plus, key| {
write!(&mut acc, "{}{}", if needs_plus { "+" } else { "" }, key)
.expect("Writing to a string can only fail on an Out-Of-Memory error");
true
});
acc
})
};
@ -2762,15 +2782,15 @@ fn goto_line(cx: &mut Context) {
fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
if let Some(count) = count {
let (view, doc) = current!(editor);
let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
let text = doc.text().slice(..);
let max_line = if text.line(text.len_lines() - 1).len_chars() == 0 {
// If the last line is blank, don't jump to it.
doc.text().len_lines().saturating_sub(2)
text.len_lines().saturating_sub(2)
} else {
doc.text().len_lines() - 1
text.len_lines() - 1
};
let line_idx = std::cmp::min(count.get() - 1, max_line);
let text = doc.text().slice(..);
let pos = doc.text().line_to_char(line_idx);
let pos = text.line_to_char(line_idx);
let selection = doc
.selection(view.id)
.clone()
@ -2783,14 +2803,14 @@ fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
fn goto_last_line(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let line_idx = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
let text = doc.text().slice(..);
let line_idx = if text.line(text.len_lines() - 1).len_chars() == 0 {
// If the last line is blank, don't jump to it.
doc.text().len_lines().saturating_sub(2)
text.len_lines().saturating_sub(2)
} else {
doc.text().len_lines() - 1
text.len_lines() - 1
};
let text = doc.text().slice(..);
let pos = doc.text().line_to_char(line_idx);
let pos = text.line_to_char(line_idx);
let selection = doc
.selection(view.id)
.clone()
@ -2964,22 +2984,22 @@ pub mod insert {
use helix_core::chars::char_is_word;
let mut iter = text.chars_at(cursor);
iter.reverse();
for _ in 0..config.completion_trigger_len {
match iter.next() {
Some(c) if char_is_word(c) => {}
Some(c) if config.completion_trigger_chars.contains(&c) => {}
_ => return,
}
}
super::completion(cx);
}
pub fn is_server_trigger_char(doc: &Document, ch: char) -> bool {
fn language_server_completion(cx: &mut Context, ch: char) {
use helix_lsp::lsp;
// if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return false,
None => return,
};
let capabilities = language_server.capabilities();
@ -2989,35 +3009,11 @@ pub mod insert {
..
}) = &capabilities.completion_provider
{
triggers.iter().any(|trigger| trigger.contains(ch))
} else {
false
}
}
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;
}
// TODO: what if trigger is multiple chars long
if triggers.iter().any(|trigger| trigger.contains(ch)) {
cx.editor.clear_idle_timer();
super::completion(cx);
}
cx.editor.reset_idle_timer();
return;
}
if is_server_trigger_char(doc, ch) {
cx.editor.reset_idle_timer_zero();
}
}
@ -3130,40 +3126,59 @@ pub mod insert {
let curr = contents.get_char(pos).unwrap_or(' ');
let current_line = text.char_to_line(pos);
let indent = indent::indent_for_newline(
doc.language_config(),
doc.syntax(),
&doc.indent_style,
doc.tab_width(),
text,
current_line,
pos,
current_line,
);
let mut text = String::new();
// If we are between pairs (such as brackets), we want to
// insert an additional line which is indented one level
// more and place the cursor there
let on_auto_pair = doc
.auto_pairs(cx.editor)
.and_then(|pairs| pairs.get(prev))
.and_then(|pair| if pair.close == curr { Some(pair) } else { None })
.is_some();
let local_offs = if on_auto_pair {
let inner_indent = indent.clone() + doc.indent_style.as_str();
text.reserve_exact(2 + indent.len() + inner_indent.len());
text.push_str(doc.line_ending.as_str());
text.push_str(&inner_indent);
let local_offs = text.chars().count();
text.push_str(doc.line_ending.as_str());
text.push_str(&indent);
local_offs
let line_is_only_whitespace = text
.line(current_line)
.chars()
.all(|char| char.is_ascii_whitespace());
let mut new_text = String::new();
// If the current line is all whitespace, insert a line ending at the beginning of
// the current line. This makes the current line empty and the new line contain the
// indentation of the old line.
let (from, to, local_offs) = if line_is_only_whitespace {
let line_start = text.line_to_char(current_line);
new_text.push_str(doc.line_ending.as_str());
(line_start, line_start, new_text.chars().count())
} else {
text.reserve_exact(1 + indent.len());
text.push_str(doc.line_ending.as_str());
text.push_str(&indent);
text.chars().count()
let indent = indent::indent_for_newline(
doc.language_config(),
doc.syntax(),
&doc.indent_style,
doc.tab_width(),
text,
current_line,
pos,
current_line,
);
// If we are between pairs (such as brackets), we want to
// insert an additional line which is indented one level
// more and place the cursor there
let on_auto_pair = doc
.auto_pairs(cx.editor)
.and_then(|pairs| pairs.get(prev))
.and_then(|pair| if pair.close == curr { Some(pair) } else { None })
.is_some();
let local_offs = if on_auto_pair {
let inner_indent = indent.clone() + doc.indent_style.as_str();
new_text.reserve_exact(2 + indent.len() + inner_indent.len());
new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&inner_indent);
let local_offs = new_text.chars().count();
new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent);
local_offs
} else {
new_text.reserve_exact(1 + indent.len());
new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent);
new_text.chars().count()
};
(pos, pos, local_offs)
};
let new_range = if doc.restore_cursor {
@ -3184,9 +3199,9 @@ pub mod insert {
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
// can be used with cx.mode to do replace or extend on most changes
ranges.push(new_range);
global_offs += text.chars().count();
global_offs += new_text.chars().count();
(pos, pos, Some(text.into()))
(from, to, Some(new_text.into()))
});
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
@ -3347,7 +3362,7 @@ fn undo(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
for _ in 0..count {
if !doc.undo(view) {
if !doc.undo(view.id) {
cx.editor.set_status("Already at oldest change");
break;
}
@ -3358,7 +3373,7 @@ fn redo(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
for _ in 0..count {
if !doc.redo(view) {
if !doc.redo(view.id) {
cx.editor.set_status("Already at newest change");
break;
}
@ -3370,7 +3385,7 @@ fn earlier(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
for _ in 0..count {
// rather than doing in batch we do this so get error halfway
if !doc.earlier(view, UndoKind::Steps(1)) {
if !doc.earlier(view.id, UndoKind::Steps(1)) {
cx.editor.set_status("Already at oldest change");
break;
}
@ -3382,7 +3397,7 @@ fn later(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
for _ in 0..count {
// rather than doing in batch we do this so get error halfway
if !doc.later(view, UndoKind::Steps(1)) {
if !doc.later(view.id, UndoKind::Steps(1)) {
cx.editor.set_status("Already at newest change");
break;
}
@ -3511,7 +3526,14 @@ enum Paste {
Cursor,
}
fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Paste, count: usize) {
fn paste_impl(
values: &[String],
doc: &mut Document,
view: &mut View,
action: Paste,
count: usize,
mode: Mode,
) {
if values.is_empty() {
return;
}
@ -3530,7 +3552,6 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa
.any(|value| get_line_ending_of_str(value).is_some());
// Only compiled once.
#[allow(clippy::trivial_regex)]
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap());
let mut values = values
.iter()
@ -3541,9 +3562,10 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa
let text = doc.text();
let selection = doc.selection(view.id);
let mut offset = 0;
let mut ranges = SmallVec::with_capacity(selection.len());
let transaction = Transaction::change_by_selection(text, selection, |range| {
let mut transaction = Transaction::change_by_selection(text, selection, |range| {
let pos = match (action, linewise) {
// paste linewise before
(Paste::Before, true) => text.line_to_char(text.char_to_line(range.from())),
@ -3566,13 +3588,18 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa
.as_ref()
.map(|content| content.chars().count())
.unwrap_or_default();
let anchor = offset + pos;
ranges.push(Range::new(pos, pos + value_len));
let new_range = Range::new(anchor, anchor + value_len).with_direction(range.direction());
ranges.push(new_range);
offset += value_len;
(pos, pos, value)
});
let transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
if mode == Mode::Normal {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
}
apply_transaction(&transaction, doc, view);
}
@ -3584,7 +3611,7 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
Mode::Normal => Paste::Before,
};
let (view, doc) = current!(cx.editor);
paste_impl(&[contents], doc, view, paste, count);
paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
}
fn paste_clipboard_impl(
@ -3596,7 +3623,7 @@ fn paste_clipboard_impl(
let (view, doc) = current!(editor);
match editor.clipboard_provider.get_contents(clipboard_type) {
Ok(contents) => {
paste_impl(&[contents], doc, view, action, count);
paste_impl(&[contents], doc, view, action, count, editor.mode);
Ok(())
}
Err(e) => Err(e.context("Couldn't get system clipboard contents")),
@ -3715,7 +3742,7 @@ fn paste(cx: &mut Context, pos: Paste) {
let registers = &mut cx.editor.registers;
if let Some(values) = registers.read(reg_name) {
paste_impl(values, doc, view, pos, count);
paste_impl(values, doc, view, pos, count, cx.editor.mode);
}
}
@ -3810,7 +3837,7 @@ fn format_selections(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
// via lsp if available
// else via tree-sitter indentation calculations
// TODO: else via tree-sitter indentation calculations
let language_server = match doc.language_server() {
Some(language_server) => language_server,
@ -3823,36 +3850,43 @@ fn format_selections(cx: &mut Context) {
.map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding()))
.collect();
// TODO: all of the TODO's and commented code inside the loop,
// to make this actually work.
for _range in ranges {
let _language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
};
// TODO: handle fails
// TODO: concurrent map
if ranges.len() != 1 {
cx.editor
.set_error("format_selections only supports a single selection for now");
return;
}
// TODO: need to block to get the formatting
// TODO: handle fails
// TODO: concurrent map over all ranges
// let edits = block_on(language_server.text_document_range_formatting(
// doc.identifier(),
// range,
// lsp::FormattingOptions::default(),
// ))
// .unwrap_or_default();
let range = ranges[0];
// let transaction = helix_lsp::util::generate_transaction_from_edits(
// doc.text(),
// edits,
// language_server.offset_encoding(),
// );
let request = match language_server.text_document_range_formatting(
doc.identifier(),
range,
lsp::FormattingOptions::default(),
None,
) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support range formatting");
return;
}
};
// apply_transaction(&transaction, doc, view);
}
let edits = tokio::task::block_in_place(|| helix_lsp::block_on(request)).unwrap_or_default();
let transaction = helix_lsp::util::generate_transaction_from_edits(
doc.text(),
edits,
language_server.offset_encoding(),
);
apply_transaction(&transaction, doc, view);
}
fn join_selections_inner(cx: &mut Context, select_space: bool) {
fn join_selections_impl(cx: &mut Context, select_space: bool) {
use movement::skip_while;
let (view, doc) = current!(cx.editor);
let text = doc.text();
@ -3931,11 +3965,11 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
}
fn join_selections(cx: &mut Context) {
join_selections_inner(cx, false)
join_selections_impl(cx, false)
}
fn join_selections_space(cx: &mut Context) {
join_selections_inner(cx, true)
join_selections_impl(cx, true)
}
fn keep_selections(cx: &mut Context) {
@ -3985,13 +4019,9 @@ pub fn completion(cx: &mut Context) {
let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
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 future = match language_server.completion(doc.identifier(), pos, None) {
Some(future) => future,
None => return,
};
let trigger_offset = cursor;
@ -4004,56 +4034,29 @@ pub fn completion(cx: &mut Context) {
iter.reverse();
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
let start_offset = cursor.saturating_sub(offset);
let prefix = text.slice(start_offset..cursor).to_string();
doc.savepoint();
let trigger_version = doc.version();
cx.callback(
future,
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 {
return;
}
if savepoint.0 != trigger_version {
doc.savepoint = Some(savepoint);
// we're not in insert mode anymore
return;
}
let mut items = match response {
let items = match response {
Some(lsp::CompletionResponse::Array(items)) => items,
// TODO: do something with is_incomplete
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: _is_incomplete,
items,
})) => items,
None => {
editor.set_status(
"The completion response is none and will request server again",
);
editor.reset_idle_timer();
return;
}
None => Vec::new(),
};
if !prefix.is_empty() {
items.retain(|item| {
item.filter_text
.as_ref()
.unwrap_or(&item.label)
.starts_with(&prefix)
});
}
if items.is_empty() {
// editor.set_error("No completion available".to_string());
// editor.set_error("No completion available");
return;
}
doc.savepoint = Some(savepoint);
let size = compositor.size();
let ui = compositor.find::<ui::EditorView>().unwrap();
ui.set_completion(
@ -4428,7 +4431,7 @@ fn align_view_middle(cx: &mut Context) {
view.offset.col = pos
.col
.saturating_sub((view.inner_area().width as usize) / 2);
.saturating_sub((view.inner_area(doc).width as usize) / 2);
}
fn scroll_up(cx: &mut Context) {
@ -4611,8 +4614,13 @@ fn surround_add(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id);
let (open, close) = surround::get_pair(ch);
// The number of chars in get_pair
let surround_len = 2;
let mut changes = Vec::with_capacity(selection.len() * 2);
let mut ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
for range in selection.iter() {
let mut o = Tendril::new();
o.push(open);
@ -4620,10 +4628,21 @@ fn surround_add(cx: &mut Context) {
c.push(close);
changes.push((range.from(), range.from(), Some(o)));
changes.push((range.to(), range.to(), Some(c)));
// Add 2 characters to the range to select them
ranges.push(
Range::new(offs + range.from(), offs + range.to() + surround_len)
.with_direction(range.direction()),
);
// Add 2 characters to the offset for the next ranges
offs += surround_len;
}
let transaction = Transaction::change(doc.text(), changes.into_iter());
let transaction = Transaction::change(doc.text(), changes.into_iter())
.with_selection(Selection::new(ranges, selection.primary_index()));
apply_transaction(&transaction, doc, view);
exit_select_mode(cx);
})
}
@ -4663,6 +4682,7 @@ fn surround_replace(cx: &mut Context) {
}),
);
apply_transaction(&transaction, doc, view);
exit_select_mode(cx);
});
})
}
@ -4690,6 +4710,7 @@ fn surround_delete(cx: &mut Context) {
let transaction =
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
apply_transaction(&transaction, doc, view);
exit_select_mode(cx);
})
}
@ -4854,13 +4875,24 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
let mut ranges = SmallVec::with_capacity(selection.len());
let text = doc.text().slice(..);
let mut shell_output: Option<Tendril> = None;
let mut offset = 0isize;
for range in selection.ranges() {
let fragment = range.slice(text);
let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment.into())) {
Ok(result) => result,
Err(err) => {
cx.editor.set_error(err.to_string());
return;
let (output, success) = if let Some(output) = shell_output.as_ref() {
(output.clone(), true)
} else {
let fragment = range.slice(text);
match shell_impl(shell, cmd, pipe.then(|| fragment.into())) {
Ok(result) => {
if !pipe {
shell_output = Some(result.0.clone());
}
result
}
Err(err) => {
cx.editor.set_error(err.to_string());
return;
}
}
};
@ -4869,13 +4901,23 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
return;
}
let (from, to) = match behavior {
ShellBehavior::Replace => (range.from(), range.to()),
ShellBehavior::Insert => (range.from(), range.from()),
ShellBehavior::Append => (range.to(), range.to()),
_ => (range.from(), range.from()),
let output_len = output.chars().count();
let (from, to, deleted_len) = match behavior {
ShellBehavior::Replace => (range.from(), range.to(), range.len()),
ShellBehavior::Insert => (range.from(), range.from(), 0),
ShellBehavior::Append => (range.to(), range.to(), 0),
_ => (range.from(), range.from(), 0),
};
ranges.push(Range::new(to, to + output.chars().count()));
// These `usize`s cannot underflow because selection ranges cannot overlap.
// Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked
// arithmetic to assert this.
let anchor = (to as isize + offset - deleted_len as isize) as usize;
let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
ranges.push(new_range);
offset = offset + output_len as isize - deleted_len as isize;
changes.push((from, to, Some(output)));
}
@ -4948,14 +4990,18 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
apply_transaction(&transaction, doc, view);
}
enum IncrementDirection {
Increase,
Decrease,
}
/// Increment object under cursor by count.
fn increment(cx: &mut Context) {
increment_impl(cx, cx.count() as i64);
increment_impl(cx, IncrementDirection::Increase);
}
/// Decrement object under cursor by count.
fn decrement(cx: &mut Context) {
increment_impl(cx, -(cx.count() as i64));
increment_impl(cx, IncrementDirection::Decrease);
}
/// This function differs from find_next_char_impl in that it stops searching at the newline, but also
@ -4979,7 +5025,7 @@ fn find_next_char_until_newline<M: CharMatcher>(
}
/// Decrement object under cursor by `amount`.
fn increment_impl(cx: &mut Context, amount: i64) {
fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) {
// TODO: when incrementing or decrementing a number that gets a new digit or lose one, the
// selection is updated improperly.
find_char_impl(
@ -4991,6 +5037,17 @@ fn increment_impl(cx: &mut Context, amount: i64) {
1,
);
// Increase by 1 if `IncrementDirection` is `Increase`
// Decrease by 1 if `IncrementDirection` is `Decrease`
let sign = match increment_direction {
IncrementDirection::Increase => 1,
IncrementDirection::Decrease => -1,
};
let mut amount = sign * cx.count() as i64;
// If the register is `#` then increase or decrease the `amount` by 1 per element
let increase_by = if cx.register == Some('#') { sign } else { 0 };
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id);
let text = doc.text().slice(..);
@ -5010,6 +5067,8 @@ fn increment_impl(cx: &mut Context, amount: i64) {
let (range, new_text) = incrementor.increment(amount);
amount += increase_by;
Some((range.from(), range.to(), Some(new_text)))
})
.collect();
@ -5026,16 +5085,20 @@ fn increment_impl(cx: &mut Context, amount: i64) {
overlapping_indexes.insert(i + 1);
}
}
let changes = changes.into_iter().enumerate().filter_map(|(i, change)| {
if overlapping_indexes.contains(&i) {
None
} else {
Some(change)
}
});
let changes: Vec<_> = changes
.into_iter()
.enumerate()
.filter_map(|(i, change)| {
if overlapping_indexes.contains(&i) {
None
} else {
Some(change)
}
})
.collect();
if changes.clone().count() > 0 {
let transaction = Transaction::change(doc.text(), changes);
if !changes.is_empty() {
let transaction = Transaction::change(doc.text(), changes.into_iter());
let transaction = transaction.with_selection(selection.clone());
apply_transaction(&transaction, doc, view);
@ -5057,7 +5120,7 @@ fn record_macro(cx: &mut Context) {
}
})
.collect::<String>();
cx.editor.registers.get_mut(reg).write(vec![s]);
cx.editor.registers.write(reg, vec![s]);
cx.editor
.set_status(format!("Recorded to register [{}]", reg));
} else {

File diff suppressed because it is too large Load Diff

@ -85,7 +85,7 @@ fn thread_picker(
frame.line.saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
));
Some((path, pos))
Some((path.into(), pos))
},
);
compositor.push(Box::new(picker));
@ -706,7 +706,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
.and_then(|source| source.path.clone())
.map(|path| {
(
path,
path.into(),
Some((
frame.line.saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1),

@ -156,7 +156,7 @@ fn location_to_file_location(location: &lsp::Location) -> FileLocation {
location.range.start.line as usize,
location.range.end.line as usize,
));
(path, line)
(path.into(), line)
}
// TODO: share with symbol picker(symbol.location)
@ -333,7 +333,14 @@ pub fn symbol_picker(cx: &mut Context) {
let current_url = doc.url();
let offset_encoding = language_server.offset_encoding();
let future = language_server.document_symbols(doc.identifier());
let future = match language_server.document_symbols(doc.identifier()) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support document symbols");
return;
}
};
cx.callback(
future,
@ -365,7 +372,14 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
let current_url = doc.url();
let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding();
let future = language_server.workspace_symbols("".to_string());
let future = match language_server.workspace_symbols("".to_string()) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support workspace symbols");
return;
}
};
cx.callback(
future,
@ -493,7 +507,7 @@ pub fn code_action(cx: &mut Context) {
let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
let future = language_server.code_actions(
let future = match language_server.code_actions(
doc.identifier(),
range,
// Filter and convert overlapping diagnostics
@ -509,7 +523,14 @@ pub fn code_action(cx: &mut Context) {
.collect(),
only: None,
},
);
) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support code actions");
return;
}
};
cx.callback(
future,
@ -567,35 +588,34 @@ pub fn code_action(cx: &mut Context) {
.reverse()
});
let mut picker =
ui::Menu::new(actions, false, (), move |editor, code_action, event| {
if event != PromptEvent::Validate {
return;
}
let mut picker = ui::Menu::new(actions, true, (), move |editor, code_action, event| {
if event != PromptEvent::Validate {
return;
}
// always present here
let code_action = code_action.unwrap();
// always present here
let code_action = code_action.unwrap();
match code_action {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
execute_lsp_command(editor, command.clone());
match code_action {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
execute_lsp_command(editor, command.clone());
}
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
if let Some(ref workspace_edit) = code_action.edit {
log::debug!("edit: {:?}", workspace_edit);
apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
if let Some(ref workspace_edit) = code_action.edit {
log::debug!("edit: {:?}", workspace_edit);
apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
// if code action provides both edit and command first the edit
// should be applied and then the command
if let Some(command) = &code_action.command {
execute_lsp_command(editor, command.clone());
}
// if code action provides both edit and command first the edit
// should be applied and then the command
if let Some(command) = &code_action.command {
execute_lsp_command(editor, command.clone());
}
}
});
}
});
picker.move_down(); // pre-select the first item
let popup = Popup::new("code-action", picker);
@ -645,9 +665,16 @@ pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
// the command is executed on the server and communicated back
// to the client asynchronously using workspace edits
let command_future = language_server.command(cmd);
let future = match language_server.command(cmd) {
Some(future) => future,
None => {
editor.set_error("Language server does not support executing commands");
return;
}
};
tokio::spawn(async move {
let res = command_future.await;
let res = future.await;
if let Err(e) = res {
log::error!("execute LSP command: {}", e);
@ -881,7 +908,14 @@ pub fn goto_definition(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding);
let future = language_server.goto_definition(doc.identifier(), pos, None);
let future = match language_server.goto_definition(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-definition");
return;
}
};
cx.callback(
future,
@ -899,7 +933,14 @@ pub fn goto_type_definition(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding);
let future = language_server.goto_type_definition(doc.identifier(), pos, None);
let future = match language_server.goto_type_definition(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-type-definition");
return;
}
};
cx.callback(
future,
@ -917,7 +958,14 @@ pub fn goto_implementation(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding);
let future = language_server.goto_implementation(doc.identifier(), pos, None);
let future = match language_server.goto_implementation(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-implementation");
return;
}
};
cx.callback(
future,
@ -935,7 +983,14 @@ pub fn goto_reference(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding);
let future = language_server.goto_reference(doc.identifier(), pos, None);
let future = match language_server.goto_reference(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-reference");
return;
}
};
cx.callback(
future,
@ -978,7 +1033,13 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
Some(f) => f,
None => return,
None => {
if was_manually_invoked {
cx.editor
.set_error("Language server does not support signature-help");
}
return;
}
};
cx.callback(
@ -1079,7 +1140,14 @@ pub fn hover(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding);
let future = language_server.text_document_hover(doc.identifier(), pos, None);
let future = match language_server.text_document_hover(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support hover");
return;
}
};
cx.callback(
future,
@ -1149,8 +1217,16 @@ pub fn rename_symbol(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding);
let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
match block_on(task) {
let future =
match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support symbol renaming");
return;
}
};
match block_on(future) {
Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
Err(err) => cx.editor.set_error(err.to_string()),
}
@ -1165,7 +1241,15 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding);
let future = language_server.text_document_document_highlight(doc.identifier(), pos, None);
let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None)
{
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support document highlight");
return;
}
};
cx.callback(
future,

File diff suppressed because it is too large Load Diff

@ -506,7 +506,7 @@ fn earlier(
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
let success = doc.earlier(view, uk);
let success = doc.earlier(view.id, uk);
if !success {
cx.editor.set_status("Already at oldest change");
}
@ -525,7 +525,7 @@ fn later(
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
let success = doc.later(view, uk);
let success = doc.later(view.id, uk);
if !success {
cx.editor.set_status("Already at newest change");
}
@ -1059,6 +1059,51 @@ fn reload(
})
}
fn reload_all(
cx: &mut compositor::Context,
_args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let scrolloff = cx.editor.config().scrolloff;
let view_id = view!(cx.editor).id;
let docs_view_ids: Vec<(DocumentId, Vec<ViewId>)> = cx
.editor
.documents_mut()
.map(|doc| {
let mut view_ids: Vec<_> = doc.selections().keys().cloned().collect();
if view_ids.is_empty() {
doc.ensure_view_init(view_id);
view_ids.push(view_id);
};
(doc.id(), view_ids)
})
.collect();
for (doc_id, view_ids) in docs_view_ids {
let doc = doc_mut!(cx.editor, &doc_id);
// Every doc is guaranteed to have at least 1 view at this point.
let view = view_mut!(cx.editor, view_ids[0]);
doc.reload(view)?;
for view_id in view_ids {
let view = view_mut!(cx.editor, view_id);
if view.doc.eq(&doc_id) {
view.ensure_cursor_in_view(doc, scrolloff);
}
}
}
Ok(())
}
/// Update the [`Document`] if it has been modified.
fn update(
cx: &mut compositor::Context,
@ -1077,6 +1122,77 @@ fn update(
}
}
fn lsp_workspace_command(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let (_, doc) = current!(cx.editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => {
cx.editor
.set_status("Language server not active for current buffer");
return Ok(());
}
};
let options = match &language_server.capabilities().execute_command_provider {
Some(options) => options,
None => {
cx.editor
.set_status("Workspace commands are not supported for this language server");
return Ok(());
}
};
if args.is_empty() {
let commands = options
.commands
.iter()
.map(|command| helix_lsp::lsp::Command {
title: command.clone(),
command: command.clone(),
arguments: None,
})
.collect::<Vec<_>>();
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), |cx, command, _action| {
execute_lsp_command(cx.editor, command.clone());
});
compositor.push(Box::new(overlayed(picker)))
},
));
Ok(call)
};
cx.jobs.callback(callback);
} else {
let command = args.join(" ");
if options.commands.iter().any(|c| c == &command) {
execute_lsp_command(
cx.editor,
helix_lsp::lsp::Command {
title: command.clone(),
arguments: None,
command,
},
);
} else {
cx.editor.set_status(format!(
"`{command}` is not supported for this language server"
));
return Ok(());
}
}
Ok(())
}
fn lsp_restart(
cx: &mut compositor::Context,
_args: &[Cow<str>],
@ -2012,6 +2128,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: reload,
completer: None,
},
TypableCommand {
name: "reload-all",
aliases: &[],
doc: "Discard changes and reload all documents from the source files.",
fun: reload_all,
completer: None,
},
TypableCommand {
name: "update",
aliases: &[],
@ -2019,6 +2142,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: update,
completer: None,
},
TypableCommand {
name: "lsp-workspace-command",
aliases: &[],
doc: "Open workspace command picker",
fun: lsp_workspace_command,
completer: Some(completers::lsp_workspace_command),
},
TypableCommand {
name: "lsp-restart",
aliases: &[],
@ -2214,7 +2344,10 @@ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableComma
.collect()
});
#[allow(clippy::unnecessary_unwrap)]
pub(super) fn command_mode(cx: &mut Context) {
use shellwords::Shellwords;
let mut prompt = Prompt::new(
":".into(),
Some(':'),
@ -2222,10 +2355,11 @@ pub(super) fn command_mode(cx: &mut Context) {
static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
// simple heuristic: if there's no just one part, complete command name.
// if there's a space, per command completion kicks in.
// we use .this over split_whitespace() because we care about empty segments
if input.split(' ').count() <= 1 {
let shellwords = Shellwords::from(input);
let words = shellwords.words();
if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
// If the command has not been finished yet, complete commands.
let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
.iter()
.filter_map(|command| {
@ -2241,19 +2375,29 @@ pub(super) fn command_mode(cx: &mut Context) {
.map(|(name, _)| (0.., name.into()))
.collect()
} else {
let parts = shellwords::shellwords(input);
let part = parts.last().unwrap();
// Otherwise, use the command's completer and the last shellword
// as completion input.
let (part, part_len) = if words.len() == 1 || shellwords.ends_with_whitespace() {
(&Cow::Borrowed(""), 0)
} else {
(
words.last().unwrap(),
shellwords.parts().last().unwrap().len(),
)
};
if let Some(typed::TypableCommand {
completer: Some(completer),
..
}) = typed::TYPABLE_COMMAND_MAP.get(&parts[0] as &str)
}) = typed::TYPABLE_COMMAND_MAP.get(&words[0] as &str)
{
completer(editor, part)
.into_iter()
.map(|(range, file)| {
let file = shellwords::escape(file);
// offset ranges to input
let offset = input.len() - part.len();
let offset = input.len() - part_len;
let range = (range.start + offset)..;
(range, file)
})
@ -2279,7 +2423,8 @@ pub(super) fn command_mode(cx: &mut Context) {
// Handle typable commands
if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) {
let args = shellwords::shellwords(input);
let shellwords = Shellwords::from(input);
let args = shellwords.words();
if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e));

@ -4,8 +4,6 @@
use helix_core::Position;
use helix_view::graphics::{CursorKind, Rect};
#[cfg(feature = "integration")]
use tui::backend::TestBackend;
use tui::buffer::Buffer as Surface;
pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
@ -75,67 +73,28 @@ pub trait Component: Any + AnyComponent {
}
}
use anyhow::Context as AnyhowContext;
use tui::backend::Backend;
#[cfg(not(feature = "integration"))]
use tui::backend::CrosstermBackend;
#[cfg(not(feature = "integration"))]
use std::io::stdout;
#[cfg(not(feature = "integration"))]
type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
#[cfg(feature = "integration")]
type Terminal = tui::terminal::Terminal<TestBackend>;
pub struct Compositor {
layers: Vec<Box<dyn Component>>,
terminal: Terminal,
area: Rect,
pub(crate) last_picker: Option<Box<dyn Component>>,
}
impl Compositor {
pub fn new() -> anyhow::Result<Self> {
#[cfg(not(feature = "integration"))]
let backend = CrosstermBackend::new(stdout());
#[cfg(feature = "integration")]
let backend = TestBackend::new(120, 150);
let terminal = Terminal::new(backend).context("build terminal")?;
Ok(Self {
pub fn new(area: Rect) -> Self {
Self {
layers: Vec::new(),
terminal,
area,
last_picker: None,
})
}
}
pub fn size(&self) -> Rect {
self.terminal.size().expect("couldn't get terminal size")
}
pub fn resize(&mut self, width: u16, height: u16) {
self.terminal
.resize(Rect::new(0, 0, width, height))
.expect("Unable to resize terminal")
self.area
}
pub fn save_cursor(&mut self) {
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal
.backend_mut()
.show_cursor(CursorKind::Block)
.ok();
}
}
pub fn load_cursor(&mut self) {
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal.backend_mut().hide_cursor().ok();
}
pub fn resize(&mut self, area: Rect) {
self.area = area;
}
pub fn push(&mut self, mut layer: Box<dyn Component>) {
@ -203,25 +162,10 @@ impl Compositor {
consumed
}
pub fn render(&mut self, cx: &mut Context) {
self.terminal
.autoresize()
.expect("Unable to determine terminal size");
// TODO: need to recalculate view tree if necessary
let surface = self.terminal.current_buffer_mut();
let area = *surface.area();
pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
for layer in &mut self.layers {
layer.render(area, surface, cx);
}
let (pos, kind) = self.cursor(area, cx.editor);
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos, kind).unwrap();
}
pub fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {

@ -66,7 +66,10 @@ impl menu::Item for CompletionItem {
Some(lsp::CompletionItemKind::EVENT) => "event",
Some(lsp::CompletionItemKind::OPERATOR) => "operator",
Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
Some(kind) => unimplemented!("{:?}", kind),
Some(kind) => {
log::error!("Received unknown completion item kind: {:?}", kind);
""
}
None => "",
}),
// self.detail.as_deref().unwrap_or("")
@ -113,7 +116,8 @@ impl Completion {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
// TODO: support using "insert" instead of "replace" via user config
lsp::TextEdit::new(item.replace, item.new_text.clone())
}
};
@ -223,7 +227,7 @@ impl Completion {
}
};
});
let popup = Popup::new(Self::ID, menu);
let popup = Popup::new(Self::ID, menu).with_scrollbar(false);
let mut completion = Self {
popup,
start_offset,
@ -241,21 +245,13 @@ impl Completion {
completion_item: lsp::CompletionItem,
) -> Option<CompletionItem> {
let language_server = doc.language_server()?;
let completion_resolve_provider = language_server
.capabilities()
.completion_provider
.as_ref()?
.resolve_provider;
if completion_resolve_provider != Some(true) {
return None;
}
let future = language_server.resolve_completion_item(completion_item);
let future = language_server.resolve_completion_item(completion_item)?;
let response = helix_lsp::block_on(future);
match response {
Ok(completion_item) => Some(completion_item),
Ok(value) => serde_json::from_value(value).ok(),
Err(err) => {
log::error!("execute LSP command: {}", err);
log::error!("Failed to resolve completion item: {}", err);
None
}
}
@ -300,6 +296,12 @@ impl Completion {
self.popup.contents().is_empty()
}
fn replace_item(&mut self, old_item: lsp::CompletionItem, new_item: lsp::CompletionItem) {
self.popup.contents_mut().replace_option(old_item, new_item);
}
/// Asynchronously requests that the currently selection completion item is
/// resolved through LSP `completionItem/resolve`.
pub fn ensure_item_resolved(&mut self, cx: &mut commands::Context) -> bool {
// > If computing full completion items is expensive, servers can additionally provide a
// > handler for the completion item resolve request. ...
@ -309,16 +311,41 @@ impl Completion {
// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
// > The returned completion item should have the documentation property filled in.
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
match self.popup.contents_mut().selection_mut() {
Some(item) if item.documentation.is_none() => {
let doc = doc!(cx.editor);
if let Some(resolved_item) = Self::resolve_completion_item(doc, item.clone()) {
*item = resolved_item;
let current_item = match self.popup.contents().selection() {
Some(item) if item.documentation.is_none() => item.clone(),
_ => return false,
};
let language_server = match doc!(cx.editor).language_server() {
Some(language_server) => language_server,
None => return false,
};
// This method should not block the compositor so we handle the response asynchronously.
let future = match language_server.resolve_completion_item(current_item.clone()) {
Some(future) => future,
None => return false,
};
cx.callback(
future,
move |_editor, compositor, response: Option<lsp::CompletionItem>| {
let resolved_item = match response {
Some(item) => item,
None => return,
};
if let Some(completion) = &mut compositor
.find::<crate::ui::EditorView>()
.unwrap()
.completion
{
completion.replace_item(current_item, resolved_item);
}
true
}
_ => false,
}
},
);
true
}
}

@ -81,9 +81,10 @@ impl EditorView {
surface: &mut Surface,
is_focused: bool,
) {
let inner = view.inner_area();
let inner = view.inner_area(doc);
let area = view.area;
let theme = &editor.theme;
let config = editor.config();
// DAP: Highlight current stack frame position
let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
@ -119,10 +120,10 @@ impl EditorView {
}
}
if is_focused && editor.config().cursorline {
if is_focused && config.cursorline {
Self::highlight_cursorline(doc, view, surface, theme);
}
if is_focused && editor.config().cursorcolumn {
if is_focused && config.cursorcolumn {
Self::highlight_cursorcolumn(doc, view, surface, theme);
}
@ -143,22 +144,14 @@ impl EditorView {
doc,
view,
theme,
&editor.config().cursor_shape,
&config.cursor_shape,
),
))
} else {
Box::new(highlights)
};
Self::render_text_highlights(
doc,
view.offset,
inner,
surface,
theme,
highlights,
&editor.config(),
);
Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config);
Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
Self::render_rulers(editor, doc, view, inner, surface, theme);
@ -178,7 +171,7 @@ impl EditorView {
}
}
self.render_diagnostics(doc, view, inner, surface, theme);
Self::render_diagnostics(doc, view, inner, surface, theme);
let statusline_area = view
.area
@ -759,9 +752,10 @@ impl EditorView {
// avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8);
for (constructor, width) in view.gutters() {
let gutter = constructor(editor, doc, view, theme, is_focused, *width);
text.reserve(*width); // ensure there's enough space for the gutter
for gutter_type in view.gutters() {
let gutter = gutter_type.style(editor, doc, view, theme, is_focused);
let width = gutter_type.width(view, doc);
text.reserve(width); // ensure there's enough space for the gutter
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
let selected = cursors.contains(&line);
let x = viewport.x + offset;
@ -774,13 +768,13 @@ impl EditorView {
};
if let Some(style) = gutter(line, selected, &mut text) {
surface.set_stringn(x, y, &text, *width, gutter_style.patch(style));
surface.set_stringn(x, y, &text, width, gutter_style.patch(style));
} else {
surface.set_style(
Rect {
x,
y,
width: *width as u16,
width: width as u16,
height: 1,
},
gutter_style,
@ -789,12 +783,11 @@ impl EditorView {
text.clear();
}
offset += *width as u16;
offset += width as u16;
}
}
pub fn render_diagnostics(
&self,
doc: &Document,
view: &View,
viewport: Rect,
@ -905,7 +898,7 @@ impl EditorView {
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
.unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
let inner_area = view.inner_area();
let inner_area = view.inner_area(doc);
let offset = view.offset.col;
let selection = doc.selection(view.id);
@ -1377,7 +1370,9 @@ impl Component for EditorView {
cx.editor.status_msg = None;
let mode = cx.editor.mode();
let (view, _) = current!(cx.editor);
let (view, doc) = current!(cx.editor);
let original_doc_id = doc.id();
let original_doc_revision = doc.get_current_revision();
let focus = view.id;
if let Some(on_next_key) = self.on_next_key.take() {
@ -1454,13 +1449,31 @@ impl Component for EditorView {
let view = view_mut!(cx.editor, focus);
let doc = doc_mut!(cx.editor, &view.doc);
view.ensure_cursor_in_view(doc, config.scrolloff);
// Store a history state if not in insert mode. This also takes care of
// committing changes when leaving insert mode.
if mode != Mode::Insert {
doc.append_changes_to_history(view.id);
}
// If the current document has been changed, apply the changes to all views.
// This ensures that selections in jumplists follow changes.
if doc.id() == original_doc_id
&& doc.get_current_revision() != original_doc_revision
{
if let Some(transaction) =
doc.history.get_mut().changes_since(original_doc_revision)
{
let doc = doc!(cx.editor, &original_doc_id);
for (view, _focused) in cx.editor.tree.views_mut() {
view.apply(&transaction, doc);
}
}
}
let view = view_mut!(cx.editor, focus);
let doc = doc_mut!(cx.editor, &view.doc);
view.ensure_cursor_in_view(doc, config.scrolloff);
}
EventResult::Consumed(callback)

@ -234,6 +234,17 @@ impl<T: Item> Menu<T> {
}
}
impl<T: Item + PartialEq> Menu<T> {
pub fn replace_option(&mut self, old_option: T, new_option: T) {
for option in &mut self.options {
if old_option == *option {
*option = new_option;
break;
}
}
}
}
use super::PromptEvent as MenuEvent;
impl<T: Item + 'static> Component for Menu<T> {
@ -330,11 +341,6 @@ impl<T: Item + 'static> Component for Menu<T> {
(a + b - 1) / b
}
let scroll_height = std::cmp::min(div_ceil(win_height.pow(2), len), win_height as usize);
let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height));
let rows = options.iter().map(|option| option.row(&self.editor_data));
let table = Table::new(rows)
.style(style)
@ -367,20 +373,24 @@ impl<T: Item + 'static> Component for Menu<T> {
let fits = len <= win_height;
let scroll_style = theme.get("ui.menu.scroll");
for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
let cell = &mut surface[(area.x + area.width - 1, area.y + i as u16)];
if !fits {
let scroll_height = div_ceil(win_height.pow(2), len).min(win_height);
let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height));
if !fits {
// Draw scroll track
cell.set_symbol("▐"); // right half block
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
let mut cell;
for i in 0..win_height {
cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
cell.set_symbol("▐"); // right half block
if !fits && is_marked {
// Draw scroll thumb
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
if scroll_line <= i && i < scroll_line + scroll_height {
// Draw scroll thumb
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
} else {
// Draw scroll track
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
}
}
}

@ -234,7 +234,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
cx.editor.set_error(err);
}
},
|_editor, path| Some((path.clone(), None)),
|_editor, path| Some((path.clone().into(), None)),
)
}
@ -394,6 +394,45 @@ pub mod completers {
.collect()
}
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default();
let (_, doc) = current_ref!(editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => {
return vec![];
}
};
let options = match &language_server.capabilities().execute_command_provider {
Some(options) => options,
None => {
return vec![];
}
};
let mut matches: Vec<_> = options
.commands
.iter()
.filter_map(|command| {
matcher
.fuzzy_match(command, input)
.map(|score| (command, score))
})
.collect();
matches.sort_unstable_by(|(command1, score1), (command2, score2)| {
(Reverse(*score1), command1).cmp(&(Reverse(*score2), command2))
});
matches
.into_iter()
.map(|(command, _score)| ((0..), command.clone().into()))
.collect()
}
pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
filename_impl(editor, input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());

@ -11,20 +11,15 @@ use tui::{
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use tui::widgets::Widget;
use std::time::Instant;
use std::{
cmp::Reverse,
collections::HashMap,
io::Read,
path::{Path, PathBuf},
};
use std::{cmp::Ordering, time::Instant};
use std::{collections::HashMap, io::Read, path::PathBuf};
use crate::ui::{Prompt, PromptEvent};
use helix_core::{movement::Direction, Position};
use helix_view::{
editor::Action,
graphics::{CursorKind, Margin, Modifier, Rect},
Document, Editor,
Document, DocumentId, Editor,
};
use super::menu::Item;
@ -33,8 +28,36 @@ pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
#[derive(PartialEq, Eq, Hash)]
pub enum PathOrId {
Id(DocumentId),
Path(PathBuf),
}
impl PathOrId {
fn get_canonicalized(self) -> std::io::Result<Self> {
use PathOrId::*;
Ok(match self {
Path(path) => Path(helix_core::path::get_canonicalized_path(&path)?),
Id(id) => Id(id),
})
}
}
impl From<PathBuf> for PathOrId {
fn from(v: PathBuf) -> Self {
Self::Path(v)
}
}
impl From<DocumentId> for PathOrId {
fn from(v: DocumentId) -> Self {
Self::Id(v)
}
}
/// File path and range of lines (used to align and highlight lines)
pub type FileLocation = (PathBuf, Option<(usize, usize)>);
pub type FileLocation = (PathOrId, Option<(usize, usize)>);
pub struct FilePicker<T: Item> {
picker: Picker<T>,
@ -113,62 +136,71 @@ impl<T: Item> FilePicker<T> {
self.picker
.selection()
.and_then(|current| (self.file_fn)(editor, current))
.and_then(|(path, line)| {
helix_core::path::get_canonicalized_path(&path)
.ok()
.zip(Some(line))
})
.and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
}
/// Get (cached) preview for a given path. If a document corresponding
/// to the path is already open in the editor, it is used instead.
fn get_preview<'picker, 'editor>(
&'picker mut self,
path: &Path,
path_or_id: PathOrId,
editor: &'editor Editor,
) -> Preview<'picker, 'editor> {
if let Some(doc) = editor.document_by_path(path) {
return Preview::EditorDocument(doc);
}
match path_or_id {
PathOrId::Path(path) => {
let path = &path;
if let Some(doc) = editor.document_by_path(path) {
return Preview::EditorDocument(doc);
}
if self.preview_cache.contains_key(path) {
return Preview::Cached(&self.preview_cache[path]);
}
if self.preview_cache.contains_key(path) {
return Preview::Cached(&self.preview_cache[path]);
}
let data = std::fs::File::open(path).and_then(|file| {
let metadata = file.metadata()?;
// Read up to 1kb to detect the content type
let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
let content_type = content_inspector::inspect(&self.read_buffer[..n]);
self.read_buffer.clear();
Ok((metadata, content_type))
});
let preview = data
.map(
|(metadata, content_type)| match (metadata.len(), content_type) {
(_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
Document::open(path, None, None)
.map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
},
)
.unwrap_or(CachedPreview::NotFound);
self.preview_cache.insert(path.to_owned(), preview);
Preview::Cached(&self.preview_cache[path])
let data = std::fs::File::open(path).and_then(|file| {
let metadata = file.metadata()?;
// Read up to 1kb to detect the content type
let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
let content_type = content_inspector::inspect(&self.read_buffer[..n]);
self.read_buffer.clear();
Ok((metadata, content_type))
});
let preview = data
.map(
|(metadata, content_type)| match (metadata.len(), content_type) {
(_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
CachedPreview::LargeFile
}
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
Document::open(path, None, None)
.map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
},
)
.unwrap_or(CachedPreview::NotFound);
self.preview_cache.insert(path.to_owned(), preview);
Preview::Cached(&self.preview_cache[path])
}
PathOrId::Id(id) => {
let doc = editor.documents.get(&id).unwrap();
Preview::EditorDocument(doc)
}
}
}
fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
// Try to find a document in the cache
let doc = self
.current_file(cx.editor)
.and_then(|(path, _range)| self.preview_cache.get_mut(&path))
.and_then(|cache| match cache {
CachedPreview::Document(doc) => Some(doc),
_ => None,
.and_then(|(path, _range)| match path {
PathOrId::Id(doc_id) => Some(doc_mut!(cx.editor, &doc_id)),
PathOrId::Path(path) => match self.preview_cache.get_mut(&path) {
Some(CachedPreview::Document(doc)) => Some(doc),
_ => None,
},
});
// Then attempt to highlight it if it has no language set
@ -225,7 +257,7 @@ impl<T: Item + 'static> Component for FilePicker<T> {
block.render(preview_area, surface);
if let Some((path, range)) = self.current_file(cx.editor) {
let preview = self.get_preview(&path, cx.editor);
let preview = self.get_preview(path, cx.editor);
let doc = match preview.document() {
Some(doc) => doc,
None => {
@ -309,13 +341,34 @@ impl<T: Item + 'static> Component for FilePicker<T> {
}
}
#[derive(PartialEq, Eq, Debug)]
struct PickerMatch {
index: usize,
score: i64,
len: usize,
}
impl PartialOrd for PickerMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PickerMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.cmp(&other.score)
.reverse()
.then_with(|| self.len.cmp(&other.len))
}
}
pub struct Picker<T: Item> {
options: Vec<T>,
editor_data: T::Data,
// filter: String,
matcher: Box<Matcher>,
/// (index, score)
matches: Vec<(usize, i64)>,
matches: Vec<PickerMatch>,
/// Current height of the completions box
completion_height: u16,
@ -361,13 +414,16 @@ impl<T: Item> Picker<T> {
// scoring on empty input:
// TODO: just reuse score()
picker.matches.extend(
picker
.options
.iter()
.enumerate()
.map(|(index, _option)| (index, 0)),
);
picker
.matches
.extend(picker.options.iter().enumerate().map(|(index, option)| {
let text = option.filter_text(&picker.editor_data);
PickerMatch {
index,
score: 0,
len: text.chars().count(),
}
}));
picker
}
@ -384,32 +440,34 @@ impl<T: Item> Picker<T> {
if pattern.is_empty() {
// Fast path for no pattern.
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.map(|(index, _option)| (index, 0)),
);
self.matches
.extend(self.options.iter().enumerate().map(|(index, option)| {
let text = option.filter_text(&self.editor_data);
PickerMatch {
index,
score: 0,
len: text.chars().count(),
}
}));
} else if pattern.starts_with(&self.previous_pattern) {
let query = FuzzyQuery::new(pattern);
// optimization: if the pattern is a more specific version of the previous one
// then we can score the filtered set.
self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index];
self.matches.retain_mut(|pmatch| {
let option = &self.options[pmatch.index];
let text = option.sort_text(&self.editor_data);
match query.fuzzy_match(&text, &self.matcher) {
Some(s) => {
// Update the score
*score = s;
pmatch.score = s;
true
}
None => false,
}
});
self.matches
.sort_unstable_by_key(|(_, score)| Reverse(*score));
self.matches.sort_unstable();
} else {
let query = FuzzyQuery::new(pattern);
self.matches.clear();
@ -422,11 +480,14 @@ impl<T: Item> Picker<T> {
query
.fuzzy_match(&text, &self.matcher)
.map(|score| (index, score))
.map(|score| PickerMatch {
index,
score,
len: text.chars().count(),
})
}),
);
self.matches
.sort_unstable_by_key(|(_, score)| Reverse(*score));
self.matches.sort_unstable();
}
log::debug!("picker score {:?}", Instant::now().duration_since(now));
@ -478,7 +539,7 @@ impl<T: Item> Picker<T> {
pub fn selection(&self) -> Option<&T> {
self.matches
.get(self.cursor)
.map(|(index, _score)| &self.options[*index])
.map(|pmatch| &self.options[pmatch.index])
}
pub fn toggle_preview(&mut self) {
@ -625,7 +686,7 @@ impl<T: Item + 'static> Component for Picker<T> {
.matches
.iter()
.skip(offset)
.map(|(index, _score)| (*index, self.options.get(*index).unwrap()));
.map(|pmatch| (pmatch.index, self.options.get(pmatch.index).unwrap()));
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
let is_active = i == (self.cursor - offset);

@ -22,6 +22,7 @@ pub struct Popup<T: Component> {
auto_close: bool,
ignore_escape_key: bool,
id: &'static str,
has_scrollbar: bool,
}
impl<T: Component> Popup<T> {
@ -37,6 +38,7 @@ impl<T: Component> Popup<T> {
auto_close: false,
ignore_escape_key: false,
id,
has_scrollbar: true,
}
}
@ -128,6 +130,14 @@ impl<T: Component> Popup<T> {
}
}
/// Toggles the Popup's scrollbar.
/// Consider disabling the scrollbar in case the child
/// already has its own.
pub fn with_scrollbar(mut self, enable_scrollbar: bool) -> Self {
self.has_scrollbar = enable_scrollbar;
self
}
pub fn contents(&self) -> &T {
&self.contents
}
@ -228,6 +238,40 @@ impl<T: Component> Component for Popup<T> {
let inner = area.inner(&self.margin);
self.contents.render(inner, surface, cx);
// render scrollbar if contents do not fit
if self.has_scrollbar {
let win_height = inner.height as usize;
let len = self.child_size.1 as usize;
let fits = len <= win_height;
let scroll = self.scroll;
let scroll_style = cx.editor.theme.get("ui.menu.scroll");
const fn div_ceil(a: usize, b: usize) -> usize {
(a + b - 1) / b
}
if !fits {
let scroll_height = div_ceil(win_height.pow(2), len).min(win_height);
let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height));
let mut cell;
for i in 0..win_height {
cell = &mut surface[(inner.right() - 1, inner.top() + i as u16)];
cell.set_symbol("▐"); // right half block
if scroll_line <= i && i < scroll_line + scroll_height {
// Draw scroll thumb
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
} else {
// Draw scroll track
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
}
}
}
}
fn id(&self) -> Option<&'static str> {

@ -1,6 +1,5 @@
use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui};
use helix_core::shellwords;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
use std::{borrow::Cow, ops::RangeFrom};
@ -295,23 +294,22 @@ impl Prompt {
direction: CompletionDirection,
) {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
let register = cx.editor.registers.get_mut(register).read();
if register.is_empty() {
return;
}
let values = match cx.editor.registers.read(register) {
Some(values) if !values.is_empty() => values,
_ => return,
};
let end = register.len().saturating_sub(1);
let end = values.len().saturating_sub(1);
let index = match direction {
CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
CompletionDirection::Backward => {
self.history_pos.unwrap_or(register.len()).saturating_sub(1)
self.history_pos.unwrap_or(values.len()).saturating_sub(1)
}
}
.min(end);
self.line = register[index].clone();
self.line = values[index].clone();
self.history_pos = Some(index);
@ -336,10 +334,7 @@ impl Prompt {
let (range, item) = &self.completion[index];
// since we are using shellwords to parse arguments, make sure
// that whitespace in files is properly escaped.
let item = shellwords::escape(item);
self.line.replace_range(range.clone(), &item);
self.line.replace_range(range.clone(), item);
self.move_end();
}
@ -552,10 +547,7 @@ impl Component for Prompt {
if last_item != self.line {
// store in history
if let Some(register) = self.history_register {
cx.editor
.registers
.get_mut(register)
.push(self.line.clone());
cx.editor.registers.push(register, self.line.clone());
};
}

@ -1,4 +1,5 @@
use helix_core::{coords_at_pos, encoding, Position};
use helix_lsp::lsp::DiagnosticSeverity;
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect,
@ -68,7 +69,9 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface
// Left side of the status line.
let element_ids = &context.editor.config().statusline.left;
let config = context.editor.config();
let element_ids = &config.statusline.left;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
@ -83,7 +86,7 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface
// Right side of the status line.
let element_ids = &context.editor.config().statusline.right;
let element_ids = &config.statusline.right;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
@ -101,7 +104,7 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface
// Center of the status line.
let element_ids = &context.editor.config().statusline.center;
let element_ids = &config.statusline.center;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
@ -141,7 +144,11 @@ where
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type,
helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics,
helix_view::editor::StatusLineElement::WorkspaceDiagnostics => render_workspace_diagnostics,
helix_view::editor::StatusLineElement::Selections => render_selections,
helix_view::editor::StatusLineElement::PrimarySelectionLength => {
render_primary_selection_length
}
helix_view::editor::StatusLineElement::Position => render_position,
helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage,
helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers,
@ -155,7 +162,8 @@ where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let visible = context.focused;
let modenames = &context.editor.config().statusline.mode;
let config = context.editor.config();
let modenames = &config.statusline.mode;
write(
context,
format!(
@ -171,7 +179,7 @@ where
" "
}
),
if visible && context.editor.config().color_modes {
if visible && config.color_modes {
match context.editor.mode() {
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
@ -242,6 +250,48 @@ where
}
}
fn render_workspace_diagnostics<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let (warnings, errors) =
context
.editor
.diagnostics
.values()
.flatten()
.fold((0, 0), |mut counts, diag| {
match diag.severity {
Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
_ => {}
}
counts
});
if warnings > 0 || errors > 0 {
write(context, format!(" {} ", "W"), None);
}
if warnings > 0 {
write(
context,
"●".to_string(),
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
}
if errors > 0 {
write(
context,
"●".to_string(),
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
}
}
fn render_selections<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
@ -254,6 +304,18 @@ where
);
}
fn render_primary_selection_length<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let tot_sel = context.doc.selection(context.view.id).primary().len();
write(
context,
format!(" {} char{} ", tot_sel, if tot_sel == 1 { "" } else { "s" }),
None,
);
}
fn get_position(context: &RenderContext) -> Position {
coords_at_pos(
context.doc.text().slice(..),

@ -193,3 +193,121 @@ async fn test_goto_file_impl() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_multi_selection_paste() -> anyhow::Result<()> {
test((
platform_line(indoc! {"\
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"})
.as_str(),
"yp",
platform_line(indoc! {"\
lorem#[|lorem]#
ipsum#(|ipsum)#
dolor#(|dolor)#
"})
.as_str(),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
// pipe
test((
platform_line(indoc! {"\
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"})
.as_str(),
"|echo foo<ret>",
platform_line(indoc! {"\
#[|foo
]#
#(|foo
)#
#(|foo
)#
"})
.as_str(),
))
.await?;
// insert-output
test((
platform_line(indoc! {"\
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"})
.as_str(),
"!echo foo<ret>",
platform_line(indoc! {"\
#[|foo
]#lorem
#(|foo
)#ipsum
#(|foo
)#dolor
"})
.as_str(),
))
.await?;
// append-output
test((
platform_line(indoc! {"\
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"})
.as_str(),
"<A-!>echo foo<ret>",
platform_line(indoc! {"\
lorem#[|foo
]#
ipsum#(|foo
)#
dolor#(|foo
)#
"})
.as_str(),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_undo_redo() -> anyhow::Result<()> {
// A jumplist selection is created at a point which is undone.
//
// * 2[<space> Add two newlines at line start. We're now on line 3.
// * <C-s> Save the selection on line 3 in the jumplist.
// * u Undo the two newlines. We're now on line 1.
// * <C-o><C-i> Jump forward an back again in the jumplist. This would panic
// if the jumplist were not being updated correctly.
test(("#[|]#", "2[<space><C-s>u<C-o><C-i>", "#[|]#")).await?;
// A jumplist selection is passed through an edit and then an undo and then a redo.
//
// * [<space> Add a newline at line start. We're now on line 2.
// * <C-s> Save the selection on line 2 in the jumplist.
// * kd Delete line 1. The jumplist selection should be adjusted to the new line 1.
// * uU Undo and redo the `kd` edit.
// * <C-o> Jump back in the jumplist. This would panic if the jumplist were not being
// updated correctly.
// * <C-i> Jump forward to line 1.
test(("#[|]#", "[<space><C-s>kduU<C-o><C-i>", "#[|]#")).await?;
// In this case we 'redo' manually to ensure that the transactions are composing correctly.
test(("#[|]#", "[<space>u[<space>u", "#[|]#")).await?;
Ok(())
}

@ -127,3 +127,29 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> {
// See <https://github.com/helix-editor/helix/issues/4732>.
// Transactions must be applied to any view that has the changed document open.
// This sequence would panic since the jumplist entry would be modified in one
// window but not the other. Attempting to update the changelist in the other
// window would cause a panic since it would point outside of the document.
// The key sequence here:
// * <C-w>v Create a vertical split of the current buffer.
// Both views look at the same doc.
// * [<space> Add a line ending to the beginning of the document.
// The cursor is now at line 2 in window 2.
// * <C-s> Save that selection to the jumplist in window 2.
// * <C-w>w Switch to window 1.
// * kd Delete line 1 in window 1.
// * <C-w>q Close window 1, focusing window 2.
// * d Delete line 1 in window 2.
//
// This panicked in the past because the jumplist entry on line 2 of window 2
// was not updated and after the `kd` step, pointed outside of the document.
test(("#[|]#", "<C-w>v[<space><C-s><C-w>wkd<C-w>qd", "#[|]#")).await?;
Ok(())
}

@ -360,14 +360,14 @@ impl Buffer {
let mut start_index = self.index_of(x, y);
let mut index = self.index_of(max_offset as u16, y);
let total_width = string.width();
let truncated = total_width > width;
let content_width = string.width();
let truncated = content_width > width;
if ellipsis && truncated {
self.content[start_index].set_symbol("…");
start_index += 1;
}
if !truncated {
index -= width - total_width;
index -= width - content_width;
}
for (byte_offset, s) in graphemes.rev() {
let width = s.width();
@ -384,6 +384,7 @@ impl Buffer {
self.content[i].reset();
}
index -= width;
x_offset += width;
}
}
(x_offset as u16, y)

@ -16,11 +16,11 @@ use std::sync::Arc;
use helix_core::{
encoding,
history::{History, UndoKind},
history::{History, State, UndoKind},
indent::{auto_detect_indent_style, IndentStyle},
line_ending::auto_detect_line_ending,
syntax::{self, LanguageConfiguration},
ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, Syntax, Transaction,
DEFAULT_LINE_ENDING,
};
@ -873,11 +873,11 @@ impl Document {
success
}
fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool {
fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool {
let mut history = self.history.take();
let txn = if undo { history.undo() } else { history.redo() };
let success = if let Some(txn) = txn {
self.apply_impl(txn, view.id) && view.apply(txn, self)
self.apply_impl(txn, view_id)
} else {
false
};
@ -891,13 +891,13 @@ impl Document {
}
/// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
pub fn undo(&mut self, view: &mut View) -> bool {
self.undo_redo_impl(view, true)
pub fn undo(&mut self, view_id: ViewId) -> bool {
self.undo_redo_impl(view_id, true)
}
/// Redo the last modification to the [`Document`]. Returns whether the redo was successful.
pub fn redo(&mut self, view: &mut View) -> bool {
self.undo_redo_impl(view, false)
pub fn redo(&mut self, view_id: ViewId) -> bool {
self.undo_redo_impl(view_id, false)
}
pub fn savepoint(&mut self) {
@ -910,7 +910,7 @@ impl Document {
}
}
fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool {
fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool {
let txns = if earlier {
self.history.get_mut().earlier(uk)
} else {
@ -918,7 +918,7 @@ impl Document {
};
let mut success = false;
for txn in txns {
if self.apply_impl(&txn, view.id) && view.apply(&txn, self) {
if self.apply_impl(&txn, view_id) {
success = true;
}
}
@ -930,13 +930,13 @@ impl Document {
}
/// Undo modifications to the [`Document`] according to `uk`.
pub fn earlier(&mut self, view: &mut View, uk: UndoKind) -> bool {
self.earlier_later_impl(view, uk, true)
pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
self.earlier_later_impl(view_id, uk, true)
}
/// Redo modifications to the [`Document`] according to `uk`.
pub fn later(&mut self, view: &mut View, uk: UndoKind) -> bool {
self.earlier_later_impl(view, uk, false)
pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
self.earlier_later_impl(view_id, uk, false)
}
/// Commit pending changes to history

@ -388,9 +388,15 @@ pub enum StatusLineElement {
/// A summary of the number of errors and warnings
Diagnostics,
/// A summary of the number of errors and warnings on file and workspace
WorkspaceDiagnostics,
/// The number of selections (cursors)
Selections,
/// The number of characters currently in primary selection
PrimarySelectionLength,
/// The cursor position
Position,
@ -519,6 +525,7 @@ impl std::str::FromStr for GutterType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"diagnostics" => Ok(Self::Diagnostics),
"spacer" => Ok(Self::Spacer),
"line-numbers" => Ok(Self::LineNumbers),
_ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
}
@ -662,7 +669,11 @@ impl Default for Config {
line_number: LineNumber::Absolute,
cursorline: true,
cursorcolumn: false,
gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
gutters: vec![
GutterType::Diagnostics,
GutterType::Spacer,
GutterType::LineNumbers,
],
middle_click_paste: true,
auto_pairs: AutoPairConfig::default(),
auto_completion: true,
@ -1011,7 +1022,7 @@ impl Editor {
)
})
{
doc.set_language_server(Some(client));
doc.set_language_server(client);
}
};
Some(())
@ -1218,9 +1229,10 @@ impl Editor {
}
pub fn close(&mut self, id: ViewId) {
let (_view, doc) = current!(self);
// remove selection
doc.remove_view(id);
// Remove selections for the closed view on all documents.
for doc in self.documents_mut() {
doc.remove_view(id);
}
self.tree.remove(id);
self._refresh();
}
@ -1345,16 +1357,7 @@ impl Editor {
}
pub fn focus_next(&mut self) {
let prev_id = self.tree.focus;
self.tree.focus_next();
let id = self.tree.focus;
// if leaving the view: mode should reset and the cursor should be
// within view
if prev_id != id {
self.mode = Mode::Normal;
self.ensure_cursor_in_view(id);
}
self.focus(self.tree.next());
}
pub fn focus_direction(&mut self, direction: tree::Direction) {
@ -1421,7 +1424,7 @@ impl Editor {
.primary()
.cursor(doc.text().slice(..));
if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
let inner = view.inner_area();
let inner = view.inner_area(doc);
pos.col += inner.x as usize;
pos.row += inner.y as usize;
let cursorkind = config.cursor_shape.from_mode(self.mode);

@ -1,21 +1,54 @@
use std::fmt::Write;
use crate::{
editor::GutterType,
graphics::{Color, Style, UnderlineStyle},
Document, Editor, Theme, View,
};
fn count_digits(n: usize) -> usize {
// NOTE: if int_log gets standardized in stdlib, can use checked_log10
// (https://github.com/rust-lang/rust/issues/70887#issue)
std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
}
pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
pub type Gutter =
for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
impl GutterType {
pub fn style<'doc>(
self,
editor: &'doc Editor,
doc: &'doc Document,
view: &View,
theme: &Theme,
is_focused: bool,
) -> GutterFn<'doc> {
match self {
GutterType::Diagnostics => {
diagnostics_or_breakpoints(editor, doc, view, theme, is_focused)
}
GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused),
GutterType::Spacer => padding(editor, doc, view, theme, is_focused),
}
}
pub fn width(self, _view: &View, doc: &Document) -> usize {
match self {
GutterType::Diagnostics => 1,
GutterType::LineNumbers => line_numbers_width(_view, doc),
GutterType::Spacer => 1,
}
}
}
pub fn diagnostic<'doc>(
_editor: &'doc Editor,
doc: &'doc Document,
_view: &View,
theme: &Theme,
_is_focused: bool,
_width: usize,
) -> GutterFn<'doc> {
let warning = theme.get("warning");
let error = theme.get("error");
@ -56,10 +89,11 @@ pub fn line_numbers<'doc>(
view: &View,
theme: &Theme,
is_focused: bool,
width: usize,
) -> GutterFn<'doc> {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
let width = GutterType::LineNumbers.width(view, doc);
// Whether to draw the line number for the last line of the
// document or not. We only draw it if it's not an empty line.
let draw_last = text.line_to_byte(last_line) < text.len_bytes();
@ -91,24 +125,35 @@ pub fn line_numbers<'doc>(
} else {
line + 1
};
let style = if selected && is_focused {
linenr_select
} else {
linenr
};
write!(out, "{:>1$}", display_num, width).unwrap();
Some(style)
}
})
}
pub fn line_numbers_width(_view: &View, doc: &Document) -> usize {
let text = doc.text();
let last_line = text.len_lines().saturating_sub(1);
let draw_last = text.line_to_byte(last_line) < text.len_bytes();
let last_drawn = if draw_last { last_line + 1 } else { last_line };
// set a lower bound to 2-chars to minimize ambiguous relative line numbers
std::cmp::max(count_digits(last_drawn), 2)
}
pub fn padding<'doc>(
_editor: &'doc Editor,
_doc: &'doc Document,
_view: &View,
_theme: &Theme,
_is_focused: bool,
_width: usize,
) -> GutterFn<'doc> {
Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
}
@ -128,7 +173,6 @@ pub fn breakpoints<'doc>(
_view: &View,
theme: &Theme,
_is_focused: bool,
_width: usize,
) -> GutterFn<'doc> {
let warning = theme.get("warning");
let error = theme.get("error");
@ -181,10 +225,9 @@ pub fn diagnostics_or_breakpoints<'doc>(
view: &View,
theme: &Theme,
is_focused: bool,
width: usize,
) -> GutterFn<'doc> {
let diagnostics = diagnostic(editor, doc, view, theme, is_focused, width);
let breakpoints = breakpoints(editor, doc, view, theme, is_focused, width);
let diagnostics = diagnostic(editor, doc, view, theme, is_focused);
let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
Box::new(move |line, selected, out| {
breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))

@ -55,7 +55,7 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
let last_line_height = view.inner_area().height.saturating_sub(1) as usize;
let last_line_height = view.inner_height().saturating_sub(1);
let relative = match align {
Align::Center => last_line_height / 2,
@ -71,12 +71,10 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
pub fn apply_transaction(
transaction: &helix_core::Transaction,
doc: &mut Document,
view: &mut View,
view: &View,
) -> bool {
// This is a short function but it's easy to call `Document::apply`
// without calling `View::apply` or in the wrong order. The transaction
// must be applied to the document before the view.
doc.apply(transaction, view.id) && view.apply(transaction, doc)
// TODO remove this helper function. Just call Document::apply everywhere directly.
doc.apply(transaction, view.id)
}
pub use document::Document;

@ -67,7 +67,7 @@ macro_rules! view {
#[macro_export]
macro_rules! doc {
($editor:expr, $id:expr) => {{
$editor.documents[$id]
&$editor.documents[$id]
}};
($editor:expr) => {{
$crate::current_ref!($editor).1

@ -219,7 +219,7 @@ impl Tree {
if self.focus == index {
// focus on something else
self.focus_next();
self.focus = self.prev();
}
stack.push(index);
@ -499,7 +499,7 @@ impl Tree {
// in a vertical container (and already correct based on previous search)
child_id = *container.children.iter().min_by_key(|id| {
let x = match &self.nodes[**id].content {
Content::View(view) => view.inner_area().left(),
Content::View(view) => view.area.left(),
Content::Container(container) => container.area.left(),
};
(current_x as i16 - x as i16).abs()
@ -510,7 +510,7 @@ impl Tree {
// in a horizontal container (and already correct based on previous search)
child_id = *container.children.iter().min_by_key(|id| {
let y = match &self.nodes[**id].content {
Content::View(view) => view.inner_area().top(),
Content::View(view) => view.area.top(),
Content::Container(container) => container.area.top(),
};
(current_y as i16 - y as i16).abs()
@ -521,7 +521,27 @@ impl Tree {
Some(child_id)
}
pub fn focus_next(&mut self) {
pub fn prev(&self) -> ViewId {
// This function is very dumb, but that's because we don't store any parent links.
// (we'd be able to go parent.prev_sibling() recursively until we find something)
// For now that's okay though, since it's unlikely you'll be able to open a large enough
// number of splits to notice.
let mut views = self
.traverse()
.rev()
.skip_while(|&(id, _view)| id != self.focus)
.skip(1); // Skip focused value
if let Some((id, _)) = views.next() {
id
} else {
// extremely crude, take the last item
let (key, _) = self.traverse().rev().next().unwrap();
key
}
}
pub fn next(&self) -> ViewId {
// This function is very dumb, but that's because we don't store any parent links.
// (we'd be able to go parent.next_sibling() recursively until we find something)
// For now that's okay though, since it's unlikely you'll be able to open a large enough
@ -532,11 +552,11 @@ impl Tree {
.skip_while(|&(id, _view)| id != self.focus)
.skip(1); // Skip focused value
if let Some((id, _)) = views.next() {
self.focus = id;
id
} else {
// extremely crude, take the first item again
let (key, _) = self.traverse().next().unwrap();
self.focus = key;
key
}
}
@ -661,6 +681,23 @@ impl<'a> Iterator for Traverse<'a> {
}
}
impl<'a> DoubleEndedIterator for Traverse<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
loop {
let key = self.stack.pop()?;
let node = &self.tree.nodes[key];
match &node.content {
Content::View(view) => return Some((key, view)),
Content::Container(container) => {
self.stack.extend(container.children.iter());
}
}
}
}
}
#[cfg(test)]
mod test {
use super::*;

@ -1,35 +1,37 @@
use crate::{
graphics::Rect,
gutter::{self, Gutter},
Document, DocumentId, ViewId,
};
use crate::{editor::GutterType, graphics::Rect, Document, DocumentId, ViewId};
use helix_core::{
pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction,
};
use std::fmt;
use std::{collections::VecDeque, fmt};
const JUMP_LIST_CAPACITY: usize = 30;
type Jump = (DocumentId, Selection);
#[derive(Debug, Clone)]
pub struct JumpList {
jumps: Vec<Jump>,
jumps: VecDeque<Jump>,
current: usize,
}
impl JumpList {
pub fn new(initial: Jump) -> Self {
Self {
jumps: vec![initial],
current: 0,
}
let mut jumps = VecDeque::with_capacity(JUMP_LIST_CAPACITY);
jumps.push_back(initial);
Self { jumps, current: 0 }
}
pub fn push(&mut self, jump: Jump) {
self.jumps.truncate(self.current);
// don't push duplicates
if self.jumps.last() != Some(&jump) {
self.jumps.push(jump);
if self.jumps.back() != Some(&jump) {
// If the jumplist is full, drop the oldest item.
while self.jumps.len() >= JUMP_LIST_CAPACITY {
self.jumps.pop_front();
}
self.jumps.push_back(jump);
self.current = self.jumps.len();
}
}
@ -61,8 +63,8 @@ impl JumpList {
self.jumps.retain(|(other_id, _)| other_id != doc_id);
}
pub fn get(&self) -> &[Jump] {
&self.jumps
pub fn iter(&self) -> impl Iterator<Item = &Jump> {
self.jumps.iter()
}
/// Applies a [`Transaction`] of changes to the jumplist.
@ -98,11 +100,8 @@ pub struct View {
pub last_modified_docs: [Option<DocumentId>; 2],
/// used to store previous selections of tree-sitter objects
pub object_selections: Vec<Selection>,
/// Gutter (constructor) and width of gutter, used to calculate
/// `gutter_offset`
gutters: Vec<(Gutter, usize)>,
/// cached total width of gutter
gutter_offset: u16,
/// GutterTypes used to fetch Gutter (constructor) and width for rendering
gutters: Vec<GutterType>,
}
impl fmt::Debug for View {
@ -117,28 +116,6 @@ impl fmt::Debug for View {
impl View {
pub fn new(doc: DocumentId, gutter_types: Vec<crate::editor::GutterType>) -> Self {
let mut gutters: Vec<(Gutter, usize)> = vec![];
let mut gutter_offset = 0;
use crate::editor::GutterType;
for gutter_type in &gutter_types {
let width = match gutter_type {
GutterType::Diagnostics => 1,
GutterType::LineNumbers => 5,
GutterType::Spacer => 1,
};
gutter_offset += width;
gutters.push((
match gutter_type {
GutterType::Diagnostics => gutter::diagnostics_or_breakpoints,
GutterType::LineNumbers => gutter::line_numbers,
GutterType::Spacer => gutter::padding,
},
width as usize,
));
}
if !gutter_types.is_empty() {
gutter_offset += 1;
}
Self {
id: ViewId::default(),
doc,
@ -148,8 +125,7 @@ impl View {
docs_access_history: Vec::new(),
last_modified_docs: [None, None],
object_selections: Vec::new(),
gutters,
gutter_offset,
gutters: gutter_types,
}
}
@ -160,15 +136,32 @@ impl View {
self.docs_access_history.push(id);
}
pub fn inner_area(&self) -> Rect {
// TODO add abilty to not use cached offset for runtime configurable gutter
self.area.clip_left(self.gutter_offset).clip_bottom(1) // -1 for statusline
pub fn inner_area(&self, doc: &Document) -> Rect {
self.area.clip_left(self.gutter_offset(doc)).clip_bottom(1) // -1 for statusline
}
pub fn inner_height(&self) -> usize {
self.area.clip_bottom(1).height.into() // -1 for statusline
}
pub fn gutters(&self) -> &[(Gutter, usize)] {
pub fn gutters(&self) -> &[GutterType] {
&self.gutters
}
pub fn gutter_offset(&self, doc: &Document) -> u16 {
let mut offset = self
.gutters
.iter()
.map(|gutter| gutter.width(self, doc) as u16)
.sum();
if offset > 0 {
offset += 1
}
offset
}
//
pub fn offset_coords_to_in_view(
&self,
@ -183,7 +176,7 @@ impl View {
let Position { col, row: line } =
visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
let inner_area = self.inner_area();
let inner_area = self.inner_area(doc);
let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
// - 1 so we have at least one gap in the middle.
@ -233,10 +226,9 @@ impl View {
/// Calculates the last visible line on screen
#[inline]
pub fn last_line(&self, doc: &Document) -> usize {
let height = self.inner_area().height;
std::cmp::min(
// Saturating subs to make it inclusive zero indexing.
(self.offset.row + height as usize).saturating_sub(1),
(self.offset.row + self.inner_height()).saturating_sub(1),
doc.text().len_lines().saturating_sub(1),
)
}
@ -270,12 +262,13 @@ impl View {
pub fn text_pos_at_screen_coords(
&self,
text: &RopeSlice,
doc: &Document,
row: u16,
column: u16,
tab_width: usize,
) -> Option<usize> {
let inner = self.inner_area();
let text = doc.text().slice(..);
let inner = self.inner_area(doc);
// 1 for status
if row < inner.top() || row >= inner.bottom() {
return None;
@ -293,7 +286,7 @@ impl View {
let text_col = (column - inner.x) as usize + self.offset.col;
Some(pos_at_visual_coords(
*text,
text,
Position {
row: text_row,
col: text_col,
@ -305,7 +298,7 @@ impl View {
/// Translates a screen position to position in the text document.
/// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
pub fn pos_at_screen_coords(&self, doc: &Document, row: u16, column: u16) -> Option<usize> {
self.text_pos_at_screen_coords(&doc.text().slice(..), row, column, doc.tab_width())
self.text_pos_at_screen_coords(doc, row, column, doc.tab_width())
}
/// Translates screen coordinates into coordinates on the gutter of the view.
@ -358,6 +351,7 @@ impl View {
/// which applies a transaction to the [`Document`] and view together.
pub fn apply(&mut self, transaction: &Transaction, doc: &Document) -> bool {
self.jumps.apply(transaction, doc);
// TODO: remove the boolean return. This is unused.
true
}
}
@ -366,9 +360,10 @@ impl View {
mod tests {
use super::*;
use helix_core::Rope;
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
const OFFSET: u16 = 4; // 1 diagnostic + 2 linenr (< 100 lines) + 1 gutter
const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
// const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
use crate::document::Document;
use crate::editor::GutterType;
#[test]
@ -379,45 +374,45 @@ mod tests {
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
let text = rope.slice(..);
let doc = Document::from(rope, None);
assert_eq!(view.text_pos_at_screen_coords(&text, 40, 2, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 2, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&text, 40, 41, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 41, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&text, 0, 2, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 2, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&text, 0, 49, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 49, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&text, 0, 41, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 41, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&text, 40, 81, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 81, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&text, 78, 41, 4), None);
assert_eq!(view.text_pos_at_screen_coords(&doc, 78, 41, 4), None);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 3, 4),
Some(3)
);
assert_eq!(view.text_pos_at_screen_coords(&text, 40, 80, 4), Some(3));
assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 80, 4), Some(3));
assert_eq!(
view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 1, 4),
view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 1, 4),
Some(4)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 4, 4),
view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 4, 4),
Some(5)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 7, 4),
view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 7, 4),
Some(8)
);
assert_eq!(view.text_pos_at_screen_coords(&text, 41, 80, 4), Some(8));
assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 80, 4), Some(8));
}
#[test]
@ -425,9 +420,9 @@ mod tests {
let mut view = View::new(DocumentId::default(), vec![GutterType::Diagnostics]);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
let text = rope.slice(..);
let doc = Document::from(rope, None);
assert_eq!(
view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET_WITHOUT_LINE_NUMBERS + 1, 4),
view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET_WITHOUT_LINE_NUMBERS + 1, 4),
Some(4)
);
}
@ -437,11 +432,8 @@ mod tests {
let mut view = View::new(DocumentId::default(), vec![]);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
let text = rope.slice(..);
assert_eq!(
view.text_pos_at_screen_coords(&text, 41, 40 + 1, 4),
Some(4)
);
let doc = Document::from(rope, None);
assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 40 + 1, 4), Some(4));
}
#[test]
@ -452,34 +444,34 @@ mod tests {
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hi! こんにちは皆さん");
let text = rope.slice(..);
let doc = Document::from(rope, None);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET, 4),
Some(0)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 4, 4),
Some(4)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 5, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 5, 4),
Some(4)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 6, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 6, 4),
Some(5)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 7, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 7, 4),
Some(5)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 8, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 8, 4),
Some(6)
);
}
@ -492,30 +484,30 @@ mod tests {
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hèl̀l̀ò world!");
let text = rope.slice(..);
let doc = Document::from(rope, None);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET, 4),
Some(0)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 1, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 1, 4),
Some(1)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 2, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 2, 4),
Some(3)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 3, 4),
Some(5)
);
assert_eq!(
view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 4, 4),
Some(7)
);
}

@ -50,7 +50,7 @@ args = { attachCommands = [ "platform select remote-gdb-server", "platform conne
[[grammar]]
name = "rust"
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "41e23b454f503e6fe63ec4b6d9f7f2cf7788ab8e" }
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
[[language]]
name = "toml"
@ -399,7 +399,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "typescript"
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a", subpath = "typescript" }
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "typescript" }
[[language]]
name = "tsx"
@ -413,7 +413,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "tsx"
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a", subpath = "tsx" }
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
[[language]]
name = "css"
@ -454,7 +454,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "html"
source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "d93af487cc75120c89257195e6be46c999c6ba18" }
source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53d8f4f2335e61bf6418ab8958dac3282077a" }
[[language]]
name = "python"
@ -495,7 +495,7 @@ file-types = ["nix"]
shebangs = []
roots = []
comment-token = "#"
language-server = { command = "rnix-lsp" }
language-server = { command = "nil" }
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -617,7 +617,7 @@ indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "java"
source = { git = "https://github.com/tree-sitter/tree-sitter-java", rev = "bd6186c24d5eb13b4623efac9d944dcc095c0dad" }
source = { git = "https://github.com/tree-sitter/tree-sitter-java", rev = "09d650def6cdf7f479f4b78f595e9ef5b58ce31e" }
[[language]]
name = "ledger"
@ -732,7 +732,7 @@ source = { git = "https://github.com/ikatyang/tree-sitter-yaml", rev = "0e36bed1
name = "haskell"
scope = "source.haskell"
injection-regex = "haskell"
file-types = ["hs"]
file-types = ["hs", "hs-boot"]
roots = ["Setup.hs", "stack.yaml", "*.cabal"]
comment-token = "--"
language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
@ -831,7 +831,7 @@ injection-regex = "cmake"
[[grammar]]
name = "cmake"
source = { git = "https://github.com/uyha/tree-sitter-cmake", rev = "f6616f1e417ee8b62daf251aa1daa5d73781c596" }
source = { git = "https://github.com/uyha/tree-sitter-cmake", rev = "6e51463ef3052dd3b328322c22172eda093727ad" }
[[language]]
name = "make"
@ -997,8 +997,8 @@ source = { git = "https://github.com/UserNobody14/tree-sitter-dart", rev = "2d7f
[[language]]
name = "scala"
scope = "source.scala"
roots = ["build.sbt", "pom.xml"]
file-types = ["scala", "sbt"]
roots = ["build.sbt", "build.sc", "build.gradle", "pom.xml", ".scala-build"]
file-types = ["scala", "sbt", "sc"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-server = { command = "metals" }
@ -1184,7 +1184,7 @@ language-server = { command = "erlang_ls" }
[[grammar]]
name = "erlang"
source = { git = "https://github.com/the-mikedavis/tree-sitter-erlang", rev = "0e7d677d11a7379686c53c616825714ccb728059" }
source = { git = "https://github.com/the-mikedavis/tree-sitter-erlang", rev = "ce0ed253d72c199ab93caba7542b6f62075339c4" }
[[language]]
name = "kotlin"
@ -1263,7 +1263,7 @@ language-server = { command = "gleam", args = ["lsp"] }
[[grammar]]
name = "gleam"
source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "d7861b2a4b4d594c58bb4f1be5f1f4ee4c67e5c3" }
source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "d6cbdf3477fcdb0b4d811518a356f9b5cd1795ed" }
[[language]]
name = "ron"
@ -1355,10 +1355,12 @@ injection-regex = "heex"
file-types = ["heex"]
roots = ["mix.exs", "mix.lock"]
indent = { tab-width = 2, unit = " " }
language-server = { command = "elixir-ls" }
config = { elixirLS.dialyzerEnabled = false }
[[grammar]]
name = "heex"
source = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "881f1c805f51485a26ecd7865d15c9ef8d606a78" }
source = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
[[language]]
name = "sql"
@ -1508,7 +1510,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "meson"
source = { git = "https://github.com/bearcove/tree-sitter-meson", rev = "feea83be9225842490066522ced2d13eb9cce0bd" }
source = { git = "https://github.com/staysail/tree-sitter-meson", rev = "32a83e8f200c347232fa795636cfe60dde22957a" }
[[language]]
name = "sshclientconfig"
@ -1572,7 +1574,7 @@ indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "edoc"
source = { git = "https://github.com/the-mikedavis/tree-sitter-edoc", rev = "1691ec0aa7ad1ed9fa295590545f27e570d12d60" }
source = { git = "https://github.com/the-mikedavis/tree-sitter-edoc", rev = "74774af7b45dd9cefbf9510328fc6ff2374afc50" }
[[language]]
name = "jsdoc"
@ -1964,3 +1966,30 @@ roots = []
[[grammar]]
name = "ini"
source = { git = "https://github.com/justinmk/tree-sitter-ini", rev = "4d247fb876b4ae6b347687de4a179511bf67fcbc" }
[[language]]
name = "bicep"
scope = "source.bicep"
file-types = ["bicep"]
roots = []
auto-format = true
comment-token = "//"
indent = { tab-width = 2, unit = " "}
language-server = { command = "bicep-langserver" }
[[grammar]]
name = "bicep"
source = { git = "https://github.com/the-mikedavis/tree-sitter-bicep", rev = "d8e097fcfa143854861ef737161163a09cc2916b" }
[[language]]
name = "qml"
scope = "source.qml"
file-types = ["qml"]
roots = []
language-server = { command = "qmlls" }
indent = { tab-width = 4, unit = " " }
grammar = "qmljs"
[[grammar]]
name = "qmljs"
source = { git = "https://github.com/yuja/tree-sitter-qmljs", rev = "0b2b25bcaa7d4925d5f0dda16f6a99c588a437f1" }

@ -0,0 +1,73 @@
; Keywords
[
"module"
"var"
"param"
"import"
"resource"
"existing"
"if"
"targetScope"
"output"
] @keyword
; Functions
(decorator) @function.builtin
(functionCall) @function
(functionCall
(functionArgument
(variableAccess) @variable))
; Literals/Types
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
(resourceDeclaration
(string
(stringLiteral) @string.special))
(moduleDeclaration
(string
(stringLiteral) @string.special))
[
(string)
(stringLiteral)
] @string
(nullLiteral) @keyword
(booleanLiteral) @constant.builtin.boolean
(integerLiteral) @constant.numeric.integer
(comment) @comment
(string
(variableAccess
(identifier) @variable))
(type) @type
; Variables
(localVariable) @variable
; Statements
(object
(objectProperty
(identifier) @identifier))
(propertyAccess
(identifier) @identifier)
(ifCondition) @keyword.control.conditional

@ -1,4 +1,7 @@
(macro_def) @function.around
[
(macro_def)
(function_def)
] @function.around
(argument) @parameter.inside

@ -48,3 +48,5 @@
(language_identifier)
(quote_content)
] @markup.raw.block
(parameter) @variable.parameter

@ -16,5 +16,5 @@
(tag) @_tag
(argument) @injection.content)
(#eq? @_tag "@type")
(#set injection.language "erlang")
(#set injection.include-children))
(#set! injection.language "erlang")
(#set! injection.include-children))

@ -0,0 +1,13 @@
[
(after_block)
(anonymous_function)
(catch_block)
(do_block)
(else_block)
(rescue_block)
(stab_clause)
] @indent
[
"end"
] @outdent

@ -65,6 +65,37 @@
(function_capture module: (atom) @namespace)
(function_capture function: (atom) @function)
; Ignored variables
((variable) @comment.discard
(#match? @comment.discard "^_"))
; Parameters
; specs
((attribute
name: (atom) @keyword
(stab_clause
pattern: (arguments (variable) @variable.parameter)
body: (variable)? @variable.parameter))
(#match? @keyword "(spec|callback)"))
; functions
(function_clause pattern: (arguments (variable) @variable.parameter))
; anonymous functions
(stab_clause pattern: (arguments (variable) @variable.parameter))
; parametric types
((attribute
name: (atom) @keyword
(arguments
(binary_operator
left: (call (arguments (variable) @variable.parameter))
operator: "::")))
(#match? @keyword "(type|opaque)"))
; macros
((attribute
name: (atom) @keyword
(arguments
(call (arguments (variable) @variable.parameter))))
(#eq? @keyword "define"))
; Records
(record_content
(binary_operator
@ -94,10 +125,6 @@
(unary_operator operator: _ @operator)
["/" ":" "->"] @operator
(tripledot) @comment.discard
(comment) @comment
; Macros
(macro
"?"+ @constant
@ -109,11 +136,14 @@
name: (_) @keyword.directive)
; Comments
((variable) @comment.discard
(#match? @comment.discard "^_"))
(tripledot) @comment.discard
[(comment) (line_comment) (shebang)] @comment
; Basic types
(variable) @variable
((atom) @constant.builtin.boolean
(#match? @constant.builtin.boolean "^(true|false)$"))
(atom) @string.special.symbol
(string) @string
(character) @constant.character

@ -1,2 +1,7 @@
((comment_content) @injection.content
(#set! injection.language "edoc"))
((line_comment (comment_content) @injection.content)
(#set! injection.language "edoc")
(#set! injection.include-children)
(#set! injection.combined))
((comment (comment_content) @injection.content)
(#set! injection.language "comment"))

@ -0,0 +1,30 @@
; Specs and Callbacks
(attribute
(stab_clause
pattern: (arguments (variable) @local.definition)
; If a spec uses a variable as the return type (and later a `when` clause to type it):
body: (variable)? @local.definition)) @local.scope
; parametric `-type`s
((attribute
name: (atom) @_type
(arguments
(binary_operator
left: (call (arguments (variable) @local.definition))
operator: "::") @local.scope))
(#match? @_type "(type|opaque)"))
; macros
((attribute
name: (atom) @_define
(arguments
(call (arguments (variable) @local.definition)))) @local.scope
(#eq? @_define "define"))
; `fun`s
(anonymous_function (stab_clause pattern: (arguments (variable) @local.definition))) @local.scope
; Ordinary functions
(function_clause pattern: (arguments (variable) @local.definition)) @local.scope
(variable) @local.reference

@ -80,6 +80,7 @@
"todo"
"try"
"type"
"use"
] @keyword
; Punctuation
@ -103,4 +104,5 @@
"->"
".."
"-"
"<-"
] @punctuation.delimiter

@ -6,7 +6,11 @@
; <%= if true do %>
; <p>hello, tree-sitter!</p>
; <% end %>
((directive (partial_expression_value) @injection.content)
((directive
[
(partial_expression_value)
(ending_expression_value)
] @injection.content)
(#set! injection.language "elixir")
(#set! injection.include-children)
(#set! injection.combined))

@ -14,6 +14,7 @@
">"
"</"
"/>"
"<!"
] @punctuation.bracket
"=" @punctuation.delimiter

@ -21,6 +21,8 @@
name: (identifier) @type)
(class_declaration
name: (identifier) @type)
(record_declaration
name: (identifier) @type)
(enum_declaration
name: (identifier) @type)
@ -33,6 +35,8 @@
(constructor_declaration
name: (identifier) @type)
(compact_constructor_declaration
name: (identifier) @type)
(type_identifier) @type
@ -59,6 +63,7 @@
(hex_integer_literal)
(decimal_integer_literal)
(octal_integer_literal)
(binary_integer_literal)
] @constant.numeric.integer
[
@ -67,7 +72,11 @@
] @constant.numeric.float
(character_literal) @constant.character
(string_literal) @string
[
(string_literal)
(text_block)
] @string
[
(true)
@ -75,7 +84,8 @@
(null_literal)
] @constant.builtin
(comment) @comment
(line_comment) @comment
(block_comment) @comment
; Keywords
@ -104,15 +114,19 @@
"module"
"native"
"new"
"non-sealed"
"open"
"opens"
"package"
"permits"
"private"
"protected"
"provides"
"public"
"requires"
"record"
"return"
"sealed"
"static"
"strictfp"
"switch"
@ -127,4 +141,5 @@
"volatile"
"while"
"with"
"yield"
] @keyword

@ -1,2 +1,2 @@
((comment) @injection.content
([(line_comment) (block_comment)] @injection.content
(#set! injection.language "comment"))

@ -0,0 +1,35 @@
(method_declaration
body: (_) @function.inside) @function.around
(interface_declaration
body: (_) @class.inside) @class.around
(class_declaration
body: (_) @class.inside) @class.around
(record_declaration
body: (_) @class.inside) @class.around
(enum_declaration
body: (_) @class.inside) @class.around
(formal_parameters
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(type_parameters
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(type_arguments
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(argument_list
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
[
(line_comment)
(block_comment)
] @comment.inside
(line_comment)+ @comment.around
(block_comment) @comment.around

@ -1,62 +1,62 @@
(string_literal) @string
(comment) @comment
(boolean_literal) @constant.builtin.boolean
(integer_literal) @constant.numeric.integer
(comment) @comment.line
(function_id) @function
(keyword_arg_key) @variable.other.member
(id_expression) @variable
; these are listed first, because they override keyword queries
(function_expression (identifier) @function)
[
"if"
"elif"
"else"
"endif"
] @keyword.control.conditional
(assignment_operator)
(additive_operator)
(multiplicative_operator)
(equality_operator)
">="
"<="
"<"
">"
"+"
"-"
] @operator
[
"foreach"
"endforeach"
] @keyword.control.repeat
(and)
(or)
(not)
(in)
] @keyword.operator
[
"break"
"continue"
] @keyword.control
"(" ")" "[" "]" "{" "}"
] @punctuation.bracket
[
"not"
"in"
"and"
"or"
] @keyword.operator
(if)
(elif)
(else)
(endif)
] @keyword.control.conditional
[
"!"
"+"
"-"
"*"
"/"
"%"
"=="
"!="
">"
"<"
">="
"<="
] @operator
(foreach)
(endforeach)
(break)
(continue)
] @keyword.control.repeat
(boolean_literal) @constant.builtin.boolean
(int_literal) @constant.numeric.integer
(keyword_argument keyword: (identifier) @variable.parameter)
(escape_sequence) @constant.character.escape
(bad_escape) @warning
[
":"
","
"."
","
":"
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
(string_literal)
(fstring_literal)
] @string
(identifier) @variable

@ -1,5 +1,5 @@
; Indentation queries for helix
[
(method_expression)
(function_expression)
(array_literal)
(dictionary_literal)
@ -7,10 +7,11 @@
(iteration_statement)
] @indent
; question - what about else, elif
[
")"
"]"
"}"
"endif"
"endforeach"
(endif)
(endforeach)
] @outdent

@ -0,0 +1,90 @@
(comment) @comment
(ui_import
source: _ @namespace
version: _? @constant
alias: _? @namespace)
(ui_pragma
name: (identifier) @attribute
value: (identifier)? @constant)
(ui_annotation
"@" @punctuation
type_name: _ @type)
;;; Declarations
(enum_declaration
name: (identifier) @type)
(enum_assignment
name: (identifier) @constant
value: _ @constant)
(enum_body
name: (identifier) @constant)
(ui_inline_component
name: (identifier) @type)
(ui_object_definition
type_name: _ @type)
(ui_object_definition_binding
type_name: _ @type
name: _ @variable.other.member)
(ui_property
type: _ @type
name: (identifier) @variable.other.member)
(ui_signal
name: (identifier) @function)
(ui_signal_parameter
name: (identifier) @variable.parameter
type: _ @type)
(ui_signal_parameter
type: _ @type
name: (identifier) @variable.parameter);;; Properties and bindings
;;; Bindings
(ui_binding
name: _ @variable.other.member)
;;; Other
[
"("
")"
"{"
"}"
] @punctuation.bracket
(ui_list_property_type [
"<"
">"
] @punctuation.bracket)
[
","
"."
":"
] @punctuation.delimiter
[
"as"
"component"
"default"
"enum"
"import"
"on"
"pragma"
"property"
"readonly"
"required"
"signal"
] @keyword

@ -0,0 +1,6 @@
[
(enum_body)
(ui_object_initializer)
] @indent
"}" @outdent

@ -0,0 +1,16 @@
((comment) @injection.content
(#set! injection.language "comment"))
([
(empty_statement)
(expression_statement)
(function_declaration)
(generator_function_declaration)
(statement_block)
(switch_statement)
(try_statement)
(variable_declaration)
(with_statement)
] @injection.content
(#set! injection.include-children)
(#set! injection.language "javascript"))

@ -1,11 +1,6 @@
; Class
(class) @class.around
(class [(constant) (scope_resolution)] !superclass
(_)+ @class.inside)
(class [(constant) (scope_resolution)] (superclass)
(_)+ @class.inside)
; Class and Modules
(class
body: (_)? @class.inside) @class.around
(singleton_class
value: (_)
@ -17,37 +12,32 @@
(#match? @class_const "Class")
(#match? @class_method "new")
(do_block (_)+ @class.inside)) @class.around
(module
body: (_)? @class.inside) @class.around
; Functions
(method) @function.around
; Functions and Blocks
(singleton_method
body: (_)? @function.inside) @function.around
(method (identifier) (method_parameters)
(_)+ @function.inside)
(do_block !parameters
(_)+ @function.inside)
(do_block (block_parameters)
(_)+ @function.inside)
(block (block_parameters)
(_)+ @function.inside)
(block !parameters
(_)+ @function.inside)
(method (identifier) !parameters
(_)+ @function.inside)
(method
body: (_)? @function.inside) @function.around
(do_block
body: (_)? @function.inside) @function.around
(block
body: (_)? @function.inside) @function.around
; Parameters
(method_parameters
(_) @parameter.inside)
(_) @parameter.inside) @parameter.around
(block_parameters
(_) @parameter.inside)
(_) @parameter.inside) @parameter.around
(lambda_parameters
(_) @parameter.inside)
(_) @parameter.inside) @parameter.around
; Comments
(comment) @comment.inside

@ -271,10 +271,14 @@
; ---
; Macros
; ---
(meta_item
(attribute
(identifier) @function.macro)
(attr_item
(identifier) @function.macro
(attribute
[
(identifier) @function.macro
(scoped_identifier
name: (identifier) @function.macro)
]
(token_tree (identifier) @function.macro)?)
(inner_attribute_item) @attribute

@ -51,13 +51,14 @@
.
(_) @expr-start
value: (_) @indent
alternative: (_)? @indent
(#not-same-line? @indent @expr-start)
(#set! "scope" "all")
)
(if_let_expression
(if_expression
.
(_) @expr-start
value: (_) @indent
condition: (_) @indent
(#not-same-line? @indent @expr-start)
(#set! "scope" "all")
)

@ -42,7 +42,7 @@
(; #[test]
(attribute_item
(meta_item
(attribute
(identifier) @_test_attribute))
; allow other attributes like #[should_panic] and comments
[

@ -34,6 +34,7 @@
"implements"
"keyof"
"namespace"
"override"
] @keyword
[
@ -62,3 +63,15 @@
((identifier) @type
(#match? @type "^[A-Z]"))
; Literals
[
(template_literal_type)
] @string
; Tokens
(template_type
"${" @punctuation.special
"}" @punctuation.special) @embedded

@ -6,7 +6,6 @@
(ContainerDecl)
(ErrorUnionExpr)
(InitList)
(Statement)
(SwitchExpr)
(TestDecl)
] @indent

@ -2,56 +2,56 @@
# Based on the AYU theme colors from https://github.com/dempfi/ayu
# Syntax highlighting
"type" = { fg = "blue" }
"type.builtin" = { fg = "blue" }
"constructor" = { fg = "green" }
"constant" = { fg = "magenta" }
"string" = { fg = "green" }
"string.regexp" = { fg = "orange" }
"string.special" = { fg = "yellow" }
"type" = "blue"
"type.builtin" = "blue"
"constructor" = "green"
"constant" = "magenta"
"string" = "green"
"string.regexp" = "orange"
"string.special" = "yellow"
"comment" = { fg = "gray", modifiers = ["italic"] }
"variable" = { fg = "foreground" }
"variable.parameter" = { fg = "yellow" }
"label" = { fg = "orange" }
"punctuation" = { fg = "foreground" }
"keyword" = { fg = "orange" }
"keyword.control" = { fg = "yellow" }
"keyword.directive" = { fg = "yellow" }
"operator" = { fg = "orange" }
"function" = { fg = "yellow", modifiers = ["bold"] }
"tag" = { fg = "blue" }
"namespace" = { fg = "blue" }
"markup.heading" = { fg = "orange" }
"markup.list" = { fg = "yellow" }
"comment.block.documentation" = { fg = "blue", modifiers = ["italic"] }
"variable" = "foreground"
"label" = "orange"
"punctuation" = "foreground"
"keyword" = "orange"
"keyword.control" = "yellow"
"keyword.directive" = "yellow"
"operator" = "orange"
"function" = "yellow"
"tag" = "blue"
"namespace" = "blue"
"markup.heading" = "orange"
"markup.list" = "yellow"
"markup.raw.block" = { bg = "gray", fg = "orange" }
"markup.link.url" = { fg = "blue" }
"markup.link.text" = { fg = "yellow" }
"markup.link.label" = { fg = "green" }
"markup.quote" = { fg = "yellow" }
"diff.plus" = { fg = "green" }
"diff.minus" = { fg = "red" }
"diff.delta" = { fg = "green" }
"markup.link.url" = "blue"
"markup.link.text" = "yellow"
"markup.link.label" = "green"
"markup.quote" = "yellow"
"diff.plus" = "green"
"diff.minus" = "red"
"diff.delta" = "yellow"
# Interface
"ui.background"= { bg = "background" }
"ui.cursor" = { bg = "yellow", fg = "dark_gray" }
"ui.cursor.match" = { fg = "orange" }
"ui.linenr" = { fg = "dark_gray" }
"ui.linenr.selected" = { fg = "orange" }
"ui.cursor" = { modifiers = ["reversed"] }
"ui.cursor.match" = "orange"
"ui.linenr" = "dark_gray"
"ui.linenr.selected" = "gray"
"ui.statusline" = { fg = "foreground", bg = "black" }
"ui.cursorline" = { bg = "black" }
"ui.popup" = { fg = "#7B91b3", bg = "black" }
"ui.window" = { fg = "dark_gray" }
"ui.window" = "dark_gray"
"ui.help" = { fg = "#7B91b3", bg = "black" }
"ui.text" = { fg = "foreground" }
"ui.text" = "foreground"
"ui.text.focus" = { bg = "dark_gray", fg = "foreground" }
"ui.text.info" = { fg = "foreground" }
"ui.virtual.whitespace" = { fg = "dark_gray" }
"ui.text.info" = "foreground"
"ui.virtual.whitespace" = "dark_gray"
"ui.virtual.ruler" = { bg = "black" }
"ui.menu" = { fg = "foreground", bg = "black" }
"ui.menu.selected" = { bg = "orange", fg = "background" }
"ui.menu.selected" = { bg = "gray", fg = "background" }
"ui.selection" = { bg = "dark_gray" }
"warning" = { fg = "yellow" }
"warning" = "yellow"
"error" = { fg = "red", modifiers = ["bold"] }
"info" = { fg = "blue", modifiers = ["bold"] }
"hint" = { fg = "blue", modifiers = ["bold"] }
@ -62,7 +62,7 @@
"ui.bufferline" = { fg = "gray", bg = "background" }
"ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" }
"special" = { fg = "orange" }
"special" = "orange"
[palette]
background = "#0f1419"

@ -2,55 +2,56 @@
# Based on the AYU theme colors from https://github.com/dempfi/ayu
# Syntax highlighting
"type" = { fg = "blue" }
"type.builtin" = { fg = "blue" }
"constructor" = { fg = "green" }
"constant" = { fg = "magenta" }
"string" = { fg = "green" }
"string.regexp" = { fg = "orange" }
"string.special" = { fg = "yellow" }
"type" = "blue"
"type.builtin" = "blue"
"constructor" = "green"
"constant" = "magenta"
"string" = "green"
"string.regexp" = "orange"
"string.special" = "yellow"
"comment" = { fg = "gray", modifiers = ["italic"] }
"variable" = { fg = "foreground" }
"variable.parameter" = { fg = "yellow" }
"label" = { fg = "orange" }
"punctuation" = { fg = "foreground" }
"keyword" = { fg = "orange" }
"keyword.control" = { fg = "yellow" }
"keyword.directive" = { fg = "yellow" }
"operator" = { fg = "orange" }
"function" = { fg = "yellow", modifiers = ["bold"] }
"tag" = { fg = "blue" }
"namespace" = { fg = "blue" }
"markup.heading" = { fg = "orange" }
"markup.list" = { fg = "yellow" }
"comment.block.documentation" = { fg = "blue", modifiers = ["italic"] }
"variable" = "foreground"
"label" = "orange"
"punctuation" = "foreground"
"keyword" = "orange"
"keyword.control" = "yellow"
"keyword.directive" = "yellow"
"operator" = "orange"
"function" = "yellow"
"tag" = "blue"
"namespace" = "blue"
"markup.heading" = "orange"
"markup.list" = "yellow"
"markup.raw.block" = { bg = "gray", fg = "orange" }
"markup.link.url" = { fg = "blue" }
"markup.link.text" = { fg = "yellow" }
"markup.link.label" = { fg = "green" }
"markup.quote" = { fg = "yellow" }
"diff.plus" = { fg = "green" }
"diff.minus" = { fg = "red" }
"diff.delta" = { fg = "green" }
"markup.link.url" = "blue"
"markup.link.text" = "yellow"
"markup.link.label" = "green"
"markup.quote" = "yellow"
"diff.plus" = "green"
"diff.minus" = "red"
"diff.delta" = "yellow"
# Interface
"ui.background"= { bg = "background" }
"ui.cursor" = { bg = "yellow", fg = "light_gray" }
"ui.cursor.match" = { fg = "orange" }
"ui.linenr" = { fg = "light_gray" }
"ui.linenr.selected" = { fg = "orange" }
"ui.cursor" = { modifiers = ["reversed"] }
"ui.cursor.match" = "orange"
"ui.linenr" = "dark_gray"
"ui.linenr.selected" = "gray"
"ui.statusline" = { fg = "foreground", bg = "black" }
"ui.cursorline" = { bg = "black" }
"ui.popup" = { bg = "black" }
"ui.window" = { fg = "light_gray" }
"ui.help" = { fg = "foreground", bg = "black" }
"ui.text" = { fg = "foreground" }
"ui.text.focus" = { bg = "light_gray", fg = "foreground" }
"ui.text.info" = { fg = "foreground" }
"ui.virtual.whitespace" = { fg = "light_gray" }
"ui.popup" = { fg = "#7B91b3", bg = "black" }
"ui.window" = "dark_gray"
"ui.help" = { fg = "#7B91b3", bg = "black" }
"ui.text" = "foreground"
"ui.text.focus" = { bg = "dark_gray", fg = "foreground" }
"ui.text.info" = "foreground"
"ui.virtual.whitespace" = "dark_gray"
"ui.virtual.ruler" = { bg = "black" }
"ui.menu" = { fg = "foreground", bg = "black" }
"ui.menu.selected" = { bg = "orange", fg = "background" }
"ui.selection" = { bg = "light_gray" }
"warning" = { fg = "yellow" }
"ui.menu.selected" = { bg = "gray", fg = "background" }
"ui.selection" = { bg = "dark_gray" }
"warning" = "yellow"
"error" = { fg = "red", modifiers = ["bold"] }
"info" = { fg = "blue", modifiers = ["bold"] }
"hint" = { fg = "blue", modifiers = ["bold"] }
@ -58,30 +59,22 @@
"diagnostic.info"= { fg = "blue", modifiers = ["underlined"] }
"diagnostic.warning"= { fg = "yellow", modifiers = ["underlined"] }
"diagnostic.error"= { fg = "red", modifiers = ["underlined"] }
"ui.bufferline" = { fg = "ui_foreground", bg = "ui_background" }
"ui.bufferline.active" = { fg = "ui_background", bg = "ui_foreground" }
"ui.statusline" = { fg = "ui_foreground", bg = "ui_background" }
"ui.statusline.inactive" = { fg = "ui_foreground", bg = "ui_background" }
"ui.statusline.normal" = { fg = "white", bg = "light_blue" }
"ui.statusline.insert" = { fg = "white", bg = "orange" }
"ui.statusline.select" = { fg = "white", bg = "magenta" }
"ui.bufferline" = { fg = "gray", bg = "background" }
"ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" }
"special" = { fg = "orange" }
"special" = "orange"
[palette]
background = "#fcfcfc"
background = "#fafafa"
foreground = "#5c6166"
ui_foreground = "#8a9199"
ui_background = "#f8f9fa"
black = "#e7eaed"
white = "#fcfcfc"
blue = "#399ee6"
light_blue = "#55b4d4"
cyan = "#478acc"
light_gray = "#e7eaed"
gray = "#787b8099"
dark_gray = "#d8d8d7"
gray = "#828c9a"
green = "#86b300"
magenta = "#a37acc"
orange = "#fa8d3e"

@ -2,57 +2,56 @@
# Based on the AYU theme colors from https://github.com/dempfi/ayu
# Syntax highlighting
"type" = { fg = "blue" }
"type.builtin" = { fg = "blue" }
"constructor" = { fg = "green" }
"constant" = { fg = "magenta" }
"string" = { fg = "green" }
"string.regexp" = { fg = "orange" }
"string.special" = { fg = "yellow" }
"type" = "blue"
"type.builtin" = "blue"
"constructor" = "green"
"constant" = "magenta"
"string" = "green"
"string.regexp" = "orange"
"string.special" = "yellow"
"comment" = { fg = "gray", modifiers = ["italic"] }
"variable" = { fg = "foreground" }
"variable.parameter" = { fg = "yellow" }
"label" = { fg = "orange" }
"punctuation" = { fg = "foreground" }
"keyword" = { fg = "orange" }
"keyword.control" = { fg = "yellow" }
"keyword.directive" = { fg = "yellow" }
"operator" = { fg = "orange" }
"function" = { fg = "yellow", modifiers = ["bold"] }
"tag" = { fg = "blue" }
"namespace" = { fg = "blue" }
"markup.heading" = { fg = "orange" }
"markup.list" = { fg = "yellow" }
"comment.block.documentation" = { fg = "blue", modifiers = ["italic"] }
"variable" = "foreground"
"label" = "orange"
"punctuation" = "foreground"
"keyword" = "orange"
"keyword.control" = "yellow"
"keyword.directive" = "yellow"
"operator" = "orange"
"function" = "yellow"
"tag" = "blue"
"namespace" = "blue"
"markup.heading" = "orange"
"markup.list" = "yellow"
"markup.raw.block" = { bg = "gray", fg = "orange" }
"markup.link.url" = { fg = "blue" }
"markup.link.text" = { fg = "yellow" }
"markup.link.label" = { fg = "green" }
"markup.quote" = { fg = "yellow" }
"diff.plus" = { fg = "green" }
"diff.minus" = { fg = "red" }
"diff.delta" = { fg = "green" }
"markup.link.url" = "blue"
"markup.link.text" = "yellow"
"markup.link.label" = "green"
"markup.quote" = "yellow"
"diff.plus" = "green"
"diff.minus" = "red"
"diff.delta" = "yellow"
# Interface
"ui.background"= { bg = "background" }
"ui.cursor" = { bg = "green", fg = "dark_gray" }
"ui.cursor.primary" = { bg = "orange", fg = "dark_gray" }
"ui.cursor.match" = { fg = "orange" }
"ui.linenr" = { fg = "dark_gray" }
"ui.linenr.selected" = { fg = "orange" }
"ui.cursorline" = { bg = "black" }
"ui.cursor" = { modifiers = ["reversed"] }
"ui.cursor.match" = "orange"
"ui.linenr" = "dark_gray"
"ui.linenr.selected" = "gray"
"ui.statusline" = { fg = "foreground", bg = "black" }
"ui.popup" = { bg = "black" }
"ui.window" = { fg = "dark_gray" }
"ui.help" = { fg = "foreground", bg = "black" }
"ui.text" = { fg = "foreground" }
"ui.cursorline" = { bg = "black" }
"ui.popup" = { fg = "#7B91b3", bg = "black" }
"ui.window" = "dark_gray"
"ui.help" = { fg = "#7B91b3", bg = "black" }
"ui.text" = "foreground"
"ui.text.focus" = { bg = "dark_gray", fg = "foreground" }
"ui.text.info" = { fg = "foreground" }
"ui.virtual.whitespace" = { fg = "dark_gray" }
"ui.text.info" = "foreground"
"ui.virtual.whitespace" = "dark_gray"
"ui.virtual.ruler" = { bg = "black" }
"ui.menu" = { fg = "foreground", bg = "black" }
"ui.menu.selected" = { bg = "orange", fg = "background" }
"ui.menu.selected" = { bg = "gray", fg = "background" }
"ui.selection" = { bg = "dark_gray" }
"warning" = { fg = "yellow" }
"warning" = "yellow"
"error" = { fg = "red", modifiers = ["bold"] }
"info" = { fg = "blue", modifiers = ["bold"] }
"hint" = { fg = "blue", modifiers = ["bold"] }
@ -60,19 +59,19 @@
"diagnostic.info"= { fg = "blue", modifiers = ["underlined"] }
"diagnostic.warning"= { fg = "yellow", modifiers = ["underlined"] }
"diagnostic.error"= { fg = "red", modifiers = ["underlined"] }
"ui.bufferline" = { fg = "gray", bg = "black" }
"ui.bufferline.active" = { fg = "foreground", bg = "background" }
"ui.bufferline" = { fg = "gray", bg = "background" }
"ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" }
"special" = { fg = "orange" }
"special" = "orange"
[palette]
background = "#242936"
background = "#1f2430"
foreground = "#cccac2"
black = "#1a1f29"
blue = "#73d0ff"
dark_gray = "#8a919959"
cyan = "#80bfff"
dark_gray = "#323843"
cyan = "#444b55"
gray = "#565b66"
green = "#d5ff80"
magenta = "#dfbfff"

@ -49,9 +49,9 @@
"markup.quote" = "dark_green"
"markup.raw" = "orange"
"diff.plus" = { fg = "pale_green" }
"diff.delta" = { fg = "gold" }
"diff.minus" = { fg = "red" }
"diff.plus" = { fg = "dark_green2" }
"diff.delta" = { fg = "blue4" }
"diff.minus" = { fg = "orange_red" }
"ui.background" = { fg = "light_gray", bg = "dark_gray2" }
@ -75,6 +75,8 @@
"ui.cursorline.primary" = { bg = "dark_gray3" }
"ui.statusline" = { fg = "white", bg = "blue" }
"ui.statusline.inactive" = { fg = "white", bg = "blue" }
"ui.statusline.insert" = { fg = "white", bg = "yellow" }
"ui.statusline.select" = { fg = "white", bg = "magenta" }
"ui.bufferline" = { fg = "text", bg = "widget" }
"ui.bufferline.active" = { fg = "white", bg = "blue" }
@ -108,6 +110,7 @@ gold = "#d7ba7d"
gold2 = "#cca700"
pale_green = "#b5cea8"
dark_green = "#6A9955"
dark_green2 = "#487e02"
light_gray = "#d4d4d4"
light_gray2 = "#c6c6c6"
light_gray3 = "#eeeeee"
@ -118,10 +121,12 @@ dark_gray4 = "#404040"
blue = "#007acc"
blue2 = "#569CD6"
blue3 = "#6796E6"
blue4 = "#1b81a8"
light_blue = "#75beff"
dark_blue = "#264f78"
dark_blue2 = "#094771"
red = "#ff1212"
orange_red = "#f14c4c"
type = "#4EC9B0"
special = "#C586C0"

@ -1,101 +1,116 @@
# Author: Kristoffer Flottorp <kr.fl@outlook.com>
# A take on the JetBrains Fleet theme sprinkled with some creative freedom
"type" = { fg = "yellow" } # .builtin
"constructor" = { fg = "yellow" }
"constant" = { fg = "cyan" }
"type" = "light_blue"
"type.builtin" = "orange"
"constructor" = "yellow"
"constant" = "cyan"
# "constant.builtin" = {} # .boolean
"constant.builtin.boolean" = { fg = "cyan" } # .boolean
# "constant.character" = {} #.escape
"constant.numeric" = { fg = "yellow" } # .integer / .float
"string" = { fg = "pink" } # .regexp
# "string.special" = {} #.path / .url / .symbol
"string.special" = { modifiers = ["underlined"] } #.path / .url / .symbol
"comment" = { fg = "dark_gray" } # .line
"constant.builtin.boolean" = "yellow"
"constant.character" = "yellow"
"constant.characted.escape" = "light"
"constant.numeric" = "yellow"
"string" = "pink"
"string.regexp" = "light"
"string.special" = { fg = "yellow", modifiers = ["underlined"] } #.path / .url / .symbol
"comment" = "light_gray" # .line
# "comment.block" = {} # .documentation
"variable" = { fg = "light" } # .builtin / .parameter
"variable" = "light" # .builtin
"variable.parameter" = "light"
# "variable.other" = {} # .member
"variable.other.member" = { fg = "purple" }
"label" = { fg = "yellow" }
# "punctuation" = {} # .delimiter / .bracket
"keyword" = { fg = "cyan" } # .operator / .directive / .function
# "keyword.control" = { fg = "orange" } # .conditional / .repeat / .import / .return / .exception
"operator" = { fg = "light" }
"function" = { fg = "blue" } # .builtin / .method / .macro / .special
"function.macro" = { fg = "green" }
"function.special" = { fg = "green" }
"tag" = { fg = "green"}
"special" = { fg = "green" }
"namespace" = { fg = "light" }
"markup" = { fg = "purple" } # .bold / .italic / .quote
"markup.heading" = { fg = "light" } # .marker / .1 / .2 / .3 / .4 / .5 / .6
"markup.heading.1" = { fg = "yellow" }
"markup.heading.2" = { fg = "green" }
"markup.heading.3" = { fg = "pink" }
"markup.heading.4" = { fg = "purple" }
"markup.heading.5" = { fg = "cyan" }
"markup.heading.6" = { fg = "blue" }
"markup.list" = { fg = "cyan" } # .unnumbered / .numbered
"markup.link" = { fg = "green" } # .url / .label / .text
"markup.raw" = { fg = "pink" } # .inline / .block
# "diff" = {} # .plus / .minus
"diff.plus" = { fg = "cyan" }
"diff.minus" = { fg = "yellow" }
"diff.delta" = { fg = "purple" } # .moved
"variable.other.member" = "yellow"
"label" = "yellow"
"punctuation" = "light" # .delimiter / .bracket
"keyword" = "cyan" # .operator / .directive / .function
"keyword.control" = "yellow" # .conditional / .repeat / .import / .return / .exception
"operator" = "light"
"function" = "yellow"
"function.macro" = "green"
"function.builtin" = "green"
"function.special" = "green"
"function.method" = "light"
"tag" = "green"
"special" = "green"
"namespace" = "light"
# used in theming
# "markup.normal" = {} # .completion / .hover
# "markup.heading" = {} # .completion / .hover
# "markup.raw.inline" = {} # .completion / .hover
"markup" = "purple" # .quote
"markup.bold" = { fg = "purple", modifiers = ["bold"] }
"markup.italic" = { fg = "purple", modifiers = ["italic"] }
"markup.heading" = "light" # .marker
"markup.heading.1" = "yellow"
"markup.heading.2" = "green"
"markup.heading.3" = "pink"
"markup.heading.4" = "purple"
"markup.heading.5" = "cyan"
"markup.heading.6" = "light_blue"
"markup.list" = "cyan" # .unnumbered / .numbered
"markup.link" = "green"
"markup.link.url" = "pink"
"markup.link.text" = "cyan"
"markup.link.label" = "yellow"
"markup.raw" = "pink" # .inline
"markup.raw.block" = "orange"
"diff.plus" = "cyan"
"diff.minus" = "yellow"
"diff.delta" = "purple"
# ui specific
"ui.background" = { bg = "darkest" } # .separator
"ui.background" = { bg = "#0d0d0d" } # .separator
"ui.cursor" = { bg = "dark_gray", modifiers = ["reversed"] } # .insert / .select / .match / .primary
"ui.cursor.match" = { fg = "light", bg = "blue_accent" } # .insert / .select / .match / .primary
"ui.cursorline" = { bg = "darker" }
"ui.linenr" = { fg = "dark_gray" } # .selected
"ui.linenr" = "dark_gray"
"ui.linenr.selected" = { fg = "light_gray", bg = "darker" }
"ui.statusline" = { fg = "light", bg = "darker" } # .inactive / .normal / .insert / .select
"ui.statusline.inactive" = { fg = "dark", bg = "darker" } # .inactive / .normal / .insert / .select
"ui.statusline.normal" = { fg = "lightest", bg = "darker"} # .inactive / .normal / .insert / .select
"ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" } # .inactive / .normal / .insert / .select
"ui.statusline.select" = { fg = "lightest", bg = "orange_accent" } # .inactive / .normal / .insert / .select
"ui.popup" = { fg = "light", bg = "dark" } # .info
"ui.statusline.inactive" = { fg = "dark", bg = "darker" }
"ui.statusline.normal" = { fg = "lightest", bg = "darker"}
"ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" }
"ui.statusline.select" = { fg = "lightest", bg = "orange_accent" }
"ui.popup" = { fg = "light", bg = "darkest" } # .info
"ui.window" = { fg = "dark", bg = "darkest" }
"ui.help" = { fg = "light", bg = "dark" }
"ui.text" = { fg = "light" } # .focus / .info
"ui.virtual" = { fg = "dark" } # .ruler / .whitespace
"ui.help" = { fg = "light", bg = "darkest" }
"ui.text" = "light" # .focus / .info
"ui.virtual" = "dark" # .whitespace
"ui.virtual.ruler" = { bg = "darker"}
"ui.menu" = { fg = "light", bg = "dark" } # .selected
"ui.menu" = { fg = "light", bg = "darker" } # .selected
"ui.menu.selected" = { fg = "lightest", bg = "blue_accent" } # .selected
"ui.selection" = { bg = "darker" } # .primary
"ui.selection.primary" = { bg = "select" } # .primary
"hint" = { fg = "blue_accent"}
"info" = { fg = "yellow_accent" }
"warning" = { fg = "orange_accent" }
"error" = { fg = "diff_red_accent" }
"diagnostic". underline = { style = "curl" }
"hint" = "blue"
"info" = "yellow_accent"
"warning" = "orange_accent"
"error" = "red"
"diagnostic" = { modifiers = [] }
"diagnostic.hint" = { underline = { color = "light", style = "curl" } }
"diagnostic.info" = { underline = { color = "blue", style = "curl" } }
"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } }
"diagnostic.error" = { underline = { color = "red", style = "curl" } }
[palette]
darkest = "#0F0F0F"
darker = "#222222"
dark = "#383838"
select = "#102F5B"
darkest = "#1e1e1e"
darker = "#262626"
dark = "#898989"
select = "#102f5b"
light = "#F0F0F0"
lightest = "#FFFFFF"
light = "#d6d6dd"
lightest = "#ffffff"
dark_gray = "#5B5B5B"
light_gray = "#757575"
purple = "#AC9CF9"
blue = "#52A7F6" #"#94C1FA"
pink = "#D898D8"
green = "#AFCB85"
cyan = "#78D0BD"
orange = "#ECA775"
yellow = "#E5C995"
dark_gray = "#535353"
light_gray = "#6d6d6d"
purple = "#a390f0"
light_blue = "#7dbeff"
blue = "#52a7f6"
pink = "#d898d8"
green = "#afcb85"
cyan = "#78d0bd"
orange = "#efb080"
yellow = "#e5c995"
red = "#f44747"
purple_accent = "#6363EE"
blue_accent = "#2197F3"
pink_accent = "#E44C7A"
green_accent = "#00AF99"
@ -104,11 +119,3 @@ yellow_accent = "#DEA407"
# variables intended for future updates
checkmark = "#44B254"
diff_blue_accent = "#0079FF"
diff_blue_bg = "#072037"
diff_blue_fg = "#0079FF"
diff_red_accent = "#EE113C"
diff_red_bg = "#390B14"
diff_red_fg = "#EC123B"

@ -52,9 +52,9 @@
"ui.help" = { bg = "bg1", fg = "fg1" }
"ui.text" = { fg = "fg1" }
"ui.text.focus" = { fg = "fg1" }
"ui.selection" = { bg = "bg3", modifiers = ["reversed"] }
"ui.cursor.primary" = { modifiers = ["reversed"] }
"ui.cursor.match" = { bg = "bg2" }
"ui.selection" = { bg = "bg2" }
"ui.cursor.primary" = { bg = "fg4", fg = "bg1" }
"ui.cursor.match" = { bg = "bg3" }
"ui.menu" = { fg = "fg1", bg = "bg2" }
"ui.menu.selected" = { fg = "bg2", bg = "blue1", modifiers = ["bold"] }
"ui.virtual.whitespace" = "bg2"

@ -0,0 +1,138 @@
# Author : Rohit K Viswanath <kvrohit@gmail.com>
# Theme: Mellow
"attribute" = { fg = "blue", modifiers = ["italic"] }
"keyword" = "blue"
"keyword.control.conditional" = { fg = "blue", modifiers = ["italic"] }
"keyword.directive" = "magenta" # -- preprocessor comments (#if in C)
"namespace" = { fg = "cyan", modifiers = ["italic"] }
"punctuation" = "gray06"
"punctuation.delimiter" = "gray06"
"operator" = "yellow"
"special" = "yellow"
"variable" = "fg"
"variable.builtin" = "bright_blue"
"variable.parameter" = "cyan"
"variable.other.member" = "white"
"type" = "bright_blue"
"type.builtin" = "magenta"
"type.enum.variant" = "magenta"
"constructor" = "yellow"
"function" = "white"
"function.macro" = "bright_cyan"
"function.builtin" = "bright_blue"
"tag" = "cyan"
"comment" = { fg = "gray05", modifiers = ["italic"] }
"string" = "green"
"string.regexp" = "green"
"string.special" = "yellow"
"constant" = "cyan"
"constant.builtin" = "yellow"
"constant.numeric" = "magenta"
"constant.character.escape" = "cyan"
# used for lifetimes
"label" = "yellow"
"markup.heading.marker" = { fg = "gray06" }
"markup.heading" = { fg = "bright_blue", modifiers = ["bold"] }
"markup.list" = "gray06"
"markup.bold" = { modifiers = ["bold"] }
"markup.italic" = { modifiers = ["italic"] }
"markup.link.url" = { fg = "green", modifiers = ["underlined"] }
"markup.link.text" = { fg = "blue", modifiers = ["italic"] }
"markup.raw" = "yellow"
"diff.plus" = "bright_green"
"diff.minus" = "bright_red"
"diff.delta" = "bright_blue"
"ui.background" = { bg = "bg" }
"ui.background.separator" = { fg = "fg" }
"ui.linenr" = { fg = "gray04" }
"ui.linenr.selected" = { fg = "fg" }
"ui.statusline" = { fg = "fg", bg = "gray01" }
"ui.statusline.inactive" = { fg = "fg", bg = "gray01", modifiers = ["dim"] }
"ui.statusline.normal" = { fg = "bg", bg = "cyan", modifiers = ["bold"] }
"ui.statusline.insert" = { fg = "bg", bg = "blue", modifiers = ["bold"] }
"ui.statusline.select" = { fg = "bg", bg = "magenta", modifiers = ["bold"] }
"ui.popup" = { bg = "gray01" }
"ui.window" = { fg = "gray02" }
"ui.help" = { bg = "gray01", fg = "white" }
"ui.text" = { fg = "fg" }
"ui.text.focus" = { fg = "fg" }
"ui.virtual" = { fg = "gray02" }
"ui.virtual.indent-guide" = { fg = "gray02" }
"ui.selection" = { bg = "gray03" }
"ui.selection.primary" = { bg = "gray03" }
"ui.cursor" = { bg = "gray04" }
"ui.cursor.match" = { fg = "yellow", modifiers = ["bold", "underlined"] }
"ui.cursorline.primary" = { bg = "gray01" }
"ui.highlight" = { bg = "gray02" }
"ui.menu" = { fg = "white", bg = "gray01" }
"ui.menu.selected" = { fg = "bright_white", bg = "gray03" }
"ui.menu.scroll" = { fg = "gray04", bg = "gray01" }
diagnostic = { modifiers = ["underlined"] }
warning = "bright_yellow"
error = "bright_red"
info = "bright_blue"
hint = "bright_cyan"
[palette]
bg = "#161617"
fg = "#c9c7cd"
bg_dark = "#131314"
black = "#27272a"
bright_black = "#353539"
red = "#f5a191"
bright_red = "#ffae9f"
green = "#90b99f"
bright_green = "#9dc6ac"
yellow = "#e6b99d"
bright_yellow = "#f0c5a9"
blue = "#aca1cf"
bright_blue = "#b9aeda"
magenta = "#e29eca"
bright_magenta = "#ecaad6"
cyan = "#ea83a5"
bright_cyan = "#f591b2"
white = "#c1c0d4"
bright_white = "#cac9dd"
gray01 = "#1b1b1d"
gray02 = "#2a2a2d"
gray03 = "#3e3e43"
gray04 = "#57575f"
gray05 = "#757581"
gray06 = "#9998a8"
gray07 = "#c1c0d4"

@ -18,6 +18,9 @@
# status bars, panels, modals, autocompletion
"ui.statusline" = { fg = "base8", bg = "base4" }
"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
"ui.statusline.normal" = { fg = "base4", bg = "blue" }
"ui.statusline.insert" = { fg = "base4", bg = "green" }
"ui.statusline.select" = { fg = "base4", bg = "purple" }
"ui.popup" = { bg = "base3" }
"ui.window" = { bg = "base3" }
"ui.help" = { fg = "base8", bg = "base3" }

@ -0,0 +1,182 @@
# Author: github.com/jhscheer
#
# This theme is an adaptation of
# github.com/EdenEast/nightfox.nvim
# INTERFACE
# These scopes are used for theming the editor interface.
"ui.background" = { bg = "bg1" } # Default background color.
"ui.window" = { fg = "bg0" } # Window border between splits.
"ui.gutter" = { fg = "fg3" } # Left gutter for diagnostics and breakpoints.
"ui.text" = { fg = "fg1" } # Default text color.
"ui.text.focus" = { bg = "sel1", fg = "fg1" } # Selection highlight in buffer-picker or file-picker.
"ui.text.info" = { fg = "fg2", bg = "sel0" } # Info popup contents (space mode menu).
"ui.cursor" = { bg = "fg3", fg = "bg1" } # Fallback cursor colour, non-primary cursors when there are multiple (shift-c).
"ui.cursor.primary" = { bg = "fg1", fg = "bg1" } # The primary cursor when there are multiple (shift-c).
"ui.cursor.match" = { fg = "yellow", modifiers = ["bold"] } # The matching parentheses of that under the cursor.
"ui.selection" = { bg = "bg3" } # All currently selected text.
"ui.selection.primary" = { bg = "bg4" } # The primary selection when there are multiple.
"ui.cursorline.primary" = { bg = "bg3" } # The line of the primary cursor (if cursorline is enabled)
# "ui.cursorline.secondary" = { } # The lines of any other cursors (if cursorline is enabled)
# "ui.cursorcolumn.primary" = { } # The column of the primary cursor (if cursorcolumn is enabled)
# "ui.cursorcolumn.secondary" = { } # The columns of any other cursors (if cursorcolumn is enabled)
"ui.linenr" = { fg = "fg3" } # Line numbers.
"ui.linenr.selected" = { fg = "yellow", modifiers = ["bold"] } # Current line number.
# "ui.virtual" = { } # Namespace for additions to the editing area.
"ui.virtual.ruler" = { bg = "bg3" } # Vertical rulers (colored columns in editing area).
"ui.virtual.whitespace" = { fg = "bg3" } # Whitespace markers in editing area.
"ui.virtual.indent-guide" = { fg = "black" } # Vertical indent width guides
"ui.statusline" = { fg = "fg2", bg = "bg0" } # Status line.
"ui.statusline.inactive" = { fg = "fg3", bg = "bg0" } # Status line in unfocused windows.
"ui.statusline.normal" = { bg = "blue", fg = "bg0", modifiers = ["bold"] } # Statusline mode during normal mode (only if editor.color-modes is enabled)
"ui.statusline.insert" = { bg = "green", fg = "bg0", modifiers = ["bold"] } # Statusline mode during insert mode (only if editor.color-modes is enabled)
"ui.statusline.select" = { bg = "magenta", fg = "bg0", modifiers = ["bold"] } # Statusline mode during select mode (only if editor.color-modes is enabled)
"ui.help" = { bg = "sel0", fg = "fg1" } # Description box for commands.
"ui.menu" = { bg = "sel0", fg = "fg1" } # Code and command completion menus.
"ui.menu.selected" = { bg = "fg3" } # Selected autocomplete item.
"ui.menu.scroll" = { fg = "fg3" } # fg sets thumb color, bg sets track color of scrollbar.
"ui.popup" = { bg = "bg0", fg = "fg1" } # Documentation popups (space-k).
"ui.popup.info" = { bg = "sel0", fg = "fg1" } # Info popups box (space mode menu).
"markup.raw" = { fg = "magenta" } # Code block in Markdown.
"markup.raw.inline" = { fg = "orange" } # `Inline code block` in Markdown.
"markup.heading" = { fg = "yellow", modifiers = ["bold"] }
"markup.list" = { fg = "magenta", modifiers = ["bold"] }
"markup.bold" = { fg = "orange", modifiers = ["bold"] }
"markup.italic" = { fg = "pink" }
"markup.link" = { fg = "yellow-bright", modifiers = ["bold"] }
"markup.quote" = { fg = "blue" }
# DIAGNOSTICS
"warning" = { fg ="yellow", bg = "bg1" } # Diagnostics warning (gutter)
"error" = { fg = "red", bg = "bg1" } # Diagnostics error (gutter)
"info" = { fg = "blue", bg = "bg1" } # Diagnostics info (gutter)
"hint" = { fg = "green", bg = "bg1" } # Diagnostics hint (gutter)
"diagnostic" = { modifiers = ["underlined"] } # Diagnostics fallback style (editing area)
"diagnostic.error" = { fg = "red" } # Diagnostics error (editing area)
"diagnostic.warning" = { fg = "yellow" } # Diagnostics warning (editing area)
"diagnostic.info" = { fg = "blue" } # Diagnostics info (editing area)
"diagnostic.hint" = { fg = "green" } # Diagnostics hint (editing area)
# SYNTAX HIGHLIGHTING
# These keys match tree-sitter scopes.
"special" = { fg = "fg2" } # Special symbols e.g `?` in Rust, `...` in Hare.
"attribute" = { fg = "yellow" } # Class attributes, html tag attributes.
"type" = { fg = "yellow" } # Variable type, like integer or string, including program defined classes, structs etc..
"type.builtin" = { fg = "cyan-bright" } # Primitive types of the language (string, int, float).
"type.enum.variant" = { fg = "orange-bright" }
"constructor" = { fg = "magenta" } # Constructor method for a class or struct.
"constant" = { fg = "orange-bright" } # Constant value
"constant.builtin" = { fg = "orange-bright" } # Special constants like `true`, `false`, `none`, etc.
"constant.builtin.boolean" = { fg = "orange" } # True or False.
"constant.character" = { fg = "green" } # Constant of character type.
"constant.character.escape" = { fg = "yellow-bright", modifiers = ["bold"] } # escape codes like \n.
"constant.numeric" = { fg = "orange" } # constant integer or float value.
"string" = { fg = "green" } # String literal.
"string.regexp" = { fg = "yellow-bright" } # Regular expression literal.
"string.special" = { fg = "yellow-bright", modifiers = ["bold"] } # Strings containing a path, URL, etc.
"string.special.url" = { fg = "cyan", modifiers = ["bold"] } # String containing a web URL.
"comment" = { fg = "comment" } # This is a comment.
"comment.block.documentation" = { fg = "comment", modifiers = ["bold"] } # Doc comments, e.g '///' in rust.
"variable" = { fg = "white" } # Variable names.
"variable.builtin" = { fg = "red" } # Language reserved variables: `this`, `self`, `super`, etc.
"variable.parameter" = { fg = "cyan-bright" } # Function parameters.
"variable.other.member" = { fg = "fg2" } # Fields of composite data types (e.g. structs, unions).
"label" = { fg = "magenta-bright" } # lifetimes - Loop labels, among other things.
"punctuation" = { fg = "fg2" } # Any punctuation symbol.
# "punctuation.delimiter" = { fg = "fg2" } # Commas, colons or other delimiter depending on the language.
# "punctuation.bracket" = { fg = "fg2" } # Parentheses, angle brackets, etc.
# "punctuation.special" = { fg = "fg2" } # String interpolation brackets
"keyword" = { fg = "magenta" } # Language reserved keywords.
"keyword.control" = { fg = "pink" } # Control keywords.
"keyword.control.conditional" = { fg = "magenta-bright" } # `if`, `else`, `elif`.
"keyword.control.repeat" = { fg = "magenta-bright" } # `for`, `while`, `loop`.
"keyword.control.import" = { fg = "pink-bright" } # `import`, `export` `use`.
"keyword.control.return" = { fg = "magenta" } # `return` in most languages.
"keyword.control.exception" = { fg = "magenta" } # `try`, `catch`, `raise`/`throw` and related.
"keyword.operator" = { fg = "fg2", modifiers = ["bold"] } # 'or', 'and', 'in'.
"keyword.directive" = { fg = "pink-bright" } # Preprocessor directives (#if in C...).
"keyword.function" = { fg = "red" } # The keyword to define a funtion: 'def', 'fun', 'fn'.
"keyword.storage" = { fg = "magenta" } # Keywords describing how things are stored
"keyword.storage.type" = { fg = "magenta" } # The type of something, class, function, var, let, etc.
"keyword.storage.modifier" = { fg = "yellow" } # Storage modifiers like static, mut, const, ref, etc.
"operator" = { fg = "fg2" } # Logical, mathematical, and other operators.
"function" = { fg = "blue-bright" }
"function.builtin" = { fg = "red" }
"function.macro" = { fg = "red" }
# "function.special" = { fg = "blue-bright" } # Preprocessor function in C.
# "function.method" = { fg = "blue-bright" } # Class / Struct methods.
"tag" = { fg = "blue-bright" } # As in <body> for html, css tags.
"namespace" = { fg = "cyan-bright" } # Namespace or module identifier.
# Diff ==============================
# Version control changes.
"diff.plus" = "green-dim" # Additions.
"diff.minus" = "red-dim" # Deletions.
"diff.delta" = "blue-dim" # Modifications.
"diff.delta.moved" = "cyan-dim" # Renamed or moved files.
# color palette
[palette]
black = "#393b44"
red = "#c94f6d"
red-dim = "#2f2837"
green = "#81b29a"
green-dim = "#26343c"
yellow = "#dbc074"
yellow-bright = "#e0c989"
blue = "#719cd6"
blue-bright = "#86abdc"
blue-dim = "#2f2837"
magenta = "#9d79d6"
magenta-bright = "#baa1e2"
cyan = "#63cdcf"
cyan-bright = "#7ad4d6"
cyan-dim = "#253f4a"
white = "#dfdfe0"
orange = "#f4a261"
orange-bright = "#f6b079"
pink = "#d67ad2"
pink-bright = "#dc8ed9"
comment = "#738091"
# spec
bg0 = "#131a24" # Dark bg (status line and float)
bg1 = "#192330" # Default bg
bg2 = "#212e3f" # Lighter bg (colorcolm folds)
bg3 = "#29394f" # Lighter bg (cursor line)
bg4 = "#39506d" # Conceal, border fg
fg0 = "#d6d6d7" # Lighter fg
fg1 = "#cdcecf" # Default fg
fg2 = "#aeafb0" # Darker fg (status line)
fg3 = "#71839b" # Darker fg (line numbers, fold colums)
sel0 = "#2b3b51" # Popup bg, visual selection bg
sel1 = "#3c5372" # Popup sel bg, search bg

@ -0,0 +1,60 @@
# A unofficial port of VIM's zenburn theme: https://github.com/jnurmine/Zenburn/
"ui.background" = { bg = "bg" }
"ui.menu" = { fg = "#9f9f9f", bg = "uibg" }
"ui.menu.selected" = { fg = "#d0d0a0", bg = "#242424", modifiers = ["bold"] }
"ui.linenr" = { fg = "#9fafaf", bg = "#262626"}
"ui.linenr.selected" = { modifiers = ["bold"]}
"ui.popup" = { bg = "uibg" }
"ui.selection" = { bg = "#2f2f2f" }
"comment" = { fg = "#7f9f7f" }
"comment.block.documentation" = { fg = "black", modifiers = ["bold"] }
"ui.statusline" = { bg = "statusbg", fg = "#ccdc90" }
"ui.statusline.inactive" = { fg = '#2e3330', bg = '#88b090' }
"ui.cursor" = { fg = "#000d18", bg = "#8faf9f", modifiers = ["bold"] }
"ui.text" = { fg = "normal"}
"operator" = { fg = "#f0efd0" , modifiers = []}
"variable" = "normal"
"variable.builtin" = {fg = "constant", modifiers = ["bold"]}
"constant.numeric" = "numeric"
"constant" = { fg = "constant", modifiers = ["bold"] }
"type" = { fg = "#dfdfbf", modifiers = ["bold"] }
"ui.cursor.match" = { fg = "#343434", bg = "#284f28", modifiers = ["bold"] }
"string" = "#cc9393"
"variable.other.member" = "#efef8f"
"constant.character.escape" = { fg = "#dca3a3", modifiers = ["bold"]}
"function" = "#efef8f"
"function.macro" = { fg = "#ffcfaf", modifiers = ["bold"] }
"special" = "#cfbfaf"
"keyword" = { fg = "#f0dfaf", modifiers = ["bold"]}
"keyword.storage-class" = { fg = "#c3bf9f", modifiers = ["bold"]}
"label" = { fg = "#dfcfaf", modifiers = ["underlined"] }
"ui.help" = { fg = "white", bg = "black" }
"ui.virtual.ruler" = { bg = "#484848" }
"ui.virtual.whitespace" = { fg = "#5b605e", modifiers = ["bold"]}
"punctuation.delimiter" = "#8f8f8f"
"ui.virtual.indent-guide" = "#4f4f4f"
"diff.plus" = {fg = "#709080", bg = "#313c36", modifiers = ["bold"] }
"diff.delta" = "#333333"
"diff.minus" = {fg = "#333333", bg = "#464646"}
"diagnostic" = {bg = "statusbg"}
"diagnostic.error" = { fg = "errorfg", bg = "errorbg"}
"ui.gutter" = { bg = "statusbg" }
"hint" = {fg = "numeric", bg = "statusbg"}
"warning" = "numeric"
"error" = "errorfg"
[palette]
bg = "#3f3f3f"
uibg = "#2c2e2e"
constant = "#dca3a3"
normal = "#dcdccc"
todo = "#dfdfdf"
errorfg = "#e37170"
errorbg = "#3d3535"
statusbg = "#313633"
numeric = "#8cd0d3"

@ -596,8 +596,8 @@ _________________________________________________________________
= CHAPTER 5 RECAP =
=================================================================
* Type C to copy the current selection to below and Alt-C for
above.
* Type C to duplicate the cursor to the next suitable line
and Alt-C for previous suitable line.
* Type s to select all instances of a regex pattern inside
the current selection.
@ -845,11 +845,11 @@ lines.
Type q to repeat the macro from register @ (the default).
1. Move the cursor to the first line marked '-->' below.
Ensure your cursor is on the > of the arrow.
Ensure your cursor is on the '>' of the arrow.
2. Type Q to start recording.
3. Edit the line to look like the bottom one.
4. Exit insert and Type Q again to stop recording.
5. Move to the line below and put your cursor on the > again.
5. Move to the line below and put your cursor on '>' again.
6. Type q to repeat the macro.
--> ... sentence doesn't have it's first and last ... .

@ -17,8 +17,8 @@ pub fn query_check() -> Result<(), DynError> {
let language_name = &language.language_id;
let grammar_name = language.grammar.as_ref().unwrap_or(language_name);
for query_file in query_files {
let language = get_language(&grammar_name);
let query_text = read_query(&language_name, query_file);
let language = get_language(grammar_name);
let query_text = read_query(language_name, query_file);
if let Ok(lang) = language {
if !query_text.is_empty() {
if let Err(reason) = Query::new(lang, &query_text) {

Loading…
Cancel
Save