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]] [[grammar]]
name = "rust" 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]] [[language]]
name = "nix" name = "nix"

@ -24,13 +24,10 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- uses: Swatinem/rust-cache@v1 - uses: Swatinem/rust-cache@v2
- name: Fetch tree-sitter grammars - name: Fetch tree-sitter grammars
uses: actions-rs/cargo@v1 run: cargo run --package=helix-loader --bin=hx-loader
with:
command: run
args: --package=helix-loader --bin=hx-loader
- name: Bundle grammars - name: Bundle grammars
run: tar cJf grammars.tar.xz -C runtime/grammars/sources . run: tar cJf grammars.tar.xz -C runtime/grammars/sources .
@ -198,16 +195,6 @@ jobs:
- uses: actions/download-artifact@v3 - 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 - name: Build archive
shell: bash shell: bash
run: | run: |
@ -227,7 +214,7 @@ jobs:
if [[ $platform =~ "windows" ]]; then if [[ $platform =~ "windows" ]]; then
exe=".exe" exe=".exe"
fi fi
pkgname=helix-$TAG-$platform pkgname=helix-$GITHUB_REF_NAME-$platform
mkdir $pkgname mkdir $pkgname
cp $source/LICENSE $source/README.md $pkgname cp $source/LICENSE $source/README.md $pkgname
mkdir $pkgname/contrib mkdir $pkgname/contrib
@ -247,7 +234,7 @@ jobs:
fi fi
done 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/ mv dist $source/
- name: Upload binaries to release - name: Upload binaries to release
@ -257,7 +244,7 @@ jobs:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/* file: dist/*
file_glob: true file_glob: true
tag: ${{ steps.tagname.outputs.val }} tag: ${{ github.ref_name }}
overwrite: true overwrite: true
- name: Upload binaries as artifact - 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", "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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.18" version = "0.7.18"
@ -92,9 +104,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.74" version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -115,9 +127,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.22" version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"num-integer", "num-integer",
@ -400,18 +412,29 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [ 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]] [[package]]
name = "helix-core" name = "helix-core"
version = "0.6.0" version = "0.6.0"
dependencies = [ dependencies = [
"ahash 0.8.2",
"arc-swap", "arc-swap",
"bitflags", "bitflags",
"chrono", "chrono",
"encoding_rs", "encoding_rs",
"etcetera", "etcetera",
"hashbrown 0.13.1",
"helix-loader", "helix-loader",
"log", "log",
"once_cell", "once_cell",
@ -655,9 +678,9 @@ checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.7.3" version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"winapi", "winapi",
@ -876,9 +899,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -959,9 +982,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.87" version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1023,9 +1046,9 @@ dependencies = [
[[package]] [[package]]
name = "similar" name = "similar"
version = "2.2.0" version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803" checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
[[package]] [[package]]
name = "slab" name = "slab"
@ -1196,9 +1219,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.21.2" version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -1288,7 +1311,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [ dependencies = [
"hashbrown", "hashbrown 0.12.3",
"regex", "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 (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env: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 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:** **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. 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 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` | | `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` | | `cursorline` | Highlight all lines with a cursor. | `false` |
| `cursorcolumn` | Highlight all columns 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-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `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` | | `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 | | `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file | | `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors | | `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections | | `selections` | The number of active selections |
| `primary-selection-length` | The number of characters currently in primary selection |
| `position` | The cursor position | | `position` | The cursor position |
| `position-percentage` | The cursor position as a percentage of the total number of lines | | `position-percentage` | The cursor position as a percentage of the total number of lines |
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) | | `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |

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

@ -44,7 +44,9 @@
| `:show-directory`, `:pwd` | Show the current working directory. | | `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. | | `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
| `:reload` | Discard changes and reload from the source file. | | `: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. | | `: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 | | `: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. | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | | `:debug-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 (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env: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 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:** **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)) COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- $2))
;; ;;
esac esac
} && complete -F _hx hx } && complete -o filenames -F _hx hx

@ -1,22 +1,5 @@
{ {
"nodes": { "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": { "crane": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -52,47 +35,45 @@
"dream2nix": { "dream2nix": {
"inputs": { "inputs": {
"alejandra": [ "alejandra": [
"nci", "nci"
"nixpkgs" ],
"all-cabal-json": [
"nci"
], ],
"all-cabal-json": "all-cabal-json",
"crane": "crane", "crane": "crane",
"devshell": [ "devshell": [
"nci", "nci",
"devshell" "devshell"
], ],
"flake-utils-pre-commit": [ "flake-utils-pre-commit": [
"nci", "nci"
"nixpkgs" ],
"ghc-utils": [
"nci"
], ],
"ghc-utils": "ghc-utils",
"gomod2nix": [ "gomod2nix": [
"nci", "nci"
"nixpkgs"
], ],
"mach-nix": [ "mach-nix": [
"nci", "nci"
"nixpkgs"
], ],
"nixpkgs": [ "nixpkgs": [
"nci", "nci",
"nixpkgs" "nixpkgs"
], ],
"poetry2nix": [ "poetry2nix": [
"nci", "nci"
"nixpkgs"
], ],
"pre-commit-hooks": [ "pre-commit-hooks": [
"nci", "nci"
"nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1667429039, "lastModified": 1668851003,
"narHash": "sha256-Lu6da25JioHzerkLHAHSO9suCQFzJ/XBjkcGCIbasLM=", "narHash": "sha256-X7RCQQynbxStZR2m7HW38r/msMQwVl3afD6UXOCtvx4=",
"owner": "nix-community", "owner": "nix-community",
"repo": "dream2nix", "repo": "dream2nix",
"rev": "5252794e58eedb02d607fa3187ffead7becc81b0", "rev": "c77e8379d8fe01213ba072e40946cbfb7b58e628",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -116,22 +97,6 @@
"type": "github" "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": { "nci": {
"inputs": { "inputs": {
"devshell": "devshell", "devshell": "devshell",
@ -144,11 +109,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1667542401, "lastModified": 1669011203,
"narHash": "sha256-mdWjP5tjSf8n6FAtpSgL23kX4+eWBwLrSYo9iY3mA8Q=", "narHash": "sha256-Lymj4HktNEFmVXtwI0Os7srDXHZbZW0Nzw3/+5Hf8ko=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "nix-cargo-integration", "repo": "nix-cargo-integration",
"rev": "cd5e5cbd81c80dc219455dd3b1e0ddb55fae51ec", "rev": "c5133b91fc1d549087c91228bd213f2518728a4b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -159,11 +124,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1667482890, "lastModified": 1668905981,
"narHash": "sha256-pua0jp87iwN7NBY5/ypx0s9L9CG49Ju/NI4wGwurHc4=", "narHash": "sha256-RBQa/+9Uk1eFTqIOXBSBezlEbA3v5OkgP+qptQs1OxY=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a2a777538d971c6b01c6e54af89ddd6567c055e8", "rev": "690ffff026b4e635b46f69002c0f4e81c65dfc2e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -188,11 +153,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1667487142, "lastModified": 1668998422,
"narHash": "sha256-bVuzLs1ZVggJAbJmEDVO9G6p8BH3HRaolK70KXvnWnU=", "narHash": "sha256-G/BklIplCHZEeDIabaaxqgITdIXtMolRGlwxn9jG2/Q=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "cf668f737ac986c0a89e83b6b2e3c5ddbd8cf33b", "rev": "68ab029c93f8f8eed4cf3ce9a89a9fd4504b2d6e",
"type": "github" "type": "github"
}, },
"original": { "original": {

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

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

@ -100,43 +100,41 @@ mod test {
#[test] #[test]
fn test_find_line_comment() { fn test_find_line_comment() {
use crate::State;
// four lines, two space indented, except for line 1 which is blank. // four lines, two space indented, except for line 1 which is blank.
let doc = Rope::from(" 1\n\n 2\n 3"); let mut doc = Rope::from(" 1\n\n 2\n 3");
let mut state = State::new(doc);
// select whole document // 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); let res = find_line_comment("//", text, 0..3);
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1) // (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1)
assert_eq!(res, (false, vec![0, 2], 2, 1)); assert_eq!(res, (false, vec![0, 2], 2, 1));
// comment // comment
let transaction = toggle_line_comments(&state.doc, &state.selection, None); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut state.doc); transaction.apply(&mut doc);
state.selection = state.selection.map(transaction.changes()); 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 // uncomment
let transaction = toggle_line_comments(&state.doc, &state.selection, None); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut state.doc); transaction.apply(&mut doc);
state.selection = state.selection.map(transaction.changes()); selection = selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3"); assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
// 0 margin comments // 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. // 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); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut state.doc); transaction.apply(&mut doc);
state.selection = state.selection.map(transaction.changes()); selection = selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3"); 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 // 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 once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::time::{Duration, Instant}; 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. /// Stores the history of changes to a buffer.
/// ///
/// Currently the history is represented as a vector of revisions. The vector /// 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. /// A single point in history. See [History] for more information.
#[derive(Debug)] #[derive(Debug, Clone)]
struct Revision { struct Revision {
parent: usize, parent: usize,
last_child: Option<NonZeroUsize>, last_child: Option<NonZeroUsize>,
@ -113,6 +119,37 @@ impl History {
self.current == 0 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. /// Undo the last edit.
pub fn undo(&mut self) -> Option<&Transaction> { pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() { if self.at_root() {
@ -366,12 +403,16 @@ impl std::str::FromStr for UndoKind {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::Selection;
#[test] #[test]
fn test_undo_redo() { fn test_undo_redo() {
let mut history = History::default(); let mut history = History::default();
let doc = Rope::from("hello"); let doc = Rope::from("hello");
let mut state = State::new(doc); let mut state = State {
doc,
selection: Selection::point(0),
};
let transaction1 = let transaction1 =
Transaction::change(&state.doc, vec![(5, 5, Some(" world!".into()))].into_iter()); Transaction::change(&state.doc, vec![(5, 5, Some(" world!".into()))].into_iter());
@ -420,7 +461,10 @@ mod test {
fn test_earlier_later() { fn test_earlier_later() {
let mut history = History::default(); let mut history = History::default();
let doc = Rope::from("a\n"); 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) { fn undo(history: &mut History, state: &mut State) {
if let Some(transaction) = history.undo() { if let Some(transaction) = history.undo() {

@ -74,12 +74,12 @@ impl DateTimeIncrementor {
(true, false) => { (true, false) => {
let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?; 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) => { (false, true) => {
let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?; 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, (false, false) => return None,
}; };
@ -312,10 +312,10 @@ fn ndays_in_month(year: i32, month: u32) -> u32 {
} else { } else {
(year, month + 1) (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. // ...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> { 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)); 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> { 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()); let ndays = ndays_in_month(year, date_time.month());
if date_time.day() > ndays { if date_time.day() > ndays {
let d = NaiveDate::from_ymd(year, date_time.month(), ndays); NaiveDate::from_ymd_opt(year, date_time.month(), ndays)
Some(d.succ().and_time(date_time.time())) .and_then(|date| date.succ_opt().map(|date| date.and_time(date_time.time())))
} else { } else {
date_time.with_year(year) date_time.with_year(year)
} }

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

@ -21,7 +21,6 @@ pub mod register;
pub mod search; pub mod search;
pub mod selection; pub mod selection;
pub mod shellwords; pub mod shellwords;
mod state;
pub mod surround; pub mod surround;
pub mod syntax; pub mod syntax;
pub mod test; pub mod test;
@ -103,7 +102,6 @@ pub use smallvec::{smallvec, SmallVec};
pub use syntax::Syntax; pub use syntax::Syntax;
pub use diagnostic::Diagnostic; pub use diagnostic::Diagnostic;
pub use state::State;
pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING}; pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction}; 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 { pub fn new_with_values(name: char, values: Vec<String>) -> Self {
if name == '_' { Self { name, values }
Self::new(name)
} else {
Self { name, values }
}
} }
pub const fn name(&self) -> char { pub const fn name(&self) -> char {
@ -31,15 +27,11 @@ impl Register {
} }
pub fn write(&mut self, values: Vec<String>) { pub fn write(&mut self, values: Vec<String>) {
if self.name != '_' { self.values = values;
self.values = values;
}
} }
pub fn push(&mut self, value: String) { 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) self.inner.get(&name)
} }
pub fn get_mut(&mut self, name: char) -> &mut Register { pub fn read(&self, name: char) -> Option<&[String]> {
self.inner self.get(name).map(|reg| reg.read())
.entry(name)
.or_insert_with(|| Register::new(name))
} }
pub fn write(&mut self, name: char, values: Vec<String>) { pub fn write(&mut self, name: char, values: Vec<String>) {
self.inner if name != '_' {
.insert(name, Register::new_with_values(name, values)); self.inner
.insert(name, Register::new_with_values(name, values));
}
} }
pub fn read(&self, name: char) -> Option<&[String]> { pub fn push(&mut self, name: char, value: String) {
self.get(name).map(|reg| reg.read()) 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> { pub fn first(&self, name: char) -> Option<&String> {

@ -1,9 +1,9 @@
use std::borrow::Cow; use std::borrow::Cow;
/// Auto escape for shellwords usage. /// 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()) { if !input.chars().any(|x| x.is_ascii_whitespace()) {
Cow::Borrowed(input) input
} else if cfg!(unix) { } else if cfg!(unix) {
Cow::Owned(input.chars().fold(String::new(), |mut buf, c| { Cow::Owned(input.chars().fold(String::new(), |mut buf, c| {
if c.is_ascii_whitespace() { 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 enum State {
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> { OnWhitespace,
enum State { Unquoted,
OnWhitespace, UnquotedEscaped,
Unquoted, Quoted,
UnquotedEscaped, QuoteEscaped,
Quoted, Dquoted,
QuoteEscaped, DquoteEscaped,
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; impl<'a> From<&'a str> for Shellwords<'a> {
let mut args: Vec<Cow<str>> = Vec::new(); fn from(input: &'a str) -> Self {
let mut escaped = String::with_capacity(input.len()); use State::*;
let mut start = 0; let mut state = Unquoted;
let mut end = 0; 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() { let mut part_start = 0;
state = match state { let mut unescaped_start = 0;
OnWhitespace => match c { let mut end = 0;
'"' => {
end = i; for (i, c) in input.char_indices() {
Dquoted state = match state {
} OnWhitespace => match c {
'\'' => { '"' => {
end = i; end = i;
Quoted Dquoted
} }
'\\' => { '\'' => {
if cfg!(unix) { end = i;
escaped.push_str(&input[start..i]); Quoted
start = i + 1; }
UnquotedEscaped '\\' => {
} else { 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 OnWhitespace
} }
} _ => Unquoted,
c if c.is_ascii_whitespace() => { },
end = i; Unquoted => match c {
OnWhitespace '\\' => {
} if cfg!(unix) {
_ => Unquoted, escaped.push_str(&input[unescaped_start..i]);
}, unescaped_start = i + 1;
Unquoted => match c { UnquotedEscaped
'\\' => { } else {
if cfg!(unix) { Unquoted
escaped.push_str(&input[start..i]); }
start = i + 1;
UnquotedEscaped
} else {
Unquoted
} }
} c if c.is_ascii_whitespace() => {
c if c.is_ascii_whitespace() => { end = i;
end = i; OnWhitespace
OnWhitespace
}
_ => Unquoted,
},
UnquotedEscaped => Unquoted,
Quoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[start..i]);
start = i + 1;
QuoteEscaped
} else {
Quoted
} }
} _ => Unquoted,
'\'' => { },
end = i; UnquotedEscaped => Unquoted,
OnWhitespace Quoted => match c {
} '\\' => {
_ => Quoted, if cfg!(unix) {
}, escaped.push_str(&input[unescaped_start..i]);
QuoteEscaped => Quoted, unescaped_start = i + 1;
Dquoted => match c { QuoteEscaped
'\\' => { } else {
if cfg!(unix) { Quoted
escaped.push_str(&input[start..i]); }
start = i + 1;
DquoteEscaped
} else {
Dquoted
} }
} '\'' => {
'"' => { end = i;
end = i; OnWhitespace
OnWhitespace }
} _ => Quoted,
_ => Dquoted, },
}, QuoteEscaped => Quoted,
DquoteEscaped => Dquoted, 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 { if i >= input.len() - 1 && end == 0 {
end = i + 1; end = i + 1;
} }
if end > 0 { if end > 0 {
let esc_trim = escaped.trim(); let esc_trim = escaped.trim();
let inp = &input[start..end]; let inp = &input[unescaped_start..end];
if !(esc_trim.is_empty() && inp.trim().is_empty()) { if !(esc_trim.is_empty() && inp.trim().is_empty()) {
if esc_trim.is_empty() { if esc_trim.is_empty() {
args.push(inp.into()); words.push(inp.into());
} else { parts.push(inp);
args.push([escaped, inp.into()].concat().into()); } else {
escaped = "".to_string(); 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)] #[cfg(test)]
@ -148,7 +203,8 @@ mod test {
#[cfg(windows)] #[cfg(windows)]
fn test_normal() { fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#; 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![ let expected = vec![
Cow::from(":o"), Cow::from(":o"),
Cow::from("single_word"), Cow::from("single_word"),
@ -166,7 +222,8 @@ mod test {
#[cfg(unix)] #[cfg(unix)]
fn test_normal() { fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#; 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![ let expected = vec![
Cow::from(":o"), Cow::from(":o"),
Cow::from("single_word"), Cow::from("single_word"),
@ -183,7 +240,8 @@ mod test {
fn test_quoted() { fn test_quoted() {
let quoted = let quoted =
r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#; 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![ let expected = vec![
Cow::from(":o"), Cow::from(":o"),
Cow::from("single_word"), Cow::from("single_word"),
@ -198,7 +256,8 @@ mod test {
#[cfg(unix)] #[cfg(unix)]
fn test_dquoted() { fn test_dquoted() {
let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#; 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![ let expected = vec![
Cow::from(":o"), Cow::from(":o"),
Cow::from("single_word"), Cow::from("single_word"),
@ -213,7 +272,8 @@ mod test {
#[cfg(unix)] #[cfg(unix)]
fn test_mixed() { fn test_mixed() {
let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#; 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![ let expected = vec![
Cow::from(":o"), Cow::from(":o"),
Cow::from("single_word"), Cow::from("single_word"),
@ -234,7 +294,8 @@ mod test {
fn test_lists() { fn test_lists() {
let input = let input =
r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#; r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#;
let result = shellwords(input); let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![ let expected = vec![
Cow::from(":set"), Cow::from(":set"),
Cow::from("statusline.center"), Cow::from("statusline.center"),
@ -247,15 +308,29 @@ mod test {
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
fn test_escaping_unix() { fn test_escaping_unix() {
assert_eq!(escape("foobar"), Cow::Borrowed("foobar")); assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar"), Cow::Borrowed("foo\\ bar")); assert_eq!(escape("foo bar".into()), Cow::Borrowed("foo\\ bar"));
assert_eq!(escape("foo\tbar"), Cow::Borrowed("foo\\\tbar")); assert_eq!(escape("foo\tbar".into()), Cow::Borrowed("foo\\\tbar"));
} }
#[test] #[test]
#[cfg(windows)] #[cfg(windows)]
fn test_escaping_windows() { fn test_escaping_windows() {
assert_eq!(escape("foobar"), Cow::Borrowed("foobar")); assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar"), Cow::Borrowed("\"foo bar\"")); 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, Rope, RopeSlice, Tendril,
}; };
use ahash::RandomState;
use arc_swap::{ArcSwap, Guard}; use arc_swap::{ArcSwap, Guard};
use bitflags::bitflags; use bitflags::bitflags;
use hashbrown::raw::RawTable;
use slotmap::{DefaultKey as LayerId, HopSlotMap}; use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{ use std::{
@ -16,7 +18,8 @@ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
fmt, fmt,
mem::replace, hash::{Hash, Hasher},
mem::{replace, transmute},
path::Path, path::Path,
str::FromStr, str::FromStr,
sync::Arc, 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 { impl TextObjectQuery {
/// Run the query on the given node and return sub nodes which match given /// Run the query on the given node and return sub nodes which match given
/// capture ("function.inside", "class.around", etc). /// capture ("function.inside", "class.around", etc).
@ -394,6 +417,8 @@ impl TextObjectQuery {
.iter() .iter()
.find_map(|cap| self.query.capture_index_for_name(cap))?; .find_map(|cap| self.query.capture_index_for_name(cap))?;
cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
let nodes = cursor let nodes = cursor
.captures(&self.query, node, RopeProvider(slice)) .captures(&self.query, node, RopeProvider(slice))
.filter_map(move |(mat, _)| { .filter_map(move |(mat, _)| {
@ -748,30 +773,38 @@ impl Syntax {
// Convert the changeset into tree sitter edits. // Convert the changeset into tree sitter edits.
let edits = generate_edits(old_source, changeset); 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 // Use the edits to update all layers markers
if !edits.is_empty() { fn point_add(a: Point, b: Point) -> Point {
fn point_add(a: Point, b: Point) -> Point { if b.row > 0 {
if b.row > 0 { Point::new(a.row.saturating_add(b.row), b.column)
Point::new(a.row.saturating_add(b.row), b.column) } else {
} else { Point::new(0, a.column.saturating_add(b.column))
Point::new(0, a.column.saturating_add(b.column))
}
} }
fn point_sub(a: Point, b: Point) -> Point { }
if a.row > b.row { fn point_sub(a: Point, b: Point) -> Point {
Point::new(a.row.saturating_sub(b.row), a.column) if a.row > b.row {
} else { Point::new(a.row.saturating_sub(b.row), a.column)
Point::new(0, a.column.saturating_sub(b.column)) } else {
} Point::new(0, a.column.saturating_sub(b.column))
} }
}
for layer in self.layers.values_mut() { for (layer_id, layer) in self.layers.iter_mut() {
// The root layer always covers the whole range (0..usize::MAX) // The root layer always covers the whole range (0..usize::MAX)
if layer.depth == 0 { if layer.depth == 0 {
layer.flags = LayerUpdateFlags::MODIFIED; layer.flags = LayerUpdateFlags::MODIFIED;
continue; continue;
} }
if !edits.is_empty() {
for range in &mut layer.ranges { for range in &mut layer.ranges {
// Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720 // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720
for edit in edits.iter().rev() { 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| { PARSER.with(|ts_parser| {
@ -843,6 +882,7 @@ impl Syntax {
let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
// TODO: might need to set cursor range // TODO: might need to set cursor range
cursor.set_byte_range(0..usize::MAX); cursor.set_byte_range(0..usize::MAX);
cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
let source_slice = source.slice(..); let source_slice = source.slice(..);
@ -959,27 +999,23 @@ impl Syntax {
let depth = layer.depth + 1; let depth = layer.depth + 1;
// TODO: can't inline this since matches borrows self.layers // TODO: can't inline this since matches borrows self.layers
for (config, ranges) in injections { for (config, ranges) in injections {
// Find an existing layer let new_layer = LanguageLayer {
let layer = self tree: None,
.layers config,
.iter_mut() depth,
.find(|(_, layer)| { ranges,
layer.depth == depth && // TODO: track parent id instead flags: LayerUpdateFlags::empty(),
layer.config.language == config.language && layer.ranges == ranges };
// 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. // ...or insert a new one.
let layer_id = layer.unwrap_or_else(|| { let layer_id = layer.unwrap_or_else(|| self.layers.insert(new_layer));
self.layers.insert(LanguageLayer {
tree: None,
config,
depth,
ranges,
// set the modified flag to ensure the layer is parsed
flags: LayerUpdateFlags::empty(),
})
});
queue.push_back(layer_id); queue.push_back(layer_id);
} }
@ -1032,6 +1068,7 @@ impl Syntax {
// if reusing cursors & no range this resets to whole range // 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_byte_range(range.clone().unwrap_or(0..usize::MAX));
cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT);
let mut captures = cursor_ref let mut captures = cursor_ref
.captures( .captures(
@ -1115,6 +1152,34 @@ pub struct LanguageLayer {
flags: LayerUpdateFlags, 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 { impl LanguageLayer {
pub fn tree(&self) -> &Tree { pub fn tree(&self) -> &Tree {
// TODO: no unwrap // TODO: no unwrap
@ -1260,7 +1325,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize}; use std::{iter, mem, ops, str, usize};
use tree_sitter::{ use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError, 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; 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 { 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>( fn pretty_print_tree_impl<W: fmt::Write>(
fmt: &mut W, fmt: &mut W,
node: Node, cursor: &mut TreeCursor,
is_root: bool,
field_name: Option<&str>,
depth: usize, depth: usize,
) -> fmt::Result { ) -> fmt::Result {
fn is_visible(node: Node) -> bool { let node = cursor.node();
node.is_missing() let visible = node_is_visible(&node);
|| (node.is_named() && node.language().node_kind_is_visible(node.kind_id()))
}
if is_visible(node) { if visible {
let indentation_columns = depth * 2; let indentation_columns = depth * 2;
write!(fmt, "{:indentation_columns$}", "")?; 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, "{}: ", field_name)?;
} }
write!(fmt, "({}", node.kind())?; write!(fmt, "({}", node.kind())?;
} else if is_root {
write!(fmt, "(\"{}\")", node.kind())?;
} }
for child_idx in 0..node.child_count() { // Handle children.
if let Some(child) = node.child(child_idx) { if cursor.goto_first_child() {
if is_visible(child) { loop {
if node_is_visible(&cursor.node()) {
fmt.write_char('\n')?; fmt.write_char('\n')?;
} }
pretty_print_tree_impl( pretty_print_tree_impl(fmt, cursor, depth + 1)?;
fmt,
child, if !cursor.goto_next_sibling() {
false, break;
node.field_name_for_child(child_idx as u32), }
depth + 1,
)?;
} }
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) { if visible {
write!(fmt, ")")?; fmt.write_char(')')?;
} }
Ok(()) Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -2353,11 +2429,17 @@ mod test {
} }
#[track_caller] #[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 source = Rope::from_str(source);
let loader = Loader::new(Configuration { language: vec![] }); 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 config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)); let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
@ -2377,13 +2459,14 @@ mod test {
#[test] #[test]
fn test_pretty_print() { fn test_pretty_print() {
let source = r#"/// Hello"#; 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: // A large tree should be indented with fields:
let source = r#"fn main() { let source = r#"fn main() {
println!("Hello, World!"); println!("Hello, World!");
}"#; }"#;
assert_pretty_print( assert_pretty_print(
"rust",
source, source,
concat!( concat!(
"(function_item\n", "(function_item\n",
@ -2402,11 +2485,34 @@ mod test {
// Selecting a token should print just that token: // Selecting a token should print just that token:
let source = r#"fn main() {}"#; 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: // Error nodes are printed as errors:
let source = r#"}{"#; 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] #[test]

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

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

@ -10,4 +10,4 @@ indent = { tab-width = 4, unit = " " }
[[grammar]] [[grammar]]
name = "rust" 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 = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "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" tokio-stream = "0.1.11"
which = "4.2" which = "4.2"

@ -4,7 +4,6 @@ use crate::{
Call, Error, OffsetEncoding, Result, Call, Error, OffsetEncoding, Result,
}; };
use anyhow::anyhow;
use helix_core::{find_root, ChangeSet, Rope}; use helix_core::{find_root, ChangeSet, Rope};
use lsp_types as lsp; use lsp_types as lsp;
use serde::Deserialize; use serde::Deserialize;
@ -314,6 +313,7 @@ impl Client {
String::from("additionalTextEdits"), String::from("additionalTextEdits"),
], ],
}), }),
insert_replace_support: Some(true),
..Default::default() ..Default::default()
}), }),
completion_item_kind: Some(lsp::CompletionItemKindCapability { completion_item_kind: Some(lsp::CompletionItemKindCapability {
@ -545,16 +545,17 @@ impl Client {
new_text: &Rope, new_text: &Rope,
changes: &ChangeSet, changes: &ChangeSet,
) -> Option<impl Future<Output = Result<()>>> { ) -> Option<impl Future<Output = Result<()>>> {
// figure out what kind of sync the server supports
let capabilities = self.capabilities.get().unwrap(); let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document sync.
let sync_capabilities = match capabilities.text_document_sync { let sync_capabilities = match capabilities.text_document_sync {
Some(lsp::TextDocumentSyncCapability::Kind(kind)) Some(
| Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions { lsp::TextDocumentSyncCapability::Kind(kind)
change: Some(kind), | lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
.. change: Some(kind),
})) => kind, ..
}),
) => kind,
// None | SyncOptions { changes: None } // None | SyncOptions { changes: None }
_ => return None, _ => return None,
}; };
@ -630,8 +631,12 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>, work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> Option<impl Future<Output = Result<Value>>> {
// ) -> Result<Vec<lsp::CompletionItem>> { let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support completion.
capabilities.completion_provider.as_ref()?;
let params = lsp::CompletionParams { let params = lsp::CompletionParams {
text_document_position: lsp::TextDocumentPositionParams { text_document_position: lsp::TextDocumentPositionParams {
text_document, text_document,
@ -646,15 +651,25 @@ impl Client {
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), } // 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, &self,
completion_item: lsp::CompletionItem, completion_item: lsp::CompletionItem,
) -> Result<lsp::CompletionItem> { ) -> Option<impl Future<Output = Result<Value>>> {
self.request::<lsp::request::ResolveCompletionItem>(completion_item) let capabilities = self.capabilities.get().unwrap();
.await
// 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( pub fn text_document_signature_help(
@ -665,7 +680,7 @@ impl Client {
) -> Option<impl Future<Output = Result<Value>>> { ) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap(); 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()?; capabilities.signature_help_provider.as_ref()?;
let params = lsp::SignatureHelpParams { let params = lsp::SignatureHelpParams {
@ -686,7 +701,18 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>, 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 { let params = lsp::HoverParams {
text_document_position_params: lsp::TextDocumentPositionParams { text_document_position_params: lsp::TextDocumentPositionParams {
text_document, text_document,
@ -696,7 +722,7 @@ impl Client {
// lsp::SignatureHelpContext // lsp::SignatureHelpContext
}; };
self.call::<lsp::request::HoverRequest>(params) Some(self.call::<lsp::request::HoverRequest>(params))
} }
// formatting // formatting
@ -709,13 +735,11 @@ impl Client {
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> { ) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
let capabilities = self.capabilities.get().unwrap(); 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 { match capabilities.document_formatting_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (), Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
// None | Some(false)
_ => return None, _ => return None,
}; };
// TODO: return err::unavailable so we can fall back to tree sitter formatting
// merge FormattingOptions with 'config.format' // merge FormattingOptions with 'config.format'
let config_format = self let config_format = self
@ -750,22 +774,20 @@ impl Client {
}) })
} }
pub async fn text_document_range_formatting( pub fn text_document_range_formatting(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
range: lsp::Range, range: lsp::Range,
options: lsp::FormattingOptions, options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>, 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(); 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 { match capabilities.document_range_formatting_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (), Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) => (),
// None | Some(false) _ => return None,
_ => return Ok(Vec::new()),
}; };
// TODO: return err::unavailable so we can fall back to tree sitter formatting
let params = lsp::DocumentRangeFormattingParams { let params = lsp::DocumentRangeFormattingParams {
text_document, text_document,
@ -774,11 +796,13 @@ impl Client {
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token }, work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
}; };
let response = self let request = self.call::<lsp::request::RangeFormatting>(params);
.request::<lsp::request::RangeFormatting>(params)
.await?;
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( pub fn text_document_document_highlight(
@ -786,7 +810,15 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>, 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 { let params = lsp::DocumentHighlightParams {
text_document_position_params: lsp::TextDocumentPositionParams { text_document_position_params: lsp::TextDocumentPositionParams {
text_document, text_document,
@ -798,7 +830,7 @@ impl Client {
}, },
}; };
self.call::<lsp::request::DocumentHighlightRequest>(params) Some(self.call::<lsp::request::DocumentHighlightRequest>(params))
} }
fn goto_request< fn goto_request<
@ -831,8 +863,20 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>, work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> Option<impl Future<Output = Result<Value>>> {
self.goto_request::<lsp::request::GotoDefinition>(text_document, position, work_done_token) 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( pub fn goto_type_definition(
@ -840,12 +884,23 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>, work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> Option<impl Future<Output = Result<Value>>> {
self.goto_request::<lsp::request::GotoTypeDefinition>( 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, text_document,
position, position,
work_done_token, work_done_token,
) ))
} }
pub fn goto_implementation( pub fn goto_implementation(
@ -853,12 +908,23 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>, work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> Option<impl Future<Output = Result<Value>>> {
self.goto_request::<lsp::request::GotoImplementation>( 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, text_document,
position, position,
work_done_token, work_done_token,
) ))
} }
pub fn goto_reference( pub fn goto_reference(
@ -866,7 +932,15 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>, 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 { let params = lsp::ReferenceParams {
text_document_position: lsp::TextDocumentPositionParams { text_document_position: lsp::TextDocumentPositionParams {
text_document, 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( pub fn document_symbols(
&self, &self,
text_document: lsp::TextDocumentIdentifier, 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 { let params = lsp::DocumentSymbolParams {
text_document, text_document,
work_done_progress_params: lsp::WorkDoneProgressParams::default(), work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::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 // 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 { let params = lsp::WorkspaceSymbolParams {
query, query,
work_done_progress_params: lsp::WorkDoneProgressParams::default(), work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(), partial_result_params: lsp::PartialResultParams::default(),
}; };
self.call::<lsp::request::WorkspaceSymbol>(params) Some(self.call::<lsp::request::WorkspaceSymbol>(params))
} }
pub fn code_actions( pub fn code_actions(
@ -913,7 +1003,18 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
range: lsp::Range, range: lsp::Range,
context: lsp::CodeActionContext, 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 { let params = lsp::CodeActionParams {
text_document, text_document,
range, range,
@ -922,26 +1023,22 @@ impl Client {
partial_result_params: lsp::PartialResultParams::default(), 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, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
new_name: String, new_name: String,
) -> anyhow::Result<lsp::WorkspaceEdit> { ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
let capabilities = self.capabilities.get().unwrap(); 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 { match capabilities.rename_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (), Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
// None | Some(false) // None | Some(false)
_ => { _ => return None,
log::warn!("rename_symbol failed: The server does not support rename");
let err = "The server does not support rename";
return Err(anyhow!(err));
}
}; };
let params = lsp::RenameParams { let params = lsp::RenameParams {
@ -955,11 +1052,21 @@ impl Client {
}, },
}; };
let response = self.request::<lsp::request::Rename>(params).await?; let request = self.call::<lsp::request::Rename>(params);
Ok(response.unwrap_or_default())
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 { let params = lsp::ExecuteCommandParams {
command: command.command, command: command.command,
arguments: command.arguments.unwrap_or_default(), 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 { pub enum Notification {
// we inject this notification to signal the LSP is ready // we inject this notification to signal the LSP is ready
Initialized, Initialized,
// and this notification to signal that the LSP exited
Exit,
PublishDiagnostics(lsp::PublishDiagnosticsParams), PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams), ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams), LogMessage(lsp::LogMessageParams),
@ -297,6 +299,7 @@ impl Notification {
let notification = match method { let notification = match method {
lsp::notification::Initialized::METHOD => Self::Initialized, lsp::notification::Initialized::METHOD => Self::Initialized,
lsp::notification::Exit::METHOD => Self::Exit,
lsp::notification::PublishDiagnostics::METHOD => { lsp::notification::PublishDiagnostics::METHOD => {
let params: lsp::PublishDiagnosticsParams = params.parse()?; let params: lsp::PublishDiagnosticsParams = params.parse()?;
Self::PublishDiagnostics(params) Self::PublishDiagnostics(params)
@ -353,7 +356,11 @@ impl Registry {
.map(|(_, client)| client.as_ref()) .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, &mut self,
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
@ -363,9 +370,11 @@ impl Registry {
None => return Ok(None), None => return Ok(None),
}; };
match self.inner.entry(language_config.scope.clone()) { let scope = language_config.scope.clone();
Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())),
Entry::Vacant(entry) => { match self.inner.entry(scope) {
Entry::Vacant(_) => Ok(None),
Entry::Occupied(mut entry) => {
// initialize a new client // initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed); let id = self.counter.fetch_add(1, Ordering::Relaxed);
@ -373,83 +382,41 @@ impl Registry {
start_client(id, language_config, config, doc_path)?; start_client(id, language_config, config, doc_path)?;
self.incoming.push(UnboundedReceiverStream::new(incoming)); 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)) Ok(Some(client))
} }
} }
} }
pub fn restart( pub fn get(
&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, &mut self,
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
config: &helix_core::syntax::LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>,
id: usize, ) -> Result<Option<Arc<Client>>> {
path: Option<&PathBuf>, let config = match &language_config.language_server {
) -> Result<Arc<Client>> { Some(config) => config,
let (client, incoming, initialize_notify) = Client::start( None => return Ok(None),
&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> match self.inner.entry(language_config.scope.clone()) {
_client Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())),
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {}) Entry::Vacant(entry) => {
.await // initialize a new client
.unwrap(); 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>> { 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) => { Err(err) => {
error!("err: <- {:?}", err); error!("err: <- {:?}", err);
break; break;

@ -10,11 +10,13 @@ use helix_view::{
align_view, align_view,
document::DocumentSavedEventResult, document::DocumentSavedEventResult,
editor::{ConfigEvent, EditorEvent}, editor::{ConfigEvent, EditorEvent},
graphics::Rect,
theme, theme,
tree::Layout, tree::Layout,
Align, Editor, Align, Editor,
}; };
use serde_json::json; use serde_json::json;
use tui::backend::Backend;
use crate::{ use crate::{
args::Args, args::Args,
@ -53,8 +55,21 @@ type Signals = futures_util::stream::Empty<()>;
const LSP_DEADLINE: Duration = Duration::from_millis(16); 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 { pub struct Application {
compositor: Compositor, compositor: Compositor,
terminal: Terminal,
pub editor: Editor, pub editor: Editor,
config: Arc<ArcSwap<Config>>, config: Arc<ArcSwap<Config>>,
@ -143,10 +158,18 @@ impl Application {
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
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 config = Arc::new(ArcSwap::from_pointee(config));
let mut editor = Editor::new( let mut editor = Editor::new(
compositor.size(), area,
theme_loader.clone(), theme_loader.clone(),
syn_loader.clone(), syn_loader.clone(),
Box::new(Map::new(Arc::clone(&config), |config: &Config| { Box::new(Map::new(Arc::clone(&config), |config: &Config| {
@ -245,6 +268,7 @@ impl Application {
let app = Self { let app = Self {
compositor, compositor,
terminal,
editor, editor,
config, config,
@ -266,15 +290,26 @@ impl Application {
#[cfg(not(feature = "integration"))] #[cfg(not(feature = "integration"))]
fn render(&mut self) { fn render(&mut self) {
let compositor = &mut self.compositor;
let mut cx = crate::compositor::Context { let mut cx = crate::compositor::Context {
editor: &mut self.editor, editor: &mut self.editor,
jobs: &mut self.jobs, jobs: &mut self.jobs,
scroll: None, 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) pub async fn event_loop<S>(&mut self, input_stream: &mut S)
@ -404,19 +439,24 @@ impl Application {
#[cfg(not(windows))] #[cfg(not(windows))]
pub async fn handle_signals(&mut self, signal: i32) { pub async fn handle_signals(&mut self, signal: i32) {
use helix_view::graphics::Rect;
match signal { match signal {
signal::SIGTSTP => { 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(); restore_term().unwrap();
low_level::emulate_default_handler(signal::SIGTSTP).unwrap(); low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
} }
signal::SIGCONT => { signal::SIGCONT => {
self.claim_term().await.unwrap(); self.claim_term().await.unwrap();
// redraw the terminal // redraw the terminal
let Rect { width, height, .. } = self.compositor.size(); let area = self.terminal.size().expect("couldn't get terminal size");
self.compositor.resize(width, height); self.compositor.resize(area);
self.compositor.load_cursor(); self.terminal.clear().expect("couldn't clear terminal");
self.render(); self.render();
} }
signal::SIGUSR1 => { signal::SIGUSR1 => {
@ -553,7 +593,14 @@ impl Application {
// Handle key events // Handle key events
let should_redraw = match event.unwrap() { let should_redraw = match event.unwrap() {
CrosstermEvent::Resize(width, height) => { 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 self.compositor
.handle_event(&Event::Resize(width, height), &mut cx) .handle_event(&Event::Resize(width, height), &mut cx)
} }
@ -836,6 +883,32 @@ impl Application {
Notification::ProgressMessage(_params) => { Notification::ProgressMessage(_params) => {
// do nothing // 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 { Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
@ -936,7 +1009,11 @@ impl Application {
} }
async fn claim_term(&mut self) -> Result<(), Error> { async fn claim_term(&mut self) -> Result<(), Error> {
use helix_view::graphics::CursorKind;
terminal::enable_raw_mode()?; terminal::enable_raw_mode()?;
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal.backend_mut().hide_cursor().ok();
}
let mut stdout = stdout(); let mut stdout = stdout();
execute!( execute!(
stdout, stdout,
@ -970,6 +1047,13 @@ impl Application {
self.event_loop(input_stream).await; self.event_loop(input_stream).await;
let close_errs = self.close().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()?; restore_term()?;
for err in close_errs { for err in close_errs {

@ -54,7 +54,7 @@ use crate::{
use crate::job::{self, Jobs}; use crate::job::{self, Jobs};
use futures_util::StreamExt; 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::{collections::HashSet, num::NonZeroUsize};
use std::{ use std::{
@ -210,17 +210,18 @@ impl MappableCommand {
copy_selection_on_prev_line, "Copy selection on previous line", copy_selection_on_prev_line, "Copy selection on previous line",
move_next_word_start, "Move to start of next word", move_next_word_start, "Move to start of next word",
move_prev_word_start, "Move to start of previous 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_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_next_long_word_start, "Move to start of next long word",
move_prev_long_word_start, "Move to start of previous 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", move_next_long_word_end, "Move to end of next long word",
extend_next_word_start, "Extend to start of next word", extend_next_word_start, "Extend to start of next word",
extend_prev_word_start, "Extend to start of previous 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_next_long_word_start, "Extend to start of next long word",
extend_prev_long_word_start, "Extend to start of previous 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_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_till_char, "Move till next occurrence of char",
find_next_char, "Move to next occurrence of char", find_next_char, "Move to next occurrence of char",
extend_till_char, "Extend till 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_next, "Add next search match to selection",
extend_search_prev, "Add previous search match to selection", extend_search_prev, "Add previous search match to selection",
search_selection, "Use current selection as search pattern", 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", 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, "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", 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_line_end, "Goto line end",
goto_next_buffer, "Goto next buffer", goto_next_buffer, "Goto next buffer",
goto_previous_buffer, "Goto previous buffer", goto_previous_buffer, "Goto previous buffer",
// TODO: different description ? goto_line_end_newline, "Goto newline at line end",
goto_line_end_newline, "Goto line end",
goto_first_nonwhitespace, "Goto first non-blank in line", goto_first_nonwhitespace, "Goto first non-blank in line",
trim_selections, "Trim whitespace from selections", trim_selections, "Trim whitespace from selections",
extend_to_line_start, "Extend to line start", extend_to_line_start, "Extend to line start",
@ -783,11 +784,7 @@ fn trim_selections(cx: &mut Context) {
let mut end = range.to(); let mut end = range.to();
start = movement::skip_while(text, start, |x| x.is_whitespace()).unwrap_or(start); 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); end = movement::backwards_skip_while(text, end, |x| x.is_whitespace()).unwrap_or(end);
if range.anchor < range.head { Some(Range::new(start, end).with_direction(range.direction()))
Some(Range::new(start, end))
} else {
Some(Range::new(end, start))
}
}) })
.collect(); .collect();
@ -874,7 +871,7 @@ fn goto_window(cx: &mut Context, align: Align) {
let config = cx.editor.config(); let config = cx.editor.config();
let (view, doc) = current!(cx.editor); 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 // respect user given count if any
// - 1 so we have at least one gap in the middle. // - 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) 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) { fn extend_next_long_word_start(cx: &mut Context) {
extend_word_impl(cx, movement::move_next_long_word_start) 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() doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
} }
KeyEvent {
code: KeyCode::Tab, ..
} => '\t',
KeyEvent { KeyEvent {
code: KeyCode::Char(ch), code: KeyCode::Char(ch),
.. ..
@ -1280,6 +1285,9 @@ fn replace(cx: &mut Context) {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
} => Some(doc.line_ending.as_str()), } => Some(doc.line_ending.as_str()),
KeyEvent {
code: KeyCode::Tab, ..
} => Some("\t"),
_ => None, _ => None,
}; };
@ -1376,9 +1384,9 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
return; 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 { view.offset.row = match direction {
Forward => view.offset.row + offset, 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) { fn page_up(cx: &mut Context) {
let view = view!(cx.editor); let view = view!(cx.editor);
let offset = view.inner_area().height as usize; let offset = view.inner_height();
scroll(cx, offset, Direction::Backward); scroll(cx, offset, Direction::Backward);
} }
fn page_down(cx: &mut Context) { fn page_down(cx: &mut Context) {
let view = view!(cx.editor); let view = view!(cx.editor);
let offset = view.inner_area().height as usize; let offset = view.inner_height();
scroll(cx, offset, Direction::Forward); scroll(cx, offset, Direction::Forward);
} }
fn half_page_up(cx: &mut Context) { fn half_page_up(cx: &mut Context) {
let view = view!(cx.editor); 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); scroll(cx, offset, Direction::Backward);
} }
fn half_page_down(cx: &mut Context) { fn half_page_down(cx: &mut Context) {
let view = view!(cx.editor); 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); scroll(cx, offset, Direction::Forward);
} }
@ -1655,11 +1663,7 @@ fn search_impl(
// Determine range direction based on the primary range // Determine range direction based on the primary range
let primary = selection.primary(); let primary = selection.primary();
let range = if primary.head < primary.anchor { let range = Range::new(start, end).with_direction(primary.direction());
Range::new(end, start)
} else {
Range::new(start, end)
};
let selection = match movement { let selection = match movement {
Movement::Extend => selection.clone().push(range), Movement::Extend => selection.clone().push(range),
@ -1805,7 +1809,36 @@ fn search_selection(cx: &mut Context) {
.join("|"); .join("|");
let msg = format!("register '{}' set to '{}'", '/', &regex); 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); cx.editor.set_status(msg);
} }
@ -1976,7 +2009,7 @@ fn global_search(cx: &mut Context) {
align_view(doc, view, Align::Center); align_view(doc, view, Align::Center);
}, },
|_editor, FileResult { path, line_num }| { |_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))); 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 start = text.line_to_char(start_line);
let end = text.line_to_char((end_line + 1).min(text.len_lines())); let end = text.line_to_char((end_line + 1).min(text.len_lines()));
if range.anchor <= range.head { Range::new(start, end).with_direction(range.direction())
Range::new(start, end)
} else {
Range::new(end, start)
}
}), }),
); );
} }
@ -2104,11 +2133,7 @@ fn shrink_to_line_bounds(cx: &mut Context) {
end = text.line_to_char(end_line); end = text.line_to_char(end_line);
} }
if range.anchor <= range.head { Range::new(start, end).with_direction(range.direction())
Range::new(start, end)
} else {
Range::new(end, start)
}
}), }),
); );
} }
@ -2121,16 +2146,14 @@ enum Operation {
fn delete_selection_impl(cx: &mut Context, op: Operation) { fn delete_selection_impl(cx: &mut Context, op: Operation) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
if cx.register != Some('_') { if cx.register != Some('_') {
// first yank the selection // first yank the selection
let text = doc.text().slice(..);
let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect(); let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
let reg_name = cx.register.unwrap_or('"'); let reg_name = cx.register.unwrap_or('"');
let registers = &mut cx.editor.registers; cx.editor.registers.write(reg_name, values);
let reg = registers.get_mut(reg_name);
reg.write(values);
}; };
// then delete // then delete
@ -2378,7 +2401,7 @@ fn buffer_picker(cx: &mut Context) {
.selection(view_id) .selection(view_id)
.primary() .primary()
.cursor_line(doc.text().slice(..)); .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))); cx.push_layer(Box::new(overlayed(picker)));
@ -2445,7 +2468,6 @@ fn jumplist_picker(cx: &mut Context) {
.views() .views()
.flat_map(|(view, _)| { .flat_map(|(view, _)| {
view.jumps view.jumps
.get()
.iter() .iter()
.map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone())) .map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone()))
}) })
@ -2459,7 +2481,7 @@ fn jumplist_picker(cx: &mut Context) {
|editor, meta| { |editor, meta| {
let doc = &editor.documents.get(&meta.id)?; let doc = &editor.documents.get(&meta.id)?;
let line = meta.selection.primary().cursor_line(doc.text().slice(..)); 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))); 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 { let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
bindings.iter().fold(String::new(), |mut acc, bind| { bindings.iter().fold(String::new(), |mut acc, bind| {
if !acc.is_empty() { if !acc.is_empty() {
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 acc
}) })
}; };
@ -2762,15 +2782,15 @@ fn goto_line(cx: &mut Context) {
fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) { fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
if let Some(count) = count { if let Some(count) = count {
let (view, doc) = current!(editor); 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. // 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 { } else {
doc.text().len_lines() - 1 text.len_lines() - 1
}; };
let line_idx = std::cmp::min(count.get() - 1, max_line); let line_idx = std::cmp::min(count.get() - 1, max_line);
let text = doc.text().slice(..); let pos = text.line_to_char(line_idx);
let pos = doc.text().line_to_char(line_idx);
let selection = doc let selection = doc
.selection(view.id) .selection(view.id)
.clone() .clone()
@ -2783,14 +2803,14 @@ fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
fn goto_last_line(cx: &mut Context) { fn goto_last_line(cx: &mut Context) {
let (view, doc) = current!(cx.editor); 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. // 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 { } else {
doc.text().len_lines() - 1 text.len_lines() - 1
}; };
let text = doc.text().slice(..); let pos = text.line_to_char(line_idx);
let pos = doc.text().line_to_char(line_idx);
let selection = doc let selection = doc
.selection(view.id) .selection(view.id)
.clone() .clone()
@ -2964,22 +2984,22 @@ pub mod insert {
use helix_core::chars::char_is_word; use helix_core::chars::char_is_word;
let mut iter = text.chars_at(cursor); let mut iter = text.chars_at(cursor);
iter.reverse(); iter.reverse();
for _ in 0..config.completion_trigger_len { for _ in 0..config.completion_trigger_len {
match iter.next() { match iter.next() {
Some(c) if char_is_word(c) => {} Some(c) if char_is_word(c) => {}
Some(c) if config.completion_trigger_chars.contains(&c) => {}
_ => return, _ => return,
} }
} }
super::completion(cx); 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; use helix_lsp::lsp;
// if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor);
let language_server = match doc.language_server() { let language_server = match doc.language_server() {
Some(language_server) => language_server, Some(language_server) => language_server,
None => return false, None => return,
}; };
let capabilities = language_server.capabilities(); let capabilities = language_server.capabilities();
@ -2989,35 +3009,11 @@ pub mod insert {
.. ..
}) = &capabilities.completion_provider }) = &capabilities.completion_provider
{ {
triggers.iter().any(|trigger| trigger.contains(ch)) // TODO: what if trigger is multiple chars long
} else { if triggers.iter().any(|trigger| trigger.contains(ch)) {
false cx.editor.clear_idle_timer();
} super::completion(cx);
}
fn language_server_completion(cx: &mut Context, ch: char) {
use helix_core::chars::char_is_word;
let config = cx.editor.config();
if !config.auto_completion {
return;
}
let (view, doc) = current_ref!(cx.editor);
if char_is_word(ch) && doc.savepoint.is_none() {
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let mut iter = text.chars_at(cursor);
iter.reverse();
for _ in 0..config.completion_trigger_len {
if iter.next().map_or(true, |c| !char_is_word(c)) {
return;
}
} }
cx.editor.reset_idle_timer();
return;
}
if is_server_trigger_char(doc, ch) {
cx.editor.reset_idle_timer_zero();
} }
} }
@ -3130,40 +3126,59 @@ pub mod insert {
let curr = contents.get_char(pos).unwrap_or(' '); let curr = contents.get_char(pos).unwrap_or(' ');
let current_line = text.char_to_line(pos); let current_line = text.char_to_line(pos);
let indent = indent::indent_for_newline( let line_is_only_whitespace = text
doc.language_config(), .line(current_line)
doc.syntax(), .chars()
&doc.indent_style, .all(|char| char.is_ascii_whitespace());
doc.tab_width(),
text, let mut new_text = String::new();
current_line,
pos, // If the current line is all whitespace, insert a line ending at the beginning of
current_line, // the current line. This makes the current line empty and the new line contain the
); // indentation of the old line.
let mut text = String::new(); let (from, to, local_offs) = if line_is_only_whitespace {
// If we are between pairs (such as brackets), we want to let line_start = text.line_to_char(current_line);
// insert an additional line which is indented one level new_text.push_str(doc.line_ending.as_str());
// more and place the cursor there
let on_auto_pair = doc (line_start, line_start, new_text.chars().count())
.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
} else { } else {
text.reserve_exact(1 + indent.len()); let indent = indent::indent_for_newline(
text.push_str(doc.line_ending.as_str()); doc.language_config(),
text.push_str(&indent); doc.syntax(),
text.chars().count() &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 { 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 // 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 // can be used with cx.mode to do replace or extend on most changes
ranges.push(new_range); 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())); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
@ -3347,7 +3362,7 @@ fn undo(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
for _ in 0..count { for _ in 0..count {
if !doc.undo(view) { if !doc.undo(view.id) {
cx.editor.set_status("Already at oldest change"); cx.editor.set_status("Already at oldest change");
break; break;
} }
@ -3358,7 +3373,7 @@ fn redo(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
for _ in 0..count { for _ in 0..count {
if !doc.redo(view) { if !doc.redo(view.id) {
cx.editor.set_status("Already at newest change"); cx.editor.set_status("Already at newest change");
break; break;
} }
@ -3370,7 +3385,7 @@ fn earlier(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
for _ in 0..count { for _ in 0..count {
// rather than doing in batch we do this so get error halfway // 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"); cx.editor.set_status("Already at oldest change");
break; break;
} }
@ -3382,7 +3397,7 @@ fn later(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
for _ in 0..count { for _ in 0..count {
// rather than doing in batch we do this so get error halfway // 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"); cx.editor.set_status("Already at newest change");
break; break;
} }
@ -3511,7 +3526,14 @@ enum Paste {
Cursor, 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() { if values.is_empty() {
return; 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()); .any(|value| get_line_ending_of_str(value).is_some());
// Only compiled once. // Only compiled once.
#[allow(clippy::trivial_regex)]
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap()); static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap());
let mut values = values let mut values = values
.iter() .iter()
@ -3541,9 +3562,10 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa
let text = doc.text(); let text = doc.text();
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
let mut offset = 0;
let mut ranges = SmallVec::with_capacity(selection.len()); 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) { let pos = match (action, linewise) {
// paste linewise before // paste linewise before
(Paste::Before, true) => text.line_to_char(text.char_to_line(range.from())), (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() .as_ref()
.map(|content| content.chars().count()) .map(|content| content.chars().count())
.unwrap_or_default(); .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) (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); apply_transaction(&transaction, doc, view);
} }
@ -3584,7 +3611,7 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
Mode::Normal => Paste::Before, Mode::Normal => Paste::Before,
}; };
let (view, doc) = current!(cx.editor); 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( fn paste_clipboard_impl(
@ -3596,7 +3623,7 @@ fn paste_clipboard_impl(
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
match editor.clipboard_provider.get_contents(clipboard_type) { match editor.clipboard_provider.get_contents(clipboard_type) {
Ok(contents) => { Ok(contents) => {
paste_impl(&[contents], doc, view, action, count); paste_impl(&[contents], doc, view, action, count, editor.mode);
Ok(()) Ok(())
} }
Err(e) => Err(e.context("Couldn't get system clipboard contents")), 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; let registers = &mut cx.editor.registers;
if let Some(values) = registers.read(reg_name) { 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); let (view, doc) = current!(cx.editor);
// via lsp if available // via lsp if available
// else via tree-sitter indentation calculations // TODO: else via tree-sitter indentation calculations
let language_server = match doc.language_server() { let language_server = match doc.language_server() {
Some(language_server) => 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())) .map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding()))
.collect(); .collect();
// TODO: all of the TODO's and commented code inside the loop, if ranges.len() != 1 {
// to make this actually work. cx.editor
for _range in ranges { .set_error("format_selections only supports a single selection for now");
let _language_server = match doc.language_server() { return;
Some(language_server) => language_server, }
None => return,
};
// TODO: handle fails
// TODO: concurrent map
// 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( let range = ranges[0];
// doc.identifier(),
// range,
// lsp::FormattingOptions::default(),
// ))
// .unwrap_or_default();
// let transaction = helix_lsp::util::generate_transaction_from_edits( let request = match language_server.text_document_range_formatting(
// doc.text(), doc.identifier(),
// edits, range,
// language_server.offset_encoding(), 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; use movement::skip_while;
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let text = doc.text(); 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) { fn join_selections(cx: &mut Context) {
join_selections_inner(cx, false) join_selections_impl(cx, false)
} }
fn join_selections_space(cx: &mut Context) { fn join_selections_space(cx: &mut Context) {
join_selections_inner(cx, true) join_selections_impl(cx, true)
} }
fn keep_selections(cx: &mut Context) { 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 pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
let future = language_server.completion(doc.identifier(), pos, None); let future = match language_server.completion(doc.identifier(), pos, None) {
let future = async move { Some(future) => future,
match future.await { None => return,
Ok(v) => Ok(v),
Err(helix_lsp::Error::Timeout) => Ok(serde_json::Value::Null),
Err(e) => Err(e),
}
}; };
let trigger_offset = cursor; let trigger_offset = cursor;
@ -4004,56 +4034,29 @@ pub fn completion(cx: &mut Context) {
iter.reverse(); iter.reverse();
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
let start_offset = cursor.saturating_sub(offset); 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( cx.callback(
future, future,
move |editor, compositor, response: Option<lsp::CompletionResponse>| { move |editor, compositor, response: Option<lsp::CompletionResponse>| {
let doc = doc_mut!(editor);
let savepoint = match doc.savepoint.take() {
Some(s) => s,
None => return,
};
if editor.mode != Mode::Insert { if editor.mode != Mode::Insert {
return; // we're not in insert mode anymore
}
if savepoint.0 != trigger_version {
doc.savepoint = Some(savepoint);
return; return;
} }
let mut items = match response { let items = match response {
Some(lsp::CompletionResponse::Array(items)) => items, Some(lsp::CompletionResponse::Array(items)) => items,
// TODO: do something with is_incomplete // TODO: do something with is_incomplete
Some(lsp::CompletionResponse::List(lsp::CompletionList { Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: _is_incomplete, is_incomplete: _is_incomplete,
items, items,
})) => items, })) => items,
None => { None => Vec::new(),
editor.set_status(
"The completion response is none and will request server again",
);
editor.reset_idle_timer();
return;
}
}; };
if !prefix.is_empty() {
items.retain(|item| {
item.filter_text
.as_ref()
.unwrap_or(&item.label)
.starts_with(&prefix)
});
}
if items.is_empty() { if items.is_empty() {
// editor.set_error("No completion available".to_string()); // editor.set_error("No completion available");
return; return;
} }
doc.savepoint = Some(savepoint);
let size = compositor.size(); let size = compositor.size();
let ui = compositor.find::<ui::EditorView>().unwrap(); let ui = compositor.find::<ui::EditorView>().unwrap();
ui.set_completion( ui.set_completion(
@ -4428,7 +4431,7 @@ fn align_view_middle(cx: &mut Context) {
view.offset.col = pos view.offset.col = pos
.col .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) { fn scroll_up(cx: &mut Context) {
@ -4611,8 +4614,13 @@ fn surround_add(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
let (open, close) = surround::get_pair(ch); 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 changes = Vec::with_capacity(selection.len() * 2);
let mut ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
for range in selection.iter() { for range in selection.iter() {
let mut o = Tendril::new(); let mut o = Tendril::new();
o.push(open); o.push(open);
@ -4620,10 +4628,21 @@ fn surround_add(cx: &mut Context) {
c.push(close); c.push(close);
changes.push((range.from(), range.from(), Some(o))); changes.push((range.from(), range.from(), Some(o)));
changes.push((range.to(), range.to(), Some(c))); 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); apply_transaction(&transaction, doc, view);
exit_select_mode(cx);
}) })
} }
@ -4663,6 +4682,7 @@ fn surround_replace(cx: &mut Context) {
}), }),
); );
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
exit_select_mode(cx);
}); });
}) })
} }
@ -4690,6 +4710,7 @@ fn surround_delete(cx: &mut Context) {
let transaction = let transaction =
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None))); Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
apply_transaction(&transaction, doc, view); 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 mut ranges = SmallVec::with_capacity(selection.len());
let text = doc.text().slice(..); let text = doc.text().slice(..);
let mut shell_output: Option<Tendril> = None;
let mut offset = 0isize;
for range in selection.ranges() { for range in selection.ranges() {
let fragment = range.slice(text); let (output, success) = if let Some(output) = shell_output.as_ref() {
let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment.into())) { (output.clone(), true)
Ok(result) => result, } else {
Err(err) => { let fragment = range.slice(text);
cx.editor.set_error(err.to_string()); match shell_impl(shell, cmd, pipe.then(|| fragment.into())) {
return; 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; return;
} }
let (from, to) = match behavior { let output_len = output.chars().count();
ShellBehavior::Replace => (range.from(), range.to()),
ShellBehavior::Insert => (range.from(), range.from()), let (from, to, deleted_len) = match behavior {
ShellBehavior::Append => (range.to(), range.to()), ShellBehavior::Replace => (range.from(), range.to(), range.len()),
_ => (range.from(), range.from()), 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))); changes.push((from, to, Some(output)));
} }
@ -4948,14 +4990,18 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
} }
enum IncrementDirection {
Increase,
Decrease,
}
/// Increment object under cursor by count. /// Increment object under cursor by count.
fn increment(cx: &mut Context) { fn increment(cx: &mut Context) {
increment_impl(cx, cx.count() as i64); increment_impl(cx, IncrementDirection::Increase);
} }
/// Decrement object under cursor by count. /// Decrement object under cursor by count.
fn decrement(cx: &mut Context) { 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 /// 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`. /// 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 // TODO: when incrementing or decrementing a number that gets a new digit or lose one, the
// selection is updated improperly. // selection is updated improperly.
find_char_impl( find_char_impl(
@ -4991,6 +5037,17 @@ fn increment_impl(cx: &mut Context, amount: i64) {
1, 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 (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
let text = doc.text().slice(..); let text = doc.text().slice(..);
@ -5010,6 +5067,8 @@ fn increment_impl(cx: &mut Context, amount: i64) {
let (range, new_text) = incrementor.increment(amount); let (range, new_text) = incrementor.increment(amount);
amount += increase_by;
Some((range.from(), range.to(), Some(new_text))) Some((range.from(), range.to(), Some(new_text)))
}) })
.collect(); .collect();
@ -5026,16 +5085,20 @@ fn increment_impl(cx: &mut Context, amount: i64) {
overlapping_indexes.insert(i + 1); overlapping_indexes.insert(i + 1);
} }
} }
let changes = changes.into_iter().enumerate().filter_map(|(i, change)| { let changes: Vec<_> = changes
if overlapping_indexes.contains(&i) { .into_iter()
None .enumerate()
} else { .filter_map(|(i, change)| {
Some(change) if overlapping_indexes.contains(&i) {
} None
}); } else {
Some(change)
}
})
.collect();
if changes.clone().count() > 0 { if !changes.is_empty() {
let transaction = Transaction::change(doc.text(), changes); let transaction = Transaction::change(doc.text(), changes.into_iter());
let transaction = transaction.with_selection(selection.clone()); let transaction = transaction.with_selection(selection.clone());
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
@ -5057,7 +5120,7 @@ fn record_macro(cx: &mut Context) {
} }
}) })
.collect::<String>(); .collect::<String>();
cx.editor.registers.get_mut(reg).write(vec![s]); cx.editor.registers.write(reg, vec![s]);
cx.editor cx.editor
.set_status(format!("Recorded to register [{}]", reg)); .set_status(format!("Recorded to register [{}]", reg));
} else { } else {

File diff suppressed because it is too large Load Diff

@ -85,7 +85,7 @@ fn thread_picker(
frame.line.saturating_sub(1), frame.line.saturating_sub(1),
frame.end_line.unwrap_or(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)); compositor.push(Box::new(picker));
@ -706,7 +706,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
.and_then(|source| source.path.clone()) .and_then(|source| source.path.clone())
.map(|path| { .map(|path| {
( (
path, path.into(),
Some(( Some((
frame.line.saturating_sub(1), frame.line.saturating_sub(1),
frame.end_line.unwrap_or(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.start.line as usize,
location.range.end.line as usize, location.range.end.line as usize,
)); ));
(path, line) (path.into(), line)
} }
// TODO: share with symbol picker(symbol.location) // TODO: share with symbol picker(symbol.location)
@ -333,7 +333,14 @@ pub fn symbol_picker(cx: &mut Context) {
let current_url = doc.url(); let current_url = doc.url();
let offset_encoding = language_server.offset_encoding(); 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( cx.callback(
future, future,
@ -365,7 +372,14 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
let current_url = doc.url(); let current_url = doc.url();
let language_server = language_server!(cx.editor, doc); let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding(); 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( cx.callback(
future, 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 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(), doc.identifier(),
range, range,
// Filter and convert overlapping diagnostics // Filter and convert overlapping diagnostics
@ -509,7 +523,14 @@ pub fn code_action(cx: &mut Context) {
.collect(), .collect(),
only: None, only: None,
}, },
); ) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support code actions");
return;
}
};
cx.callback( cx.callback(
future, future,
@ -567,35 +588,34 @@ pub fn code_action(cx: &mut Context) {
.reverse() .reverse()
}); });
let mut picker = let mut picker = ui::Menu::new(actions, true, (), move |editor, code_action, event| {
ui::Menu::new(actions, false, (), move |editor, code_action, event| { if event != PromptEvent::Validate {
if event != PromptEvent::Validate { return;
return; }
}
// always present here // always present here
let code_action = code_action.unwrap(); let code_action = code_action.unwrap();
match code_action { match code_action {
lsp::CodeActionOrCommand::Command(command) => { lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command); log::debug!("code action command: {:?}", command);
execute_lsp_command(editor, command.clone()); 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 // if code action provides both edit and command first the edit
// should be applied and then the command // should be applied and then the command
if let Some(command) = &code_action.command { if let Some(command) = &code_action.command {
execute_lsp_command(editor, command.clone()); execute_lsp_command(editor, command.clone());
}
} }
} }
}); }
});
picker.move_down(); // pre-select the first item picker.move_down(); // pre-select the first item
let popup = Popup::new("code-action", picker); 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 // the command is executed on the server and communicated back
// to the client asynchronously using workspace edits // 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 { tokio::spawn(async move {
let res = command_future.await; let res = future.await;
if let Err(e) = res { if let Err(e) = res {
log::error!("execute LSP command: {}", e); 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 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( cx.callback(
future, future,
@ -899,7 +933,14 @@ pub fn goto_type_definition(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding); 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( cx.callback(
future, future,
@ -917,7 +958,14 @@ pub fn goto_implementation(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding); 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( cx.callback(
future, future,
@ -935,7 +983,14 @@ pub fn goto_reference(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding); 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( cx.callback(
future, 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) { let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
Some(f) => f, Some(f) => f,
None => return, None => {
if was_manually_invoked {
cx.editor
.set_error("Language server does not support signature-help");
}
return;
}
}; };
cx.callback( cx.callback(
@ -1079,7 +1140,14 @@ pub fn hover(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding); 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( cx.callback(
future, future,
@ -1149,8 +1217,16 @@ pub fn rename_symbol(cx: &mut Context) {
let pos = doc.position(view.id, offset_encoding); let pos = doc.position(view.id, offset_encoding);
let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string()); let future =
match block_on(task) { 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), Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
Err(err) => cx.editor.set_error(err.to_string()), 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 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( cx.callback(
future, 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 uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let success = doc.earlier(view, uk); let success = doc.earlier(view.id, uk);
if !success { if !success {
cx.editor.set_status("Already at oldest change"); 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 uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let success = doc.later(view, uk); let success = doc.later(view.id, uk);
if !success { if !success {
cx.editor.set_status("Already at newest change"); 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. /// Update the [`Document`] if it has been modified.
fn update( fn update(
cx: &mut compositor::Context, 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( fn lsp_restart(
cx: &mut compositor::Context, cx: &mut compositor::Context,
_args: &[Cow<str>], _args: &[Cow<str>],
@ -2012,6 +2128,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: reload, fun: reload,
completer: None, completer: None,
}, },
TypableCommand {
name: "reload-all",
aliases: &[],
doc: "Discard changes and reload all documents from the source files.",
fun: reload_all,
completer: None,
},
TypableCommand { TypableCommand {
name: "update", name: "update",
aliases: &[], aliases: &[],
@ -2019,6 +2142,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: update, fun: update,
completer: None, completer: None,
}, },
TypableCommand {
name: "lsp-workspace-command",
aliases: &[],
doc: "Open workspace command picker",
fun: lsp_workspace_command,
completer: Some(completers::lsp_workspace_command),
},
TypableCommand { TypableCommand {
name: "lsp-restart", name: "lsp-restart",
aliases: &[], aliases: &[],
@ -2214,7 +2344,10 @@ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableComma
.collect() .collect()
}); });
#[allow(clippy::unnecessary_unwrap)]
pub(super) fn command_mode(cx: &mut Context) { pub(super) fn command_mode(cx: &mut Context) {
use shellwords::Shellwords;
let mut prompt = Prompt::new( let mut prompt = Prompt::new(
":".into(), ":".into(),
Some(':'), Some(':'),
@ -2222,10 +2355,11 @@ pub(super) fn command_mode(cx: &mut Context) {
static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> = static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
// simple heuristic: if there's no just one part, complete command name. let shellwords = Shellwords::from(input);
// if there's a space, per command completion kicks in. let words = shellwords.words();
// we use .this over split_whitespace() because we care about empty segments
if input.split(' ').count() <= 1 { 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 let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
.iter() .iter()
.filter_map(|command| { .filter_map(|command| {
@ -2241,19 +2375,29 @@ pub(super) fn command_mode(cx: &mut Context) {
.map(|(name, _)| (0.., name.into())) .map(|(name, _)| (0.., name.into()))
.collect() .collect()
} else { } else {
let parts = shellwords::shellwords(input); // Otherwise, use the command's completer and the last shellword
let part = parts.last().unwrap(); // 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 { if let Some(typed::TypableCommand {
completer: Some(completer), completer: Some(completer),
.. ..
}) = typed::TYPABLE_COMMAND_MAP.get(&parts[0] as &str) }) = typed::TYPABLE_COMMAND_MAP.get(&words[0] as &str)
{ {
completer(editor, part) completer(editor, part)
.into_iter() .into_iter()
.map(|(range, file)| { .map(|(range, file)| {
let file = shellwords::escape(file);
// offset ranges to input // offset ranges to input
let offset = input.len() - part.len(); let offset = input.len() - part_len;
let range = (range.start + offset)..; let range = (range.start + offset)..;
(range, file) (range, file)
}) })
@ -2279,7 +2423,8 @@ pub(super) fn command_mode(cx: &mut Context) {
// Handle typable commands // Handle typable commands
if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) { 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) { if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e)); cx.editor.set_error(format!("{}", e));

@ -4,8 +4,6 @@
use helix_core::Position; use helix_core::Position;
use helix_view::graphics::{CursorKind, Rect}; use helix_view::graphics::{CursorKind, Rect};
#[cfg(feature = "integration")]
use tui::backend::TestBackend;
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>; 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 { pub struct Compositor {
layers: Vec<Box<dyn Component>>, layers: Vec<Box<dyn Component>>,
terminal: Terminal, area: Rect,
pub(crate) last_picker: Option<Box<dyn Component>>, pub(crate) last_picker: Option<Box<dyn Component>>,
} }
impl Compositor { impl Compositor {
pub fn new() -> anyhow::Result<Self> { pub fn new(area: Rect) -> Self {
#[cfg(not(feature = "integration"))] Self {
let backend = CrosstermBackend::new(stdout());
#[cfg(feature = "integration")]
let backend = TestBackend::new(120, 150);
let terminal = Terminal::new(backend).context("build terminal")?;
Ok(Self {
layers: Vec::new(), layers: Vec::new(),
terminal, area,
last_picker: None, last_picker: None,
}) }
} }
pub fn size(&self) -> Rect { pub fn size(&self) -> Rect {
self.terminal.size().expect("couldn't get terminal size") self.area
}
pub fn resize(&mut self, width: u16, height: u16) {
self.terminal
.resize(Rect::new(0, 0, width, height))
.expect("Unable to resize terminal")
} }
pub fn save_cursor(&mut self) { pub fn resize(&mut self, area: Rect) {
if self.terminal.cursor_kind() == CursorKind::Hidden { self.area = area;
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 push(&mut self, mut layer: Box<dyn Component>) { pub fn push(&mut self, mut layer: Box<dyn Component>) {
@ -203,25 +162,10 @@ impl Compositor {
consumed consumed
} }
pub fn render(&mut self, cx: &mut Context) { pub fn render(&mut self, area: Rect, surface: &mut Surface, 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();
for layer in &mut self.layers { for layer in &mut self.layers {
layer.render(area, surface, cx); 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) { 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::EVENT) => "event",
Some(lsp::CompletionItemKind::OPERATOR) => "operator", Some(lsp::CompletionItemKind::OPERATOR) => "operator",
Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param", Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
Some(kind) => unimplemented!("{:?}", kind), Some(kind) => {
log::error!("Received unknown completion item kind: {:?}", kind);
""
}
None => "", None => "",
}), }),
// self.detail.as_deref().unwrap_or("") // self.detail.as_deref().unwrap_or("")
@ -113,7 +116,8 @@ impl Completion {
let edit = match edit { let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(), lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => { 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 { let mut completion = Self {
popup, popup,
start_offset, start_offset,
@ -241,21 +245,13 @@ impl Completion {
completion_item: lsp::CompletionItem, completion_item: lsp::CompletionItem,
) -> Option<CompletionItem> { ) -> Option<CompletionItem> {
let language_server = doc.language_server()?; 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); let response = helix_lsp::block_on(future);
match response { match response {
Ok(completion_item) => Some(completion_item), Ok(value) => serde_json::from_value(value).ok(),
Err(err) => { Err(err) => {
log::error!("execute LSP command: {}", err); log::error!("Failed to resolve completion item: {}", err);
None None
} }
} }
@ -300,6 +296,12 @@ impl Completion {
self.popup.contents().is_empty() 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 { pub fn ensure_item_resolved(&mut self, cx: &mut commands::Context) -> bool {
// > If computing full completion items is expensive, servers can additionally provide a // > If computing full completion items is expensive, servers can additionally provide a
// > handler for the completion item resolve request. ... // > 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. // > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
// > The returned completion item should have the documentation property filled in. // > 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 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
match self.popup.contents_mut().selection_mut() { let current_item = match self.popup.contents().selection() {
Some(item) if item.documentation.is_none() => { Some(item) if item.documentation.is_none() => item.clone(),
let doc = doc!(cx.editor); _ => return false,
if let Some(resolved_item) = Self::resolve_completion_item(doc, item.clone()) { };
*item = resolved_item;
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, surface: &mut Surface,
is_focused: bool, is_focused: bool,
) { ) {
let inner = view.inner_area(); let inner = view.inner_area(doc);
let area = view.area; let area = view.area;
let theme = &editor.theme; let theme = &editor.theme;
let config = editor.config();
// DAP: Highlight current stack frame position // DAP: Highlight current stack frame position
let stack_frame = editor.debugger.as_ref().and_then(|debugger| { 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); 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); Self::highlight_cursorcolumn(doc, view, surface, theme);
} }
@ -143,22 +144,14 @@ impl EditorView {
doc, doc,
view, view,
theme, theme,
&editor.config().cursor_shape, &config.cursor_shape,
), ),
)) ))
} else { } else {
Box::new(highlights) Box::new(highlights)
}; };
Self::render_text_highlights( Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config);
doc,
view.offset,
inner,
surface,
theme,
highlights,
&editor.config(),
);
Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused); Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
Self::render_rulers(editor, doc, view, inner, surface, theme); 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 let statusline_area = view
.area .area
@ -759,9 +752,10 @@ impl EditorView {
// avoid lots of small allocations by reusing a text buffer for each line // avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8); let mut text = String::with_capacity(8);
for (constructor, width) in view.gutters() { for gutter_type in view.gutters() {
let gutter = constructor(editor, doc, view, theme, is_focused, *width); let gutter = gutter_type.style(editor, doc, view, theme, is_focused);
text.reserve(*width); // ensure there's enough space for the gutter 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() { for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
let selected = cursors.contains(&line); let selected = cursors.contains(&line);
let x = viewport.x + offset; let x = viewport.x + offset;
@ -774,13 +768,13 @@ impl EditorView {
}; };
if let Some(style) = gutter(line, selected, &mut text) { 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 { } else {
surface.set_style( surface.set_style(
Rect { Rect {
x, x,
y, y,
width: *width as u16, width: width as u16,
height: 1, height: 1,
}, },
gutter_style, gutter_style,
@ -789,12 +783,11 @@ impl EditorView {
text.clear(); text.clear();
} }
offset += *width as u16; offset += width as u16;
} }
} }
pub fn render_diagnostics( pub fn render_diagnostics(
&self,
doc: &Document, doc: &Document,
view: &View, view: &View,
viewport: Rect, viewport: Rect,
@ -905,7 +898,7 @@ impl EditorView {
.or_else(|| theme.try_get_exact("ui.cursorcolumn")) .or_else(|| theme.try_get_exact("ui.cursorcolumn"))
.unwrap_or_else(|| theme.get("ui.cursorline.secondary")); .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 offset = view.offset.col;
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
@ -1377,7 +1370,9 @@ impl Component for EditorView {
cx.editor.status_msg = None; cx.editor.status_msg = None;
let mode = cx.editor.mode(); 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; let focus = view.id;
if let Some(on_next_key) = self.on_next_key.take() { 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 view = view_mut!(cx.editor, focus);
let doc = doc_mut!(cx.editor, &view.doc); 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 // Store a history state if not in insert mode. This also takes care of
// committing changes when leaving insert mode. // committing changes when leaving insert mode.
if mode != Mode::Insert { if mode != Mode::Insert {
doc.append_changes_to_history(view.id); 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) 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; use super::PromptEvent as MenuEvent;
impl<T: Item + 'static> Component for Menu<T> { impl<T: Item + 'static> Component for Menu<T> {
@ -330,11 +341,6 @@ impl<T: Item + 'static> Component for Menu<T> {
(a + b - 1) / b (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 rows = options.iter().map(|option| option.row(&self.editor_data));
let table = Table::new(rows) let table = Table::new(rows)
.style(style) .style(style)
@ -367,20 +373,24 @@ impl<T: Item + 'static> Component for Menu<T> {
let fits = len <= win_height; let fits = len <= win_height;
let scroll_style = theme.get("ui.menu.scroll"); let scroll_style = theme.get("ui.menu.scroll");
for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() { if !fits {
let cell = &mut surface[(area.x + area.width - 1, area.y + i as u16)]; 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 { let mut cell;
// Draw scroll track for i in 0..win_height {
cell.set_symbol("▐"); // right half block cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
let is_marked = i >= scroll_line && i < scroll_line + scroll_height; cell.set_symbol("▐"); // right half block
if !fits && is_marked { if scroll_line <= i && i < scroll_line + scroll_height {
// Draw scroll thumb // Draw scroll thumb
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); 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); 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() .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> { pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
filename_impl(editor, input, |entry| { filename_impl(editor, input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); 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 fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use tui::widgets::Widget; use tui::widgets::Widget;
use std::time::Instant; use std::{cmp::Ordering, time::Instant};
use std::{ use std::{collections::HashMap, io::Read, path::PathBuf};
cmp::Reverse,
collections::HashMap,
io::Read,
path::{Path, PathBuf},
};
use crate::ui::{Prompt, PromptEvent}; use crate::ui::{Prompt, PromptEvent};
use helix_core::{movement::Direction, Position}; use helix_core::{movement::Direction, Position};
use helix_view::{ use helix_view::{
editor::Action, editor::Action,
graphics::{CursorKind, Margin, Modifier, Rect}, graphics::{CursorKind, Margin, Modifier, Rect},
Document, Editor, Document, DocumentId, Editor,
}; };
use super::menu::Item; use super::menu::Item;
@ -33,8 +28,36 @@ pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes /// Biggest file size to preview in bytes
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; 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) /// 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> { pub struct FilePicker<T: Item> {
picker: Picker<T>, picker: Picker<T>,
@ -113,62 +136,71 @@ impl<T: Item> FilePicker<T> {
self.picker self.picker
.selection() .selection()
.and_then(|current| (self.file_fn)(editor, current)) .and_then(|current| (self.file_fn)(editor, current))
.and_then(|(path, line)| { .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
helix_core::path::get_canonicalized_path(&path)
.ok()
.zip(Some(line))
})
} }
/// Get (cached) preview for a given path. If a document corresponding /// Get (cached) preview for a given path. If a document corresponding
/// to the path is already open in the editor, it is used instead. /// to the path is already open in the editor, it is used instead.
fn get_preview<'picker, 'editor>( fn get_preview<'picker, 'editor>(
&'picker mut self, &'picker mut self,
path: &Path, path_or_id: PathOrId,
editor: &'editor Editor, editor: &'editor Editor,
) -> Preview<'picker, 'editor> { ) -> Preview<'picker, 'editor> {
if let Some(doc) = editor.document_by_path(path) { match path_or_id {
return Preview::EditorDocument(doc); 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) { if self.preview_cache.contains_key(path) {
return Preview::Cached(&self.preview_cache[path]); return Preview::Cached(&self.preview_cache[path]);
} }
let data = std::fs::File::open(path).and_then(|file| { let data = std::fs::File::open(path).and_then(|file| {
let metadata = file.metadata()?; let metadata = file.metadata()?;
// Read up to 1kb to detect the content type // Read up to 1kb to detect the content type
let n = file.take(1024).read_to_end(&mut self.read_buffer)?; let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
let content_type = content_inspector::inspect(&self.read_buffer[..n]); let content_type = content_inspector::inspect(&self.read_buffer[..n]);
self.read_buffer.clear(); self.read_buffer.clear();
Ok((metadata, content_type)) Ok((metadata, content_type))
}); });
let preview = data let preview = data
.map( .map(
|(metadata, content_type)| match (metadata.len(), content_type) { |(metadata, content_type)| match (metadata.len(), content_type) {
(_, content_inspector::ContentType::BINARY) => CachedPreview::Binary, (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile, (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))) // TODO: enable syntax highlighting; blocked by async rendering
.unwrap_or(CachedPreview::NotFound) 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]) .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 { fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
// Try to find a document in the cache // Try to find a document in the cache
let doc = self let doc = self
.current_file(cx.editor) .current_file(cx.editor)
.and_then(|(path, _range)| self.preview_cache.get_mut(&path)) .and_then(|(path, _range)| match path {
.and_then(|cache| match cache { PathOrId::Id(doc_id) => Some(doc_mut!(cx.editor, &doc_id)),
CachedPreview::Document(doc) => Some(doc), PathOrId::Path(path) => match self.preview_cache.get_mut(&path) {
_ => None, Some(CachedPreview::Document(doc)) => Some(doc),
_ => None,
},
}); });
// Then attempt to highlight it if it has no language set // 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); block.render(preview_area, surface);
if let Some((path, range)) = self.current_file(cx.editor) { 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() { let doc = match preview.document() {
Some(doc) => doc, Some(doc) => doc,
None => { 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> { pub struct Picker<T: Item> {
options: Vec<T>, options: Vec<T>,
editor_data: T::Data, editor_data: T::Data,
// filter: String, // filter: String,
matcher: Box<Matcher>, matcher: Box<Matcher>,
/// (index, score) matches: Vec<PickerMatch>,
matches: Vec<(usize, i64)>,
/// Current height of the completions box /// Current height of the completions box
completion_height: u16, completion_height: u16,
@ -361,13 +414,16 @@ impl<T: Item> Picker<T> {
// scoring on empty input: // scoring on empty input:
// TODO: just reuse score() // TODO: just reuse score()
picker.matches.extend( picker
picker .matches
.options .extend(picker.options.iter().enumerate().map(|(index, option)| {
.iter() let text = option.filter_text(&picker.editor_data);
.enumerate() PickerMatch {
.map(|(index, _option)| (index, 0)), index,
); score: 0,
len: text.chars().count(),
}
}));
picker picker
} }
@ -384,32 +440,34 @@ impl<T: Item> Picker<T> {
if pattern.is_empty() { if pattern.is_empty() {
// Fast path for no pattern. // Fast path for no pattern.
self.matches.clear(); self.matches.clear();
self.matches.extend( self.matches
self.options .extend(self.options.iter().enumerate().map(|(index, option)| {
.iter() let text = option.filter_text(&self.editor_data);
.enumerate() PickerMatch {
.map(|(index, _option)| (index, 0)), index,
); score: 0,
len: text.chars().count(),
}
}));
} else if pattern.starts_with(&self.previous_pattern) { } else if pattern.starts_with(&self.previous_pattern) {
let query = FuzzyQuery::new(pattern); let query = FuzzyQuery::new(pattern);
// optimization: if the pattern is a more specific version of the previous one // optimization: if the pattern is a more specific version of the previous one
// then we can score the filtered set. // then we can score the filtered set.
self.matches.retain_mut(|(index, score)| { self.matches.retain_mut(|pmatch| {
let option = &self.options[*index]; let option = &self.options[pmatch.index];
let text = option.sort_text(&self.editor_data); let text = option.sort_text(&self.editor_data);
match query.fuzzy_match(&text, &self.matcher) { match query.fuzzy_match(&text, &self.matcher) {
Some(s) => { Some(s) => {
// Update the score // Update the score
*score = s; pmatch.score = s;
true true
} }
None => false, None => false,
} }
}); });
self.matches self.matches.sort_unstable();
.sort_unstable_by_key(|(_, score)| Reverse(*score));
} else { } else {
let query = FuzzyQuery::new(pattern); let query = FuzzyQuery::new(pattern);
self.matches.clear(); self.matches.clear();
@ -422,11 +480,14 @@ impl<T: Item> Picker<T> {
query query
.fuzzy_match(&text, &self.matcher) .fuzzy_match(&text, &self.matcher)
.map(|score| (index, score)) .map(|score| PickerMatch {
index,
score,
len: text.chars().count(),
})
}), }),
); );
self.matches self.matches.sort_unstable();
.sort_unstable_by_key(|(_, score)| Reverse(*score));
} }
log::debug!("picker score {:?}", Instant::now().duration_since(now)); log::debug!("picker score {:?}", Instant::now().duration_since(now));
@ -478,7 +539,7 @@ impl<T: Item> Picker<T> {
pub fn selection(&self) -> Option<&T> { pub fn selection(&self) -> Option<&T> {
self.matches self.matches
.get(self.cursor) .get(self.cursor)
.map(|(index, _score)| &self.options[*index]) .map(|pmatch| &self.options[pmatch.index])
} }
pub fn toggle_preview(&mut self) { pub fn toggle_preview(&mut self) {
@ -625,7 +686,7 @@ impl<T: Item + 'static> Component for Picker<T> {
.matches .matches
.iter() .iter()
.skip(offset) .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() { for (i, (_index, option)) in files.take(rows as usize).enumerate() {
let is_active = i == (self.cursor - offset); let is_active = i == (self.cursor - offset);

@ -22,6 +22,7 @@ pub struct Popup<T: Component> {
auto_close: bool, auto_close: bool,
ignore_escape_key: bool, ignore_escape_key: bool,
id: &'static str, id: &'static str,
has_scrollbar: bool,
} }
impl<T: Component> Popup<T> { impl<T: Component> Popup<T> {
@ -37,6 +38,7 @@ impl<T: Component> Popup<T> {
auto_close: false, auto_close: false,
ignore_escape_key: false, ignore_escape_key: false,
id, 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 { pub fn contents(&self) -> &T {
&self.contents &self.contents
} }
@ -228,6 +238,40 @@ impl<T: Component> Component for Popup<T> {
let inner = area.inner(&self.margin); let inner = area.inner(&self.margin);
self.contents.render(inner, surface, cx); 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> { fn id(&self) -> Option<&'static str> {

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

@ -1,4 +1,5 @@
use helix_core::{coords_at_pos, encoding, Position}; use helix_core::{coords_at_pos, encoding, Position};
use helix_lsp::lsp::DiagnosticSeverity;
use helix_view::{ use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME}, document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect, graphics::Rect,
@ -68,7 +69,9 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface
// Left side of the status line. // 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 element_ids
.iter() .iter()
.map(|element_id| get_render_function(*element_id)) .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. // Right side of the status line.
let element_ids = &context.editor.config().statusline.right; let element_ids = &config.statusline.right;
element_ids element_ids
.iter() .iter()
.map(|element_id| get_render_function(*element_id)) .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. // Center of the status line.
let element_ids = &context.editor.config().statusline.center; let element_ids = &config.statusline.center;
element_ids element_ids
.iter() .iter()
.map(|element_id| get_render_function(*element_id)) .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::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type, helix_view::editor::StatusLineElement::FileType => render_file_type,
helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics, 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::Selections => render_selections,
helix_view::editor::StatusLineElement::PrimarySelectionLength => {
render_primary_selection_length
}
helix_view::editor::StatusLineElement::Position => render_position, helix_view::editor::StatusLineElement::Position => render_position,
helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage, helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage,
helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers, helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers,
@ -155,7 +162,8 @@ where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{ {
let visible = context.focused; let visible = context.focused;
let modenames = &context.editor.config().statusline.mode; let config = context.editor.config();
let modenames = &config.statusline.mode;
write( write(
context, context,
format!( format!(
@ -171,7 +179,7 @@ where
" " " "
} }
), ),
if visible && context.editor.config().color_modes { if visible && config.color_modes {
match context.editor.mode() { match context.editor.mode() {
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")), Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")), 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) fn render_selections<F>(context: &mut RenderContext, write: F)
where where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, 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 { fn get_position(context: &RenderContext) -> Position {
coords_at_pos( coords_at_pos(
context.doc.text().slice(..), context.doc.text().slice(..),

@ -193,3 +193,121 @@ async fn test_goto_file_impl() -> anyhow::Result<()> {
Ok(()) 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(()) 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 start_index = self.index_of(x, y);
let mut index = self.index_of(max_offset as u16, y); let mut index = self.index_of(max_offset as u16, y);
let total_width = string.width(); let content_width = string.width();
let truncated = total_width > width; let truncated = content_width > width;
if ellipsis && truncated { if ellipsis && truncated {
self.content[start_index].set_symbol("…"); self.content[start_index].set_symbol("…");
start_index += 1; start_index += 1;
} }
if !truncated { if !truncated {
index -= width - total_width; index -= width - content_width;
} }
for (byte_offset, s) in graphemes.rev() { for (byte_offset, s) in graphemes.rev() {
let width = s.width(); let width = s.width();
@ -384,6 +384,7 @@ impl Buffer {
self.content[i].reset(); self.content[i].reset();
} }
index -= width; index -= width;
x_offset += width;
} }
} }
(x_offset as u16, y) (x_offset as u16, y)

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

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

@ -1,21 +1,54 @@
use std::fmt::Write; use std::fmt::Write;
use crate::{ use crate::{
editor::GutterType,
graphics::{Color, Style, UnderlineStyle}, graphics::{Color, Style, UnderlineStyle},
Document, Editor, Theme, View, 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 GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
pub type Gutter = pub type Gutter =
for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>; 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>( pub fn diagnostic<'doc>(
_editor: &'doc Editor, _editor: &'doc Editor,
doc: &'doc Document, doc: &'doc Document,
_view: &View, _view: &View,
theme: &Theme, theme: &Theme,
_is_focused: bool, _is_focused: bool,
_width: usize,
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
let warning = theme.get("warning"); let warning = theme.get("warning");
let error = theme.get("error"); let error = theme.get("error");
@ -56,10 +89,11 @@ pub fn line_numbers<'doc>(
view: &View, view: &View,
theme: &Theme, theme: &Theme,
is_focused: bool, is_focused: bool,
width: usize,
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
let text = doc.text().slice(..); let text = doc.text().slice(..);
let last_line = view.last_line(doc); 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 // 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. // 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(); let draw_last = text.line_to_byte(last_line) < text.len_bytes();
@ -91,24 +125,35 @@ pub fn line_numbers<'doc>(
} else { } else {
line + 1 line + 1
}; };
let style = if selected && is_focused { let style = if selected && is_focused {
linenr_select linenr_select
} else { } else {
linenr linenr
}; };
write!(out, "{:>1$}", display_num, width).unwrap(); write!(out, "{:>1$}", display_num, width).unwrap();
Some(style) 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>( pub fn padding<'doc>(
_editor: &'doc Editor, _editor: &'doc Editor,
_doc: &'doc Document, _doc: &'doc Document,
_view: &View, _view: &View,
_theme: &Theme, _theme: &Theme,
_is_focused: bool, _is_focused: bool,
_width: usize,
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
Box::new(|_line: usize, _selected: bool, _out: &mut String| None) Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
} }
@ -128,7 +173,6 @@ pub fn breakpoints<'doc>(
_view: &View, _view: &View,
theme: &Theme, theme: &Theme,
_is_focused: bool, _is_focused: bool,
_width: usize,
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
let warning = theme.get("warning"); let warning = theme.get("warning");
let error = theme.get("error"); let error = theme.get("error");
@ -181,10 +225,9 @@ pub fn diagnostics_or_breakpoints<'doc>(
view: &View, view: &View,
theme: &Theme, theme: &Theme,
is_focused: bool, is_focused: bool,
width: usize,
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
let diagnostics = diagnostic(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, width); let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
Box::new(move |line, selected, out| { Box::new(move |line, selected, out| {
breakpoints(line, selected, out).or_else(|| diagnostics(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(..)); .cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos); 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 { let relative = match align {
Align::Center => last_line_height / 2, 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( pub fn apply_transaction(
transaction: &helix_core::Transaction, transaction: &helix_core::Transaction,
doc: &mut Document, doc: &mut Document,
view: &mut View, view: &View,
) -> bool { ) -> bool {
// This is a short function but it's easy to call `Document::apply` // TODO remove this helper function. Just call Document::apply everywhere directly.
// without calling `View::apply` or in the wrong order. The transaction doc.apply(transaction, view.id)
// must be applied to the document before the view.
doc.apply(transaction, view.id) && view.apply(transaction, doc)
} }
pub use document::Document; pub use document::Document;

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

@ -219,7 +219,7 @@ impl Tree {
if self.focus == index { if self.focus == index {
// focus on something else // focus on something else
self.focus_next(); self.focus = self.prev();
} }
stack.push(index); stack.push(index);
@ -499,7 +499,7 @@ impl Tree {
// in a vertical container (and already correct based on previous search) // in a vertical container (and already correct based on previous search)
child_id = *container.children.iter().min_by_key(|id| { child_id = *container.children.iter().min_by_key(|id| {
let x = match &self.nodes[**id].content { 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(), Content::Container(container) => container.area.left(),
}; };
(current_x as i16 - x as i16).abs() (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) // in a horizontal container (and already correct based on previous search)
child_id = *container.children.iter().min_by_key(|id| { child_id = *container.children.iter().min_by_key(|id| {
let y = match &self.nodes[**id].content { 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(), Content::Container(container) => container.area.top(),
}; };
(current_y as i16 - y as i16).abs() (current_y as i16 - y as i16).abs()
@ -521,7 +521,27 @@ impl Tree {
Some(child_id) 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. // 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) // (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 // 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_while(|&(id, _view)| id != self.focus)
.skip(1); // Skip focused value .skip(1); // Skip focused value
if let Some((id, _)) = views.next() { if let Some((id, _)) = views.next() {
self.focus = id; id
} else { } else {
// extremely crude, take the first item again // extremely crude, take the first item again
let (key, _) = self.traverse().next().unwrap(); 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

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

@ -50,7 +50,7 @@ args = { attachCommands = [ "platform select remote-gdb-server", "platform conne
[[grammar]] [[grammar]]
name = "rust" 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]] [[language]]
name = "toml" name = "toml"
@ -399,7 +399,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "typescript" 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]] [[language]]
name = "tsx" name = "tsx"
@ -413,7 +413,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "tsx" 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]] [[language]]
name = "css" name = "css"
@ -454,7 +454,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "html" 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]] [[language]]
name = "python" name = "python"
@ -495,7 +495,7 @@ file-types = ["nix"]
shebangs = [] shebangs = []
roots = [] roots = []
comment-token = "#" comment-token = "#"
language-server = { command = "rnix-lsp" } language-server = { command = "nil" }
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
@ -617,7 +617,7 @@ indent = { tab-width = 4, unit = " " }
[[grammar]] [[grammar]]
name = "java" 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]] [[language]]
name = "ledger" name = "ledger"
@ -732,7 +732,7 @@ source = { git = "https://github.com/ikatyang/tree-sitter-yaml", rev = "0e36bed1
name = "haskell" name = "haskell"
scope = "source.haskell" scope = "source.haskell"
injection-regex = "haskell" injection-regex = "haskell"
file-types = ["hs"] file-types = ["hs", "hs-boot"]
roots = ["Setup.hs", "stack.yaml", "*.cabal"] roots = ["Setup.hs", "stack.yaml", "*.cabal"]
comment-token = "--" comment-token = "--"
language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] } language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
@ -831,7 +831,7 @@ injection-regex = "cmake"
[[grammar]] [[grammar]]
name = "cmake" 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]] [[language]]
name = "make" name = "make"
@ -997,8 +997,8 @@ source = { git = "https://github.com/UserNobody14/tree-sitter-dart", rev = "2d7f
[[language]] [[language]]
name = "scala" name = "scala"
scope = "source.scala" scope = "source.scala"
roots = ["build.sbt", "pom.xml"] roots = ["build.sbt", "build.sc", "build.gradle", "pom.xml", ".scala-build"]
file-types = ["scala", "sbt"] file-types = ["scala", "sbt", "sc"]
comment-token = "//" comment-token = "//"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-server = { command = "metals" } language-server = { command = "metals" }
@ -1184,7 +1184,7 @@ language-server = { command = "erlang_ls" }
[[grammar]] [[grammar]]
name = "erlang" 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]] [[language]]
name = "kotlin" name = "kotlin"
@ -1263,7 +1263,7 @@ language-server = { command = "gleam", args = ["lsp"] }
[[grammar]] [[grammar]]
name = "gleam" 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]] [[language]]
name = "ron" name = "ron"
@ -1355,10 +1355,12 @@ injection-regex = "heex"
file-types = ["heex"] file-types = ["heex"]
roots = ["mix.exs", "mix.lock"] roots = ["mix.exs", "mix.lock"]
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-server = { command = "elixir-ls" }
config = { elixirLS.dialyzerEnabled = false }
[[grammar]] [[grammar]]
name = "heex" 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]] [[language]]
name = "sql" name = "sql"
@ -1508,7 +1510,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "meson" 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]] [[language]]
name = "sshclientconfig" name = "sshclientconfig"
@ -1572,7 +1574,7 @@ indent = { tab-width = 4, unit = " " }
[[grammar]] [[grammar]]
name = "edoc" 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]] [[language]]
name = "jsdoc" name = "jsdoc"
@ -1964,3 +1966,30 @@ roots = []
[[grammar]] [[grammar]]
name = "ini" name = "ini"
source = { git = "https://github.com/justinmk/tree-sitter-ini", rev = "4d247fb876b4ae6b347687de4a179511bf67fcbc" } 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 (argument) @parameter.inside

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

@ -16,5 +16,5 @@
(tag) @_tag (tag) @_tag
(argument) @injection.content) (argument) @injection.content)
(#eq? @_tag "@type") (#eq? @_tag "@type")
(#set injection.language "erlang") (#set! injection.language "erlang")
(#set injection.include-children)) (#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 module: (atom) @namespace)
(function_capture function: (atom) @function) (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 ; Records
(record_content (record_content
(binary_operator (binary_operator
@ -94,10 +125,6 @@
(unary_operator operator: _ @operator) (unary_operator operator: _ @operator)
["/" ":" "->"] @operator ["/" ":" "->"] @operator
(tripledot) @comment.discard
(comment) @comment
; Macros ; Macros
(macro (macro
"?"+ @constant "?"+ @constant
@ -109,11 +136,14 @@
name: (_) @keyword.directive) name: (_) @keyword.directive)
; Comments ; Comments
((variable) @comment.discard (tripledot) @comment.discard
(#match? @comment.discard "^_"))
[(comment) (line_comment) (shebang)] @comment
; Basic types ; Basic types
(variable) @variable (variable) @variable
((atom) @constant.builtin.boolean
(#match? @constant.builtin.boolean "^(true|false)$"))
(atom) @string.special.symbol (atom) @string.special.symbol
(string) @string (string) @string
(character) @constant.character (character) @constant.character

@ -1,2 +1,7 @@
((comment_content) @injection.content ((line_comment (comment_content) @injection.content)
(#set! injection.language "edoc")) (#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" "todo"
"try" "try"
"type" "type"
"use"
] @keyword ] @keyword
; Punctuation ; Punctuation
@ -103,4 +104,5 @@
"->" "->"
".." ".."
"-" "-"
"<-"
] @punctuation.delimiter ] @punctuation.delimiter

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

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

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

@ -1,2 +1,2 @@
((comment) @injection.content ([(line_comment) (block_comment)] @injection.content
(#set! injection.language "comment")) (#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 ; these are listed first, because they override keyword queries
(integer_literal) @constant.numeric.integer (function_expression (identifier) @function)
(comment) @comment.line
(function_id) @function
(keyword_arg_key) @variable.other.member
(id_expression) @variable
[ [
"if" (assignment_operator)
"elif" (additive_operator)
"else" (multiplicative_operator)
"endif" (equality_operator)
] @keyword.control.conditional ">="
"<="
"<"
">"
"+"
"-"
] @operator
[ [
"foreach" (and)
"endforeach" (or)
] @keyword.control.repeat (not)
(in)
] @keyword.operator
[ [
"break" "(" ")" "[" "]" "{" "}"
"continue" ] @punctuation.bracket
] @keyword.control
[ [
"not" (if)
"in" (elif)
"and" (else)
"or" (endif)
] @keyword.operator ] @keyword.control.conditional
[ [
"!" (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
] @operator
[ [
":" "."
"," ","
":"
] @punctuation.delimiter ] @punctuation.delimiter
[ [
"(" (string_literal)
")" (fstring_literal)
"[" ] @string
"]"
"{" (identifier) @variable
"}"
] @punctuation.bracket

@ -1,5 +1,5 @@
; Indentation queries for helix
[ [
(method_expression)
(function_expression) (function_expression)
(array_literal) (array_literal)
(dictionary_literal) (dictionary_literal)
@ -7,10 +7,11 @@
(iteration_statement) (iteration_statement)
] @indent ] @indent
; question - what about else, elif
[ [
")" ")"
"]" "]"
"}" "}"
"endif" (endif)
"endforeach" (endforeach)
] @outdent ] @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 and Modules
(class) @class.around (class
body: (_)? @class.inside) @class.around
(class [(constant) (scope_resolution)] !superclass
(_)+ @class.inside)
(class [(constant) (scope_resolution)] (superclass)
(_)+ @class.inside)
(singleton_class (singleton_class
value: (_) value: (_)
@ -17,37 +12,32 @@
(#match? @class_const "Class") (#match? @class_const "Class")
(#match? @class_method "new") (#match? @class_method "new")
(do_block (_)+ @class.inside)) @class.around (do_block (_)+ @class.inside)) @class.around
(module
body: (_)? @class.inside) @class.around
; Functions ; Functions and Blocks
(method) @function.around (singleton_method
body: (_)? @function.inside) @function.around
(method (identifier) (method_parameters) (method
(_)+ @function.inside) body: (_)? @function.inside) @function.around
(do_block !parameters (do_block
(_)+ @function.inside) body: (_)? @function.inside) @function.around
(do_block (block_parameters) (block
(_)+ @function.inside) body: (_)? @function.inside) @function.around
(block (block_parameters)
(_)+ @function.inside)
(block !parameters
(_)+ @function.inside)
(method (identifier) !parameters
(_)+ @function.inside)
; Parameters ; Parameters
(method_parameters (method_parameters
(_) @parameter.inside) (_) @parameter.inside) @parameter.around
(block_parameters (block_parameters
(_) @parameter.inside) (_) @parameter.inside) @parameter.around
(lambda_parameters (lambda_parameters
(_) @parameter.inside) (_) @parameter.inside) @parameter.around
; Comments ; Comments
(comment) @comment.inside (comment) @comment.inside

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

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

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

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

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

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

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

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

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

@ -1,101 +1,116 @@
# Author: Kristoffer Flottorp <kr.fl@outlook.com> # Author: Kristoffer Flottorp <kr.fl@outlook.com>
# A take on the JetBrains Fleet theme sprinkled with some creative freedom # A take on the JetBrains Fleet theme sprinkled with some creative freedom
"type" = { fg = "yellow" } # .builtin "type" = "light_blue"
"constructor" = { fg = "yellow" } "type.builtin" = "orange"
"constant" = { fg = "cyan" } "constructor" = "yellow"
"constant" = "cyan"
# "constant.builtin" = {} # .boolean # "constant.builtin" = {} # .boolean
"constant.builtin.boolean" = { fg = "cyan" } # .boolean "constant.builtin.boolean" = "yellow"
# "constant.character" = {} #.escape "constant.character" = "yellow"
"constant.numeric" = { fg = "yellow" } # .integer / .float "constant.characted.escape" = "light"
"string" = { fg = "pink" } # .regexp "constant.numeric" = "yellow"
# "string.special" = {} #.path / .url / .symbol "string" = "pink"
"string.special" = { modifiers = ["underlined"] } #.path / .url / .symbol "string.regexp" = "light"
"comment" = { fg = "dark_gray" } # .line "string.special" = { fg = "yellow", modifiers = ["underlined"] } #.path / .url / .symbol
"comment" = "light_gray" # .line
# "comment.block" = {} # .documentation # "comment.block" = {} # .documentation
"variable" = { fg = "light" } # .builtin / .parameter "variable" = "light" # .builtin
"variable.parameter" = "light"
# "variable.other" = {} # .member # "variable.other" = {} # .member
"variable.other.member" = { fg = "purple" } "variable.other.member" = "yellow"
"label" = { fg = "yellow" } "label" = "yellow"
# "punctuation" = {} # .delimiter / .bracket "punctuation" = "light" # .delimiter / .bracket
"keyword" = { fg = "cyan" } # .operator / .directive / .function "keyword" = "cyan" # .operator / .directive / .function
# "keyword.control" = { fg = "orange" } # .conditional / .repeat / .import / .return / .exception "keyword.control" = "yellow" # .conditional / .repeat / .import / .return / .exception
"operator" = { fg = "light" } "operator" = "light"
"function" = { fg = "blue" } # .builtin / .method / .macro / .special "function" = "yellow"
"function.macro" = { fg = "green" } "function.macro" = "green"
"function.special" = { fg = "green" } "function.builtin" = "green"
"tag" = { fg = "green"} "function.special" = "green"
"special" = { fg = "green" } "function.method" = "light"
"namespace" = { fg = "light" } "tag" = "green"
"markup" = { fg = "purple" } # .bold / .italic / .quote "special" = "green"
"markup.heading" = { fg = "light" } # .marker / .1 / .2 / .3 / .4 / .5 / .6 "namespace" = "light"
"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
# used in theming # used in theming
# "markup.normal" = {} # .completion / .hover # "markup.normal" = {} # .completion / .hover
# "markup.heading" = {} # .completion / .hover
# "markup.raw.inline" = {} # .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 specific
"ui.background" = { bg = "darkest" } # .separator "ui.background" = { bg = "#0d0d0d" } # .separator
"ui.cursor" = { bg = "dark_gray", modifiers = ["reversed"] } # .insert / .select / .match / .primary "ui.cursor" = { bg = "dark_gray", modifiers = ["reversed"] } # .insert / .select / .match / .primary
"ui.cursor.match" = { fg = "light", bg = "blue_accent" } # .insert / .select / .match / .primary "ui.cursor.match" = { fg = "light", bg = "blue_accent" } # .insert / .select / .match / .primary
"ui.cursorline" = { bg = "darker" } "ui.cursorline" = { bg = "darker" }
"ui.linenr" = { fg = "dark_gray" } # .selected "ui.linenr" = "dark_gray"
"ui.linenr.selected" = { fg = "light_gray", bg = "darker" } "ui.linenr.selected" = { fg = "light_gray", bg = "darker" }
"ui.statusline" = { fg = "light", bg = "darker" } # .inactive / .normal / .insert / .select "ui.statusline" = { fg = "light", bg = "darker" } # .inactive / .normal / .insert / .select
"ui.statusline.inactive" = { fg = "dark", bg = "darker" } # .inactive / .normal / .insert / .select "ui.statusline.inactive" = { fg = "dark", bg = "darker" }
"ui.statusline.normal" = { fg = "lightest", bg = "darker"} # .inactive / .normal / .insert / .select "ui.statusline.normal" = { fg = "lightest", bg = "darker"}
"ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" } # .inactive / .normal / .insert / .select "ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" }
"ui.statusline.select" = { fg = "lightest", bg = "orange_accent" } # .inactive / .normal / .insert / .select "ui.statusline.select" = { fg = "lightest", bg = "orange_accent" }
"ui.popup" = { fg = "light", bg = "dark" } # .info "ui.popup" = { fg = "light", bg = "darkest" } # .info
"ui.window" = { fg = "dark", bg = "darkest" } "ui.window" = { fg = "dark", bg = "darkest" }
"ui.help" = { fg = "light", bg = "dark" } "ui.help" = { fg = "light", bg = "darkest" }
"ui.text" = { fg = "light" } # .focus / .info "ui.text" = "light" # .focus / .info
"ui.virtual" = { fg = "dark" } # .ruler / .whitespace "ui.virtual" = "dark" # .whitespace
"ui.virtual.ruler" = { bg = "darker"} "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.menu.selected" = { fg = "lightest", bg = "blue_accent" } # .selected
"ui.selection" = { bg = "darker" } # .primary "ui.selection" = { bg = "darker" } # .primary
"ui.selection.primary" = { bg = "select" } # .primary "ui.selection.primary" = { bg = "select" } # .primary
"hint" = { fg = "blue_accent"} "hint" = "blue"
"info" = { fg = "yellow_accent" } "info" = "yellow_accent"
"warning" = { fg = "orange_accent" } "warning" = "orange_accent"
"error" = { fg = "diff_red_accent" } "error" = "red"
"diagnostic". underline = { style = "curl" } "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] [palette]
darkest = "#0F0F0F" darkest = "#1e1e1e"
darker = "#222222" darker = "#262626"
dark = "#383838" dark = "#898989"
select = "#102F5B" select = "#102f5b"
light = "#F0F0F0" light = "#d6d6dd"
lightest = "#FFFFFF" lightest = "#ffffff"
dark_gray = "#5B5B5B" dark_gray = "#535353"
light_gray = "#757575" light_gray = "#6d6d6d"
purple = "#AC9CF9" purple = "#a390f0"
blue = "#52A7F6" #"#94C1FA" light_blue = "#7dbeff"
pink = "#D898D8" blue = "#52a7f6"
green = "#AFCB85" pink = "#d898d8"
cyan = "#78D0BD" green = "#afcb85"
orange = "#ECA775" cyan = "#78d0bd"
yellow = "#E5C995" orange = "#efb080"
yellow = "#e5c995"
red = "#f44747"
purple_accent = "#6363EE"
blue_accent = "#2197F3" blue_accent = "#2197F3"
pink_accent = "#E44C7A" pink_accent = "#E44C7A"
green_accent = "#00AF99" green_accent = "#00AF99"
@ -104,11 +119,3 @@ yellow_accent = "#DEA407"
# variables intended for future updates # variables intended for future updates
checkmark = "#44B254" 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.help" = { bg = "bg1", fg = "fg1" }
"ui.text" = { fg = "fg1" } "ui.text" = { fg = "fg1" }
"ui.text.focus" = { fg = "fg1" } "ui.text.focus" = { fg = "fg1" }
"ui.selection" = { bg = "bg3", modifiers = ["reversed"] } "ui.selection" = { bg = "bg2" }
"ui.cursor.primary" = { modifiers = ["reversed"] } "ui.cursor.primary" = { bg = "fg4", fg = "bg1" }
"ui.cursor.match" = { bg = "bg2" } "ui.cursor.match" = { bg = "bg3" }
"ui.menu" = { fg = "fg1", bg = "bg2" } "ui.menu" = { fg = "fg1", bg = "bg2" }
"ui.menu.selected" = { fg = "bg2", bg = "blue1", modifiers = ["bold"] } "ui.menu.selected" = { fg = "bg2", bg = "blue1", modifiers = ["bold"] }
"ui.virtual.whitespace" = "bg2" "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 # status bars, panels, modals, autocompletion
"ui.statusline" = { fg = "base8", bg = "base4" } "ui.statusline" = { fg = "base8", bg = "base4" }
"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" } "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.popup" = { bg = "base3" }
"ui.window" = { bg = "base3" } "ui.window" = { bg = "base3" }
"ui.help" = { fg = "base8", 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 = = CHAPTER 5 RECAP =
================================================================= =================================================================
* Type C to copy the current selection to below and Alt-C for * Type C to duplicate the cursor to the next suitable line
above. and Alt-C for previous suitable line.
* Type s to select all instances of a regex pattern inside * Type s to select all instances of a regex pattern inside
the current selection. the current selection.
@ -845,11 +845,11 @@ lines.
Type q to repeat the macro from register @ (the default). Type q to repeat the macro from register @ (the default).
1. Move the cursor to the first line marked '-->' below. 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. 2. Type Q to start recording.
3. Edit the line to look like the bottom one. 3. Edit the line to look like the bottom one.
4. Exit insert and Type Q again to stop recording. 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. 6. Type q to repeat the macro.
--> ... sentence doesn't have it's first and last ... . --> ... 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 language_name = &language.language_id;
let grammar_name = language.grammar.as_ref().unwrap_or(language_name); let grammar_name = language.grammar.as_ref().unwrap_or(language_name);
for query_file in query_files { for query_file in query_files {
let language = get_language(&grammar_name); let language = get_language(grammar_name);
let query_text = read_query(&language_name, query_file); let query_text = read_query(language_name, query_file);
if let Ok(lang) = language { if let Ok(lang) = language {
if !query_text.is_empty() { if !query_text.is_empty() {
if let Err(reason) = Query::new(lang, &query_text) { if let Err(reason) = Query::new(lang, &query_text) {

Loading…
Cancel
Save