Merge remote-tracking branch 'origin/master'

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

@ -44,10 +44,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install stable toolchain - name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1 uses: dtolnay/rust-toolchain@1.61
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@ -76,16 +73,14 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install stable toolchain - name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1 uses: dtolnay/rust-toolchain@1.61
with: with:
profile: minimal
override: true
components: rustfmt, clippy components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Run cargo fmt - name: Run cargo fmt
run: cargo fmt --all -- --check run: cargo fmt --all --check
- name: Run cargo clippy - name: Run cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings run: cargo clippy --workspace --all-targets -- -D warnings
@ -103,13 +98,13 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install stable toolchain - name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1 uses: dtolnay/rust-toolchain@1.61
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Validate queries
run: cargo xtask query-check
- name: Generate docs - name: Generate docs
run: cargo xtask docgen run: cargo xtask docgen
@ -120,20 +115,3 @@ jobs:
|| (echo "Run 'cargo xtask docgen', commit the changes and push again" \ || (echo "Run 'cargo xtask docgen', commit the changes and push again" \
&& exit 1) && 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

@ -2,13 +2,20 @@ name: Release
on: on:
push: push:
tags: tags:
- '[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+'
branches:
- 'patch/ci-release-*'
pull_request:
paths:
- '.github/workflows/release.yml'
env: env:
# Preview mode: Publishes the build output as a CI artifact instead of creating # Preview mode: Publishes the build output as a CI artifact instead of creating
# a release, allowing for manual inspection of the output. This mode is # a release, allowing for manual inspection of the output. This mode is
# activated if the CI run was triggered by events other than pushed tags, or # activated if the CI run was triggered by events other than pushed tags, or
# if the repository is a fork. # if the repository is a fork.
preview: ${{ !startsWith(github.ref, 'refs/tags/')}} preview: ${{ !startsWith(github.ref, 'refs/tags/') || github.repository != 'helix-editor/helix' }}
jobs: jobs:
fetch-grammars: fetch-grammars:
@ -19,10 +26,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install stable toolchain - name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1 uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@ -40,6 +44,16 @@ jobs:
dist: dist:
name: Dist name: Dist
needs: [fetch-grammars] needs: [fetch-grammars]
env:
# For some builds, we use cross to test on 32-bit and big-endian
# systems.
CARGO: cargo
# When CARGO is set to CROSS, this is set to `--target matrix.target`.
TARGET_FLAGS:
# When CARGO is set to CROSS, TARGET_DIR includes matrix.target.
TARGET_DIR: ./target
# Emit backtraces on panics.
RUST_BACKTRACE: 1
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false # don't fail other jobs if one fails fail-fast: false # don't fail other jobs if one fails
@ -47,17 +61,17 @@ jobs:
build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
include: include:
- build: x86_64-linux - build: x86_64-linux
os: ubuntu-20.04 os: ubuntu-latest
rust: stable rust: stable
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
cross: false cross: false
- build: aarch64-linux - build: aarch64-linux
os: ubuntu-20.04 os: ubuntu-latest
rust: stable rust: stable
target: aarch64-unknown-linux-gnu target: aarch64-unknown-linux-gnu
cross: true cross: true
- build: riscv64-linux - build: riscv64-linux
os: ubuntu-20.04 os: ubuntu-latest
rust: stable rust: stable
target: riscv64gc-unknown-linux-gnu target: riscv64gc-unknown-linux-gnu
cross: true cross: true
@ -67,7 +81,7 @@ jobs:
target: x86_64-apple-darwin target: x86_64-apple-darwin
cross: false cross: false
- build: x86_64-windows - build: x86_64-windows
os: windows-2019 os: windows-latest
rust: stable rust: stable
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
cross: false cross: false
@ -77,6 +91,14 @@ jobs:
target: aarch64-apple-darwin target: aarch64-apple-darwin
cross: false cross: false
skip_tests: true # x86_64 host can't run aarch64 code skip_tests: true # x86_64 host can't run aarch64 code
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu
# target: x86_64-pc-windows-gnu
# - build: win32-msvc
# os: windows-2019
# rust: stable
# target: i686-pc-windows-msvc
steps: steps:
- name: Checkout sources - name: Checkout sources
@ -92,7 +114,7 @@ jobs:
tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources
- name: Install ${{ matrix.rust }} toolchain - name: Install ${{ matrix.rust }} toolchain
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@master
with: with:
profile: minimal profile: minimal
toolchain: ${{ matrix.rust }} toolchain: ${{ matrix.rust }}
@ -105,7 +127,24 @@ jobs:
# 0.3.0, which includes cross-rs/cross#591, is released. # 0.3.0, which includes cross-rs/cross#591, is released.
- name: Install Cross - name: Install Cross
if: "matrix.cross" if: "matrix.cross"
run: cargo install cross --git https://github.com/cross-rs/cross.git --rev 47df5c76e7cba682823a0b6aa6d95c17b31ba63a run: |
cargo install cross --git https://github.com/cross-rs/cross.git --rev 47df5c76e7cba682823a0b6aa6d95c17b31ba63a
echo "CARGO=cross" >> $GITHUB_ENV
# echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV
# echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV
- name: Show command used for Cargo
run: |
echo "cargo command is: ${{ env.CARGO }}"
echo "target flag is: ${{ env.TARGET_FLAGS }}"
- name: Run cargo test
uses: actions-rs/cargo@v1
if: "!matrix.skip_tests"
with:
use-cross: ${{ matrix.cross }}
command: test
args: --release --locked --target ${{ matrix.target }} --workspace
- name: Set profile.release.strip = true - name: Set profile.release.strip = true
shell: bash shell: bash
@ -116,11 +155,7 @@ jobs:
EOF EOF
- name: Build release binary - name: Build release binary
uses: actions-rs/cargo@v1 run: ${{ env.CARGO }} build --release --locked --target ${{ matrix.target }}
with:
use-cross: ${{ matrix.cross }}
command: build
args: --release --locked --target ${{ matrix.target }}
- name: Build AppImage - name: Build AppImage
shell: bash shell: bash

@ -0,0 +1,281 @@
name: Release
on:
push:
tags:
env:
# Preview mode: Publishes the build output as a CI artifact instead of creating
# a release, allowing for manual inspection of the output. This mode is
# activated if the CI run was triggered by events other than pushed tags, or
# if the repository is a fork.
preview: ${{ !startsWith(github.ref, 'refs/tags/')}}
jobs:
fetch-grammars:
name: Fetch Grammars
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Fetch tree-sitter grammars
run: cargo run --package=helix-loader --bin=hx-loader
- name: Bundle grammars
run: tar cJf grammars.tar.xz -C runtime/grammars/sources .
- uses: actions/upload-artifact@v3
with:
name: grammars
path: grammars.tar.xz
dist:
name: Dist
needs: [fetch-grammars]
env:
# For some builds, we use cross to test on 32-bit and big-endian
# systems.
CARGO: cargo
# When CARGO is set to CROSS, this is set to `--target matrix.target`.
TARGET_FLAGS:
# When CARGO is set to CROSS, TARGET_DIR includes matrix.target.
TARGET_DIR: ./target
# Emit backtraces on panics.
RUST_BACKTRACE: 1
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # don't fail other jobs if one fails
matrix:
build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
include:
- build: x86_64-linux
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-gnu
cross: false
- build: aarch64-linux
os: ubuntu-latest
rust: stable
target: aarch64-unknown-linux-gnu
cross: true
- build: riscv64-linux
os: ubuntu-latest
rust: stable
target: riscv64gc-unknown-linux-gnu
cross: true
- build: x86_64-macos
os: macos-latest
rust: stable
target: x86_64-apple-darwin
cross: false
- build: x86_64-windows
os: windows-latest
rust: stable
target: x86_64-pc-windows-msvc
cross: false
- build: aarch64-macos
os: macos-latest
rust: stable
target: aarch64-apple-darwin
cross: false
skip_tests: true # x86_64 host can't run aarch64 code
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Download grammars
uses: actions/download-artifact@v3
- name: Move grammars under runtime
if: "!startsWith(matrix.os, 'windows')"
run: |
mkdir -p runtime/grammars/sources
tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources
- name: Install ${{ matrix.rust }} toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
# Install a pre-release version of Cross
# TODO: We need to pre-install Cross because we need cross-rs/cross#591 to
# get a newer C++ compiler toolchain. Remove this step when Cross
# 0.3.0, which includes cross-rs/cross#591, is released.
- name: Install Cross
if: "matrix.cross"
run: |
cargo install cross --git https://github.com/cross-rs/cross.git --rev 47df5c76e7cba682823a0b6aa6d95c17b31ba63a
echo "CARGO=cross" >> $GITHUB_ENV
# echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV
# echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV
- name: Show command used for Cargo
run: |
echo "cargo command is: ${{ env.CARGO }}"
echo "target flag is: ${{ env.TARGET_FLAGS }}"
<<<<<<< HEAD
||||||| f0f295a6
- name: Run cargo test
uses: actions-rs/cargo@v1
if: "!matrix.skip_tests"
with:
use-cross: ${{ matrix.cross }}
command: test
args: --release --locked --target ${{ matrix.target }} --workspace
=======
- name: Run cargo test
if: "!matrix.skip_tests"
run: ${{ env.CARGO }} test --release --locked --target ${{ matrix.target }} --workspace
>>>>>>> origin/master
- name: Set profile.release.strip = true
shell: bash
run: |
cat >> .cargo/config.toml <<EOF
[profile.release]
strip = true
EOF
- name: Build release binary
run: ${{ env.CARGO }} build --release --locked --target ${{ matrix.target }}
- name: Build AppImage
shell: bash
if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux'
run: |
mkdir dist
name=dev
if [[ $GITHUB_REF == refs/tags/* ]]; then
name=${GITHUB_REF:10}
fi
build="${{ matrix.build }}"
export VERSION="$name"
export ARCH=${build%-linux}
export APP=helix
export OUTPUT="helix-$VERSION-$ARCH.AppImage"
export UPDATE_INFORMATION="gh-releases-zsync|$GITHUB_REPOSITORY_OWNER|helix|latest|$APP-*-$ARCH.AppImage.zsync"
mkdir -p "$APP.AppDir"/usr/{bin,lib/helix}
cp "target/${{ matrix.target }}/release/hx" "$APP.AppDir/usr/bin/hx"
rm -rf runtime/grammars/sources
cp -r runtime "$APP.AppDir/usr/lib/helix/runtime"
cat << 'EOF' > "$APP.AppDir/AppRun"
#!/bin/sh
APPDIR="$(dirname "$(readlink -f "${0}")")"
HELIX_RUNTIME="$APPDIR/usr/lib/helix/runtime" exec "$APPDIR/usr/bin/hx" "$@"
EOF
chmod 755 "$APP.AppDir/AppRun"
curl -Lo linuxdeploy-x86_64.AppImage \
https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
./linuxdeploy-x86_64.AppImage \
--appdir "$APP.AppDir" -d contrib/Helix.desktop \
-i contrib/helix.png --output appimage
mv "$APP-$VERSION-$ARCH.AppImage" \
"$APP-$VERSION-$ARCH.AppImage.zsync" dist
- name: Build archive
shell: bash
run: |
mkdir -p dist
if [ "${{ matrix.os }}" = "windows-2019" ]; then
cp "target/${{ matrix.target }}/release/hx.exe" "dist/"
else
cp "target/${{ matrix.target }}/release/hx" "dist/"
fi
if [ -d runtime/grammars/sources ]; then
rm -rf runtime/grammars/sources
fi
cp -r runtime dist
- uses: actions/upload-artifact@v3
with:
name: bins-${{ matrix.build }}
path: dist
publish:
name: Publish
needs: [dist]
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: actions/download-artifact@v3
- name: Build archive
shell: bash
run: |
set -ex
source="$(pwd)"
mkdir -p runtime/grammars/sources
tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources
rm -rf grammars
cd "$(mktemp -d)"
mv $source/bins-* .
mkdir dist
for dir in bins-* ; do
platform=${dir#"bins-"}
if [[ $platform =~ "windows" ]]; then
exe=".exe"
fi
pkgname=helix-$GITHUB_REF_NAME-$platform
mkdir $pkgname
cp $source/LICENSE $source/README.md $pkgname
mkdir $pkgname/contrib
cp -r $source/contrib/completion $pkgname/contrib
mv bins-$platform/runtime $pkgname/
mv bins-$platform/hx$exe $pkgname
chmod +x $pkgname/hx$exe
if [[ "$platform" = "aarch64-linux" || "$platform" = "x86_64-linux" ]]; then
mv bins-$platform/helix-*.AppImage* dist/
fi
if [ "$exe" = "" ]; then
tar cJf dist/$pkgname.tar.xz $pkgname
else
7z a -r dist/$pkgname.zip $pkgname
fi
done
tar cJf dist/helix-$GITHUB_REF_NAME-source.tar.xz -C $source .
mv dist $source/
- name: Upload binaries to release
uses: svenstaro/upload-release-action@v2
if: env.preview == 'false'
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/*
file_glob: true
tag: ${{ github.ref_name }}
overwrite: true
- name: Upload binaries as artifact
uses: actions/upload-artifact@v3
if: env.preview == 'true'
with:
name: release
path: dist/*

@ -1,3 +1,289 @@
# 22.12 (2022-12-06)
This is a great big release filled with changes from a 99 contributors. A big _thank you_ to you all!
As usual, the following is a summary of each of the changes since the last release.
For the full log, check out the [git log](https://github.com/helix-editor/helix/compare/22.08.1..22.12).
Breaking changes:
- Remove readline-like navigation bindings from the default insert mode keymap ([e12690e](https://github.com/helix-editor/helix/commit/e12690e), [#3811](https://github.com/helix-editor/helix/pull/3811), [#3827](https://github.com/helix-editor/helix/pull/3827), [#3915](https://github.com/helix-editor/helix/pull/3915), [#4088](https://github.com/helix-editor/helix/pull/4088))
- Rename `append_to_line` as `insert_at_line_end` and `prepend_to_line` as `insert_at_line_start` ([#3753](https://github.com/helix-editor/helix/pull/3753))
- Swap diagnostic picker and debug mode bindings in the space keymap ([#4229](https://github.com/helix-editor/helix/pull/4229))
- Select newly inserted text on paste or from shell commands ([#4458](https://github.com/helix-editor/helix/pull/4458), [#4608](https://github.com/helix-editor/helix/pull/4608), [#4619](https://github.com/helix-editor/helix/pull/4619), [#4824](https://github.com/helix-editor/helix/pull/4824))
- Select newly inserted surrounding characters on `ms<char>` ([#4752](https://github.com/helix-editor/helix/pull/4752))
- Exit select-mode after executing `replace_*` commands ([#4554](https://github.com/helix-editor/helix/pull/4554))
- Exit select-mode after executing surround commands ([#4858](https://github.com/helix-editor/helix/pull/4858))
- Change tree-sitter text-object keys ([#3782](https://github.com/helix-editor/helix/pull/3782))
- Rename `fleetish` theme to `fleet_dark` ([#4997](https://github.com/helix-editor/helix/pull/4997))
Features:
- Bufferline ([#2759](https://github.com/helix-editor/helix/pull/2759))
- Support underline styles and colors ([#4061](https://github.com/helix-editor/helix/pull/4061), [98c121c](https://github.com/helix-editor/helix/commit/98c121c))
- Inheritance for themes ([#3067](https://github.com/helix-editor/helix/pull/3067), [#4096](https://github.com/helix-editor/helix/pull/4096))
- Cursorcolumn ([#4084](https://github.com/helix-editor/helix/pull/4084))
- Overhauled system for writing files and quiting ([#2267](https://github.com/helix-editor/helix/pull/2267), [#4397](https://github.com/helix-editor/helix/pull/4397))
- Autosave when terminal loses focus ([#3178](https://github.com/helix-editor/helix/pull/3178))
- Use OSC52 as a fallback for the system clipboard ([#3220](https://github.com/helix-editor/helix/pull/3220))
- Show git diffs in the gutter ([#3890](https://github.com/helix-editor/helix/pull/3890), [#5012](https://github.com/helix-editor/helix/pull/5012), [#4995](https://github.com/helix-editor/helix/pull/4995))
- Add a logo ([dc1ec56](https://github.com/helix-editor/helix/commit/dc1ec56))
- Multi-cursor completion ([#4496](https://github.com/helix-editor/helix/pull/4496))
Commands:
- `file_picker_in_current_directory` (`<space>F`) ([#3701](https://github.com/helix-editor/helix/pull/3701))
- `:lsp-restart` to restart the current document's language server ([#3435](https://github.com/helix-editor/helix/pull/3435), [#3972](https://github.com/helix-editor/helix/pull/3972))
- `join_selections_space` (`A-j`) which joins selections and selects the joining whitespace ([#3549](https://github.com/helix-editor/helix/pull/3549))
- `:update` to write the current file if it is modified ([#4426](https://github.com/helix-editor/helix/pull/4426))
- `:lsp-workspace-command` for picking LSP commands to execute ([#3140](https://github.com/helix-editor/helix/pull/3140))
- `extend_prev_word_end` - the extend variant for `move_prev_word_end` ([7468fa2](https://github.com/helix-editor/helix/commit/7468fa2))
- `make_search_word_bounded` which adds regex word boundaries to the current search register value ([#4322](https://github.com/helix-editor/helix/pull/4322))
- `:reload-all` - `:reload` for all open buffers ([#4663](https://github.com/helix-editor/helix/pull/4663), [#4901](https://github.com/helix-editor/helix/pull/4901))
- `goto_next_change` (`]g`), `goto_prev_change` (`[g`), `goto_first_change` (`[G`), `goto_last_change` (`]G`) textobjects for jumping between VCS changes ([#4650](https://github.com/helix-editor/helix/pull/4650))
Usability improvements and fixes:
- Don't log 'LSP not defined' errors in the logfile ([1caba2d](https://github.com/helix-editor/helix/commit/1caba2d))
- Look for the external formatter program before invoking it ([#3670](https://github.com/helix-editor/helix/pull/3670))
- Don't send LSP didOpen events for documents without URLs ([44b4479](https://github.com/helix-editor/helix/commit/44b4479))
- Fix off-by-one in `extend_line_above` command ([#3689](https://github.com/helix-editor/helix/pull/3689))
- Use the original scroll offset when opening a split ([1acdfaa](https://github.com/helix-editor/helix/commit/1acdfaa))
- Handle auto-formatting failures and save the file anyway ([#3684](https://github.com/helix-editor/helix/pull/3684))
- Ensure the cursor is in view after `:reflow` ([#3733](https://github.com/helix-editor/helix/pull/3733))
- Add default rulers and reflow config for git commit messages ([#3738](https://github.com/helix-editor/helix/pull/3738))
- Improve grammar fetching and building output ([#3773](https://github.com/helix-editor/helix/pull/3773))
- Add a `text` language to language completion ([cc47d3f](https://github.com/helix-editor/helix/commit/cc47d3f))
- Improve error handling for `:set-language` ([e8add6f](https://github.com/helix-editor/helix/commit/e8add6f))
- Improve error handling for `:config-reload` ([#3668](https://github.com/helix-editor/helix/pull/3668))
- Improve error handling when passing improper ranges to syntax highlighting ([#3826](https://github.com/helix-editor/helix/pull/3826))
- Render `<code>` tags as raw markup in markdown ([#3425](https://github.com/helix-editor/helix/pull/3425))
- Remove border around the LSP code-actions popup ([#3444](https://github.com/helix-editor/helix/pull/3444))
- Canonicalize the path to the runtime directory ([#3794](https://github.com/helix-editor/helix/pull/3794))
- Add a `themelint` xtask for linting themes ([#3234](https://github.com/helix-editor/helix/pull/3234))
- Re-sort LSP diagnostics after applying transactions ([#3895](https://github.com/helix-editor/helix/pull/3895), [#4319](https://github.com/helix-editor/helix/pull/4319))
- Add a command-line flag to specify the log file ([#3807](https://github.com/helix-editor/helix/pull/3807))
- Track source and tag information in LSP diagnostics ([#3898](https://github.com/helix-editor/helix/pull/3898), [1df32c9](https://github.com/helix-editor/helix/commit/1df32c9))
- Fix theme returning to normal when exiting the `:theme` completion ([#3644](https://github.com/helix-editor/helix/pull/3644))
- Improve error messages for invalid commands in the keymap ([#3931](https://github.com/helix-editor/helix/pull/3931))
- Deduplicate regexs in `search_selection` command ([#3941](https://github.com/helix-editor/helix/pull/3941))
- Split the finding of LSP root and config roots ([#3929](https://github.com/helix-editor/helix/pull/3929))
- Ensure that the cursor is within view after auto-formatting ([#4047](https://github.com/helix-editor/helix/pull/4047))
- Add pseudo-pending to commands with on-next-key callbacks ([#4062](https://github.com/helix-editor/helix/pull/4062), [#4077](https://github.com/helix-editor/helix/pull/4077))
- Add live preview to `:goto` ([#2982](https://github.com/helix-editor/helix/pull/2982))
- Show regex compilation failure in a popup ([#3049](https://github.com/helix-editor/helix/pull/3049))
- Add 'cycled to end' and 'no more matches' for search ([#3176](https://github.com/helix-editor/helix/pull/3176), [#4101](https://github.com/helix-editor/helix/pull/4101))
- Add extending behavior to tree-sitter textobjects ([#3266](https://github.com/helix-editor/helix/pull/3266))
- Add `ui.gutter.selected` option for themes ([#3303](https://github.com/helix-editor/helix/pull/3303))
- Make statusline mode names configurable ([#3311](https://github.com/helix-editor/helix/pull/3311))
- Add a statusline element for total line count ([#3960](https://github.com/helix-editor/helix/pull/3960))
- Add extending behavior to `goto_window_*` commands ([#3985](https://github.com/helix-editor/helix/pull/3985))
- Fix a panic in signature help when the preview is too large ([#4030](https://github.com/helix-editor/helix/pull/4030))
- Add command names to the command palette ([#4071](https://github.com/helix-editor/helix/pull/4071), [#4223](https://github.com/helix-editor/helix/pull/4223), [#4495](https://github.com/helix-editor/helix/pull/4495))
- Find the LSP workspace root from the current document's path ([#3553](https://github.com/helix-editor/helix/pull/3553))
- Add an option to skip indent-guide levels ([#3819](https://github.com/helix-editor/helix/pull/3819), [2c36e33](https://github.com/helix-editor/helix/commit/2c36e33))
- Change focus to modified docs on quit ([#3872](https://github.com/helix-editor/helix/pull/3872))
- Respond to `USR1` signal by reloading config ([#3952](https://github.com/helix-editor/helix/pull/3952))
- Exit gracefully when the close operation fails ([#4081](https://github.com/helix-editor/helix/pull/4081))
- Fix goto/view center mismatch ([#4135](https://github.com/helix-editor/helix/pull/4135))
- Highlight the current file picker document on idle-timeout ([#3172](https://github.com/helix-editor/helix/pull/3172), [a85e386](https://github.com/helix-editor/helix/commit/a85e386))
- Apply transactions to jumplist selections ([#4186](https://github.com/helix-editor/helix/pull/4186), [#4227](https://github.com/helix-editor/helix/pull/4227), [#4733](https://github.com/helix-editor/helix/pull/4733), [#4865](https://github.com/helix-editor/helix/pull/4865), [#4912](https://github.com/helix-editor/helix/pull/4912), [#4965](https://github.com/helix-editor/helix/pull/4965), [#4981](https://github.com/helix-editor/helix/pull/4981))
- Use space as a separator for fuzzy matcher ([#3969](https://github.com/helix-editor/helix/pull/3969))
- Overlay all diagnostics with highest severity on top ([#4113](https://github.com/helix-editor/helix/pull/4113))
- Avoid re-parsing unmodified tree-sitter injections ([#4146](https://github.com/helix-editor/helix/pull/4146))
- Add extending captures for indentation, re-enable python indentation ([#3382](https://github.com/helix-editor/helix/pull/3382), [3e84434](https://github.com/helix-editor/helix/commit/3e84434))
- Only allow either `--vsplit` or `--hsplit` CLI flags at once ([#4202](https://github.com/helix-editor/helix/pull/4202))
- Fix append cursor location when selection anchor is at the end of the document ([#4147](https://github.com/helix-editor/helix/pull/4147))
- Improve selection yanking message ([#4275](https://github.com/helix-editor/helix/pull/4275))
- Log failures to load tree-sitter grammars as errors ([#4315](https://github.com/helix-editor/helix/pull/4315))
- Fix rendering of lines longer than 65,536 columns ([#4172](https://github.com/helix-editor/helix/pull/4172))
- Skip searching `.git` in `global_search` ([#4334](https://github.com/helix-editor/helix/pull/4334))
- Display tree-sitter scopes in a popup ([#4337](https://github.com/helix-editor/helix/pull/4337))
- Fix deleting a word from the end of the buffer ([#4328](https://github.com/helix-editor/helix/pull/4328))
- Pretty print the syntax tree in `:tree-sitter-subtree` ([#4295](https://github.com/helix-editor/helix/pull/4295), [#4606](https://github.com/helix-editor/helix/pull/4606))
- Allow specifying suffixes for file-type detection ([#2455](https://github.com/helix-editor/helix/pull/2455), [#4414](https://github.com/helix-editor/helix/pull/4414))
- Fix multi-byte auto-pairs ([#4024](https://github.com/helix-editor/helix/pull/4024))
- Improve sort scoring for LSP code-actions and completions ([#4134](https://github.com/helix-editor/helix/pull/4134))
- Fix the handling of quotes within shellwords ([#4098](https://github.com/helix-editor/helix/pull/4098))
- Fix `delete_word_backward` and `delete_word_forward` on newlines ([#4392](https://github.com/helix-editor/helix/pull/4392))
- Fix 'no entry found for key' crash on `:write-all` ([#4384](https://github.com/helix-editor/helix/pull/4384))
- Remove lowercase requirement for tree-sitter grammars ([#4346](https://github.com/helix-editor/helix/pull/4346))
- Resolve LSP completion items on idle-timeout ([#4406](https://github.com/helix-editor/helix/pull/4406), [#4797](https://github.com/helix-editor/helix/pull/4797))
- Render diagnostics in the file picker preview ([#4324](https://github.com/helix-editor/helix/pull/4324))
- Fix terminal freezing on `shell_insert_output` ([#4156](https://github.com/helix-editor/helix/pull/4156))
- Allow use of the count in the repeat operator (`.`) ([#4450](https://github.com/helix-editor/helix/pull/4450))
- Show the current theme name on `:theme` with no arguments ([#3740](https://github.com/helix-editor/helix/pull/3740))
- Fix rendering in very large terminals ([#4318](https://github.com/helix-editor/helix/pull/4318))
- Sort LSP preselected items to the top of the completion menu ([#4480](https://github.com/helix-editor/helix/pull/4480))
- Trim braces and quotes from paths in goto-file ([#4370](https://github.com/helix-editor/helix/pull/4370))
- Prevent automatic signature help outside of insert mode ([#4456](https://github.com/helix-editor/helix/pull/4456))
- Fix freezes with external programs that process stdin and stdout concurrently ([#4180](https://github.com/helix-editor/helix/pull/4180))
- Make `scroll` aware of tabs and wide characters ([#4519](https://github.com/helix-editor/helix/pull/4519))
- Correctly handle escaping in `command_mode` completion ([#4316](https://github.com/helix-editor/helix/pull/4316), [#4587](https://github.com/helix-editor/helix/pull/4587), [#4632](https://github.com/helix-editor/helix/pull/4632))
- Fix `delete_char_backward` for paired characters ([#4558](https://github.com/helix-editor/helix/pull/4558))
- Fix crash from two windows editing the same document ([#4570](https://github.com/helix-editor/helix/pull/4570))
- Fix pasting from the blackhole register ([#4497](https://github.com/helix-editor/helix/pull/4497))
- Support LSP insertReplace completion items ([1312682](https://github.com/helix-editor/helix/commit/1312682))
- Dynamically resize the line number gutter width ([#3469](https://github.com/helix-editor/helix/pull/3469))
- Fix crash for unknown completion item kinds ([#4658](https://github.com/helix-editor/helix/pull/4658))
- Re-enable `format_selections` for single selection ranges ([d4f5cab](https://github.com/helix-editor/helix/commit/d4f5cab))
- Limit the number of in-progress tree-sitter query matches ([#4707](https://github.com/helix-editor/helix/pull/4707), [#4830](https://github.com/helix-editor/helix/pull/4830))
- Use the special `#` register with `increment`/`decrement` to change by range number ([#4418](https://github.com/helix-editor/helix/pull/4418))
- Add a statusline element to show number of selected chars ([#4682](https://github.com/helix-editor/helix/pull/4682))
- Add a statusline element showing global LSP diagnostic warning and error counts ([#4569](https://github.com/helix-editor/helix/pull/4569))
- Add a scrollbar to popups ([#4449](https://github.com/helix-editor/helix/pull/4449))
- Prefer shorter matches in fuzzy matcher scoring ([#4698](https://github.com/helix-editor/helix/pull/4698))
- Use key-sequence format for command palette keybinds ([#4712](https://github.com/helix-editor/helix/pull/4712))
- Remove prefix filtering from autocompletion menu ([#4578](https://github.com/helix-editor/helix/pull/4578))
- Focus on the parent buffer when closing a split ([#4766](https://github.com/helix-editor/helix/pull/4766))
- Handle language server termination ([#4797](https://github.com/helix-editor/helix/pull/4797), [#4852](https://github.com/helix-editor/helix/pull/4852))
- Allow `r`/`t`/`f` to work on tab characters ([#4817](https://github.com/helix-editor/helix/pull/4817))
- Show a preview for scratch buffers in the buffer picker ([#3454](https://github.com/helix-editor/helix/pull/3454))
- Set a limit of entries in the jumplist ([#4750](https://github.com/helix-editor/helix/pull/4750))
- Re-use shell outputs when inserting or appending shell output ([#3465](https://github.com/helix-editor/helix/pull/3465))
- Check LSP server provider capabilities ([#3554](https://github.com/helix-editor/helix/pull/3554))
- Improve tree-sitter parsing performance on files with many language layers ([#4716](https://github.com/helix-editor/helix/pull/4716))
- Move indentation to the next line when using `<ret>` on a line with only whitespace ([#4854](https://github.com/helix-editor/helix/pull/4854))
- Remove selections for closed views from all documents ([#4888](https://github.com/helix-editor/helix/pull/4888))
- Improve performance of the `:reload` command ([#4457](https://github.com/helix-editor/helix/pull/4457))
- Properly handle media keys ([#4887](https://github.com/helix-editor/helix/pull/4887))
- Support LSP diagnostic data field ([#4935](https://github.com/helix-editor/helix/pull/4935))
- Handle C-i keycode as tab ([#4961](https://github.com/helix-editor/helix/pull/4961))
- Fix view alignment for jumplist picker jumps ([#3743](https://github.com/helix-editor/helix/pull/3743))
- Use OSC52 for tmux clipboard provider ([#5027](https://github.com/helix-editor/helix/pull/5027))
Themes:
- Add `varua` ([#3610](https://github.com/helix-editor/helix/pull/3610), [#4964](https://github.com/helix-editor/helix/pull/4964))
- Update `boo_berry` ([#3653](https://github.com/helix-editor/helix/pull/3653))
- Add `rasmus` ([#3728](https://github.com/helix-editor/helix/pull/3728))
- Add `papercolor_dark` ([#3742](https://github.com/helix-editor/helix/pull/3742))
- Update `monokai_pro_spectrum` ([#3814](https://github.com/helix-editor/helix/pull/3814))
- Update `nord` ([#3792](https://github.com/helix-editor/helix/pull/3792))
- Update `fleetish` ([#3844](https://github.com/helix-editor/helix/pull/3844), [#4487](https://github.com/helix-editor/helix/pull/4487), [#4813](https://github.com/helix-editor/helix/pull/4813))
- Update `flatwhite` ([#3843](https://github.com/helix-editor/helix/pull/3843))
- Add `darcula` ([#3739](https://github.com/helix-editor/helix/pull/3739))
- Update `papercolor` ([#3938](https://github.com/helix-editor/helix/pull/3938), [#4317](https://github.com/helix-editor/helix/pull/4317))
- Add bufferline colors to multiple themes ([#3881](https://github.com/helix-editor/helix/pull/3881))
- Add `gruvbox_dark_hard` ([#3948](https://github.com/helix-editor/helix/pull/3948))
- Add `onedarker` ([#3980](https://github.com/helix-editor/helix/pull/3980), [#4060](https://github.com/helix-editor/helix/pull/4060))
- Add `dark_high_contrast` ([#3312](https://github.com/helix-editor/helix/pull/3312))
- Update `bogster` ([#4121](https://github.com/helix-editor/helix/pull/4121), [#4264](https://github.com/helix-editor/helix/pull/4264))
- Update `sonokai` ([#4089](https://github.com/helix-editor/helix/pull/4089))
- Update `ayu_*` themes ([#4140](https://github.com/helix-editor/helix/pull/4140), [#4109](https://github.com/helix-editor/helix/pull/4109), [#4662](https://github.com/helix-editor/helix/pull/4662), [#4764](https://github.com/helix-editor/helix/pull/4764))
- Update `everforest` ([#3998](https://github.com/helix-editor/helix/pull/3998))
- Update `monokai_pro_octagon` ([#4247](https://github.com/helix-editor/helix/pull/4247))
- Add `heisenberg` ([#4209](https://github.com/helix-editor/helix/pull/4209))
- Add `bogster_light` ([#4265](https://github.com/helix-editor/helix/pull/4265))
- Update `pop-dark` ([#4323](https://github.com/helix-editor/helix/pull/4323))
- Update `rose_pine` ([#4221](https://github.com/helix-editor/helix/pull/4221))
- Add `kanagawa` ([#4300](https://github.com/helix-editor/helix/pull/4300))
- Add `hex_steel`, `hex_toxic` and `hex_lavendar` ([#4367](https://github.com/helix-editor/helix/pull/4367), [#4990](https://github.com/helix-editor/helix/pull/4990))
- Update `tokyonight` and `tokyonight_storm` ([#4415](https://github.com/helix-editor/helix/pull/4415))
- Update `gruvbox` ([#4626](https://github.com/helix-editor/helix/pull/4626))
- Update `dark_plus` ([#4661](https://github.com/helix-editor/helix/pull/4661), [#4678](https://github.com/helix-editor/helix/pull/4678))
- Add `zenburn` ([#4613](https://github.com/helix-editor/helix/pull/4613), [#4977](https://github.com/helix-editor/helix/pull/4977))
- Update `monokai_pro` ([#4789](https://github.com/helix-editor/helix/pull/4789))
- Add `mellow` ([#4770](https://github.com/helix-editor/helix/pull/4770))
- Add `nightfox` ([#4769](https://github.com/helix-editor/helix/pull/4769), [#4966](https://github.com/helix-editor/helix/pull/4966))
- Update `doom_acario_dark` ([#4979](https://github.com/helix-editor/helix/pull/4979))
- Update `autumn` ([#4996](https://github.com/helix-editor/helix/pull/4996))
- Update `acme` ([#4999](https://github.com/helix-editor/helix/pull/4999))
- Update `nord_light` ([#4999](https://github.com/helix-editor/helix/pull/4999))
- Update `serika_*` ([#5015](https://github.com/helix-editor/helix/pull/5015))
LSP configurations:
- Switch to `openscad-lsp` for OpenScad ([#3750](https://github.com/helix-editor/helix/pull/3750))
- Support Jsonnet ([#3748](https://github.com/helix-editor/helix/pull/3748))
- Support Markdown ([#3499](https://github.com/helix-editor/helix/pull/3499))
- Support Bass ([#3771](https://github.com/helix-editor/helix/pull/3771))
- Set roots configuration for Elixir and HEEx ([#3917](https://github.com/helix-editor/helix/pull/3917), [#3959](https://github.com/helix-editor/helix/pull/3959))
- Support Purescript ([#4242](https://github.com/helix-editor/helix/pull/4242))
- Set roots configuration for Julia ([#4361](https://github.com/helix-editor/helix/pull/4361))
- Support D ([#4372](https://github.com/helix-editor/helix/pull/4372))
- Increase default language server timeout for Julia ([#4575](https://github.com/helix-editor/helix/pull/4575))
- Use ElixirLS for HEEx ([#4679](https://github.com/helix-editor/helix/pull/4679))
- Support Bicep ([#4403](https://github.com/helix-editor/helix/pull/4403))
- Switch to `nil` for Nix ([433ccef](https://github.com/helix-editor/helix/commit/433ccef))
- Support QML ([#4842](https://github.com/helix-editor/helix/pull/4842))
- Enable auto-format for CSS ([#4987](https://github.com/helix-editor/helix/pull/4987))
- Support CommonLisp ([4176769](https://github.com/helix-editor/helix/commit/4176769))
New languages:
- SML ([#3692](https://github.com/helix-editor/helix/pull/3692))
- Jsonnet ([#3714](https://github.com/helix-editor/helix/pull/3714))
- Godot resource ([#3759](https://github.com/helix-editor/helix/pull/3759))
- Astro ([#3829](https://github.com/helix-editor/helix/pull/3829))
- SSH config ([#2455](https://github.com/helix-editor/helix/pull/2455), [#4538](https://github.com/helix-editor/helix/pull/4538))
- Bass ([#3771](https://github.com/helix-editor/helix/pull/3771))
- WAT (WebAssembly text format) ([#4040](https://github.com/helix-editor/helix/pull/4040), [#4542](https://github.com/helix-editor/helix/pull/4542))
- Purescript ([#4242](https://github.com/helix-editor/helix/pull/4242))
- D ([#4372](https://github.com/helix-editor/helix/pull/4372), [#4562](https://github.com/helix-editor/helix/pull/4562))
- VHS ([#4486](https://github.com/helix-editor/helix/pull/4486))
- KDL ([#4481](https://github.com/helix-editor/helix/pull/4481))
- XML ([#4518](https://github.com/helix-editor/helix/pull/4518))
- WIT ([#4525](https://github.com/helix-editor/helix/pull/4525))
- ENV ([#4536](https://github.com/helix-editor/helix/pull/4536))
- INI ([#4538](https://github.com/helix-editor/helix/pull/4538))
- Bicep ([#4403](https://github.com/helix-editor/helix/pull/4403), [#4751](https://github.com/helix-editor/helix/pull/4751))
- QML ([#4842](https://github.com/helix-editor/helix/pull/4842))
- CommonLisp ([4176769](https://github.com/helix-editor/helix/commit/4176769))
Updated languages and queries:
- Zig ([#3621](https://github.com/helix-editor/helix/pull/3621), [#4745](https://github.com/helix-editor/helix/pull/4745))
- Rust ([#3647](https://github.com/helix-editor/helix/pull/3647), [#3729](https://github.com/helix-editor/helix/pull/3729), [#3927](https://github.com/helix-editor/helix/pull/3927), [#4073](https://github.com/helix-editor/helix/pull/4073), [#4510](https://github.com/helix-editor/helix/pull/4510), [#4659](https://github.com/helix-editor/helix/pull/4659), [#4717](https://github.com/helix-editor/helix/pull/4717))
- Solidity ([20ed8c2](https://github.com/helix-editor/helix/commit/20ed8c2))
- Fish ([#3704](https://github.com/helix-editor/helix/pull/3704))
- Elixir ([#3645](https://github.com/helix-editor/helix/pull/3645), [#4333](https://github.com/helix-editor/helix/pull/4333), [#4821](https://github.com/helix-editor/helix/pull/4821))
- Diff ([#3708](https://github.com/helix-editor/helix/pull/3708))
- Nix ([665e27f](https://github.com/helix-editor/helix/commit/665e27f), [1fe3273](https://github.com/helix-editor/helix/commit/1fe3273))
- Markdown ([#3749](https://github.com/helix-editor/helix/pull/3749), [#4078](https://github.com/helix-editor/helix/pull/4078), [#4483](https://github.com/helix-editor/helix/pull/4483), [#4478](https://github.com/helix-editor/helix/pull/4478))
- GDScript ([#3760](https://github.com/helix-editor/helix/pull/3760))
- JSX and TSX ([#3853](https://github.com/helix-editor/helix/pull/3853), [#3973](https://github.com/helix-editor/helix/pull/3973))
- Ruby ([#3976](https://github.com/helix-editor/helix/pull/3976), [#4601](https://github.com/helix-editor/helix/pull/4601))
- R ([#4031](https://github.com/helix-editor/helix/pull/4031))
- WGSL ([#3996](https://github.com/helix-editor/helix/pull/3996), [#4079](https://github.com/helix-editor/helix/pull/4079))
- C# ([#4118](https://github.com/helix-editor/helix/pull/4118), [#4281](https://github.com/helix-editor/helix/pull/4281), [#4213](https://github.com/helix-editor/helix/pull/4213))
- Twig ([#4176](https://github.com/helix-editor/helix/pull/4176))
- Lua ([#3552](https://github.com/helix-editor/helix/pull/3552))
- C/C++ ([#4079](https://github.com/helix-editor/helix/pull/4079), [#4278](https://github.com/helix-editor/helix/pull/4278), [#4282](https://github.com/helix-editor/helix/pull/4282))
- Cairo ([17488f1](https://github.com/helix-editor/helix/commit/17488f1), [431f9c1](https://github.com/helix-editor/helix/commit/431f9c1), [09a6df1](https://github.com/helix-editor/helix/commit/09a6df1))
- Rescript ([#4356](https://github.com/helix-editor/helix/pull/4356))
- Zig ([#4409](https://github.com/helix-editor/helix/pull/4409))
- Scala ([#4353](https://github.com/helix-editor/helix/pull/4353), [#4697](https://github.com/helix-editor/helix/pull/4697), [#4701](https://github.com/helix-editor/helix/pull/4701))
- LaTeX ([#4528](https://github.com/helix-editor/helix/pull/4528), [#4922](https://github.com/helix-editor/helix/pull/4922))
- SQL ([#4529](https://github.com/helix-editor/helix/pull/4529))
- Python ([#4560](https://github.com/helix-editor/helix/pull/4560))
- Bash/Zsh ([#4582](https://github.com/helix-editor/helix/pull/4582))
- Nu ([#4583](https://github.com/helix-editor/helix/pull/4583))
- Julia ([#4588](https://github.com/helix-editor/helix/pull/4588))
- Typescript ([#4703](https://github.com/helix-editor/helix/pull/4703))
- Meson ([#4572](https://github.com/helix-editor/helix/pull/4572))
- Haskell ([#4800](https://github.com/helix-editor/helix/pull/4800))
- CMake ([#4809](https://github.com/helix-editor/helix/pull/4809))
- HTML ([#4829](https://github.com/helix-editor/helix/pull/4829), [#4881](https://github.com/helix-editor/helix/pull/4881))
- Java ([#4886](https://github.com/helix-editor/helix/pull/4886))
- Go ([#4906](https://github.com/helix-editor/helix/pull/4906), [#4969](https://github.com/helix-editor/helix/pull/4969), [#5010](https://github.com/helix-editor/helix/pull/5010))
- CSS ([#4882](https://github.com/helix-editor/helix/pull/4882))
- Racket ([#4915](https://github.com/helix-editor/helix/pull/4915))
- SCSS ([#5003](https://github.com/helix-editor/helix/pull/5003))
Packaging:
- Filter relevant source files in the Nix flake ([#3657](https://github.com/helix-editor/helix/pull/3657))
- Build a binary for `aarch64-linux` in the release CI ([038a91d](https://github.com/helix-editor/helix/commit/038a91d))
- Build an AppImage for `aarch64-linux` in the release CI ([b738031](https://github.com/helix-editor/helix/commit/b738031))
- Enable CI builds for `riscv64-linux` ([#3685](https://github.com/helix-editor/helix/pull/3685))
- Support preview releases in CI ([0090a2d](https://github.com/helix-editor/helix/commit/0090a2d))
- Strip binaries built in CI ([#3780](https://github.com/helix-editor/helix/pull/3780))
- Fix the development shell for the Nix Flake on `aarch64-darwin` ([#3810](https://github.com/helix-editor/helix/pull/3810))
- Raise the MSRV and create an MSRV policy ([#3896](https://github.com/helix-editor/helix/pull/3896), [#3913](https://github.com/helix-editor/helix/pull/3913), [#3961](https://github.com/helix-editor/helix/pull/3961))
- Fix Fish completions for `--config` and `--log` flags ([#3912](https://github.com/helix-editor/helix/pull/3912))
- Use builtin filenames option in Bash completion ([#4648](https://github.com/helix-editor/helix/pull/4648))
# 22.08.1 (2022-09-01) # 22.08.1 (2022-09-01)
This is a patch release that fixes a panic caused by closing splits or buffers. ([#3633](https://github.com/helix-editor/helix/pull/3633)) This is a patch release that fixes a panic caused by closing splits or buffers. ([#3633](https://github.com/helix-editor/helix/pull/3633))

1265
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -7,6 +7,7 @@ members = [
"helix-lsp", "helix-lsp",
"helix-dap", "helix-dap",
"helix-loader", "helix-loader",
"helix-vcs",
"xtask", "xtask",
] ]

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

@ -0,0 +1,178 @@
<<<<<<< HEAD
# Helix Plus
<h1 style="color:red">This is an unstable fork of helix with some PRs merged and some merge conflicts resolved </h1>
# Merged PRs
- [File explorer and tree helper](https://github.com/helix-editor/helix/pull/2377)
- [with Icons](https://github.com/r0l1/helix/tree/tree_explorer_icons)
- [Add LSP workspace command picker](https://github.com/helix-editor/helix/pull/3140)
- [completion fix](https://github.com/helix-editor/helix/pull/1819)
- [Add rainbow indentation guides](https://github.com/helix-editor/helix/pull/4056)
- [Improve sorting for inline menu (codeaction + completion)](https://github.com/helix-editor/helix/pull/4134)
And others I forgot about...
# Applied Changes
- Changed opening the window popup from `ctrl + w` to `ctrl + v`
- Added an auto highlight for files in the tree explorer when jumping through opened buffers
- Changed some default settings (enabling bufferline, indent guides, the embedded explorer, cursor modes etc.)
- Added a `--show-explorer` cli flag to open the file explorer on startup (useful for embedded explorer mode)
- Added a `delete` (aliases `rm`, `del`) command to delete the file associated with the current buffer
- Changed keybind `<space> E` to close the explorer instead of toggling the recursion one
- Added a completion chars setting that triggers autocomplete when typing one of those chars
- - -
||||||| f0f295a6
# Helix
=======
<div align="center">
<h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="logo_dark.svg">
<source media="(prefers-color-scheme: light)" srcset="logo_light.svg">
<img alt="Helix" height="128" src="logo_light.svg">
</picture>
</h1>
>>>>>>> origin/master
[![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions)
[![GitHub Release](https://img.shields.io/github/v/release/helix-editor/helix)](https://github.com/helix-editor/helix/releases/latest)
[![Documentation](https://shields.io/badge/-documentation-452859)](https://docs.helix-editor.com/)
[![GitHub contributors](https://img.shields.io/github/contributors/helix-editor/helix)](https://github.com/helix-editor/helix/graphs/contributors)
[![Matrix Space](https://img.shields.io/matrix/helix-community:matrix.org)](https://matrix.to/#/#helix-community:matrix.org)
</div>
![Screenshot](./screenshot.png)
A Kakoune / Neovim inspired editor, written in Rust.
The editing model is very heavily based on Kakoune; during development I found
myself agreeing with most of Kakoune's design decisions.
For more information, see the [website](https://helix-editor.com) or
[documentation](https://docs.helix-editor.com/).
All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html).
[Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting)
# Features
- Vim-like modal editing
- Multiple selections
- Built-in language server support
- Smart, incremental syntax highlighting and code editing via tree-sitter
It's a terminal-based editor first, but I'd like to explore a custom renderer
(similar to Emacs) in wgpu or skulpin.
Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.scm`.
# Installation
Packages are available for various distributions (see [Installation docs](https://docs.helix-editor.com/install.html)).
If you would like to build from source:
```shell
git clone https://github.com/helix-editor/helix
cd helix
cargo install --path helix-term
```
This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars in `./runtime/grammars`.
Helix needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
| OS | Command |
| -------------------- | ------------------------------------------------ |
| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` |
| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` |
| Linux / macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` |
Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires
elevated privileges - i.e. PowerShell or Cmd must be run as administrator.
**PowerShell:**
```powershell
New-Item -ItemType SymbolicLink -Target "runtime" -Path "$Env:AppData\helix\runtime"
```
**Cmd:**
```cmd
cd %appdata%\helix
mklink /D runtime "<helix-repo>\runtime"
```
The runtime location can be overridden via the `HELIX_RUNTIME` environment variable.
> NOTE: if `HELIX_RUNTIME` is set prior to calling `cargo install --path helix-term`,
> tree-sitter grammars will be built in `$HELIX_RUNTIME/grammars`.
If you plan on keeping the repo locally, an alternative to copying/symlinking
runtime files is to set `HELIX_RUNTIME=/path/to/helix/runtime`
(`HELIX_RUNTIME=$PWD/runtime` if you're in the helix repo directory).
Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir.
> NOTE: running via cargo also doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
> detect the `runtime` directory in the project root.
If you want to customize your `languages.toml` config,
tree-sitter grammars may be manually fetched and built with `hx --grammar fetch` and `hx --grammar build`.
In order to use LSP features like auto-complete, you will need to
[install the appropriate Language Server](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
for a language.
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## Adding Helix to your desktop environment
If installing from source, to use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder:
```bash
cp contrib/Helix.desktop ~/.local/share/applications
```
To use another terminal than the default, you will need to modify the `.desktop` file. For example, to use `kitty`:
```bash
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
```
Please note: there is no icon for Helix yet, so the system default will be used.
## macOS
Helix can be installed on macOS through homebrew:
```
brew install helix
```
# Contributing
Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
# Getting help
Your question might already be answered on the [FAQ](https://github.com/helix-editor/helix/wiki/FAQ).
Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet).
# Credits
Thanks to [@JakeHL](https://github.com/JakeHL) for designing the logo!

@ -1 +1 @@
22.08.1 22.12

@ -7,6 +7,7 @@
"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] } "ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
"ui.selection" = { fg = "black", bg = "blue" } "ui.selection" = { fg = "black", bg = "blue" }
"ui.selection.primary" = { fg = "white", bg = "blue" } "ui.selection.primary" = { fg = "white", bg = "blue" }
"ui.text.inactive" = { fg = "gray" }
"comment" = { fg = "gray" } "comment" = { fg = "gray" }
"ui.statusline" = { fg = "black", bg = "white" } "ui.statusline" = { fg = "black", bg = "white" }
"ui.statusline.inactive" = { fg = "gray", bg = "white" } "ui.statusline.inactive" = { fg = "gray", bg = "white" }

@ -9,3 +9,4 @@ edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{pat
cname = "docs.helix-editor.com" cname = "docs.helix-editor.com"
default-theme = "colibri" default-theme = "colibri"
preferred-dark-theme = "colibri" preferred-dark-theme = "colibri"
git-repository-url = "https://github.com/helix-editor/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", "spacer", "line-numbers"]` | | `gutters` | Gutters to display: Available are `diagnostics` and `diff` 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", "spacer", "diff"]` |
| `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` |

@ -2,9 +2,10 @@
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| astro | ✓ | | | | | astro | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` | | awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | | | `bash-language-server` | | bash | ✓ | | | `bash-language-server` |
| bass | ✓ | | | `bass` | | bass | ✓ | | | `bass` |
| beancount | ✓ | | | | | beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` | | bicep | ✓ | | | `bicep-langserver` |
| c | ✓ | ✓ | ✓ | `clangd` | | c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` | | c-sharp | ✓ | ✓ | | `OmniSharp` |
@ -12,8 +13,10 @@
| clojure | ✓ | | | `clojure-lsp` | | clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` | | cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
| comment | ✓ | | | | | comment | ✓ | | | |
| common-lisp | ✓ | | | `cl-lsp` |
| cpon | ✓ | | ✓ | | | cpon | ✓ | | ✓ | |
| cpp | ✓ | ✓ | ✓ | `clangd` | | cpp | ✓ | ✓ | ✓ | `clangd` |
| crystal | ✓ | | | |
| css | ✓ | | | `vscode-css-language-server` | | css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` | | cue | ✓ | | | `cuelsp` |
| d | ✓ | ✓ | ✓ | `serve-d` | | d | ✓ | ✓ | ✓ | `serve-d` |
@ -49,7 +52,7 @@
| gowork | ✓ | | | `gopls` | | gowork | ✓ | | | `gopls` |
| graphql | ✓ | | | | | graphql | ✓ | | | |
| hare | ✓ | | | | | hare | ✓ | | | |
| haskell | ✓ | | | `haskell-language-server-wrapper` | | haskell | ✓ | | | `haskell-language-server-wrapper` |
| hcl | ✓ | | ✓ | `terraform-ls` | | hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` | | heex | ✓ | ✓ | | `elixir-ls` |
| html | ✓ | | | `vscode-html-language-server` | | html | ✓ | | | `vscode-html-language-server` |
@ -75,6 +78,8 @@
| make | ✓ | | | | | make | ✓ | | | |
| markdown | ✓ | | | `marksman` | | markdown | ✓ | | | `marksman` |
| markdown.inline | ✓ | | | | | markdown.inline | ✓ | | | |
| matlab | ✓ | | | |
| mermaid | ✓ | | | |
| meson | ✓ | | ✓ | | | meson | ✓ | | ✓ | |
| mint | | | | `mint` | | mint | | | | `mint` |
| nickel | ✓ | | ✓ | `nls` | | nickel | ✓ | | ✓ | `nls` |
@ -95,7 +100,7 @@
| python | ✓ | ✓ | ✓ | `pylsp` | | python | ✓ | ✓ | ✓ | `pylsp` |
| qml | ✓ | | ✓ | `qmlls` | | qml | ✓ | | ✓ | `qmlls` |
| r | ✓ | | | `R` | | r | ✓ | | | `R` |
| racket | | | | `racket` | | racket | | | | `racket` |
| regex | ✓ | | | | | regex | ✓ | | | |
| rescript | ✓ | ✓ | | `rescript-language-server` | | rescript | ✓ | ✓ | | `rescript-language-server` |
| rmarkdown | ✓ | | ✓ | `R` | | rmarkdown | ✓ | | ✓ | `R` |

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

@ -1,18 +1,18 @@
| Name | Description | | Name | Description |
| --- | --- | | --- | --- |
| `:quit`, `:q` | Close the current view. | | `:quit`, `:q` | Close the current view. |
| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). | | `:quit!`, `:q!` | Force close the current view, ignoring unsaved changes. |
| `:open`, `:o` | Open a file from disk into the current view. | | `:open`, `:o` | Open a file from disk into the current view. |
| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. | | `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). | | `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully, ignoring unsaved changes. |
| `:buffer-close-others`, `:bco`, `:bcloseother` | Close all buffers but the currently focused one. | | `:buffer-close-others`, `:bco`, `:bcloseother` | Close all buffers but the currently focused one. |
| `:buffer-close-others!`, `:bco!`, `:bcloseother!` | Close all buffers but the currently focused one. | | `:buffer-close-others!`, `:bco!`, `:bcloseother!` | Force close all buffers but the currently focused one. |
| `:buffer-close-all`, `:bca`, `:bcloseall` | Close all buffers, without quitting. | | `:buffer-close-all`, `:bca`, `:bcloseall` | Close all buffers without quitting. |
| `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Close all buffers forcefully (ignoring unsaved changes), without quitting. | | `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Force close all buffers ignoring unsaved changes without quitting. |
| `:buffer-next`, `:bn`, `:bnext` | Go to next buffer. | | `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. |
| `:buffer-previous`, `:bp`, `:bprev` | Go to previous buffer. | | `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. |
| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) | | `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
| `:write!`, `:w!` | Write changes to disk forcefully (creating necessary subdirectories). Accepts an optional path (:write some/path.txt) | | `:write!`, `:w!` | Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write some/path.txt) |
| `:new`, `:n` | Create a new scratch buffer. | | `:new`, `:n` | Create a new scratch buffer. |
| `:format`, `:fmt` | Format the file using the LSP formatter. | | `:format`, `:fmt` | Format the file using the LSP formatter. |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) | | `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
@ -25,10 +25,10 @@
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. | | `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. |
| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). | | `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). |
| `:quit-all`, `:qa` | Close all views. | | `:quit-all`, `:qa` | Close all views. |
| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). | | `:quit-all!`, `:qa!` | Force close all views ignoring unsaved changes. |
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | | `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:cquit!`, `:cq!` | Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2). | | `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme. | | `:theme` | Change the editor theme (show current theme if no name specified). |
| `:clipboard-yank` | Yank main selection into system clipboard. | | `:clipboard-yank` | Yank main selection into system clipboard. |
| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. | | `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. | | `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
@ -42,8 +42,12 @@
| `:show-clipboard-provider` | Show clipboard provider name in status bar. | | `:show-clipboard-provider` | Show clipboard provider name in status bar. |
| `:change-current-directory`, `:cd` | Change the current working directory. | | `:change-current-directory`, `:cd` | Change the current working directory. |
| `: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. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `: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. |
| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. | | `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
@ -53,7 +57,7 @@
| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. | | `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
| `:hsplit-new`, `:hnew` | Open a scratch buffer in a horizontal split. | | `:hsplit-new`, `:hnew` | Open a scratch buffer in a horizontal split. |
| `:tutor` | Open the tutorial. | | `:tutor` | Open the tutorial. |
| `:goto`, `:g` | Go to line number. | | `:goto`, `:g` | Goto line number. |
| `:set-language`, `:lang` | Set the language of current buffer. | | `:set-language`, `:lang` | Set the language of current buffer. |
| `:set-option`, `:set` | Set a config option at runtime.<br>For example to disable smart case search, use `:set search.smart-case false`. | | `:set-option`, `:set` | Set a config option at runtime.<br>For example to disable smart case search, use `:set search.smart-case false`. |
| `:get-option`, `:get` | Get the current value of a config option. | | `:get-option`, `:get` | Get the current value of a config option. |
@ -61,13 +65,18 @@
| `:rsort` | Sort ranges in selection in reverse order. | | `:rsort` | Sort ranges in selection in reverse order. |
| `:reflow` | Hard-wrap the current selection of lines to a given width. | | `:reflow` | Hard-wrap the current selection of lines to a given width. |
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
| `:config-reload` | Refreshes helix's config. | | `:config-reload` | Refresh user config. |
| `:config-open` | Open the helix config.toml file. | | `:config-open` | Open the user config.toml file. |
| `:log-open` | Open the helix log file. | | `:log-open` | Open the helix log file. |
| `:insert-output` | Run shell command, inserting output after each selection. | | `:insert-output` | Run shell command, inserting output before each selection. |
| `:append-output` | Run shell command, appending output after each selection. | | `:append-output` | Run shell command, appending output after each selection. |
| `:pipe` | Pipe each selection to the shell command. | | `:pipe` | Pipe each selection to the shell command. |
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
||||||| f0f295a6
=======
| `:pipe-to` | Pipe each selection to the shell command, ignoring output. |
>>>>>>> origin/master
| `:run-shell-command`, `:sh` | Run a shell command | | `:run-shell-command`, `:sh` | Run a shell command |
||||||| 4b1fe367 ||||||| 4b1fe367
======= =======

@ -111,6 +111,7 @@
| `s` | Select all regex matches inside selections | `select_regex` | | `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into subselections on regex matches | `split_selection` | | `S` | Split selection into subselections on regex matches | `split_selection` |
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `Alt-_ ` | Merge consecutive selections | `merge_consecutive_selections` |
| `&` | Align selection in columns | `align_selections` | | `&` | Align selection in columns | `align_selections` |
| `_` | Trim whitespace from the selection | `trim_selections` | | `_` | Trim whitespace from the selection | `trim_selections` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` | | `;` | Collapse selection onto a single cursor | `collapse_selection` |
@ -299,7 +300,7 @@ Displays documentation for item under cursor.
| ---- | ----------- | | ---- | ----------- |
| `Ctrl-u` | Scroll up | | `Ctrl-u` | Scroll up |
| `Ctrl-d` | Scroll down | | `Ctrl-d` | Scroll down |
#### Unimpaired #### Unimpaired
Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
@ -312,16 +313,20 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` | | `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `]f` | Go to next function (**TS**) | `goto_next_function` | | `]f` | Go to next function (**TS**) | `goto_next_function` |
| `[f` | Go to previous function (**TS**) | `goto_prev_function` | | `[f` | Go to previous function (**TS**) | `goto_prev_function` |
| `]c` | Go to next class (**TS**) | `goto_next_class` | | `]t` | Go to next type definition (**TS**) | `goto_next_class` |
| `[c` | Go to previous class (**TS**) | `goto_prev_class` | | `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` | | `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` | | `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
| `]o` | Go to next comment (**TS**) | `goto_next_comment` | | `]c` | Go to next comment (**TS**) | `goto_next_comment` |
| `[o` | Go to previous comment (**TS**) | `goto_prev_comment` | | `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]t` | Go to next test (**TS**) | `goto_next_test` | | `]T` | Go to next test (**TS**) | `goto_next_test` |
| `]t` | Go to previous test (**TS**) | `goto_prev_test` | | `[T` | Go to previous test (**TS**) | `goto_prev_test` |
| `]p` | Go to next paragraph | `goto_next_paragraph` | | `]p` | Go to next paragraph | `goto_next_paragraph` |
| `[p` | Go to previous paragraph | `goto_prev_paragraph` | | `[p` | Go to previous paragraph | `goto_prev_paragraph` |
| `]g` | Go to next change | `goto_next_change` |
| `[g` | Go to previous change | `goto_prev_change` |
| `]G` | Go to first change | `goto_first_change` |
| `[G` | Go to last change | `goto_last_change` |
| `[Space` | Add newline above | `add_newline_above` | | `[Space` | Add newline above | `add_newline_above` |
| `]Space` | Add newline below | `add_newline_below` | | `]Space` | Add newline below | `add_newline_below` |

@ -39,7 +39,7 @@ injection-regex = "^mylang$"
file-types = ["mylang", "myl"] file-types = ["mylang", "myl"]
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-server = { command = "mylang-lsp", args = ["--stdio"] } language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
formatter = { command = "mylang-formatter" , args = ["--stdin"] } formatter = { command = "mylang-formatter" , args = ["--stdin"] }
``` ```
@ -99,6 +99,7 @@ The `language-server` field takes the following keys:
| `args` | A list of arguments to pass to the language server binary | | `args` | A list of arguments to pass to the language server binary |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | | `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `language-id` | The language name to pass to the language server. Some language servers support multiple languages and use this field to determine which one is being served in a buffer | | `language-id` | The language name to pass to the language server. Some language servers support multiple languages and use this field to determine which one is being served in a buffer |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
The top-level `config` field is used to configure the LSP initialization options. A `format` The top-level `config` field is used to configure the LSP initialization options. A `format`
sub-table within `config` can be used to pass extra formatting options to sub-table within `config` can be used to pass extra formatting options to

@ -103,7 +103,7 @@ Some styles might not be supported by your terminal emulator.
| `line` | | `line` |
| `curl` | | `curl` |
| `dashed` | | `dashed` |
| `dot` | | `dotted` |
| `double_line` | | `double_line` |
@ -313,6 +313,7 @@ These scopes are used for theming the editor interface.
| `ui.help` | Description box for commands | | `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. | | `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | | | `ui.text.focus` | |
| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes | | `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) | | `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
| `ui.virtual.whitespace` | Visible whitespace characters | | `ui.virtual.whitespace` | Visible whitespace characters |

@ -143,6 +143,7 @@ though, we climb the syntax tree and then take the previous selection. So
| `a` | Argument/parameter | | `a` | Argument/parameter |
| `o` | Comment | | `o` | Comment |
| `t` | Test | | `t` | Test |
| `g` | Change |
> NOTE: `f`, `c`, etc need a tree-sitter grammar active for the current > NOTE: `f`, `c`, etc need a tree-sitter grammar active for the current
document and a special tree-sitter query file to work properly. [Only document and a special tree-sitter query file to work properly. [Only

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 42 KiB

@ -1,22 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.7 184.2"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" viewBox="663.38 37.57 575.35 903.75"> <g transform="matrix(1,0,0,1,-31352.7,-1817.25)"> <g transform="matrix(1,0,0,1,31062.7,-20.8972)"> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z" style="fill:#706bc8;"></path> </g> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z" style="fill:#55c5e4;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z" style="fill:#84ddea;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z" style="fill:#997bc8;"></path></g></g></g> </svg>
<style>
@media (prefers-color-scheme: dark) {
svg { fill: white; }
}
</style>
<path d="M189.5,36.8c0.2,2.8,0,5.1-0.6,6.8L153,162c-0.6,2.1-2,3.7-4.2,5c-2.2,1.2-4.4,1.9-6.7,1.9H31.4c-9.6,0-15.3-2.8-17.3-8.4
c-0.8-2.2-0.8-3.9,0.1-5.2c0.9-1.2,2.4-1.8,4.6-1.8H123c7.4,0,12.6-1.4,15.4-4.1s5.7-8.9,8.6-18.4l32.9-108.6
c1.8-5.9,1-11.1-2.2-15.6S169.9,0,164,0H72.7c-1,0-3.1,0.4-6.1,1.1l0.1-0.4C64.5,0.2,62.6,0,61,0.1s-3,0.5-4.3,1.4
c-1.3,0.9-2.4,1.8-3.2,2.8S52,6.5,51.2,8.1c-0.8,1.6-1.4,3-1.9,4.3s-1.1,2.7-1.8,4.2c-0.7,1.5-1.3,2.7-2,3.7c-0.5,0.6-1.2,1.5-2,2.5
s-1.6,2-2.2,2.8s-0.9,1.5-1.1,2.2c-0.2,0.7-0.1,1.8,0.2,3.2c0.3,1.4,0.4,2.4,0.4,3.1c-0.3,3-1.4,6.9-3.3,11.6
c-1.9,4.7-3.6,8.1-5.1,10.1c-0.3,0.4-1.2,1.3-2.6,2.7c-1.4,1.4-2.3,2.6-2.6,3.7c-0.3,0.4-0.3,1.5-0.1,3.4c0.3,1.8,0.4,3.1,0.3,3.8
c-0.3,2.7-1.3,6.3-3,10.8c-1.7,4.5-3.4,8.2-5,11c-0.2,0.5-0.9,1.4-2,2.8c-1.1,1.4-1.8,2.5-2,3.4c-0.2,0.6-0.1,1.8,0.1,3.4
c0.2,1.6,0.2,2.8-0.1,3.6c-0.6,3-1.8,6.7-3.6,11c-1.8,4.3-3.6,7.9-5.4,11c-0.5,0.8-1.1,1.7-2,2.8c-0.8,1.1-1.5,2-2,2.8
s-0.8,1.6-1,2.5c-0.1,0.5,0,1.3,0.4,2.3c0.3,1.1,0.4,1.9,0.4,2.6c-0.1,1.1-0.2,2.6-0.5,4.4c-0.2,1.8-0.4,2.9-0.4,3.2
c-1.8,4.8-1.7,9.9,0.2,15.2c2.2,6.2,6.2,11.5,11.9,15.8c5.7,4.3,11.7,6.4,17.8,6.4h110.7c5.2,0,10.1-1.7,14.7-5.2s7.7-7.8,9.2-12.9
l33-108.6c1.8-5.8,1-10.9-2.2-15.5C194.9,39.7,192.6,38,189.5,36.8z M59.6,122.8L73.8,80c0,0,7,0,10.8,0s28.8-1.7,25.4,17.5
c-3.4,19.2-18.8,25.2-36.8,25.4S59.6,122.8,59.6,122.8z M78.6,116.8c4.7-0.1,18.9-2.9,22.1-17.1S89.2,86.3,89.2,86.3l-8.9,0
l-10.2,30.5C70.2,116.9,74,116.9,78.6,116.8z M75.3,68.7L89,26.2h9.8l0.8,34l23.6-34h9.9l-13.6,42.5h-7.1l12.5-35.4l-24.5,35.4h-6.8
l-0.8-35L82,68.7H75.3z"/>
</svg>
<!-- Original image Copyright Dave Gandy — CC BY 4.0 License -->

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@ -3,11 +3,11 @@
"crane": { "crane": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1661875961, "lastModified": 1670900067,
"narHash": "sha256-f1h/2c6Teeu1ofAHWzrS8TwBPcnN+EEu+z1sRVmMQTk=", "narHash": "sha256-VXVa+KBfukhmWizaiGiHRVX/fuk66P8dgSFfkVN4/MY=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "d9f394e4e20e97c2a60c3ad82c2b6ef99be19e24", "rev": "59b31b41a589c0a65e4a1f86b0e5eac68081468b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -45,6 +45,7 @@
"nci", "nci",
"devshell" "devshell"
], ],
"flake-parts": "flake-parts",
"flake-utils-pre-commit": [ "flake-utils-pre-commit": [
"nci" "nci"
], ],
@ -57,6 +58,9 @@
"mach-nix": [ "mach-nix": [
"nci" "nci"
], ],
"nix-pypi-fetcher": [
"nci"
],
"nixpkgs": [ "nixpkgs": [
"nci", "nci",
"nixpkgs" "nixpkgs"
@ -69,11 +73,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1668851003, "lastModified": 1671323629,
"narHash": "sha256-X7RCQQynbxStZR2m7HW38r/msMQwVl3afD6UXOCtvx4=", "narHash": "sha256-9KHTPjIDjfnzZ4NjpE3gGIVHVHopy6weRDYO/7Y3hF8=",
"owner": "nix-community", "owner": "nix-community",
"repo": "dream2nix", "repo": "dream2nix",
"rev": "c77e8379d8fe01213ba072e40946cbfb7b58e628", "rev": "2d7d68505c8619410df2c6b6463985f97cbcba6e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -82,6 +86,24 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1668450977,
"narHash": "sha256-cfLhMhnvXn6x1vPm+Jow3RiFAUSCw/l1utktCw5rVA4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "d591857e9d7dd9ddbfba0ea02b43b927c3c0f1fa",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1659877975,
@ -109,11 +131,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1669011203, "lastModified": 1671430291,
"narHash": "sha256-Lymj4HktNEFmVXtwI0Os7srDXHZbZW0Nzw3/+5Hf8ko=", "narHash": "sha256-UIc7H8F3N8rK72J/Vj5YJdV72tvDvYjH+UPsOFvlcsE=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "nix-cargo-integration", "repo": "nix-cargo-integration",
"rev": "c5133b91fc1d549087c91228bd213f2518728a4b", "rev": "b1b0d38b8c3b0d0e6a38638d5bbe10b0bc67522c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -124,11 +146,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1668905981, "lastModified": 1671359686,
"narHash": "sha256-RBQa/+9Uk1eFTqIOXBSBezlEbA3v5OkgP+qptQs1OxY=", "narHash": "sha256-3MpC6yZo+Xn9cPordGz2/ii6IJpP2n8LE8e/ebUXLrs=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "690ffff026b4e635b46f69002c0f4e81c65dfc2e", "rev": "04f574a1c0fde90b51bf68198e2297ca4e7cccf4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -138,6 +160,24 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1665349835,
"narHash": "sha256-UK4urM3iN80UXQ7EaOappDzcisYIuEURFRoGQ/yPkug=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "34c5293a71ffdb2fe054eb5288adc1882c1eb0b1",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"nci": "nci", "nci": "nci",
@ -153,11 +193,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1668998422, "lastModified": 1671416426,
"narHash": "sha256-G/BklIplCHZEeDIabaaxqgITdIXtMolRGlwxn9jG2/Q=", "narHash": "sha256-kpSH1Jrxfk2qd0pRPJn1eQdIOseGv5JuE+YaOrqU9s4=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "68ab029c93f8f8eed4cf3ce9a89a9fd4504b2d6e", "rev": "fbaaff24f375ac25ec64268b0a0d63f91e474b7d",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -17,7 +17,7 @@ integration = []
[dependencies] [dependencies]
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
ropey = { version = "1.5", default-features = false, features = ["simd"] } ropey = { version = "1.5.1-alpha", default-features = false, features = ["simd"] }
smallvec = "1.10" smallvec = "1.10"
smartstring = "1.0.1" smartstring = "1.0.1"
unicode-segmentation = "1.10" unicode-segmentation = "1.10"
@ -38,7 +38,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
toml = "0.5" toml = "0.5"
similar = "2.2" imara-diff = "0.1.0"
encoding_rs = "0.8" encoding_rs = "0.8"

@ -45,4 +45,5 @@ pub struct Diagnostic {
pub code: Option<NumberOrString>, pub code: Option<NumberOrString>,
pub tags: Vec<DiagnosticTag>, pub tags: Vec<DiagnosticTag>,
pub source: Option<String>, pub source: Option<String>,
pub data: Option<serde_json::Value>,
} }

@ -1,58 +1,194 @@
use crate::{Rope, Transaction}; use std::ops::Range;
use std::time::Instant;
/// Compares `old` and `new` to generate a [`Transaction`] describing use imara_diff::intern::InternedInput;
/// the steps required to get from `old` to `new`. use imara_diff::Algorithm;
pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction { use ropey::RopeSlice;
// `similar` only works on contiguous data, so a `Rope` has
// to be temporarily converted into a `String`. use crate::{ChangeSet, Rope, Tendril, Transaction};
let old_converted = old.to_string();
let new_converted = new.to_string(); /// A `imara_diff::Sink` that builds a `ChangeSet` for a character diff of a hunk
struct CharChangeSetBuilder<'a> {
// A timeout is set so after 1 seconds, the algorithm will start res: &'a mut ChangeSet,
// approximating. This is especially important for big `Rope`s or hunk: &'a InternedInput<char>,
// `Rope`s that are extremely dissimilar to each other. pos: u32,
let mut config = similar::TextDiff::configure(); }
config.timeout(std::time::Duration::from_secs(1));
impl imara_diff::Sink for CharChangeSetBuilder<'_> {
let diff = config.diff_chars(&old_converted, &new_converted); type Out = ();
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
// The current position of the change needs to be tracked to self.res.retain((before.start - self.pos) as usize);
// construct the `Change`s. self.res.delete(before.len());
let mut pos = 0; self.pos = before.end;
Transaction::change(
old, let res = self.hunk.after[after.start as usize..after.end as usize]
diff.ops() .iter()
.map(|&token| self.hunk.interner[token])
.collect();
self.res.insert(res);
}
fn finish(self) -> Self::Out {
self.res.retain(self.hunk.before.len() - self.pos as usize);
}
}
struct LineChangeSetBuilder<'a> {
res: ChangeSet,
after: RopeSlice<'a>,
file: &'a InternedInput<RopeSlice<'a>>,
current_hunk: InternedInput<char>,
pos: u32,
}
impl imara_diff::Sink for LineChangeSetBuilder<'_> {
type Out = ChangeSet;
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
let len = self.file.before[self.pos as usize..before.start as usize]
.iter() .iter()
.map(|op| op.as_tag_tuple()) .map(|&it| self.file.interner[it].len_chars())
.filter_map(|(tag, old_range, new_range)| { .sum();
// `old_pos..pos` is equivalent to `start..end` for where self.res.retain(len);
// the change should be applied. self.pos = before.end;
let old_pos = pos;
pos += old_range.end - old_range.start; // do not perform diffs on large hunks
let len_before = before.end - before.start;
match tag { let len_after = after.end - after.start;
// Semantically, inserts and replacements are the same thing.
similar::DiffTag::Insert | similar::DiffTag::Replace => { // Pure insertions/removals do not require a character diff.
// This is the text from the `new` rope that should be // Very large changes are ignored because their character diff is expensive to compute
// inserted into `old`. // TODO adjust heuristic to detect large changes?
let text: &str = { if len_before == 0
let start = new.char_to_byte(new_range.start); || len_after == 0
let end = new.char_to_byte(new_range.end); || len_after > 5 * len_before
&new_converted[start..end] || 5 * len_after < len_before && len_before > 10
}; || len_before + len_after > 200
Some((old_pos, pos, Some(text.into()))) {
let remove = self.file.before[before.start as usize..before.end as usize]
.iter()
.map(|&it| self.file.interner[it].len_chars())
.sum();
self.res.delete(remove);
let mut fragment = Tendril::new();
if len_after > 500 {
// copying a rope line by line is slower then copying the entire
// rope. Use to_string for very large changes instead..
if self.file.after.len() == after.end as usize {
if after.start == 0 {
fragment = self.after.to_string().into();
} else {
let start = self.after.line_to_char(after.start as usize);
fragment = self.after.slice(start..).to_string().into();
} }
similar::DiffTag::Delete => Some((old_pos, pos, None)), } else if after.start == 0 {
similar::DiffTag::Equal => None, let end = self.after.line_to_char(after.end as usize);
fragment = self.after.slice(..end).to_string().into();
} else {
let start = self.after.line_to_char(after.start as usize);
let end = self.after.line_to_char(after.end as usize);
fragment = self.after.slice(start..end).to_string().into();
} }
}), } else {
) for &line in &self.file.after[after.start as usize..after.end as usize] {
for chunk in self.file.interner[line].chunks() {
fragment.push_str(chunk)
}
}
};
self.res.insert(fragment);
} else {
// for reasonably small hunks, generating a ChangeSet from char diff can save memory
// TODO use a tokenizer (word diff?) for improved performance
let hunk_before = self.file.before[before.start as usize..before.end as usize]
.iter()
.flat_map(|&it| self.file.interner[it].chars());
let hunk_after = self.file.after[after.start as usize..after.end as usize]
.iter()
.flat_map(|&it| self.file.interner[it].chars());
self.current_hunk.update_before(hunk_before);
self.current_hunk.update_after(hunk_after);
// the histogram heuristic does not work as well
// for characters because the same characters often reoccur
// use myer diff instead
imara_diff::diff(
Algorithm::Myers,
&self.current_hunk,
CharChangeSetBuilder {
res: &mut self.res,
hunk: &self.current_hunk,
pos: 0,
},
);
self.current_hunk.clear();
}
}
fn finish(mut self) -> Self::Out {
let len = self.file.before[self.pos as usize..]
.iter()
.map(|&it| self.file.interner[it].len_chars())
.sum();
self.res.retain(len);
self.res
}
}
struct RopeLines<'a>(RopeSlice<'a>);
impl<'a> imara_diff::intern::TokenSource for RopeLines<'a> {
type Token = RopeSlice<'a>;
type Tokenizer = ropey::iter::Lines<'a>;
fn tokenize(&self) -> Self::Tokenizer {
self.0.lines()
}
fn estimate_tokens(&self) -> u32 {
// we can provide a perfect estimate which is very nice for performance
self.0.len_lines() as u32
}
}
/// Compares `old` and `new` to generate a [`Transaction`] describing
/// the steps required to get from `old` to `new`.
pub fn compare_ropes(before: &Rope, after: &Rope) -> Transaction {
let start = Instant::now();
let res = ChangeSet::with_capacity(32);
let after = after.slice(..);
let file = InternedInput::new(RopeLines(before.slice(..)), RopeLines(after));
let builder = LineChangeSetBuilder {
res,
file: &file,
after,
pos: 0,
current_hunk: InternedInput::default(),
};
let res = imara_diff::diff(Algorithm::Histogram, &file, builder).into();
log::debug!(
"rope diff took {}s",
Instant::now().duration_since(start).as_secs_f64()
);
res
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
fn test_identity(a: &str, b: &str) {
let mut old = Rope::from(a);
let new = Rope::from(b);
compare_ropes(&old, &new).apply(&mut old);
assert_eq!(old, new);
}
quickcheck::quickcheck! { quickcheck::quickcheck! {
fn test_compare_ropes(a: String, b: String) -> bool { fn test_compare_ropes(a: String, b: String) -> bool {
let mut old = Rope::from(a); let mut old = Rope::from(a);
@ -61,4 +197,25 @@ mod tests {
old == new old == new
} }
} }
#[test]
fn equal_files() {
test_identity("foo", "foo");
}
#[test]
fn trailing_newline() {
test_identity("foo\n", "foo");
test_identity("foo", "foo\n");
}
#[test]
fn new_file() {
test_identity("", "foo");
}
#[test]
fn deleted_file() {
test_identity("foo", "");
}
} }

@ -122,32 +122,16 @@ impl History {
/// Returns the changes since the given revision composed into a transaction. /// Returns the changes since the given revision composed into a transaction.
/// Returns None if there are no changes between the current and given revisions. /// Returns None if there are no changes between the current and given revisions.
pub fn changes_since(&self, revision: usize) -> Option<Transaction> { pub fn changes_since(&self, revision: usize) -> Option<Transaction> {
use std::cmp::Ordering::*; let lca = self.lowest_common_ancestor(revision, self.current);
let up = self.path_up(revision, lca);
let down = self.path_up(self.current, lca);
let up_txns = up
.iter()
.rev()
.map(|&n| self.revisions[n].inversion.clone());
let down_txns = down.iter().map(|&n| self.revisions[n].transaction.clone());
match revision.cmp(&self.current) { down_txns.chain(up_txns).reduce(|acc, tx| tx.compose(acc))
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.

@ -69,7 +69,7 @@ pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::Path
top_marker = Some(ancestor); top_marker = Some(ancestor);
} }
if ancestor.join(".git").is_dir() { if ancestor.join(".git").exists() {
// Top marker is repo root if not root marker was detected yet // Top marker is repo root if not root marker was detected yet
if top_marker.is_none() { if top_marker.is_none() {
top_marker = Some(ancestor); top_marker = Some(ancestor);
@ -83,7 +83,7 @@ pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::Path
top_marker.map_or(current_dir, |a| a.to_path_buf()) top_marker.map_or(current_dir, |a| a.to_path_buf())
} }
pub use ropey::{str_utils, Rope, RopeBuilder, RopeSlice}; pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
// pub use tendril::StrTendril as Tendril; // pub use tendril::StrTendril as Tendril;
pub use smartstring::SmartString; pub use smartstring::SmartString;

@ -495,28 +495,53 @@ impl Selection {
/// Normalizes a `Selection`. /// Normalizes a `Selection`.
fn normalize(mut self) -> Self { fn normalize(mut self) -> Self {
let primary = self.ranges[self.primary_index]; let mut primary = self.ranges[self.primary_index];
self.ranges.sort_unstable_by_key(Range::from); self.ranges.sort_unstable_by_key(Range::from);
self.ranges.dedup_by(|curr_range, prev_range| {
if prev_range.overlaps(curr_range) {
let new_range = curr_range.merge(*prev_range);
if prev_range == &primary || curr_range == &primary {
primary = new_range;
}
*prev_range = new_range;
true
} else {
false
}
});
self.primary_index = self self.primary_index = self
.ranges .ranges
.iter() .iter()
.position(|&range| range == primary) .position(|&range| range == primary)
.unwrap(); .unwrap();
let mut prev_i = 0; self
for i in 1..self.ranges.len() { }
if self.ranges[prev_i].overlaps(&self.ranges[i]) {
self.ranges[prev_i] = self.ranges[prev_i].merge(self.ranges[i]); // Merges all ranges that are consecutive
pub fn merge_consecutive_ranges(mut self) -> Self {
let mut primary = self.ranges[self.primary_index];
self.ranges.dedup_by(|curr_range, prev_range| {
if prev_range.to() == curr_range.from() {
let new_range = curr_range.merge(*prev_range);
if prev_range == &primary || curr_range == &primary {
primary = new_range;
}
*prev_range = new_range;
true
} else { } else {
prev_i += 1; false
self.ranges[prev_i] = self.ranges[i];
} }
if i == self.primary_index { });
self.primary_index = prev_i;
}
}
self.ranges.truncate(prev_i + 1); self.primary_index = self
.ranges
.iter()
.position(|&range| range == primary)
.unwrap();
self self
} }
@ -1132,6 +1157,52 @@ mod test {
&["", "abcd", "efg", "rs", "xyz"] &["", "abcd", "efg", "rs", "xyz"]
); );
} }
#[test]
fn test_merge_consecutive_ranges() {
let selection = Selection::new(
smallvec![
Range::new(0, 1),
Range::new(1, 10),
Range::new(15, 20),
Range::new(25, 26),
Range::new(26, 30)
],
4,
);
let result = selection.merge_consecutive_ranges();
assert_eq!(
result.ranges(),
&[Range::new(0, 10), Range::new(15, 20), Range::new(25, 30)]
);
assert_eq!(result.primary_index, 2);
let selection = Selection::new(smallvec![Range::new(0, 1)], 0);
let result = selection.merge_consecutive_ranges();
assert_eq!(result.ranges(), &[Range::new(0, 1)]);
assert_eq!(result.primary_index, 0);
let selection = Selection::new(
smallvec![
Range::new(0, 1),
Range::new(1, 5),
Range::new(5, 8),
Range::new(8, 10),
Range::new(10, 15),
Range::new(18, 25)
],
3,
);
let result = selection.merge_consecutive_ranges();
assert_eq!(result.ranges(), &[Range::new(0, 15), Range::new(18, 25)]);
assert_eq!(result.primary_index, 0);
}
#[test] #[test]
fn test_selection_contains() { fn test_selection_contains() {
fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool { fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool {

@ -207,6 +207,8 @@ pub struct LanguageServerConfiguration {
#[serde(default)] #[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>, pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
#[serde(default = "default_timeout")] #[serde(default = "default_timeout")]
pub timeout: u64, pub timeout: u64,
pub language_id: Option<String>, pub language_id: Option<String>,

@ -56,7 +56,7 @@ impl ChangeSet {
} }
// Changeset builder operations: delete/insert/retain // Changeset builder operations: delete/insert/retain
fn delete(&mut self, n: usize) { pub(crate) fn delete(&mut self, n: usize) {
use Operation::*; use Operation::*;
if n == 0 { if n == 0 {
return; return;
@ -71,7 +71,7 @@ impl ChangeSet {
} }
} }
fn insert(&mut self, fragment: Tendril) { pub(crate) fn insert(&mut self, fragment: Tendril) {
use Operation::*; use Operation::*;
if fragment.is_empty() { if fragment.is_empty() {
@ -93,7 +93,7 @@ impl ChangeSet {
self.changes.push(new_last); self.changes.push(new_last);
} }
fn retain(&mut self, n: usize) { pub(crate) fn retain(&mut self, n: usize) {
use Operation::*; use Operation::*;
if n == 0 { if n == 0 {
return; return;

@ -1,6 +1,26 @@
use std::borrow::Cow;
use std::process::Command;
const VERSION: &str = include_str!("../VERSION");
fn main() { fn main() {
let git_hash = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok());
let version: Cow<_> = match git_hash {
Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
None => VERSION.into(),
};
println!( println!(
"cargo:rustc-env=BUILD_TARGET={}", "cargo:rustc-env=BUILD_TARGET={}",
std::env::var("TARGET").unwrap() std::env::var("TARGET").unwrap()
); );
println!("cargo:rerun-if-changed=../VERSION");
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
} }

@ -263,7 +263,7 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<FetchStatus> {
))?; ))?;
// create the grammar dir contains a git directory // create the grammar dir contains a git directory
if !grammar_dir.join(".git").is_dir() { if !grammar_dir.join(".git").exists() {
git(&grammar_dir, ["init"])?; git(&grammar_dir, ["init"])?;
} }

@ -4,6 +4,8 @@ pub mod grammar;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::PathBuf; use std::path::PathBuf;
pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
pub static RUNTIME_DIR: once_cell::sync::Lazy<PathBuf> = once_cell::sync::Lazy::new(runtime_dir); pub static RUNTIME_DIR: once_cell::sync::Lazy<PathBuf> = once_cell::sync::Lazy::new(runtime_dir);
static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new(); static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
@ -95,7 +97,7 @@ pub fn find_local_config_dirs() -> Vec<PathBuf> {
let mut directories = Vec::new(); let mut directories = Vec::new();
for ancestor in current_dir.ancestors() { for ancestor in current_dir.ancestors() {
if ancestor.join(".git").is_dir() { if ancestor.join(".git").exists() {
directories.push(ancestor.to_path_buf()); directories.push(ancestor.to_path_buf());
// Don't go higher than repo if we're in one // Don't go higher than repo if we're in one
break; break;

@ -13,6 +13,7 @@ homepage = "https://helix-editor.com"
[dependencies] [dependencies]
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" }
anyhow = "1.0" anyhow = "1.0"
futures-executor = "0.3" futures-executor = "0.3"
@ -22,6 +23,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.22", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio = { version = "1.23", 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"

@ -5,6 +5,7 @@ use crate::{
}; };
use helix_core::{find_root, ChangeSet, Rope}; use helix_core::{find_root, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp_types as lsp; use lsp_types as lsp;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
@ -41,10 +42,12 @@ pub struct Client {
impl Client { impl Client {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
pub fn start( pub fn start(
cmd: &str, cmd: &str,
args: &[String], args: &[String],
config: Option<Value>, config: Option<Value>,
server_environment: HashMap<String, String>,
root_markers: &[String], root_markers: &[String],
id: usize, id: usize,
req_timeout: u64, req_timeout: u64,
@ -54,6 +57,7 @@ impl Client {
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?; let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
let process = Command::new(cmd) let process = Command::new(cmd)
.envs(server_environment)
.args(args) .args(args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
@ -376,7 +380,10 @@ impl Client {
..Default::default() ..Default::default()
}, },
trace: None, trace: None,
client_info: None, client_info: Some(lsp::ClientInfo {
name: String::from("helix"),
version: Some(String::from(VERSION_AND_GIT_HASH)),
}),
locale: None, // TODO locale: None, // TODO
}; };

@ -60,7 +60,7 @@ pub enum OffsetEncoding {
pub mod util { pub mod util {
use super::*; use super::*;
use helix_core::{diagnostic::NumberOrString, Range, Rope, Transaction}; use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
/// Converts a diagnostic in the document to [`lsp::Diagnostic`]. /// Converts a diagnostic in the document to [`lsp::Diagnostic`].
/// ///
@ -105,16 +105,17 @@ pub mod util {
None None
}; };
// TODO: add support for Diagnostic.data lsp::Diagnostic {
lsp::Diagnostic::new( range: range_to_lsp_range(doc, range, offset_encoding),
range_to_lsp_range(doc, range, offset_encoding),
severity, severity,
code, code,
diag.source.clone(), source: diag.source.clone(),
diag.message.to_owned(), message: diag.message.to_owned(),
None, related_information: None,
tags, tags,
) data: diag.data.to_owned(),
..Default::default()
}
} }
/// Converts [`lsp::Position`] to a position in the document. /// Converts [`lsp::Position`] to a position in the document.
@ -198,6 +199,42 @@ pub mod util {
Some(Range::new(start, end)) Some(Range::new(start, end))
} }
/// Creates a [Transaction] from the [lsp::TextEdit] in a completion response.
/// The transaction applies the edit to all cursors.
pub fn generate_transaction_from_completion_edit(
doc: &Rope,
selection: &Selection,
edit: lsp::TextEdit,
offset_encoding: OffsetEncoding,
) -> Transaction {
let replacement: Option<Tendril> = if edit.new_text.is_empty() {
None
} else {
Some(edit.new_text.into())
};
let text = doc.slice(..);
let primary_cursor = selection.primary().cursor(text);
let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
Some(start) => start as i128 - primary_cursor as i128,
None => return Transaction::new(doc),
};
let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
Some(end) => end as i128 - primary_cursor as i128,
None => return Transaction::new(doc),
};
Transaction::change_by_selection(doc, selection, |range| {
let cursor = range.cursor(text);
(
(cursor as i128 + start_offset) as usize,
(cursor as i128 + end_offset) as usize,
replacement.clone(),
)
})
}
pub fn generate_transaction_from_edits( pub fn generate_transaction_from_edits(
doc: &Rope, doc: &Rope,
mut edits: Vec<lsp::TextEdit>, mut edits: Vec<lsp::TextEdit>,
@ -516,6 +553,7 @@ fn start_client(
&ls_config.command, &ls_config.command,
&ls_config.args, &ls_config.args,
config.config.clone(), config.config.clone(),
ls_config.environment.clone(),
&config.roots, &config.roots,
id, id,
ls_config.timeout, ls_config.timeout,

@ -17,8 +17,10 @@ build = true
app = true app = true
[features] [features]
default = ["git"]
unicode-lines = ["helix-core/unicode-lines"] unicode-lines = ["helix-core/unicode-lines"]
integration = [] integration = []
git = ["helix-vcs/git"]
[[bin]] [[bin]]
name = "hx" name = "hx"
@ -29,6 +31,7 @@ helix-core = { version = "0.6", path = "../helix-core" }
helix-view = { version = "0.6", path = "../helix-view" } helix-view = { version = "0.6", path = "../helix-view" }
helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" } helix-dap = { version = "0.6", path = "../helix-dap" }
helix-vcs = { version = "0.6", path = "../helix-vcs" }
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
anyhow = "1" anyhow = "1"
@ -75,5 +78,5 @@ helix-loader = { version = "0.6", path = "../helix-loader" }
[dev-dependencies] [dev-dependencies]
smallvec = "1.10" smallvec = "1.10"
indoc = "1.0.6" indoc = "1.0.8"
tempfile = "3.3.0" tempfile = "3.3.0"

@ -1,30 +1,9 @@
use helix_loader::grammar::{build_grammars, fetch_grammars}; use helix_loader::grammar::{build_grammars, fetch_grammars};
use std::borrow::Cow;
use std::process::Command;
const VERSION: &str = include_str!("../VERSION");
fn main() { fn main() {
let git_hash = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok());
let version: Cow<_> = match git_hash {
Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
None => VERSION.into(),
};
if std::env::var("HELIX_DISABLE_AUTO_GRAMMAR_BUILD").is_err() { if std::env::var("HELIX_DISABLE_AUTO_GRAMMAR_BUILD").is_err() {
fetch_grammars().expect("Failed to fetch tree-sitter grammars"); fetch_grammars().expect("Failed to fetch tree-sitter grammars");
build_grammars(Some(std::env::var("TARGET").unwrap())) build_grammars(Some(std::env::var("TARGET").unwrap()))
.expect("Failed to compile tree-sitter grammars"); .expect("Failed to compile tree-sitter grammars");
} }
println!("cargo:rerun-if-changed=../runtime/grammars/");
println!("cargo:rerun-if-changed=../VERSION");
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
} }

@ -239,7 +239,11 @@ impl Application {
doc.set_selection(view_id, pos); doc.set_selection(view_id, pos);
} }
} }
editor.set_status(format!("Loaded {} files.", nr_of_files)); editor.set_status(format!(
"Loaded {} file{}.",
nr_of_files,
if nr_of_files == 1 { "" } else { "s" } // avoid "Loaded 1 files." grammo
));
// align the view to center after all files are loaded, // align the view to center after all files are loaded,
// does not affect views without pos since it is at the top // does not affect views without pos since it is at the top
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
@ -286,16 +290,27 @@ impl Application {
} }
#[cfg(feature = "integration")] #[cfg(feature = "integration")]
fn render(&mut self) {} async fn render(&mut self) {}
#[cfg(not(feature = "integration"))] #[cfg(not(feature = "integration"))]
fn render(&mut self) { async fn render(&mut self) {
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,
}; };
// Acquire mutable access to the redraw_handle lock
// to ensure that there are no tasks running that want to block rendering
drop(cx.editor.redraw_handle.1.write().await);
cx.editor.needs_redraw = false;
{
// exhaust any leftover redraw notifications
let notify = cx.editor.redraw_handle.0.notified();
tokio::pin!(notify);
notify.enable();
}
let area = self let area = self
.terminal .terminal
.autoresize() .autoresize()
@ -316,7 +331,7 @@ impl Application {
where where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin, S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{ {
self.render(); self.render().await;
self.last_render = Instant::now(); self.last_render = Instant::now();
loop { loop {
@ -341,18 +356,18 @@ impl Application {
biased; biased;
Some(event) = input_stream.next() => { Some(event) = input_stream.next() => {
self.handle_terminal_events(event); self.handle_terminal_events(event).await;
} }
Some(signal) = self.signals.next() => { Some(signal) = self.signals.next() => {
self.handle_signals(signal).await; self.handle_signals(signal).await;
} }
Some(callback) = self.jobs.futures.next() => { Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render(); self.render().await;
} }
Some(callback) = self.jobs.wait_futures.next() => { Some(callback) = self.jobs.wait_futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render(); self.render().await;
} }
event = self.editor.wait_event() => { event = self.editor.wait_event() => {
let _idle_handled = self.handle_editor_event(event).await; let _idle_handled = self.handle_editor_event(event).await;
@ -457,25 +472,25 @@ impl Application {
self.compositor.resize(area); self.compositor.resize(area);
self.terminal.clear().expect("couldn't clear terminal"); self.terminal.clear().expect("couldn't clear terminal");
self.render(); self.render().await;
} }
signal::SIGUSR1 => { signal::SIGUSR1 => {
self.refresh_config(); self.refresh_config();
self.render(); self.render().await;
} }
_ => unreachable!(), _ => unreachable!(),
} }
} }
pub fn handle_idle_timeout(&mut self) { pub async fn handle_idle_timeout(&mut self) {
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,
}; };
let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx); let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
if should_render { if should_render || self.editor.needs_redraw {
self.render(); self.render().await;
} }
} }
@ -548,11 +563,11 @@ impl Application {
match event { match event {
EditorEvent::DocumentSaved(event) => { EditorEvent::DocumentSaved(event) => {
self.handle_document_write(event); self.handle_document_write(event);
self.render(); self.render().await;
} }
EditorEvent::ConfigEvent(event) => { EditorEvent::ConfigEvent(event) => {
self.handle_config_events(event); self.handle_config_events(event);
self.render(); self.render().await;
} }
EditorEvent::LanguageServerMessage((id, call)) => { EditorEvent::LanguageServerMessage((id, call)) => {
self.handle_language_server_message(call, id).await; self.handle_language_server_message(call, id).await;
@ -560,19 +575,19 @@ impl Application {
let last = self.editor.language_servers.incoming.is_empty(); let last = self.editor.language_servers.incoming.is_empty();
if last || self.last_render.elapsed() > LSP_DEADLINE { if last || self.last_render.elapsed() > LSP_DEADLINE {
self.render(); self.render().await;
self.last_render = Instant::now(); self.last_render = Instant::now();
} }
} }
EditorEvent::DebuggerEvent(payload) => { EditorEvent::DebuggerEvent(payload) => {
let needs_render = self.editor.handle_debugger_message(payload).await; let needs_render = self.editor.handle_debugger_message(payload).await;
if needs_render { if needs_render {
self.render(); self.render().await;
} }
} }
EditorEvent::IdleTimer => { EditorEvent::IdleTimer => {
self.editor.clear_idle_timer(); self.editor.clear_idle_timer();
self.handle_idle_timeout(); self.handle_idle_timeout().await;
#[cfg(feature = "integration")] #[cfg(feature = "integration")]
{ {
@ -584,7 +599,10 @@ impl Application {
false false
} }
pub fn handle_terminal_events(&mut self, event: Result<CrosstermEvent, crossterm::ErrorKind>) { pub async fn handle_terminal_events(
&mut self,
event: Result<CrosstermEvent, crossterm::ErrorKind>,
) {
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,
@ -608,7 +626,7 @@ impl Application {
}; };
if should_redraw && !self.editor.should_close() { if should_redraw && !self.editor.should_close() {
self.render(); self.render().await;
} }
} }
@ -770,7 +788,8 @@ impl Application {
severity, severity,
code, code,
tags, tags,
source: diagnostic.source.clone() source: diagnostic.source.clone(),
data: diagnostic.data.clone(),
}) })
}) })
.collect(); .collect();

@ -3,6 +3,7 @@ pub(crate) mod lsp;
pub(crate) mod typed; pub(crate) mod typed;
pub use dap::*; pub use dap::*;
use helix_vcs::Hunk;
pub use lsp::*; pub use lsp::*;
use tui::text::Spans; use tui::text::Spans;
pub use typed::*; pub use typed::*;
@ -243,6 +244,7 @@ impl MappableCommand {
select_regex, "Select all regex matches inside selections", select_regex, "Select all regex matches inside selections",
split_selection, "Split selections on regex matches", split_selection, "Split selections on regex matches",
split_selection_on_newline, "Split selection on newlines", split_selection_on_newline, "Split selection on newlines",
merge_consecutive_selections, "Merge consecutive selections",
search, "Search for regex pattern", search, "Search for regex pattern",
rsearch, "Reverse search for regex pattern", rsearch, "Reverse search for regex pattern",
search_next, "Select next search match", search_next, "Select next search match",
@ -309,6 +311,10 @@ impl MappableCommand {
goto_last_diag, "Goto last diagnostic", goto_last_diag, "Goto last diagnostic",
goto_next_diag, "Goto next diagnostic", goto_next_diag, "Goto next diagnostic",
goto_prev_diag, "Goto previous diagnostic", goto_prev_diag, "Goto previous diagnostic",
goto_next_change, "Goto next change",
goto_prev_change, "Goto previous change",
goto_first_change, "Goto first change",
goto_last_change, "Goto last change",
goto_line_start, "Goto line start", goto_line_start, "Goto line start",
goto_line_end, "Goto line end", goto_line_end, "Goto line end",
goto_next_buffer, "Goto next buffer", goto_next_buffer, "Goto next buffer",
@ -403,8 +409,8 @@ impl MappableCommand {
select_textobject_inner, "Select inside object", select_textobject_inner, "Select inside object",
goto_next_function, "Goto next function", goto_next_function, "Goto next function",
goto_prev_function, "Goto previous function", goto_prev_function, "Goto previous function",
goto_next_class, "Goto next class", goto_next_class, "Goto next type definition",
goto_prev_class, "Goto previous class", goto_prev_class, "Goto previous type definition",
goto_next_parameter, "Goto next parameter", goto_next_parameter, "Goto next parameter",
goto_prev_parameter, "Goto previous parameter", goto_prev_parameter, "Goto previous parameter",
goto_next_comment, "Goto next comment", goto_next_comment, "Goto next comment",
@ -1588,6 +1594,12 @@ fn split_selection_on_newline(cx: &mut Context) {
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
} }
fn merge_consecutive_selections(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id).clone().merge_consecutive_ranges();
doc.set_selection(view.id, selection);
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn search_impl( fn search_impl(
editor: &mut Editor, editor: &mut Editor,
@ -1671,12 +1683,7 @@ fn search_impl(
}; };
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
// TODO: is_cursor_in_view does the same calculation as ensure_cursor_in_view view.ensure_cursor_in_view_center(doc, scrolloff);
if view.is_cursor_in_view(doc, 0) {
view.ensure_cursor_in_view(doc, scrolloff);
} else {
align_view(doc, view, Align::Center)
}
}; };
} }
@ -2387,8 +2394,8 @@ fn buffer_picker(cx: &mut Context) {
let picker = FilePicker::new( let picker = FilePicker::new(
cx.editor cx.editor
.documents .documents
.iter() .values()
.map(|(_, doc)| new_meta(doc)) .map(|doc| new_meta(doc))
.collect(), .collect(),
(), (),
|cx, meta, action| { |cx, meta, action| {
@ -2475,8 +2482,10 @@ fn jumplist_picker(cx: &mut Context) {
(), (),
|cx, meta, action| { |cx, meta, action| {
cx.editor.switch(meta.id, action); cx.editor.switch(meta.id, action);
let config = cx.editor.config();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, meta.selection.clone()); doc.set_selection(view.id, meta.selection.clone());
view.ensure_cursor_in_view_center(doc, config.scrolloff);
}, },
|editor, meta| { |editor, meta| {
let doc = &editor.documents.get(&meta.id)?; let doc = &editor.documents.get(&meta.id)?;
@ -2605,7 +2614,7 @@ async fn make_format_callback(
if let Ok(format) = format { if let Ok(format) = format {
if doc.version() == doc_version { if doc.version() == doc_version {
apply_transaction(&format, doc, view); apply_transaction(&format, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
doc.detect_indent_and_line_ending(); doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff); view.ensure_cursor_in_view(doc, scrolloff);
} else { } else {
@ -2711,62 +2720,7 @@ fn open_above(cx: &mut Context) {
} }
fn normal_mode(cx: &mut Context) { fn normal_mode(cx: &mut Context) {
if cx.editor.mode == Mode::Normal { cx.editor.enter_normal_mode();
return;
}
cx.editor.mode = Mode::Normal;
let (view, doc) = current!(cx.editor);
try_restore_indent(doc, view);
// if leaving append mode, move cursor back by 1
if doc.restore_cursor {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
Range::new(
range.from(),
graphemes::prev_grapheme_boundary(text, range.to()),
)
});
doc.set_selection(view.id, selection);
doc.restore_cursor = false;
}
}
fn try_restore_indent(doc: &mut Document, view: &mut View) {
use helix_core::chars::char_is_whitespace;
use helix_core::Operation;
fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool {
if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
changes
{
move_pos + inserted_str.len() == pos
&& inserted_str.starts_with('\n')
&& inserted_str.chars().skip(1).all(char_is_whitespace)
&& pos == line_end_pos // ensure no characters exists after current position
} else {
false
}
}
let doc_changes = doc.changes().changes();
let text = doc.text().slice(..);
let range = doc.selection(view.id).primary();
let pos = range.cursor(text);
let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
// Removes tailing whitespaces.
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let line_start_pos = text.line_to_char(range.cursor_line(text));
(line_start_pos, pos, None)
});
apply_transaction(&transaction, doc, view);
}
} }
// Store a jump on the jumplist. // Store a jump on the jumplist.
@ -2892,26 +2846,27 @@ fn goto_pos(editor: &mut Editor, pos: usize) {
} }
fn goto_first_diag(cx: &mut Context) { fn goto_first_diag(cx: &mut Context) {
let doc = doc!(cx.editor); let (view, doc) = current!(cx.editor);
let pos = match doc.diagnostics().first() { let selection = match doc.diagnostics().first() {
Some(diag) => diag.range.start, Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return, None => return,
}; };
goto_pos(cx.editor, pos); doc.set_selection(view.id, selection);
align_view(doc, view, Align::Center);
} }
fn goto_last_diag(cx: &mut Context) { fn goto_last_diag(cx: &mut Context) {
let doc = doc!(cx.editor); let (view, doc) = current!(cx.editor);
let pos = match doc.diagnostics().last() { let selection = match doc.diagnostics().last() {
Some(diag) => diag.range.start, Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return, None => return,
}; };
goto_pos(cx.editor, pos); doc.set_selection(view.id, selection);
align_view(doc, view, Align::Center);
} }
fn goto_next_diag(cx: &mut Context) { fn goto_next_diag(cx: &mut Context) {
let editor = &mut cx.editor; let (view, doc) = current!(cx.editor);
let (view, doc) = current!(editor);
let cursor_pos = doc let cursor_pos = doc
.selection(view.id) .selection(view.id)
@ -2924,17 +2879,16 @@ fn goto_next_diag(cx: &mut Context) {
.find(|diag| diag.range.start > cursor_pos) .find(|diag| diag.range.start > cursor_pos)
.or_else(|| doc.diagnostics().first()); .or_else(|| doc.diagnostics().first());
let pos = match diag { let selection = match diag {
Some(diag) => diag.range.start, Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return, None => return,
}; };
doc.set_selection(view.id, selection);
goto_pos(editor, pos); align_view(doc, view, Align::Center);
} }
fn goto_prev_diag(cx: &mut Context) { fn goto_prev_diag(cx: &mut Context) {
let editor = &mut cx.editor; let (view, doc) = current!(cx.editor);
let (view, doc) = current!(editor);
let cursor_pos = doc let cursor_pos = doc
.selection(view.id) .selection(view.id)
@ -2948,12 +2902,108 @@ fn goto_prev_diag(cx: &mut Context) {
.find(|diag| diag.range.start < cursor_pos) .find(|diag| diag.range.start < cursor_pos)
.or_else(|| doc.diagnostics().last()); .or_else(|| doc.diagnostics().last());
let pos = match diag { let selection = match diag {
Some(diag) => diag.range.start, // NOTE: the selection is reversed because we're jumping to the
// previous diagnostic.
Some(diag) => Selection::single(diag.range.end, diag.range.start),
None => return, None => return,
}; };
doc.set_selection(view.id, selection);
align_view(doc, view, Align::Center);
}
fn goto_first_change(cx: &mut Context) {
goto_first_change_impl(cx, false);
}
fn goto_last_change(cx: &mut Context) {
goto_first_change_impl(cx, true);
}
fn goto_first_change_impl(cx: &mut Context, reverse: bool) {
let editor = &mut cx.editor;
let (_, doc) = current!(editor);
if let Some(handle) = doc.diff_handle() {
let hunk = {
let hunks = handle.hunks();
let idx = if reverse {
hunks.len().saturating_sub(1)
} else {
0
};
hunks.nth_hunk(idx)
};
if hunk != Hunk::NONE {
let pos = doc.text().line_to_char(hunk.after.start as usize);
goto_pos(editor, pos)
}
}
}
goto_pos(editor, pos); fn goto_next_change(cx: &mut Context) {
goto_next_change_impl(cx, Direction::Forward)
}
fn goto_prev_change(cx: &mut Context) {
goto_next_change_impl(cx, Direction::Backward)
}
fn goto_next_change_impl(cx: &mut Context, direction: Direction) {
let count = cx.count() as u32 - 1;
let motion = move |editor: &mut Editor| {
let (view, doc) = current!(editor);
let doc_text = doc.text().slice(..);
let diff_handle = if let Some(diff_handle) = doc.diff_handle() {
diff_handle
} else {
editor.set_status("Diff is not available in current buffer");
return;
};
let selection = doc.selection(view.id).clone().transform(|range| {
let cursor_line = range.cursor_line(doc_text) as u32;
let hunks = diff_handle.hunks();
let hunk_idx = match direction {
Direction::Forward => hunks
.next_hunk(cursor_line)
.map(|idx| (idx + count).min(hunks.len() - 1)),
Direction::Backward => hunks
.prev_hunk(cursor_line)
.map(|idx| idx.saturating_sub(count)),
};
// TODO refactor with let..else once MSRV reaches 1.65
let hunk_idx = if let Some(hunk_idx) = hunk_idx {
hunk_idx
} else {
return range;
};
let hunk = hunks.nth_hunk(hunk_idx);
let hunk_start = doc_text.line_to_char(hunk.after.start as usize);
let hunk_end = if hunk.after.is_empty() {
hunk_start + 1
} else {
doc_text.line_to_char(hunk.after.end as usize)
};
let new_range = Range::new(hunk_start, hunk_end);
if editor.mode == Mode::Select {
let head = if new_range.head < range.anchor {
new_range.anchor
} else {
new_range.head
};
Range::new(range.anchor, head)
} else {
new_range.with_direction(direction)
}
});
doc.set_selection(view.id, selection)
};
motion(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(motion)));
} }
pub mod insert { pub mod insert {
@ -2994,6 +3044,11 @@ pub mod insert {
} }
fn language_server_completion(cx: &mut Context, ch: char) { fn language_server_completion(cx: &mut Context, ch: char) {
let config = cx.editor.config();
if !config.auto_completion {
return;
}
use helix_lsp::lsp; use helix_lsp::lsp;
// if ch matches completion char, trigger completion // if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor); let doc = doc_mut!(cx.editor);
@ -3362,7 +3417,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.id) { if !doc.undo(view) {
cx.editor.set_status("Already at oldest change"); cx.editor.set_status("Already at oldest change");
break; break;
} }
@ -3373,7 +3428,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.id) { if !doc.redo(view) {
cx.editor.set_status("Already at newest change"); cx.editor.set_status("Already at newest change");
break; break;
} }
@ -3385,7 +3440,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.id, UndoKind::Steps(1)) { if !doc.earlier(view, UndoKind::Steps(1)) {
cx.editor.set_status("Already at oldest change"); cx.editor.set_status("Already at oldest change");
break; break;
} }
@ -3397,7 +3452,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.id, UndoKind::Steps(1)) { if !doc.later(view, UndoKind::Steps(1)) {
cx.editor.set_status("Already at newest change"); cx.editor.set_status("Already at newest change");
break; break;
} }
@ -3406,7 +3461,7 @@ fn later(cx: &mut Context) {
fn commit_undo_checkpoint(cx: &mut Context) { fn commit_undo_checkpoint(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
} }
// Yank / Paste // Yank / Paste
@ -3718,7 +3773,7 @@ fn replace_selections_with_clipboard_impl(
}); });
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
} }
Err(e) => return Err(e.context("Couldn't get system clipboard contents")), Err(e) => return Err(e.context("Couldn't get system clipboard contents")),
} }
@ -4246,6 +4301,7 @@ fn match_brackets(cx: &mut Context) {
fn jump_forward(cx: &mut Context) { fn jump_forward(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let config = cx.editor.config();
let view = view_mut!(cx.editor); let view = view_mut!(cx.editor);
let doc_id = view.doc; let doc_id = view.doc;
@ -4259,12 +4315,13 @@ fn jump_forward(cx: &mut Context) {
} }
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
align_view(doc, view, Align::Center); view.ensure_cursor_in_view_center(doc, config.scrolloff);
}; };
} }
fn jump_backward(cx: &mut Context) { fn jump_backward(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let config = cx.editor.config();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let doc_id = doc.id(); let doc_id = doc.id();
@ -4278,7 +4335,7 @@ fn jump_backward(cx: &mut Context) {
} }
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
align_view(doc, view, Align::Center); view.ensure_cursor_in_view_center(doc, config.scrolloff);
}; };
} }
@ -4557,19 +4614,41 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
) )
}; };
if ch == 'g' && doc.diff_handle().is_none() {
editor.set_status("Diff is not available in current buffer");
return;
}
let textobject_change = |range: Range| -> Range {
let diff_handle = doc.diff_handle().unwrap();
let hunks = diff_handle.hunks();
let line = range.cursor_line(text);
let hunk_idx = if let Some(hunk_idx) = hunks.hunk_at(line as u32, false) {
hunk_idx
} else {
return range;
};
let hunk = hunks.nth_hunk(hunk_idx).after;
let start = text.line_to_char(hunk.start as usize);
let end = text.line_to_char(hunk.end as usize);
Range::new(start, end).with_direction(range.direction())
};
let selection = doc.selection(view.id).clone().transform(|range| { let selection = doc.selection(view.id).clone().transform(|range| {
match ch { match ch {
'w' => textobject::textobject_word(text, range, objtype, count, false), 'w' => textobject::textobject_word(text, range, objtype, count, false),
'W' => textobject::textobject_word(text, range, objtype, count, true), 'W' => textobject::textobject_word(text, range, objtype, count, true),
'c' => textobject_treesitter("class", range), 't' => textobject_treesitter("class", range),
'f' => textobject_treesitter("function", range), 'f' => textobject_treesitter("function", range),
'a' => textobject_treesitter("parameter", range), 'a' => textobject_treesitter("parameter", range),
'o' => textobject_treesitter("comment", range), 'c' => textobject_treesitter("comment", range),
't' => textobject_treesitter("test", range), 'T' => textobject_treesitter("test", range),
'p' => textobject::textobject_paragraph(text, range, objtype, count), 'p' => textobject::textobject_paragraph(text, range, objtype, count),
'm' => textobject::textobject_pair_surround_closest( 'm' => textobject::textobject_pair_surround_closest(
text, range, objtype, count, text, range, objtype, count,
), ),
'g' => textobject_change(range),
// TODO: cancel new ranges if inconsistent surround matches across lines // TODO: cancel new ranges if inconsistent surround matches across lines
ch if !ch.is_ascii_alphanumeric() => { ch if !ch.is_ascii_alphanumeric() => {
textobject::textobject_pair_surround(text, range, objtype, ch, count) textobject::textobject_pair_surround(text, range, objtype, ch, count)
@ -4593,11 +4672,11 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
("w", "Word"), ("w", "Word"),
("W", "WORD"), ("W", "WORD"),
("p", "Paragraph"), ("p", "Paragraph"),
("c", "Class (tree-sitter)"), ("t", "Type definition (tree-sitter)"),
("f", "Function (tree-sitter)"), ("f", "Function (tree-sitter)"),
("a", "Argument/parameter (tree-sitter)"), ("a", "Argument/parameter (tree-sitter)"),
("o", "Comment (tree-sitter)"), ("c", "Comment (tree-sitter)"),
("t", "Test (tree-sitter)"), ("T", "Test (tree-sitter)"),
("m", "Closest surrounding pair to cursor"), ("m", "Closest surrounding pair to cursor"),
(" ", "... or any character acting as a pair"), (" ", "... or any character acting as a pair"),
]; ];
@ -4925,7 +5004,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
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())); .with_selection(Selection::new(ranges, selection.primary_index()));
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
} }
// after replace cursor may be out of bounds, do this to // after replace cursor may be out of bounds, do this to

@ -1,3 +1,4 @@
use futures_util::FutureExt;
use helix_lsp::{ use helix_lsp::{
block_on, block_on,
lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString}, lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString},
@ -14,7 +15,8 @@ use helix_view::{apply_transaction, document::Mode, editor::Action, theme::Style
use crate::{ use crate::{
compositor::{self, Compositor}, compositor::{self, Compositor},
ui::{ ui::{
self, lsp::SignatureHelp, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent, self, lsp::SignatureHelp, overlay::overlayed, DynamicPicker, FileLocation, FilePicker,
Popup, PromptEvent,
}, },
}; };
@ -384,10 +386,43 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
cx.callback( cx.callback(
future, future,
move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| { move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
if let Some(symbols) = response { let symbols = response.unwrap_or_default();
let picker = sym_picker(symbols, current_url, offset_encoding); let picker = sym_picker(symbols, current_url, offset_encoding);
compositor.push(Box::new(overlayed(picker))) let get_symbols = |query: String, editor: &mut Editor| {
} let doc = doc!(editor);
let language_server = match doc.language_server() {
Some(s) => s,
None => {
// This should not generally happen since the picker will not
// even open in the first place if there is no server.
return async move { Err(anyhow::anyhow!("LSP not active")) }.boxed();
}
};
let symbol_request = match language_server.workspace_symbols(query) {
Some(future) => future,
None => {
// This should also not happen since the language server must have
// supported workspace symbols before to reach this block.
return async move {
Err(anyhow::anyhow!(
"Language server does not support workspace symbols"
))
}
.boxed();
}
};
let future = async move {
let json = symbol_request.await?;
let response: Option<Vec<lsp::SymbolInformation>> =
serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
};
future.boxed()
};
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
compositor.push(Box::new(overlayed(dyn_picker)))
}, },
) )
} }
@ -733,7 +768,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
if ignore_if_exists && to.exists() { if ignore_if_exists && to.exists() {
Ok(()) Ok(())
} else { } else {
fs::rename(&from, &to) fs::rename(from, &to)
} }
} }
} }
@ -787,8 +822,9 @@ pub fn apply_workspace_edit(
text_edits, text_edits,
offset_encoding, offset_encoding,
); );
apply_transaction(&transaction, doc, view_mut!(editor, view_id)); let view = view_mut!(editor, view_id);
doc.append_changes_to_history(view_id); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view);
}; };
if let Some(ref changes) = workspace_edit.changes { if let Some(ref changes) = workspace_edit.changes {

@ -65,12 +65,28 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
ensure!(!args.is_empty(), "wrong argument count"); ensure!(!args.is_empty(), "wrong argument count");
for arg in args { for arg in args {
let (path, pos) = args::parse_file(arg); let (path, pos) = args::parse_file(arg);
let _ = cx.editor.open(&path, Action::Replace)?; // If the path is a directory, open a file picker on that directory and update the status
let (view, doc) = current!(cx.editor); // message
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true)); if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
doc.set_selection(view.id, pos); let callback = async move {
// does not affect opening a buffer without pos let call: job::Callback = job::Callback::EditorCompositor(Box::new(
align_view(doc, view, Align::Center); move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path, &editor.config());
compositor.push(Box::new(overlayed(picker)));
},
));
Ok(call)
};
cx.jobs.callback(callback);
} else {
// Otherwise, just open the file
let _ = cx.editor.open(&path, Action::Replace)?;
let (view, doc) = current!(cx.editor);
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
doc.set_selection(view.id, pos);
// does not affect opening a buffer without pos
align_view(doc, view, Align::Center);
}
} }
Ok(()) Ok(())
} }
@ -489,7 +505,7 @@ fn set_line_ending(
}), }),
); );
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
Ok(()) Ok(())
} }
@ -506,7 +522,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.id, uk); let success = doc.earlier(view, uk);
if !success { if !success {
cx.editor.set_status("Already at oldest change"); cx.editor.set_status("Already at oldest change");
} }
@ -525,7 +541,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.id, uk); let success = doc.later(view, uk);
if !success { if !success {
cx.editor.set_status("Already at newest change"); cx.editor.set_status("Already at newest change");
} }
@ -802,7 +818,7 @@ fn theme(
.editor .editor
.theme_loader .theme_loader
.load(theme_name) .load(theme_name)
.with_context(|| "Theme does not exist")?; .map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?;
if !(true_color || theme.is_16_color()) { if !(true_color || theme.is_16_color()) {
bail!("Unsupported theme: theme requires true color support"); bail!("Unsupported theme: theme requires true color support");
} }
@ -934,7 +950,7 @@ fn replace_selections_with_clipboard_impl(
}); });
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
Ok(()) Ok(())
} }
Err(e) => Err(e.context("Couldn't get system clipboard contents")), Err(e) => Err(e.context("Couldn't get system clipboard contents")),
@ -1053,10 +1069,12 @@ fn reload(
} }
let scrolloff = cx.editor.config().scrolloff; let scrolloff = cx.editor.config().scrolloff;
let redraw_handle = cx.editor.redraw_handle.clone();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.reload(view).map(|_| { doc.reload(view, &cx.editor.diff_providers, redraw_handle)
view.ensure_cursor_in_view(doc, scrolloff); .map(|_| {
}) view.ensure_cursor_in_view(doc, scrolloff);
})
} }
fn reload_all( fn reload_all(
@ -1091,7 +1109,12 @@ fn reload_all(
// Every doc is guaranteed to have at least 1 view at this point. // Every doc is guaranteed to have at least 1 view at this point.
let view = view_mut!(cx.editor, view_ids[0]); let view = view_mut!(cx.editor, view_ids[0]);
doc.reload(view)?;
// Ensure that the view is synced with the document's history.
view.sync_changes(doc);
let redraw_handle = cx.editor.redraw_handle.clone();
doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
for view_id in view_ids { for view_id in view_ids {
let view = view_mut!(cx.editor, view_id); let view = view_mut!(cx.editor, view_id);
@ -1598,7 +1621,7 @@ fn sort_impl(
); );
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
Ok(()) Ok(())
} }
@ -1642,7 +1665,7 @@ fn reflow(
}); });
apply_transaction(&transaction, doc, view); apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff); view.ensure_cursor_in_view(doc, scrolloff);
Ok(()) Ok(())
@ -1759,13 +1782,30 @@ fn insert_output(
Ok(()) Ok(())
} }
fn pipe_to(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
pipe_impl(cx, args, event, &ShellBehavior::Ignore)
}
fn pipe(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> { fn pipe(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
pipe_impl(cx, args, event, &ShellBehavior::Replace)
}
fn pipe_impl(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
behavior: &ShellBehavior,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
return Ok(()); return Ok(());
} }
ensure!(!args.is_empty(), "Shell command required"); ensure!(!args.is_empty(), "Shell command required");
shell(cx, &args.join(" "), &ShellBehavior::Replace); shell(cx, &args.join(" "), behavior);
Ok(()) Ok(())
} }
@ -2317,6 +2357,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: pipe, fun: pipe,
completer: None, completer: None,
}, },
TypableCommand {
name: "pipe-to",
aliases: &[],
doc: "Pipe each selection to the shell command, ignoring output.",
fun: pipe_to,
completer: None,
},
TypableCommand { TypableCommand {
name: "run-shell-command", name: "run-shell-command",
aliases: &["sh"], aliases: &["sh"],

@ -283,7 +283,7 @@ fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::R
if let Some(cmd) = server_cmd { if let Some(cmd) = server_cmd {
let path = match which::which(&cmd) { let path = match which::which(&cmd) {
Ok(path) => path.display().to_string().green(), Ok(path) => path.display().to_string().green(),
Err(_) => "Not found in $PATH".to_string().red(), Err(_) => format!("'{}' not found in $PATH", cmd).red(),
}; };
writeln!(stdout, "Binary for {}: {}", protocol_name, path)?; writeln!(stdout, "Binary for {}: {}", protocol_name, path)?;
} }

@ -390,18 +390,18 @@ impl Keymaps {
self.state.push(key); self.state.push(key);
match trie.search(&self.state[1..]) { match trie.search(&self.state[1..]) {
Some(&KeyTrie::Node(ref map)) => { Some(KeyTrie::Node(map)) => {
if map.is_sticky { if map.is_sticky {
self.state.clear(); self.state.clear();
self.sticky = Some(map.clone()); self.sticky = Some(map.clone());
} }
KeymapResult::Pending(map.clone()) KeymapResult::Pending(map.clone())
} }
Some(&KeyTrie::Leaf(ref cmd)) => { Some(KeyTrie::Leaf(cmd)) => {
self.state.clear(); self.state.clear();
KeymapResult::Matched(cmd.clone()) KeymapResult::Matched(cmd.clone())
} }
Some(&KeyTrie::Sequence(ref cmds)) => { Some(KeyTrie::Sequence(cmds)) => {
self.state.clear(); self.state.clear();
KeymapResult::MatchedSequence(cmds.clone()) KeymapResult::MatchedSequence(cmds.clone())
} }

@ -78,6 +78,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"s" => select_regex, "s" => select_regex,
"A-s" => split_selection_on_newline, "A-s" => split_selection_on_newline,
"A-_" => merge_consecutive_selections,
"S" => split_selection, "S" => split_selection,
";" => collapse_selection, ";" => collapse_selection,
"A-;" => flip_selections, "A-;" => flip_selections,
@ -102,22 +103,26 @@ pub fn default() -> HashMap<Mode, Keymap> {
"[" => { "Left bracket" "[" => { "Left bracket"
"d" => goto_prev_diag, "d" => goto_prev_diag,
"D" => goto_first_diag, "D" => goto_first_diag,
"g" => goto_prev_change,
"G" => goto_first_change,
"f" => goto_prev_function, "f" => goto_prev_function,
"c" => goto_prev_class, "t" => goto_prev_class,
"a" => goto_prev_parameter, "a" => goto_prev_parameter,
"o" => goto_prev_comment, "c" => goto_prev_comment,
"t" => goto_prev_test, "T" => goto_prev_test,
"p" => goto_prev_paragraph, "p" => goto_prev_paragraph,
"space" => add_newline_above, "space" => add_newline_above,
}, },
"]" => { "Right bracket" "]" => { "Right bracket"
"d" => goto_next_diag, "d" => goto_next_diag,
"D" => goto_last_diag, "D" => goto_last_diag,
"g" => goto_next_change,
"G" => goto_last_change,
"f" => goto_next_function, "f" => goto_next_function,
"c" => goto_next_class, "t" => goto_next_class,
"a" => goto_next_parameter, "a" => goto_next_parameter,
"o" => goto_next_comment, "c" => goto_next_comment,
"t" => goto_next_test, "T" => goto_next_test,
"p" => goto_next_paragraph, "p" => goto_next_paragraph,
"space" => add_newline_below, "space" => add_newline_below,
}, },
@ -200,7 +205,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
// z family for save/restore/combine from/to sels from register // z family for save/restore/combine from/to sels from register
"tab" => jump_forward, // tab == <C-i> "C-i" | "tab" => jump_forward, // tab == <C-i>
"C-o" => jump_backward, "C-o" => jump_backward,
"C-s" => save_selection, "C-s" => save_selection,

@ -1,5 +1,6 @@
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use crossterm::event::EventStream; use crossterm::event::EventStream;
use helix_loader::VERSION_AND_GIT_HASH;
use helix_term::application::Application; use helix_term::application::Application;
use helix_term::args::Args; use helix_term::args::Args;
use helix_term::config::Config; use helix_term::config::Config;
@ -75,7 +76,7 @@ FLAGS:
--show-explorer Opens the explorer on startup --show-explorer Opens the explorer on startup
", ",
env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"),
env!("VERSION_AND_GIT_HASH"), VERSION_AND_GIT_HASH,
env!("CARGO_PKG_AUTHORS"), env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"), env!("CARGO_PKG_DESCRIPTION"),
logpath.display(), logpath.display(),
@ -90,7 +91,7 @@ FLAGS:
} }
if args.display_version { if args.display_version {
println!("helix {}", env!("VERSION_AND_GIT_HASH")); println!("helix {}", VERSION_AND_GIT_HASH);
std::process::exit(0); std::process::exit(0);
} }

@ -1,5 +1,5 @@
use crate::compositor::{Component, Context, Event, EventResult}; use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::{apply_transaction, editor::CompleteAction}; use helix_view::{apply_transaction, editor::CompleteAction, ViewId};
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use tui::text::Spans; use tui::text::Spans;
@ -107,6 +107,7 @@ impl Completion {
let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| { let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction( fn item_to_transaction(
doc: &Document, doc: &Document,
view_id: ViewId,
item: &CompletionItem, item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding, offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize, start_offset: usize,
@ -121,9 +122,10 @@ impl Completion {
} }
}; };
util::generate_transaction_from_edits( util::generate_transaction_from_completion_edit(
doc.text(), doc.text(),
vec![edit], doc.selection(view_id),
edit,
offset_encoding, // TODO: should probably transcode in Client offset_encoding, // TODO: should probably transcode in Client
) )
} else { } else {
@ -132,10 +134,23 @@ impl Completion {
// in these cases we need to check for a common prefix and remove it // in these cases we need to check for a common prefix and remove it
let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset)); let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
let text = text.trim_start_matches::<&str>(&prefix); let text = text.trim_start_matches::<&str>(&prefix);
Transaction::change(
doc.text(), // TODO: this needs to be true for the numbers to work out correctly
vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(), // in the closure below. It's passed in to a callback as this same
) // formula, but can the value change between the LSP request and
// response? If it does, can we recover?
debug_assert!(
doc.selection(view_id)
.primary()
.cursor(doc.text().slice(..))
== trigger_offset
);
Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
let cursor = range.cursor(doc.text().slice(..));
(cursor, cursor, Some(text.into()))
})
}; };
transaction transaction
@ -164,6 +179,7 @@ impl Completion {
let transaction = item_to_transaction( let transaction = item_to_transaction(
doc, doc,
view.id,
item, item,
offset_encoding, offset_encoding,
start_offset, start_offset,
@ -185,6 +201,7 @@ impl Completion {
let transaction = item_to_transaction( let transaction = item_to_transaction(
doc, doc,
view.id,
item, item,
offset_encoding, offset_encoding,
start_offset, start_offset,
@ -394,7 +411,7 @@ impl Component for Completion {
"```{}\n{}\n```\n{}", "```{}\n{}\n```\n{}",
language, language,
option.detail.as_deref().unwrap_or_default(), option.detail.as_deref().unwrap_or_default(),
contents.clone() contents
), ),
cx.editor.syn_loader.clone(), cx.editor.syn_loader.clone(),
) )
@ -404,15 +421,14 @@ impl Component for Completion {
value: contents, value: contents,
})) => { })) => {
// TODO: set language based on doc scope // TODO: set language based on doc scope
Markdown::new( if let Some(detail) = &option.detail.as_deref() {
format!( Markdown::new(
"```{}\n{}\n```\n{}", format!("```{}\n{}\n```\n{}", language, detail, contents),
language, cx.editor.syn_loader.clone(),
option.detail.as_deref().unwrap_or_default(), )
contents.clone() } else {
), Markdown::new(contents.to_string(), cx.editor.syn_loader.clone())
cx.editor.syn_loader.clone(), }
)
} }
None if option.detail.is_some() => { None if option.detail.is_some() => {
// TODO: copied from above // TODO: copied from above

@ -539,8 +539,8 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes}; use helix_core::graphemes::{grapheme_width, RopeGraphemes};
for grapheme in RopeGraphemes::new(text) { for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = offset.col > (visual_x as usize) let out_of_bounds = offset.col > visual_x
|| (visual_x as usize) >= viewport.width as usize + offset.col; || visual_x >= viewport.width as usize + offset.col;
if LineEnding::from_rope_slice(&grapheme).is_some() { if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds { if !out_of_bounds {
@ -570,7 +570,7 @@ impl EditorView {
let (display_grapheme, width) = if grapheme == "\t" { let (display_grapheme, width) = if grapheme == "\t" {
is_whitespace = true; is_whitespace = true;
// make sure we display tab as appropriate amount of spaces // make sure we display tab as appropriate amount of spaces
let visual_tab_width = tab_width - (visual_x as usize % tab_width); let visual_tab_width = tab_width - (visual_x % tab_width);
let grapheme_tab_width = let grapheme_tab_width =
helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width); helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width);
@ -589,7 +589,7 @@ impl EditorView {
(grapheme.as_ref(), width) (grapheme.as_ref(), width)
}; };
let cut_off_start = offset.col.saturating_sub(visual_x as usize); let cut_off_start = offset.col.saturating_sub(visual_x);
if !out_of_bounds { if !out_of_bounds {
// if we're offscreen just keep going until we hit a new line // if we're offscreen just keep going until we hit a new line
@ -606,7 +606,7 @@ impl EditorView {
} else if cut_off_start != 0 && cut_off_start < width { } else if cut_off_start != 0 && cut_off_start < width {
// partially on screen // partially on screen
let rect = Rect::new( let rect = Rect::new(
viewport.x as u16, viewport.x,
viewport.y + line, viewport.y + line,
(width - cut_off_start) as u16, (width - cut_off_start) as u16,
1, 1,
@ -753,7 +753,7 @@ impl EditorView {
let mut text = String::with_capacity(8); let mut text = String::with_capacity(8);
for gutter_type in view.gutters() { for gutter_type in view.gutters() {
let gutter = gutter_type.style(editor, doc, view, theme, is_focused); let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
let width = gutter_type.width(view, doc); let width = gutter_type.width(view, doc);
text.reserve(width); // ensure there's enough space for the gutter 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() {
@ -1183,6 +1183,7 @@ impl EditorView {
} }
editor.focus(view_id); editor.focus(view_id);
editor.ensure_cursor_in_view(view_id);
return EventResult::Consumed(None); return EventResult::Consumed(None);
} }
@ -1219,7 +1220,8 @@ impl EditorView {
let primary = selection.primary_mut(); let primary = selection.primary_mut();
*primary = primary.put_cursor(doc.text().slice(..), pos, true); *primary = primary.put_cursor(doc.text().slice(..), pos, true);
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
let view_id = view.id;
cxt.editor.ensure_cursor_in_view(view_id);
EventResult::Consumed(None) EventResult::Consumed(None)
} }
@ -1241,6 +1243,7 @@ impl EditorView {
commands::scroll(cxt, offset, direction); commands::scroll(cxt, offset, direction);
cxt.editor.tree.focus = current_view; cxt.editor.tree.focus = current_view;
cxt.editor.ensure_cursor_in_view(current_view);
EventResult::Consumed(None) EventResult::Consumed(None)
} }
@ -1352,7 +1355,7 @@ impl Component for EditorView {
// Store a history state if not in insert mode. Otherwise wait till we exit insert // Store a history state if not in insert mode. Otherwise wait till we exit insert
// to include any edits to the paste in the history state. // to include any edits to the paste in the history state.
if mode != Mode::Insert { if mode != Mode::Insert {
doc.append_changes_to_history(view.id); doc.append_changes_to_history(view);
} }
EventResult::Consumed(None) EventResult::Consumed(None)
@ -1370,9 +1373,7 @@ 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, doc) = current!(cx.editor); let (view, _) = 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() {
@ -1449,31 +1450,13 @@ 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);
}
// 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)

@ -220,10 +220,8 @@ impl TreeItem for FileInfo {
return match self.file_type { return match self.file_type {
FileType::Dir => { FileType::Dir => {
if self.expanded { if self.expanded {
//Some(("", &helix_view::theme::Color::Yellow))
Some(("", &helix_view::theme::Color::Yellow)) Some(("", &helix_view::theme::Color::Yellow))
} else { } else {
// Some(("", &helix_view::theme::Color::Yellow))
Some(("", &helix_view::theme::Color::Yellow)) Some(("", &helix_view::theme::Color::Yellow))
} }
} }

@ -22,7 +22,7 @@ pub use editor::EditorView;
pub use explore::Explorer; pub use explore::Explorer;
pub use markdown::Markdown; pub use markdown::Markdown;
pub use menu::Menu; pub use menu::Menu;
pub use picker::{FileLocation, FilePicker, Picker}; pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
pub use popup::Popup; pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent}; pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner}; pub use spinner::{ProgressSpinners, Spinner};
@ -211,13 +211,14 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
// Cap the number of files if we aren't in a git project, preventing // Cap the number of files if we aren't in a git project, preventing
// hangs when using the picker in your home directory // hangs when using the picker in your home directory
let files: Vec<_> = if root.join(".git").is_dir() { let mut files: Vec<PathBuf> = if root.join(".git").exists() {
files.collect() files.collect()
} else { } else {
// const MAX: usize = 8192; // const MAX: usize = 8192;
const MAX: usize = 100_000; const MAX: usize = 100_000;
files.take(MAX).collect() files.take(MAX).collect()
}; };
files.sort();
log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
@ -258,8 +259,8 @@ pub mod completers {
pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> { pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
let mut names: Vec<_> = editor let mut names: Vec<_> = editor
.documents .documents
.iter() .values()
.map(|(_id, doc)| { .map(|doc| {
let name = doc let name = doc
.relative_path() .relative_path()
.map(|p| p.display().to_string()) .map(|p| p.display().to_string())

@ -69,4 +69,8 @@ impl<T: Component + 'static> Component for Overlay<T> {
let dimensions = (self.calc_child_size)(area); let dimensions = (self.calc_child_size)(area);
self.content.cursor(dimensions, ctx) self.content.cursor(dimensions, ctx)
} }
fn id(&self) -> Option<&'static str> {
self.content.id()
}
} }

@ -3,6 +3,7 @@ use crate::{
ctrl, key, shift, ctrl, key, shift,
ui::{self, fuzzy_match::FuzzyQuery, EditorView}, ui::{self, fuzzy_match::FuzzyQuery, EditorView},
}; };
use futures_util::future::BoxFuture;
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
widgets::{Block, BorderType, Borders}, widgets::{Block, BorderType, Borders},
@ -11,7 +12,10 @@ 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::{cmp::Ordering, time::Instant}; use std::{
cmp::{self, Ordering},
time::Instant,
};
use std::{collections::HashMap, io::Read, path::PathBuf}; use std::{collections::HashMap, io::Read, path::PathBuf};
use crate::ui::{Prompt, PromptEvent}; use crate::ui::{Prompt, PromptEvent};
@ -22,7 +26,7 @@ use helix_view::{
Document, DocumentId, Editor, Document, DocumentId, Editor,
}; };
use super::menu::Item; use super::{menu::Item, overlay::Overlay};
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes /// Biggest file size to preview in bytes
@ -343,11 +347,17 @@ impl<T: Item + 'static> Component for FilePicker<T> {
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
struct PickerMatch { struct PickerMatch {
index: usize,
score: i64, score: i64,
index: usize,
len: usize, len: usize,
} }
impl PickerMatch {
fn key(&self) -> impl Ord {
(cmp::Reverse(self.score), self.len, self.index)
}
}
impl PartialOrd for PickerMatch { impl PartialOrd for PickerMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
@ -356,10 +366,7 @@ impl PartialOrd for PickerMatch {
impl Ord for PickerMatch { impl Ord for PickerMatch {
fn cmp(&self, other: &Self) -> Ordering { fn cmp(&self, other: &Self) -> Ordering {
self.score self.key().cmp(&other.key())
.cmp(&other.score)
.reverse()
.then_with(|| self.len.cmp(&other.len))
} }
} }
@ -469,34 +476,42 @@ impl<T: Item> Picker<T> {
self.matches.sort_unstable(); self.matches.sort_unstable();
} else { } else {
let query = FuzzyQuery::new(pattern); self.force_score();
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
let text = option.filter_text(&self.editor_data);
query
.fuzzy_match(&text, &self.matcher)
.map(|score| PickerMatch {
index,
score,
len: text.chars().count(),
})
}),
);
self.matches.sort_unstable();
} }
log::debug!("picker score {:?}", Instant::now().duration_since(now)); log::debug!("picker score {:?}", Instant::now().duration_since(now));
// reset cursor position // reset cursor position
self.cursor = 0; self.cursor = 0;
let pattern = self.prompt.line();
self.previous_pattern.clone_from(pattern); self.previous_pattern.clone_from(pattern);
} }
pub fn force_score(&mut self) {
let pattern = self.prompt.line();
let query = FuzzyQuery::new(pattern);
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
let text = option.filter_text(&self.editor_data);
query
.fuzzy_match(&text, &self.matcher)
.map(|score| PickerMatch {
index,
score,
len: text.chars().count(),
})
}),
);
self.matches.sort_unstable();
}
/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
pub fn move_by(&mut self, amount: usize, direction: Direction) { pub fn move_by(&mut self, amount: usize, direction: Direction) {
let len = self.matches.len(); let len = self.matches.len();
@ -744,3 +759,78 @@ impl<T: Item + 'static> Component for Picker<T> {
self.prompt.cursor(area, editor) self.prompt.cursor(area, editor)
} }
} }
/// Returns a new list of options to replace the contents of the picker
/// when called with the current picker query,
pub type DynQueryCallback<T> =
Box<dyn Fn(String, &mut Editor) -> BoxFuture<'static, anyhow::Result<Vec<T>>>>;
/// A picker that updates its contents via a callback whenever the
/// query string changes. Useful for live grep, workspace symbols, etc.
pub struct DynamicPicker<T: ui::menu::Item + Send> {
file_picker: FilePicker<T>,
query_callback: DynQueryCallback<T>,
query: String,
}
impl<T: ui::menu::Item + Send> DynamicPicker<T> {
pub const ID: &'static str = "dynamic-picker";
pub fn new(file_picker: FilePicker<T>, query_callback: DynQueryCallback<T>) -> Self {
Self {
file_picker,
query_callback,
query: String::new(),
}
}
}
impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.file_picker.render(area, surface, cx);
}
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
let event_result = self.file_picker.handle_event(event, cx);
let current_query = self.file_picker.picker.prompt.line();
if !matches!(event, Event::IdleTimeout) || self.query == *current_query {
return event_result;
}
self.query.clone_from(current_query);
let new_options = (self.query_callback)(current_query.to_owned(), cx.editor);
cx.jobs.callback(async move {
let new_options = new_options.await?;
let callback =
crate::job::Callback::EditorCompositor(Box::new(move |editor, compositor| {
// Wrapping of pickers in overlay is done outside the picker code,
// so this is fragile and will break if wrapped in some other widget.
let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) {
Some(overlay) => &mut overlay.content.file_picker.picker,
None => return,
};
picker.options = new_options;
picker.cursor = 0;
picker.force_score();
editor.reset_idle_timer();
}));
anyhow::Ok(callback)
});
EventResult::Consumed(None)
}
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
self.file_picker.cursor(area, ctx)
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
self.file_picker.required_size(viewport)
}
fn id(&self) -> Option<&'static str> {
Some(Self::ID)
}
}

@ -352,6 +352,7 @@ impl Prompt {
let prompt_color = theme.get("ui.text"); let prompt_color = theme.get("ui.text");
let completion_color = theme.get("ui.menu"); let completion_color = theme.get("ui.menu");
let selected_color = theme.get("ui.menu.selected"); let selected_color = theme.get("ui.menu.selected");
let suggestion_color = theme.get("ui.text.inactive");
// completion // completion
let max_len = self let max_len = self
@ -450,21 +451,29 @@ impl Prompt {
// render buffer text // render buffer text
surface.set_string(area.x, area.y + line, &self.prompt, prompt_color); surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
let input: Cow<str> = if self.line.is_empty() { let (input, is_suggestion): (Cow<str>, bool) = if self.line.is_empty() {
// latest value in the register list // latest value in the register list
self.history_register match self
.history_register
.and_then(|reg| cx.editor.registers.last(reg)) .and_then(|reg| cx.editor.registers.last(reg))
.map(|entry| entry.into()) .map(|entry| entry.into())
.unwrap_or_else(|| Cow::from("")) {
Some(value) => (value, true),
None => (Cow::from(""), false),
}
} else { } else {
self.line.as_str().into() (self.line.as_str().into(), false)
}; };
surface.set_string( surface.set_string(
area.x + self.prompt.len() as u16, area.x + self.prompt.len() as u16,
area.y + line, area.y + line,
&input, &input,
prompt_color, if is_suggestion {
suggestion_color
} else {
prompt_color
},
); );
} }
} }

@ -151,5 +151,40 @@ async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> {
// was not updated and after the `kd` step, pointed outside of the document. // 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?; test(("#[|]#", "<C-w>v[<space><C-s><C-w>wkd<C-w>qd", "#[|]#")).await?;
// Transactions are applied to the views for windows lazily when they are focused.
// This case panics if the transactions and inversions are not applied in the
// correct order as we switch between windows.
test((
"#[|]#",
"[<space>[<space>[<space><C-w>vuuu<C-w>wUUU<C-w>quuu",
"#[|]#",
))
.await?;
// See <https://github.com/helix-editor/helix/issues/4957>.
// This sequence undoes part of the history and then adds new changes, creating a
// new branch in the history tree. `View::sync_changes` applies transactions down
// and up to the lowest common ancestor in the path between old and new revision
// numbers. If we apply these up/down transactions in the wrong order, this case
// panics.
// The key sequence:
// * 3[<space> Create three empty lines so we are at the end of the document.
// * <C-w>v<C-s> Create a split and save that point at the end of the document
// in the jumplist.
// * <C-w>w Switch back to the first window.
// * uu Undo twice (not three times which would bring us back to the
// root of the tree).
// * 3[<space> Create three empty lines. Now the end of the document is past
// where it was on step 1.
// * <C-w>q Close window 1, focusing window 2 and causing a sync. This step
// panics if we don't apply in the right order.
// * %d Clean up the buffer.
test((
"#[|]#",
"3[<space><C-w>v<C-s><C-w>wuu3[<space><C-w>q%d",
"#[|]#",
))
.await?;
Ok(()) Ok(())
} }

@ -14,6 +14,10 @@ use std::{
fmt, fmt,
io::{self, Write}, io::{self, Write},
}; };
fn term_program() -> Option<String> {
std::env::var("TERM_PROGRAM").ok()
}
fn vte_version() -> Option<usize> { fn vte_version() -> Option<usize> {
std::env::var("VTE_VERSION").ok()?.parse().ok() std::env::var("VTE_VERSION").ok()?.parse().ok()
} }
@ -35,9 +39,11 @@ impl Capabilities {
Ok(t) => Capabilities { Ok(t) => Capabilities {
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284 // Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
// Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines // Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines
// WezTerm supports underlines but a lot of distros don't properly install it's terminfo
has_extended_underlines: t.extended_cap("Smulx").is_some() has_extended_underlines: t.extended_cap("Smulx").is_some()
|| t.extended_cap("Su").is_some() || t.extended_cap("Su").is_some()
|| vte_version() >= Some(5102), || vte_version() >= Some(5102)
|| matches!(term_program().as_deref(), Some("WezTerm")),
}, },
} }
} }

@ -0,0 +1,28 @@
[package]
name = "helix-vcs"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helix-core = { version = "0.6", path = "../helix-core" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
parking_lot = "0.12"
git-repository = { version = "0.30", default-features = false , optional = true }
imara-diff = "0.1.5"
log = "0.4"
[features]
git = ["git-repository"]
[dev-dependencies]
tempfile = "3.3"

@ -0,0 +1,279 @@
use std::ops::Range;
use std::sync::Arc;
use helix_core::Rope;
use imara_diff::Algorithm;
use parking_lot::{Mutex, MutexGuard};
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
use tokio::task::JoinHandle;
use tokio::time::Instant;
use crate::diff::worker::DiffWorker;
mod line_cache;
mod worker;
type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
/// A rendering lock passed to the differ the prevents redraws from occurring
struct RenderLock {
pub lock: OwnedRwLockReadGuard<()>,
pub timeout: Option<Instant>,
}
struct Event {
text: Rope,
is_base: bool,
render_lock: Option<RenderLock>,
}
#[derive(Clone, Debug)]
pub struct DiffHandle {
channel: UnboundedSender<Event>,
render_lock: Arc<RwLock<()>>,
hunks: Arc<Mutex<Vec<Hunk>>>,
inverted: bool,
}
impl DiffHandle {
pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle {
DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0
}
fn new_with_handle(
diff_base: Rope,
doc: Rope,
redraw_handle: RedrawHandle,
) -> (DiffHandle, JoinHandle<()>) {
let (sender, receiver) = unbounded_channel();
let hunks: Arc<Mutex<Vec<Hunk>>> = Arc::default();
let worker = DiffWorker {
channel: receiver,
hunks: hunks.clone(),
new_hunks: Vec::default(),
redraw_notify: redraw_handle.0,
diff_finished_notify: Arc::default(),
};
let handle = tokio::spawn(worker.run(diff_base, doc));
let differ = DiffHandle {
channel: sender,
hunks,
inverted: false,
render_lock: redraw_handle.1,
};
(differ, handle)
}
pub fn invert(&mut self) {
self.inverted = !self.inverted;
}
pub fn hunks(&self) -> FileHunks {
FileHunks {
hunks: self.hunks.lock(),
inverted: self.inverted,
}
}
/// Updates the document associated with this redraw handle
/// This function is only intended to be called from within the rendering loop
/// if called from elsewhere it may fail to acquire the render lock and panic
pub fn update_document(&self, doc: Rope, block: bool) -> bool {
// unwrap is ok here because the rendering lock is
// only exclusively locked during redraw.
// This function is only intended to be called
// from the core rendering loop where no redraw can happen in parallel
let lock = self.render_lock.clone().try_read_owned().unwrap();
let timeout = if block {
None
} else {
Some(Instant::now() + tokio::time::Duration::from_millis(SYNC_DIFF_TIMEOUT))
};
self.update_document_impl(doc, self.inverted, Some(RenderLock { lock, timeout }))
}
pub fn update_diff_base(&self, diff_base: Rope) -> bool {
self.update_document_impl(diff_base, !self.inverted, None)
}
fn update_document_impl(
&self,
text: Rope,
is_base: bool,
render_lock: Option<RenderLock>,
) -> bool {
let event = Event {
text,
is_base,
render_lock,
};
self.channel.send(event).is_ok()
}
}
/// synchronous debounce value should be low
/// so we can update synchronously most of the time
const DIFF_DEBOUNCE_TIME_SYNC: u64 = 1;
/// maximum time that rendering should be blocked until the diff finishes
const SYNC_DIFF_TIMEOUT: u64 = 12;
const DIFF_DEBOUNCE_TIME_ASYNC: u64 = 96;
const ALGORITHM: Algorithm = Algorithm::Histogram;
const MAX_DIFF_LINES: usize = 64 * u16::MAX as usize;
// cap average line length to 128 for files with MAX_DIFF_LINES
const MAX_DIFF_BYTES: usize = MAX_DIFF_LINES * 128;
/// A single change in a file potentially spanning multiple lines
/// Hunks produced by the differs are always ordered by their position
/// in the file and non-overlapping.
/// Specifically for any two hunks `x` and `y` the following properties hold:
///
/// ``` no_compile
/// assert!(x.before.end <= y.before.start);
/// assert!(x.after.end <= y.after.start);
/// ```
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Hunk {
pub before: Range<u32>,
pub after: Range<u32>,
}
impl Hunk {
/// Can be used instead of `Option::None` for better performance
/// because lines larger then `i32::MAX` are not supported by `imara-diff` anyways.
/// Has some nice properties where it usually is not necessary to check for `None` separately:
/// Empty ranges fail contains checks and also fails smaller then checks.
pub const NONE: Hunk = Hunk {
before: u32::MAX..u32::MAX,
after: u32::MAX..u32::MAX,
};
/// Inverts a change so that `before`
pub fn invert(&self) -> Hunk {
Hunk {
before: self.after.clone(),
after: self.before.clone(),
}
}
pub fn is_pure_insertion(&self) -> bool {
self.before.is_empty()
}
pub fn is_pure_removal(&self) -> bool {
self.after.is_empty()
}
}
/// A list of changes in a file sorted in ascending
/// non-overlapping order
#[derive(Debug)]
pub struct FileHunks<'a> {
hunks: MutexGuard<'a, Vec<Hunk>>,
inverted: bool,
}
impl FileHunks<'_> {
pub fn is_inverted(&self) -> bool {
self.inverted
}
/// Returns the `Hunk` for the `n`th change in this file.
/// if there is no `n`th change `Hunk::NONE` is returned instead.
pub fn nth_hunk(&self, n: u32) -> Hunk {
match self.hunks.get(n as usize) {
Some(hunk) if self.inverted => hunk.invert(),
Some(hunk) => hunk.clone(),
None => Hunk::NONE,
}
}
pub fn len(&self) -> u32 {
self.hunks.len() as u32
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn next_hunk(&self, line: u32) -> Option<u32> {
let hunk_range = if self.inverted {
|hunk: &Hunk| hunk.before.clone()
} else {
|hunk: &Hunk| hunk.after.clone()
};
let res = self
.hunks
.binary_search_by_key(&line, |hunk| hunk_range(hunk).start);
match res {
// Search found a hunk that starts exactly at this line, return the next hunk if it exists.
Ok(pos) if pos + 1 == self.hunks.len() => None,
Ok(pos) => Some(pos as u32 + 1),
// No hunk starts exactly at this line, so the search returns
// the position where a hunk starting at this line should be inserted.
// That position is exactly the position of the next hunk or the end
// of the list if no such hunk exists
Err(pos) if pos == self.hunks.len() => None,
Err(pos) => Some(pos as u32),
}
}
pub fn prev_hunk(&self, line: u32) -> Option<u32> {
let hunk_range = if self.inverted {
|hunk: &Hunk| hunk.before.clone()
} else {
|hunk: &Hunk| hunk.after.clone()
};
let res = self
.hunks
.binary_search_by_key(&line, |hunk| hunk_range(hunk).end);
match res {
// Search found a hunk that ends exactly at this line (so it does not include the current line).
// We can usually just return that hunk, however a special case for empty hunk is necessary
// which represents a pure removal.
// Removals are technically empty but are still shown as single line hunks
// and as such we must jump to the previous hunk (if it exists) if we are already inside the removal
Ok(pos) if !hunk_range(&self.hunks[pos]).is_empty() => Some(pos as u32),
// No hunk ends exactly at this line, so the search returns
// the position where a hunk ending at this line should be inserted.
// That position before this one is exactly the position of the previous hunk
Err(0) | Ok(0) => None,
Err(pos) | Ok(pos) => Some(pos as u32 - 1),
}
}
pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option<u32> {
let hunk_range = if self.inverted {
|hunk: &Hunk| hunk.before.clone()
} else {
|hunk: &Hunk| hunk.after.clone()
};
let res = self
.hunks
.binary_search_by_key(&line, |hunk| hunk_range(hunk).start);
match res {
// Search found a hunk that starts exactly at this line, return it
Ok(pos) => Some(pos as u32),
// No hunk starts exactly at this line, so the search returns
// the position where a hunk starting at this line should be inserted.
// The previous hunk contains this hunk if it exists and doesn't end before this line
Err(0) => None,
Err(pos) => {
let hunk = hunk_range(&self.hunks[pos - 1]);
if hunk.end > line || include_removal && hunk.start == line && hunk.is_empty() {
Some(pos as u32 - 1)
} else {
None
}
}
}
}
}

@ -0,0 +1,130 @@
//! This modules encapsulates a tiny bit of unsafe code that
//! makes diffing significantly faster and more ergonomic to implement.
//! This code is necessary because diffing requires quick random
//! access to the lines of the text that is being diffed.
//!
//! Therefore it is best to collect the `Rope::lines` iterator into a vec
//! first because access to the vec is `O(1)` where `Rope::line` is `O(log N)`.
//! However this process can allocate a (potentially quite large) vector.
//!
//! To avoid reallocation for every diff, the vector is reused.
//! However the RopeSlice references the original rope and therefore forms a self-referential data structure.
//! A transmute is used to change the lifetime of the slice to static to circumvent that project.
use std::mem::transmute;
use helix_core::{Rope, RopeSlice};
use imara_diff::intern::{InternedInput, Interner};
use super::{MAX_DIFF_BYTES, MAX_DIFF_LINES};
/// A cache that stores the `lines` of a rope as a vector.
/// It allows safely reusing the allocation of the vec when updating the rope
pub(crate) struct InternedRopeLines {
diff_base: Rope,
doc: Rope,
num_tokens_diff_base: u32,
interned: InternedInput<RopeSlice<'static>>,
}
impl InternedRopeLines {
pub fn new(diff_base: Rope, doc: Rope) -> InternedRopeLines {
let mut res = InternedRopeLines {
interned: InternedInput {
before: Vec::with_capacity(diff_base.len_lines()),
after: Vec::with_capacity(doc.len_lines()),
interner: Interner::new(diff_base.len_lines() + doc.len_lines()),
},
diff_base,
doc,
// will be populated by update_diff_base_impl
num_tokens_diff_base: 0,
};
res.update_diff_base_impl();
res
}
/// Updates the `diff_base` and optionally the document if `doc` is not None
pub fn update_diff_base(&mut self, diff_base: Rope, doc: Option<Rope>) {
self.interned.clear();
self.diff_base = diff_base;
if let Some(doc) = doc {
self.doc = doc
}
if !self.is_too_large() {
self.update_diff_base_impl();
}
}
/// Updates the `doc` without reinterning the `diff_base`, this function
/// is therefore significantly faster than `update_diff_base` when only the document changes.
pub fn update_doc(&mut self, doc: Rope) {
// Safety: we clear any tokens that were added after
// the interning of `self.diff_base` finished so
// all lines that refer to `self.doc` have been purged.
self.interned
.interner
.erase_tokens_after(self.num_tokens_diff_base.into());
self.doc = doc;
if self.is_too_large() {
self.interned.after.clear();
} else {
self.update_doc_impl();
}
}
fn update_diff_base_impl(&mut self) {
// Safety: This transmute is safe because it only transmutes a lifetime, which has no effect.
// The backing storage for the RopeSlices referred to by the lifetime is stored in `self.diff_base`.
// Therefore as long as `self.diff_base` is not dropped/replaced this memory remains valid.
// `self.diff_base` is only changed in `self.update_diff_base`, which clears the interner.
// When the interned lines are exposed to consumer in `self.diff_input`, the lifetime is bounded to a reference to self.
// That means that on calls to update there exist no references to `self.interned`.
let before = self
.diff_base
.lines()
.map(|line: RopeSlice| -> RopeSlice<'static> { unsafe { transmute(line) } });
self.interned.update_before(before);
self.num_tokens_diff_base = self.interned.interner.num_tokens();
// the has to be interned again because the interner was fully cleared
self.update_doc_impl()
}
fn update_doc_impl(&mut self) {
// Safety: This transmute is save because it only transmutes a lifetime, which has no effect.
// The backing storage for the RopeSlices referred to by the lifetime is stored in `self.doc`.
// Therefore as long as `self.doc` is not dropped/replaced this memory remains valid.
// `self.doc` is only changed in `self.update_doc`, which clears the interner.
// When the interned lines are exposed to consumer in `self.diff_input`, the lifetime is bounded to a reference to self.
// That means that on calls to update there exist no references to `self.interned`.
let after = self
.doc
.lines()
.map(|line: RopeSlice| -> RopeSlice<'static> { unsafe { transmute(line) } });
self.interned.update_after(after);
}
fn is_too_large(&self) -> bool {
// bound both lines and bytes to avoid huge files with few (but huge) lines
// or huge file with tiny lines. While this makes no difference to
// diff itself (the diff performance only depends on the number of tokens)
// the interning runtime depends mostly on filesize and is actually dominant
// for large files
self.doc.len_lines() > MAX_DIFF_LINES
|| self.diff_base.len_lines() > MAX_DIFF_LINES
|| self.doc.len_bytes() > MAX_DIFF_BYTES
|| self.diff_base.len_bytes() > MAX_DIFF_BYTES
}
/// Returns the `InternedInput` for performing the diff.
/// If `diff_base` or `doc` is so large that performing a diff could slow the editor
/// this function returns `None`.
pub fn interned_lines(&self) -> Option<&InternedInput<RopeSlice>> {
if self.is_too_large() {
None
} else {
Some(&self.interned)
}
}
}

@ -0,0 +1,207 @@
use std::mem::swap;
use std::ops::Range;
use std::sync::Arc;
use helix_core::{Rope, RopeSlice};
use imara_diff::intern::InternedInput;
use parking_lot::Mutex;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::Notify;
use tokio::time::{timeout, timeout_at, Duration};
use crate::diff::{
Event, RenderLock, ALGORITHM, DIFF_DEBOUNCE_TIME_ASYNC, DIFF_DEBOUNCE_TIME_SYNC,
};
use super::line_cache::InternedRopeLines;
use super::Hunk;
#[cfg(test)]
mod test;
pub(super) struct DiffWorker {
pub channel: UnboundedReceiver<Event>,
pub hunks: Arc<Mutex<Vec<Hunk>>>,
pub new_hunks: Vec<Hunk>,
pub redraw_notify: Arc<Notify>,
pub diff_finished_notify: Arc<Notify>,
}
impl DiffWorker {
async fn accumulate_events(&mut self, event: Event) -> (Option<Rope>, Option<Rope>) {
let mut accumulator = EventAccumulator::new();
accumulator.handle_event(event).await;
accumulator
.accumulate_debounced_events(
&mut self.channel,
self.redraw_notify.clone(),
self.diff_finished_notify.clone(),
)
.await;
(accumulator.doc, accumulator.diff_base)
}
pub async fn run(mut self, diff_base: Rope, doc: Rope) {
let mut interner = InternedRopeLines::new(diff_base, doc);
if let Some(lines) = interner.interned_lines() {
self.perform_diff(lines);
}
self.apply_hunks();
while let Some(event) = self.channel.recv().await {
let (doc, diff_base) = self.accumulate_events(event).await;
let process_accumulated_events = || {
if let Some(new_base) = diff_base {
interner.update_diff_base(new_base, doc)
} else {
interner.update_doc(doc.unwrap())
}
if let Some(lines) = interner.interned_lines() {
self.perform_diff(lines)
}
};
// Calculating diffs is computationally expensive and should
// not run inside an async function to avoid blocking other futures.
// Note: tokio::task::block_in_place does not work during tests
#[cfg(test)]
process_accumulated_events();
#[cfg(not(test))]
tokio::task::block_in_place(process_accumulated_events);
self.apply_hunks();
}
}
/// update the hunks (used by the gutter) by replacing it with `self.new_hunks`.
/// `self.new_hunks` is always empty after this function runs.
/// To improve performance this function tries to reuse the allocation of the old diff previously stored in `self.line_diffs`
fn apply_hunks(&mut self) {
swap(&mut *self.hunks.lock(), &mut self.new_hunks);
self.diff_finished_notify.notify_waiters();
self.new_hunks.clear();
}
fn perform_diff(&mut self, input: &InternedInput<RopeSlice>) {
imara_diff::diff(ALGORITHM, input, |before: Range<u32>, after: Range<u32>| {
self.new_hunks.push(Hunk { before, after })
})
}
}
struct EventAccumulator {
diff_base: Option<Rope>,
doc: Option<Rope>,
render_lock: Option<RenderLock>,
}
impl<'a> EventAccumulator {
fn new() -> EventAccumulator {
EventAccumulator {
diff_base: None,
doc: None,
render_lock: None,
}
}
async fn handle_event(&mut self, event: Event) {
let dst = if event.is_base {
&mut self.diff_base
} else {
&mut self.doc
};
*dst = Some(event.text);
// always prefer the most synchronous requested render mode
if let Some(render_lock) = event.render_lock {
match &mut self.render_lock {
Some(RenderLock { timeout, .. }) => {
// A timeout of `None` means that the render should
// always wait for the diff to complete (so no timeout)
// remove the existing timeout, otherwise keep the previous timeout
// because it will be shorter then the current timeout
if render_lock.timeout.is_none() {
timeout.take();
}
}
None => self.render_lock = Some(render_lock),
}
}
}
async fn accumulate_debounced_events(
&mut self,
channel: &mut UnboundedReceiver<Event>,
redraw_notify: Arc<Notify>,
diff_finished_notify: Arc<Notify>,
) {
let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
let sync_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_SYNC);
loop {
// if we are not blocking rendering use a much longer timeout
let debounce = if self.render_lock.is_none() {
async_debounce
} else {
sync_debounce
};
if let Ok(Some(event)) = timeout(debounce, channel.recv()).await {
self.handle_event(event).await;
} else {
break;
}
}
// setup task to trigger the rendering
match self.render_lock.take() {
// diff is performed outside of the rendering loop
// request a redraw after the diff is done
None => {
tokio::spawn(async move {
diff_finished_notify.notified().await;
redraw_notify.notify_one();
});
}
// diff is performed inside the rendering loop
// block redraw until the diff is done or the timeout is expired
Some(RenderLock {
lock,
timeout: Some(timeout),
}) => {
tokio::spawn(async move {
let res = {
// Acquire a lock on the redraw handle.
// The lock will block the rendering from occurring while held.
// The rendering waits for the diff if it doesn't time out
timeout_at(timeout, diff_finished_notify.notified()).await
};
// we either reached the timeout or the diff is finished, release the render lock
drop(lock);
if res.is_ok() {
// Diff finished in time we are done.
return;
}
// Diff failed to complete in time log the event
// and wait until the diff occurs to trigger an async redraw
log::info!("Diff computation timed out, update of diffs might appear delayed");
diff_finished_notify.notified().await;
redraw_notify.notify_one();
});
}
// a blocking diff is performed inside the rendering loop
// block redraw until the diff is done
Some(RenderLock {
lock,
timeout: None,
}) => {
tokio::spawn(async move {
diff_finished_notify.notified().await;
// diff is done release the lock
drop(lock)
});
}
};
}
}

@ -0,0 +1,149 @@
use helix_core::Rope;
use tokio::task::JoinHandle;
use crate::diff::{DiffHandle, Hunk};
impl DiffHandle {
fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
DiffHandle::new_with_handle(
Rope::from_str(diff_base),
Rope::from_str(doc),
Default::default(),
)
}
async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
let hunks = self.hunks;
// dropping the channel terminates the task
drop(self.channel);
handle.await.unwrap();
let hunks = hunks.lock();
Vec::clone(&*hunks)
}
}
#[tokio::test]
async fn append_line() {
let (differ, handle) = DiffHandle::new_test("foo\n", "foo\nbar\n");
let line_diffs = differ.into_diff(handle).await;
assert_eq!(
&line_diffs,
&[Hunk {
before: 1..1,
after: 1..2
}]
)
}
#[tokio::test]
async fn prepend_line() {
let (differ, handle) = DiffHandle::new_test("foo\n", "bar\nfoo\n");
let line_diffs = differ.into_diff(handle).await;
assert_eq!(
&line_diffs,
&[Hunk {
before: 0..0,
after: 0..1
}]
)
}
#[tokio::test]
async fn modify() {
let (differ, handle) = DiffHandle::new_test("foo\nbar\n", "foo bar\nbar\n");
let line_diffs = differ.into_diff(handle).await;
assert_eq!(
&line_diffs,
&[Hunk {
before: 0..1,
after: 0..1
}]
)
}
#[tokio::test]
async fn delete_line() {
let (differ, handle) = DiffHandle::new_test("foo\nfoo bar\nbar\n", "foo\nbar\n");
let line_diffs = differ.into_diff(handle).await;
assert_eq!(
&line_diffs,
&[Hunk {
before: 1..2,
after: 1..1
}]
)
}
#[tokio::test]
async fn delete_line_and_modify() {
let (differ, handle) = DiffHandle::new_test("foo\nbar\ntest\nfoo", "foo\ntest\nfoo bar");
let line_diffs = differ.into_diff(handle).await;
assert_eq!(
&line_diffs,
&[
Hunk {
before: 1..2,
after: 1..1
},
Hunk {
before: 3..4,
after: 2..3
},
]
)
}
#[tokio::test]
async fn add_use() {
let (differ, handle) = DiffHandle::new_test(
"use ropey::Rope;\nuse tokio::task::JoinHandle;\n",
"use ropey::Rope;\nuse ropey::RopeSlice;\nuse tokio::task::JoinHandle;\n",
);
let line_diffs = differ.into_diff(handle).await;
assert_eq!(
&line_diffs,
&[Hunk {
before: 1..1,
after: 1..2
},]
)
}
#[tokio::test]
async fn update_document() {
let (differ, handle) = DiffHandle::new_test("foo\nbar\ntest\nfoo", "foo\nbar\ntest\nfoo");
differ.update_document(Rope::from_str("foo\ntest\nfoo bar"), false);
let line_diffs = differ.into_diff(handle).await;
assert_eq!(
&line_diffs,
&[
Hunk {
before: 1..2,
after: 1..1
},
Hunk {
before: 3..4,
after: 2..3
},
]
)
}
#[tokio::test]
async fn update_base() {
let (differ, handle) = DiffHandle::new_test("foo\ntest\nfoo bar", "foo\ntest\nfoo bar");
differ.update_diff_base(Rope::from_str("foo\nbar\ntest\nfoo"));
let line_diffs = differ.into_diff(handle).await;
assert_eq!(
&line_diffs,
&[
Hunk {
before: 1..2,
after: 1..1
},
Hunk {
before: 3..4,
after: 2..3
},
]
)
}

@ -0,0 +1,105 @@
use std::path::Path;
use git::objs::tree::EntryMode;
use git::sec::trust::DefaultForLevel;
use git::{Commit, ObjectId, Repository, ThreadSafeRepository};
use git_repository as git;
use crate::DiffProvider;
#[cfg(test)]
mod test;
pub struct Git;
impl Git {
fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Option<ThreadSafeRepository> {
// custom open options
let mut git_open_opts_map = git::sec::trust::Mapping::<git::open::Options>::default();
// On windows various configuration options are bundled as part of the installations
// This path depends on the install location of git and therefore requires some overhead to lookup
// This is basically only used on windows and has some overhead hence it's disabled on other platforms.
// `gitoxide` doesn't use this as default
let config = git::permissions::Config {
system: true,
git: true,
user: true,
env: true,
includes: true,
git_binary: cfg!(windows),
};
// change options for config permissions without touching anything else
git_open_opts_map.reduced = git_open_opts_map.reduced.permissions(git::Permissions {
config,
..git::Permissions::default_for_level(git::sec::Trust::Reduced)
});
git_open_opts_map.full = git_open_opts_map.full.permissions(git::Permissions {
config,
..git::Permissions::default_for_level(git::sec::Trust::Full)
});
let mut open_options = git::discover::upwards::Options::default();
if let Some(ceiling_dir) = ceiling_dir {
open_options.ceiling_dirs = vec![ceiling_dir.to_owned()];
}
ThreadSafeRepository::discover_with_environment_overrides_opts(
path,
open_options,
git_open_opts_map,
)
.ok()
}
}
impl DiffProvider for Git {
fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
debug_assert!(!file.exists() || file.is_file());
debug_assert!(file.is_absolute());
// TODO cache repository lookup
let repo = Git::open_repo(file.parent()?, None)?.to_thread_local();
let head = repo.head_commit().ok()?;
let file_oid = find_file_in_commit(&repo, &head, file)?;
let file_object = repo.find_object(file_oid).ok()?;
let mut data = file_object.detach().data;
// convert LF to CRLF if configured to avoid showing every line as changed
if repo
.config_snapshot()
.boolean("core.autocrlf")
.unwrap_or(false)
{
let mut normalized_file = Vec::with_capacity(data.len());
let mut at_cr = false;
for &byte in &data {
if byte == b'\n' {
// if this is a LF instead of a CRLF (last byte was not a CR)
// insert a new CR to generate a CRLF
if !at_cr {
normalized_file.push(b'\r');
}
}
at_cr = byte == b'\r';
normalized_file.push(byte)
}
data = normalized_file
}
Some(data)
}
}
/// Finds the object that contains the contents of a file at a specific commit.
fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Option<ObjectId> {
let repo_dir = repo.work_dir()?;
let rel_path = file.strip_prefix(repo_dir).ok()?;
let tree = commit.tree().ok()?;
let tree_entry = tree.lookup_entry_by_path(rel_path).ok()??;
match tree_entry.mode() {
// not a file, everything is new, do not show diff
EntryMode::Tree | EntryMode::Commit | EntryMode::Link => None,
// found a file
EntryMode::Blob | EntryMode::BlobExecutable => Some(tree_entry.object_id()),
}
}

@ -0,0 +1,121 @@
use std::{fs::File, io::Write, path::Path, process::Command};
use tempfile::TempDir;
use crate::{DiffProvider, Git};
fn exec_git_cmd(args: &str, git_dir: &Path) {
let res = Command::new("git")
.arg("-C")
.arg(git_dir) // execute the git command in this directory
.args(args.split_whitespace())
.env_remove("GIT_DIR")
.env_remove("GIT_ASKPASS")
.env_remove("SSH_ASKPASS")
.env("GIT_TERMINAL_PROMPT", "false")
.env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000")
.env("GIT_AUTHOR_EMAIL", "author@example.com")
.env("GIT_AUTHOR_NAME", "author")
.env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000")
.env("GIT_COMMITTER_EMAIL", "committer@example.com")
.env("GIT_COMMITTER_NAME", "committer")
.env("GIT_CONFIG_COUNT", "2")
.env("GIT_CONFIG_KEY_0", "commit.gpgsign")
.env("GIT_CONFIG_VALUE_0", "false")
.env("GIT_CONFIG_KEY_1", "init.defaultBranch")
.env("GIT_CONFIG_VALUE_1", "main")
.output()
.unwrap_or_else(|_| panic!("`git {args}` failed"));
if !res.status.success() {
println!("{}", String::from_utf8_lossy(&res.stdout));
eprintln!("{}", String::from_utf8_lossy(&res.stderr));
panic!("`git {args}` failed (see output above)")
}
}
fn create_commit(repo: &Path, add_modified: bool) {
if add_modified {
exec_git_cmd("add -A", repo);
}
exec_git_cmd("commit -m message", repo);
}
fn empty_git_repo() -> TempDir {
let tmp = tempfile::tempdir().expect("create temp dir for git testing");
exec_git_cmd("init", tmp.path());
exec_git_cmd("config user.email test@helix.org", tmp.path());
exec_git_cmd("config user.name helix-test", tmp.path());
tmp
}
#[test]
fn missing_file() {
let temp_git = empty_git_repo();
let file = temp_git.path().join("file.txt");
File::create(&file).unwrap().write_all(b"foo").unwrap();
assert_eq!(Git.get_diff_base(&file), None);
}
#[test]
fn unmodified_file() {
let temp_git = empty_git_repo();
let file = temp_git.path().join("file.txt");
let contents = b"foo".as_slice();
File::create(&file).unwrap().write_all(contents).unwrap();
create_commit(temp_git.path(), true);
assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
}
#[test]
fn modified_file() {
let temp_git = empty_git_repo();
let file = temp_git.path().join("file.txt");
let contents = b"foo".as_slice();
File::create(&file).unwrap().write_all(contents).unwrap();
create_commit(temp_git.path(), true);
File::create(&file).unwrap().write_all(b"bar").unwrap();
assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
}
/// Test that `get_file_head` does not return content for a directory.
/// This is important to correctly cover cases where a directory is removed and replaced by a file.
/// If the contents of the directory object were returned a diff between a path and the directory children would be produced.
#[test]
fn directory() {
let temp_git = empty_git_repo();
let dir = temp_git.path().join("file.txt");
std::fs::create_dir(&dir).expect("");
let file = dir.join("file.txt");
let contents = b"foo".as_slice();
File::create(&file).unwrap().write_all(contents).unwrap();
create_commit(temp_git.path(), true);
std::fs::remove_dir_all(&dir).unwrap();
File::create(&dir).unwrap().write_all(b"bar").unwrap();
assert_eq!(Git.get_diff_base(&dir), None);
}
/// Test that `get_file_head` does not return content for a symlink.
/// This is important to correctly cover cases where a symlink is removed and replaced by a file.
/// If the contents of the symlink object were returned a diff between a path and the actual file would be produced (bad ui).
#[cfg(any(unix, windows))]
#[test]
fn symlink() {
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(not(unix))]
use std::os::windows::fs::symlink_file as symlink;
let temp_git = empty_git_repo();
let file = temp_git.path().join("file.txt");
let contents = b"foo".as_slice();
File::create(&file).unwrap().write_all(contents).unwrap();
let file_link = temp_git.path().join("file_link.txt");
symlink("file.txt", &file_link).unwrap();
create_commit(temp_git.path(), true);
assert_eq!(Git.get_diff_base(&file_link), None);
assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents)));
}

@ -0,0 +1,51 @@
use std::path::Path;
#[cfg(feature = "git")]
pub use git::Git;
#[cfg(not(feature = "git"))]
pub use Dummy as Git;
#[cfg(feature = "git")]
mod git;
mod diff;
pub use diff::{DiffHandle, Hunk};
pub trait DiffProvider {
/// Returns the data that a diff should be computed against
/// if this provider is used.
/// The data is returned as raw byte without any decoding or encoding performed
/// to ensure all file encodings are handled correctly.
fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>>;
}
#[doc(hidden)]
pub struct Dummy;
impl DiffProvider for Dummy {
fn get_diff_base(&self, _file: &Path) -> Option<Vec<u8>> {
None
}
}
pub struct DiffProviderRegistry {
providers: Vec<Box<dyn DiffProvider>>,
}
impl DiffProviderRegistry {
pub fn get_diff_base(&self, file: &Path) -> Option<Vec<u8>> {
self.providers
.iter()
.find_map(|provider| provider.get_diff_base(file))
}
}
impl Default for DiffProviderRegistry {
fn default() -> Self {
// currently only git is supported
// TODO make this configurable when more providers are added
let git: Box<dyn DiffProvider> = Box::new(Git);
let providers = vec![git];
DiffProviderRegistry { providers }
}
}

@ -21,6 +21,7 @@ helix-loader = { version = "0.6", path = "../helix-loader" }
helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" } helix-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.25", optional = true } crossterm = { version = "0.25", optional = true }
helix-vcs = { version = "0.6", path = "../helix-vcs" }
# Conversion traits # Conversion traits
once_cell = "1.16" once_cell = "1.16"
@ -43,6 +44,7 @@ log = "~0.4"
which = "4.2" which = "4.2"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
clipboard-win = { version = "4.4", features = ["std"] } clipboard-win = { version = "4.4", features = ["std"] }

@ -136,7 +136,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
} else if env_var_is_set("TMUX") && binary_exists("tmux") { } else if env_var_is_set("TMUX") && binary_exists("tmux") {
command_provider! { command_provider! {
paste => "tmux", "save-buffer", "-"; paste => "tmux", "save-buffer", "-";
copy => "tmux", "load-buffer", "-"; copy => "tmux", "load-buffer", "-w", "-";
} }
} else { } else {
Box::new(provider::FallbackProvider::new()) Box::new(provider::FallbackProvider::new())

@ -3,6 +3,8 @@ use futures_util::future::BoxFuture;
use futures_util::FutureExt; use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs; use helix_core::auto_pairs::AutoPairs;
use helix_core::Range; use helix_core::Range;
use helix_vcs::{DiffHandle, DiffProviderRegistry};
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize; use serde::Serialize;
use std::borrow::Cow; use std::borrow::Cow;
@ -24,6 +26,7 @@ use helix_core::{
DEFAULT_LINE_ENDING, DEFAULT_LINE_ENDING,
}; };
use crate::editor::RedrawHandle;
use crate::{apply_transaction, DocumentId, Editor, View, ViewId}; use crate::{apply_transaction, DocumentId, Editor, View, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s. /// 8kB of buffer space for encoding and decoding `Rope`s.
@ -133,6 +136,8 @@ pub struct Document {
diagnostics: Vec<Diagnostic>, diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>, language_server: Option<Arc<helix_lsp::Client>>,
diff_handle: Option<DiffHandle>,
} }
use std::{fmt, mem}; use std::{fmt, mem};
@ -371,6 +376,7 @@ impl Document {
last_saved_revision: 0, last_saved_revision: 0,
modified_since_accessed: false, modified_since_accessed: false,
language_server: None, language_server: None,
diff_handle: None,
} }
} }
@ -639,16 +645,20 @@ impl Document {
} }
/// Reload the document from its path. /// Reload the document from its path.
pub fn reload(&mut self, view: &mut View) -> Result<(), Error> { pub fn reload(
&mut self,
view: &mut View,
provider_registry: &DiffProviderRegistry,
redraw_handle: RedrawHandle,
) -> Result<(), Error> {
let encoding = &self.encoding; let encoding = &self.encoding;
let path = self.path().filter(|path| path.exists()); let path = self
.path()
// If there is no path or the path no longer exists. .filter(|path| path.exists())
if path.is_none() { .ok_or_else(|| anyhow!("can't find file to reload from"))?
bail!("can't find file to reload from"); .to_owned();
}
let mut file = std::fs::File::open(path.unwrap())?; let mut file = std::fs::File::open(&path)?;
let (rope, ..) = from_reader(&mut file, Some(encoding))?; let (rope, ..) = from_reader(&mut file, Some(encoding))?;
// Calculate the difference between the buffer and source text, and apply it. // Calculate the difference between the buffer and source text, and apply it.
@ -656,11 +666,16 @@ impl Document {
// of the encoding. // of the encoding.
let transaction = helix_core::diff::compare_ropes(self.text(), &rope); let transaction = helix_core::diff::compare_ropes(self.text(), &rope);
apply_transaction(&transaction, self, view); apply_transaction(&transaction, self, view);
self.append_changes_to_history(view.id); self.append_changes_to_history(view);
self.reset_modified(); self.reset_modified();
self.detect_indent_and_line_ending(); self.detect_indent_and_line_ending();
match provider_registry.get_diff_base(&path) {
Some(diff_base) => self.set_diff_base(diff_base, redraw_handle),
None => self.diff_handle = None,
}
Ok(()) Ok(())
} }
@ -802,6 +817,10 @@ impl Document {
if !transaction.changes().is_empty() { if !transaction.changes().is_empty() {
self.version += 1; self.version += 1;
// start computing the diff in parallel
if let Some(diff_handle) = &self.diff_handle {
diff_handle.update_document(self.text.clone(), false);
}
// generate revert to savepoint // generate revert to savepoint
if self.savepoint.is_some() { if self.savepoint.is_some() {
@ -873,11 +892,11 @@ impl Document {
success success
} }
fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool { fn undo_redo_impl(&mut self, view: &mut View, 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) self.apply_impl(txn, view.id)
} else { } else {
false false
}; };
@ -886,18 +905,20 @@ impl Document {
if success { if success {
// reset changeset to fix len // reset changeset to fix len
self.changes = ChangeSet::new(self.text()); self.changes = ChangeSet::new(self.text());
// Sync with changes with the jumplist selections.
view.sync_changes(self);
} }
success success
} }
/// 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_id: ViewId) -> bool { pub fn undo(&mut self, view: &mut View) -> bool {
self.undo_redo_impl(view_id, true) self.undo_redo_impl(view, 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_id: ViewId) -> bool { pub fn redo(&mut self, view: &mut View) -> bool {
self.undo_redo_impl(view_id, false) self.undo_redo_impl(view, false)
} }
pub fn savepoint(&mut self) { pub fn savepoint(&mut self) {
@ -910,7 +931,7 @@ impl Document {
} }
} }
fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool { fn earlier_later_impl(&mut self, view: &mut View, 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,29 +939,31 @@ 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) { if self.apply_impl(&txn, view.id) {
success = true; success = true;
} }
} }
if success { if success {
// reset changeset to fix len // reset changeset to fix len
self.changes = ChangeSet::new(self.text()); self.changes = ChangeSet::new(self.text());
// Sync with changes with the jumplist selections.
view.sync_changes(self);
} }
success success
} }
/// Undo modifications to the [`Document`] according to `uk`. /// Undo modifications to the [`Document`] according to `uk`.
pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool { pub fn earlier(&mut self, view: &mut View, uk: UndoKind) -> bool {
self.earlier_later_impl(view_id, uk, true) self.earlier_later_impl(view, uk, true)
} }
/// Redo modifications to the [`Document`] according to `uk`. /// Redo modifications to the [`Document`] according to `uk`.
pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool { pub fn later(&mut self, view: &mut View, uk: UndoKind) -> bool {
self.earlier_later_impl(view_id, uk, false) self.earlier_later_impl(view, uk, false)
} }
/// Commit pending changes to history /// Commit pending changes to history
pub fn append_changes_to_history(&mut self, view_id: ViewId) { pub fn append_changes_to_history(&mut self, view: &mut View) {
if self.changes.is_empty() { if self.changes.is_empty() {
return; return;
} }
@ -950,7 +973,7 @@ impl Document {
// Instead of doing this messy merge we could always commit, and based on transaction // Instead of doing this messy merge we could always commit, and based on transaction
// annotations either add a new layer or compose into the previous one. // annotations either add a new layer or compose into the previous one.
let transaction = let transaction =
Transaction::from(changes).with_selection(self.selection(view_id).clone()); Transaction::from(changes).with_selection(self.selection(view.id).clone());
// HAXX: we need to reconstruct the state as it was before the changes.. // HAXX: we need to reconstruct the state as it was before the changes..
let old_state = self.old_state.take().expect("no old_state available"); let old_state = self.old_state.take().expect("no old_state available");
@ -958,6 +981,9 @@ impl Document {
let mut history = self.history.take(); let mut history = self.history.take();
history.commit_revision(&transaction, &old_state); history.commit_revision(&transaction, &old_state);
self.history.set(history); self.history.set(history);
// Update jumplist entries in the view.
view.apply(&transaction, self);
} }
pub fn id(&self) -> DocumentId { pub fn id(&self) -> DocumentId {
@ -1055,6 +1081,23 @@ impl Document {
server.is_initialized().then(|| server) server.is_initialized().then(|| server)
} }
pub fn diff_handle(&self) -> Option<&DiffHandle> {
self.diff_handle.as_ref()
}
/// Intialize/updates the differ for this document with a new base.
pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) {
if let Ok((diff_base, _)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
if let Some(differ) = &self.diff_handle {
differ.update_diff_base(diff_base);
return;
}
self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone(), redraw_handle))
} else {
self.diff_handle = None;
}
}
#[inline] #[inline]
/// Tree-sitter AST tree /// Tree-sitter AST tree
pub fn syntax(&self) -> Option<&Syntax> { pub fn syntax(&self) -> Option<&Syntax> {

@ -9,6 +9,7 @@ use crate::{
tree::{self, Tree}, tree::{self, Tree},
Align, Document, DocumentId, View, ViewId, Align, Document, DocumentId, View, ViewId,
}; };
use helix_vcs::DiffProviderRegistry;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt}; use futures_util::{future, StreamExt};
@ -26,7 +27,10 @@ use std::{
}; };
use tokio::{ use tokio::{
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
Notify, RwLock,
},
time::{sleep, Duration, Instant, Sleep}, time::{sleep, Duration, Instant, Sleep},
}; };
@ -517,6 +521,8 @@ pub enum GutterType {
LineNumbers, LineNumbers,
/// Show one blank space /// Show one blank space
Spacer, Spacer,
/// Highlight local changes
Diff,
} }
impl std::str::FromStr for GutterType { impl std::str::FromStr for GutterType {
@ -527,6 +533,7 @@ impl std::str::FromStr for GutterType {
"diagnostics" => Ok(Self::Diagnostics), "diagnostics" => Ok(Self::Diagnostics),
"spacer" => Ok(Self::Spacer), "spacer" => Ok(Self::Spacer),
"line-numbers" => Ok(Self::LineNumbers), "line-numbers" => Ok(Self::LineNumbers),
"diff" => Ok(Self::Diff),
_ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."), _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
} }
} }
@ -673,6 +680,8 @@ impl Default for Config {
GutterType::Diagnostics, GutterType::Diagnostics,
GutterType::Spacer, GutterType::Spacer,
GutterType::LineNumbers, GutterType::LineNumbers,
GutterType::Spacer,
GutterType::Diff,
], ],
middle_click_paste: true, middle_click_paste: true,
auto_pairs: AutoPairConfig::default(), auto_pairs: AutoPairConfig::default(),
@ -756,6 +765,7 @@ pub struct Editor {
pub macro_replaying: Vec<char>, pub macro_replaying: Vec<char>,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>, pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>, pub debugger: Option<dap::Client>,
pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>, pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
@ -786,8 +796,15 @@ pub struct Editor {
pub exit_code: i32, pub exit_code: i32,
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>), pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
/// Allows asynchronous tasks to control the rendering
/// The `Notify` allows asynchronous tasks to request the editor to perform a redraw
/// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired
pub redraw_handle: RedrawHandle,
pub needs_redraw: bool,
} }
pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
#[derive(Debug)] #[derive(Debug)]
pub enum EditorEvent { pub enum EditorEvent {
DocumentSaved(DocumentSavedEventResult), DocumentSaved(DocumentSavedEventResult),
@ -873,6 +890,7 @@ impl Editor {
theme: theme_loader.default(), theme: theme_loader.default(),
language_servers: helix_lsp::Registry::new(), language_servers: helix_lsp::Registry::new(),
diagnostics: BTreeMap::new(), diagnostics: BTreeMap::new(),
diff_providers: DiffProviderRegistry::default(),
debugger: None, debugger: None,
debugger_events: SelectAll::new(), debugger_events: SelectAll::new(),
breakpoints: HashMap::new(), breakpoints: HashMap::new(),
@ -891,6 +909,8 @@ impl Editor {
auto_pairs, auto_pairs,
exit_code: 0, exit_code: 0,
config_events: unbounded_channel(), config_events: unbounded_channel(),
redraw_handle: Default::default(),
needs_redraw: false,
} }
} }
@ -1072,7 +1092,8 @@ impl Editor {
fn _refresh(&mut self) { fn _refresh(&mut self) {
let config = self.config(); let config = self.config();
for (view, _) in self.tree.views_mut() { for (view, _) in self.tree.views_mut() {
let doc = &self.documents[&view.doc]; let doc = doc_mut!(self, &view.doc);
view.sync_changes(doc);
view.ensure_cursor_in_view(doc, config.scrolloff) view.ensure_cursor_in_view(doc, config.scrolloff)
} }
} }
@ -1084,6 +1105,7 @@ impl Editor {
let doc = doc_mut!(self, &doc_id); let doc = doc_mut!(self, &doc_id);
doc.ensure_view_init(view.id); doc.ensure_view_init(view.id);
view.sync_changes(doc);
align_view(doc, view, Align::Center); align_view(doc, view, Align::Center);
} }
@ -1096,6 +1118,8 @@ impl Editor {
return; return;
} }
self.enter_normal_mode();
match action { match action {
Action::Replace => { Action::Replace => {
let (view, doc) = current_ref!(self); let (view, doc) = current_ref!(self);
@ -1116,6 +1140,9 @@ impl Editor {
let (view, doc) = current!(self); let (view, doc) = current!(self);
let view_id = view.id; let view_id = view.id;
// Append any outstanding changes to history in the old document.
doc.append_changes_to_history(view);
if remove_empty_scratch { if remove_empty_scratch {
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
// borrow, invalidating direct access to `doc.id`. // borrow, invalidating direct access to `doc.id`.
@ -1220,7 +1247,9 @@ impl Editor {
let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?; let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
let _ = Self::launch_language_server(&mut self.language_servers, &mut doc); let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
doc.set_diff_base(diff_base, self.redraw_handle.clone());
}
self.new_document(doc) self.new_document(doc)
}; };
@ -1351,8 +1380,14 @@ impl Editor {
// if leaving the view: mode should reset and the cursor should be // if leaving the view: mode should reset and the cursor should be
// within view // within view
if prev_id != view_id { if prev_id != view_id {
self.mode = Mode::Normal; self.enter_normal_mode();
self.ensure_cursor_in_view(view_id); self.ensure_cursor_in_view(view_id);
// Update jumplist selections with new document changes.
for (view, _focused) in self.tree.views_mut() {
let doc = doc_mut!(self, &view.doc);
view.sync_changes(doc);
}
} }
} }
@ -1453,24 +1488,39 @@ impl Editor {
} }
pub async fn wait_event(&mut self) -> EditorEvent { pub async fn wait_event(&mut self) -> EditorEvent {
tokio::select! { // the loop only runs once or twice and would be better implemented with a recursion + const generic
biased; // however due to limitations with async functions that can not be implemented right now
loop {
tokio::select! {
biased;
Some(event) = self.save_queue.next() => {
self.write_count -= 1;
return EditorEvent::DocumentSaved(event)
}
Some(config_event) = self.config_events.1.recv() => {
return EditorEvent::ConfigEvent(config_event)
}
Some(message) = self.language_servers.incoming.next() => {
return EditorEvent::LanguageServerMessage(message)
}
Some(event) = self.debugger_events.next() => {
return EditorEvent::DebuggerEvent(event)
}
Some(event) = self.save_queue.next() => { _ = self.redraw_handle.0.notified() => {
self.write_count -= 1; if !self.needs_redraw{
EditorEvent::DocumentSaved(event) self.needs_redraw = true;
} let timeout = Instant::now() + Duration::from_millis(96);
Some(config_event) = self.config_events.1.recv() => { if timeout < self.idle_timer.deadline(){
EditorEvent::ConfigEvent(config_event) self.idle_timer.as_mut().reset(timeout)
} }
Some(message) = self.language_servers.incoming.next() => { }
EditorEvent::LanguageServerMessage(message) }
}
Some(event) = self.debugger_events.next() => { _ = &mut self.idle_timer => {
EditorEvent::DebuggerEvent(event) return EditorEvent::IdleTimer
} }
_ = &mut self.idle_timer => {
EditorEvent::IdleTimer
} }
} }
} }
@ -1495,4 +1545,67 @@ impl Editor {
Ok(()) Ok(())
} }
/// Switches the editor into normal mode.
pub fn enter_normal_mode(&mut self) {
use helix_core::{graphemes, Range};
if self.mode == Mode::Normal {
return;
}
self.mode = Mode::Normal;
let (view, doc) = current!(self);
try_restore_indent(doc, view);
// if leaving append mode, move cursor back by 1
if doc.restore_cursor {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
Range::new(
range.from(),
graphemes::prev_grapheme_boundary(text, range.to()),
)
});
doc.set_selection(view.id, selection);
doc.restore_cursor = false;
}
}
}
fn try_restore_indent(doc: &mut Document, view: &mut View) {
use helix_core::{
chars::char_is_whitespace, line_ending::line_end_char_index, Operation, Transaction,
};
fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool {
if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
changes
{
move_pos + inserted_str.len() == pos
&& inserted_str.starts_with('\n')
&& inserted_str.chars().skip(1).all(char_is_whitespace)
&& pos == line_end_pos // ensure no characters exists after current position
} else {
false
}
}
let doc_changes = doc.changes().changes();
let text = doc.text().slice(..);
let range = doc.selection(view.id).primary();
let pos = range.cursor(text);
let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
// Removes tailing whitespaces.
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let line_start_pos = text.line_to_char(range.cursor_line(text));
(line_start_pos, pos, None)
});
crate::apply_transaction(&transaction, doc, view);
}
} }

@ -12,7 +12,7 @@ fn count_digits(n: usize) -> usize {
std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count() 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 FnMut(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>;
@ -31,6 +31,7 @@ impl GutterType {
} }
GutterType::LineNumbers => line_numbers(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), GutterType::Spacer => padding(editor, doc, view, theme, is_focused),
GutterType::Diff => diff(editor, doc, view, theme, is_focused),
} }
} }
@ -39,6 +40,7 @@ impl GutterType {
GutterType::Diagnostics => 1, GutterType::Diagnostics => 1,
GutterType::LineNumbers => line_numbers_width(_view, doc), GutterType::LineNumbers => line_numbers_width(_view, doc),
GutterType::Spacer => 1, GutterType::Spacer => 1,
GutterType::Diff => 1,
} }
} }
} }
@ -83,6 +85,53 @@ pub fn diagnostic<'doc>(
}) })
} }
pub fn diff<'doc>(
_editor: &'doc Editor,
doc: &'doc Document,
_view: &View,
theme: &Theme,
_is_focused: bool,
) -> GutterFn<'doc> {
let added = theme.get("diff.plus");
let deleted = theme.get("diff.minus");
let modified = theme.get("diff.delta");
if let Some(diff_handle) = doc.diff_handle() {
let hunks = diff_handle.hunks();
let mut hunk_i = 0;
let mut hunk = hunks.nth_hunk(hunk_i);
Box::new(move |line: usize, _selected: bool, out: &mut String| {
// truncating the line is fine here because we don't compute diffs
// for files with more lines than i32::MAX anyways
// we need to special case removals here
// these technically do not have a range of lines to highlight (`hunk.after.start == hunk.after.end`).
// However we still want to display these hunks correctly we must not yet skip to the next hunk here
while hunk.after.end < line as u32
|| !hunk.is_pure_removal() && line as u32 == hunk.after.end
{
hunk_i += 1;
hunk = hunks.nth_hunk(hunk_i);
}
if hunk.after.start > line as u32 {
return None;
}
let (icon, style) = if hunk.is_pure_insertion() {
("▍", added)
} else if hunk.is_pure_removal() {
("▔", deleted)
} else {
("▍", modified)
};
write!(out, "{}", icon).unwrap();
Some(style)
})
} else {
Box::new(move |_, _, _| None)
}
}
pub fn line_numbers<'doc>( pub fn line_numbers<'doc>(
editor: &'doc Editor, editor: &'doc Editor,
doc: &'doc Document, doc: &'doc Document,
@ -226,8 +275,8 @@ pub fn diagnostics_or_breakpoints<'doc>(
theme: &Theme, theme: &Theme,
is_focused: bool, is_focused: bool,
) -> GutterFn<'doc> { ) -> GutterFn<'doc> {
let diagnostics = diagnostic(editor, doc, view, theme, is_focused); let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
let breakpoints = breakpoints(editor, doc, view, theme, is_focused); let mut 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))

@ -4,7 +4,7 @@ use helix_core::unicode::{segmentation::UnicodeSegmentation, width::UnicodeWidth
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use std::fmt; use std::fmt;
pub use crate::keyboard::{KeyCode, KeyModifiers}; pub use crate::keyboard::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode};
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)] #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)]
pub enum Event { pub enum Event {
@ -119,6 +119,40 @@ pub(crate) mod keys {
pub(crate) const MINUS: &str = "minus"; pub(crate) const MINUS: &str = "minus";
pub(crate) const LESS_THAN: &str = "lt"; pub(crate) const LESS_THAN: &str = "lt";
pub(crate) const GREATER_THAN: &str = "gt"; pub(crate) const GREATER_THAN: &str = "gt";
pub(crate) const CAPS_LOCK: &str = "capslock";
pub(crate) const SCROLL_LOCK: &str = "scrolllock";
pub(crate) const NUM_LOCK: &str = "numlock";
pub(crate) const PRINT_SCREEN: &str = "printscreen";
pub(crate) const PAUSE: &str = "pause";
pub(crate) const MENU: &str = "menu";
pub(crate) const KEYPAD_BEGIN: &str = "keypadbegin";
pub(crate) const PLAY: &str = "play";
pub(crate) const PAUSE_MEDIA: &str = "pausemedia";
pub(crate) const PLAY_PAUSE: &str = "playpause";
pub(crate) const REVERSE: &str = "reverse";
pub(crate) const STOP: &str = "stop";
pub(crate) const FAST_FORWARD: &str = "fastforward";
pub(crate) const REWIND: &str = "rewind";
pub(crate) const TRACK_NEXT: &str = "tracknext";
pub(crate) const TRACK_PREVIOUS: &str = "trackprevious";
pub(crate) const RECORD: &str = "record";
pub(crate) const LOWER_VOLUME: &str = "lowervolume";
pub(crate) const RAISE_VOLUME: &str = "raisevolume";
pub(crate) const MUTE_VOLUME: &str = "mutevolume";
pub(crate) const LEFT_SHIFT: &str = "leftshift";
pub(crate) const LEFT_CONTROL: &str = "leftcontrol";
pub(crate) const LEFT_ALT: &str = "leftalt";
pub(crate) const LEFT_SUPER: &str = "leftsuper";
pub(crate) const LEFT_HYPER: &str = "lefthyper";
pub(crate) const LEFT_META: &str = "leftmeta";
pub(crate) const RIGHT_SHIFT: &str = "rightshift";
pub(crate) const RIGHT_CONTROL: &str = "rightcontrol";
pub(crate) const RIGHT_ALT: &str = "rightalt";
pub(crate) const RIGHT_SUPER: &str = "rightsuper";
pub(crate) const RIGHT_HYPER: &str = "righthyper";
pub(crate) const RIGHT_META: &str = "rightmeta";
pub(crate) const ISO_LEVEL_3_SHIFT: &str = "isolevel3shift";
pub(crate) const ISO_LEVEL_5_SHIFT: &str = "isolevel5shift";
} }
impl fmt::Display for KeyEvent { impl fmt::Display for KeyEvent {
@ -163,6 +197,44 @@ impl fmt::Display for KeyEvent {
KeyCode::Char('>') => f.write_str(keys::GREATER_THAN)?, KeyCode::Char('>') => f.write_str(keys::GREATER_THAN)?,
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?, KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?, KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
KeyCode::CapsLock => f.write_str(keys::CAPS_LOCK)?,
KeyCode::ScrollLock => f.write_str(keys::SCROLL_LOCK)?,
KeyCode::NumLock => f.write_str(keys::NUM_LOCK)?,
KeyCode::PrintScreen => f.write_str(keys::PRINT_SCREEN)?,
KeyCode::Pause => f.write_str(keys::PAUSE)?,
KeyCode::Menu => f.write_str(keys::MENU)?,
KeyCode::KeypadBegin => f.write_str(keys::KEYPAD_BEGIN)?,
KeyCode::Media(MediaKeyCode::Play) => f.write_str(keys::PLAY)?,
KeyCode::Media(MediaKeyCode::Pause) => f.write_str(keys::PAUSE_MEDIA)?,
KeyCode::Media(MediaKeyCode::PlayPause) => f.write_str(keys::PLAY_PAUSE)?,
KeyCode::Media(MediaKeyCode::Stop) => f.write_str(keys::STOP)?,
KeyCode::Media(MediaKeyCode::Reverse) => f.write_str(keys::REVERSE)?,
KeyCode::Media(MediaKeyCode::FastForward) => f.write_str(keys::FAST_FORWARD)?,
KeyCode::Media(MediaKeyCode::Rewind) => f.write_str(keys::REWIND)?,
KeyCode::Media(MediaKeyCode::TrackNext) => f.write_str(keys::TRACK_NEXT)?,
KeyCode::Media(MediaKeyCode::TrackPrevious) => f.write_str(keys::TRACK_PREVIOUS)?,
KeyCode::Media(MediaKeyCode::Record) => f.write_str(keys::RECORD)?,
KeyCode::Media(MediaKeyCode::LowerVolume) => f.write_str(keys::LOWER_VOLUME)?,
KeyCode::Media(MediaKeyCode::RaiseVolume) => f.write_str(keys::RAISE_VOLUME)?,
KeyCode::Media(MediaKeyCode::MuteVolume) => f.write_str(keys::MUTE_VOLUME)?,
KeyCode::Modifier(ModifierKeyCode::LeftShift) => f.write_str(keys::LEFT_SHIFT)?,
KeyCode::Modifier(ModifierKeyCode::LeftControl) => f.write_str(keys::LEFT_CONTROL)?,
KeyCode::Modifier(ModifierKeyCode::LeftAlt) => f.write_str(keys::LEFT_ALT)?,
KeyCode::Modifier(ModifierKeyCode::LeftSuper) => f.write_str(keys::LEFT_SUPER)?,
KeyCode::Modifier(ModifierKeyCode::LeftHyper) => f.write_str(keys::LEFT_HYPER)?,
KeyCode::Modifier(ModifierKeyCode::LeftMeta) => f.write_str(keys::LEFT_META)?,
KeyCode::Modifier(ModifierKeyCode::RightShift) => f.write_str(keys::RIGHT_SHIFT)?,
KeyCode::Modifier(ModifierKeyCode::RightControl) => f.write_str(keys::RIGHT_CONTROL)?,
KeyCode::Modifier(ModifierKeyCode::RightAlt) => f.write_str(keys::RIGHT_ALT)?,
KeyCode::Modifier(ModifierKeyCode::RightSuper) => f.write_str(keys::RIGHT_SUPER)?,
KeyCode::Modifier(ModifierKeyCode::RightHyper) => f.write_str(keys::RIGHT_HYPER)?,
KeyCode::Modifier(ModifierKeyCode::RightMeta) => f.write_str(keys::RIGHT_META)?,
KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift) => {
f.write_str(keys::ISO_LEVEL_3_SHIFT)?
}
KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift) => {
f.write_str(keys::ISO_LEVEL_5_SHIFT)?
}
}; };
Ok(()) Ok(())
} }
@ -192,6 +264,40 @@ impl UnicodeWidthStr for KeyEvent {
KeyCode::F(1..=9) => 2, KeyCode::F(1..=9) => 2,
KeyCode::F(_) => 3, KeyCode::F(_) => 3,
KeyCode::Char(c) => c.width().unwrap_or(0), KeyCode::Char(c) => c.width().unwrap_or(0),
KeyCode::CapsLock => keys::CAPS_LOCK.len(),
KeyCode::ScrollLock => keys::SCROLL_LOCK.len(),
KeyCode::NumLock => keys::NUM_LOCK.len(),
KeyCode::PrintScreen => keys::PRINT_SCREEN.len(),
KeyCode::Pause => keys::PAUSE.len(),
KeyCode::Menu => keys::MENU.len(),
KeyCode::KeypadBegin => keys::KEYPAD_BEGIN.len(),
KeyCode::Media(MediaKeyCode::Play) => keys::PLAY.len(),
KeyCode::Media(MediaKeyCode::Pause) => keys::PAUSE_MEDIA.len(),
KeyCode::Media(MediaKeyCode::PlayPause) => keys::PLAY_PAUSE.len(),
KeyCode::Media(MediaKeyCode::Stop) => keys::STOP.len(),
KeyCode::Media(MediaKeyCode::Reverse) => keys::REVERSE.len(),
KeyCode::Media(MediaKeyCode::FastForward) => keys::FAST_FORWARD.len(),
KeyCode::Media(MediaKeyCode::Rewind) => keys::REWIND.len(),
KeyCode::Media(MediaKeyCode::TrackNext) => keys::TRACK_NEXT.len(),
KeyCode::Media(MediaKeyCode::TrackPrevious) => keys::TRACK_PREVIOUS.len(),
KeyCode::Media(MediaKeyCode::Record) => keys::RECORD.len(),
KeyCode::Media(MediaKeyCode::LowerVolume) => keys::LOWER_VOLUME.len(),
KeyCode::Media(MediaKeyCode::RaiseVolume) => keys::RAISE_VOLUME.len(),
KeyCode::Media(MediaKeyCode::MuteVolume) => keys::MUTE_VOLUME.len(),
KeyCode::Modifier(ModifierKeyCode::LeftShift) => keys::LEFT_SHIFT.len(),
KeyCode::Modifier(ModifierKeyCode::LeftControl) => keys::LEFT_CONTROL.len(),
KeyCode::Modifier(ModifierKeyCode::LeftAlt) => keys::LEFT_ALT.len(),
KeyCode::Modifier(ModifierKeyCode::LeftSuper) => keys::LEFT_SUPER.len(),
KeyCode::Modifier(ModifierKeyCode::LeftHyper) => keys::LEFT_HYPER.len(),
KeyCode::Modifier(ModifierKeyCode::LeftMeta) => keys::LEFT_META.len(),
KeyCode::Modifier(ModifierKeyCode::RightShift) => keys::RIGHT_SHIFT.len(),
KeyCode::Modifier(ModifierKeyCode::RightControl) => keys::RIGHT_CONTROL.len(),
KeyCode::Modifier(ModifierKeyCode::RightAlt) => keys::RIGHT_ALT.len(),
KeyCode::Modifier(ModifierKeyCode::RightSuper) => keys::RIGHT_SUPER.len(),
KeyCode::Modifier(ModifierKeyCode::RightHyper) => keys::RIGHT_HYPER.len(),
KeyCode::Modifier(ModifierKeyCode::RightMeta) => keys::RIGHT_META.len(),
KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift) => keys::ISO_LEVEL_3_SHIFT.len(),
KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift) => keys::ISO_LEVEL_5_SHIFT.len(),
}; };
if self.modifiers.contains(KeyModifiers::SHIFT) { if self.modifiers.contains(KeyModifiers::SHIFT) {
width += 2; width += 2;
@ -235,6 +341,40 @@ impl std::str::FromStr for KeyEvent {
keys::MINUS => KeyCode::Char('-'), keys::MINUS => KeyCode::Char('-'),
keys::LESS_THAN => KeyCode::Char('<'), keys::LESS_THAN => KeyCode::Char('<'),
keys::GREATER_THAN => KeyCode::Char('>'), keys::GREATER_THAN => KeyCode::Char('>'),
keys::CAPS_LOCK => KeyCode::CapsLock,
keys::SCROLL_LOCK => KeyCode::ScrollLock,
keys::NUM_LOCK => KeyCode::NumLock,
keys::PRINT_SCREEN => KeyCode::PrintScreen,
keys::PAUSE => KeyCode::Pause,
keys::MENU => KeyCode::Menu,
keys::KEYPAD_BEGIN => KeyCode::KeypadBegin,
keys::PLAY => KeyCode::Media(MediaKeyCode::Play),
keys::PAUSE_MEDIA => KeyCode::Media(MediaKeyCode::Pause),
keys::PLAY_PAUSE => KeyCode::Media(MediaKeyCode::PlayPause),
keys::STOP => KeyCode::Media(MediaKeyCode::Stop),
keys::REVERSE => KeyCode::Media(MediaKeyCode::Reverse),
keys::FAST_FORWARD => KeyCode::Media(MediaKeyCode::FastForward),
keys::REWIND => KeyCode::Media(MediaKeyCode::Rewind),
keys::TRACK_NEXT => KeyCode::Media(MediaKeyCode::TrackNext),
keys::TRACK_PREVIOUS => KeyCode::Media(MediaKeyCode::TrackPrevious),
keys::RECORD => KeyCode::Media(MediaKeyCode::Record),
keys::LOWER_VOLUME => KeyCode::Media(MediaKeyCode::LowerVolume),
keys::RAISE_VOLUME => KeyCode::Media(MediaKeyCode::RaiseVolume),
keys::MUTE_VOLUME => KeyCode::Media(MediaKeyCode::MuteVolume),
keys::LEFT_SHIFT => KeyCode::Modifier(ModifierKeyCode::LeftShift),
keys::LEFT_CONTROL => KeyCode::Modifier(ModifierKeyCode::LeftControl),
keys::LEFT_ALT => KeyCode::Modifier(ModifierKeyCode::LeftAlt),
keys::LEFT_SUPER => KeyCode::Modifier(ModifierKeyCode::LeftSuper),
keys::LEFT_HYPER => KeyCode::Modifier(ModifierKeyCode::LeftHyper),
keys::LEFT_META => KeyCode::Modifier(ModifierKeyCode::LeftMeta),
keys::RIGHT_SHIFT => KeyCode::Modifier(ModifierKeyCode::RightShift),
keys::RIGHT_CONTROL => KeyCode::Modifier(ModifierKeyCode::RightControl),
keys::RIGHT_ALT => KeyCode::Modifier(ModifierKeyCode::RightAlt),
keys::RIGHT_SUPER => KeyCode::Modifier(ModifierKeyCode::RightSuper),
keys::RIGHT_HYPER => KeyCode::Modifier(ModifierKeyCode::RightHyper),
keys::RIGHT_META => KeyCode::Modifier(ModifierKeyCode::RightMeta),
keys::ISO_LEVEL_3_SHIFT => KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift),
keys::ISO_LEVEL_5_SHIFT => KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift),
single if single.chars().count() == 1 => KeyCode::Char(single.chars().next().unwrap()), single if single.chars().count() == 1 => KeyCode::Char(single.chars().next().unwrap()),
function if function.len() > 1 && function.starts_with('F') => { function if function.len() > 1 && function.starts_with('F') => {
let function: String = function.chars().skip(1).collect(); let function: String = function.chars().skip(1).collect();

@ -53,6 +53,164 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
} }
} }
/// Represents a media key (as part of [`KeyCode::Media`]).
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
pub enum MediaKeyCode {
/// Play media key.
Play,
/// Pause media key.
Pause,
/// Play/Pause media key.
PlayPause,
/// Reverse media key.
Reverse,
/// Stop media key.
Stop,
/// Fast-forward media key.
FastForward,
/// Rewind media key.
Rewind,
/// Next-track media key.
TrackNext,
/// Previous-track media key.
TrackPrevious,
/// Record media key.
Record,
/// Lower-volume media key.
LowerVolume,
/// Raise-volume media key.
RaiseVolume,
/// Mute media key.
MuteVolume,
}
#[cfg(feature = "term")]
impl From<MediaKeyCode> for crossterm::event::MediaKeyCode {
fn from(media_key_code: MediaKeyCode) -> Self {
use crossterm::event::MediaKeyCode as CMediaKeyCode;
match media_key_code {
MediaKeyCode::Play => CMediaKeyCode::Play,
MediaKeyCode::Pause => CMediaKeyCode::Pause,
MediaKeyCode::PlayPause => CMediaKeyCode::PlayPause,
MediaKeyCode::Reverse => CMediaKeyCode::Reverse,
MediaKeyCode::Stop => CMediaKeyCode::Stop,
MediaKeyCode::FastForward => CMediaKeyCode::FastForward,
MediaKeyCode::Rewind => CMediaKeyCode::Rewind,
MediaKeyCode::TrackNext => CMediaKeyCode::TrackNext,
MediaKeyCode::TrackPrevious => CMediaKeyCode::TrackPrevious,
MediaKeyCode::Record => CMediaKeyCode::Record,
MediaKeyCode::LowerVolume => CMediaKeyCode::LowerVolume,
MediaKeyCode::RaiseVolume => CMediaKeyCode::RaiseVolume,
MediaKeyCode::MuteVolume => CMediaKeyCode::MuteVolume,
}
}
}
#[cfg(feature = "term")]
impl From<crossterm::event::MediaKeyCode> for MediaKeyCode {
fn from(val: crossterm::event::MediaKeyCode) -> Self {
use crossterm::event::MediaKeyCode as CMediaKeyCode;
match val {
CMediaKeyCode::Play => MediaKeyCode::Play,
CMediaKeyCode::Pause => MediaKeyCode::Pause,
CMediaKeyCode::PlayPause => MediaKeyCode::PlayPause,
CMediaKeyCode::Reverse => MediaKeyCode::Reverse,
CMediaKeyCode::Stop => MediaKeyCode::Stop,
CMediaKeyCode::FastForward => MediaKeyCode::FastForward,
CMediaKeyCode::Rewind => MediaKeyCode::Rewind,
CMediaKeyCode::TrackNext => MediaKeyCode::TrackNext,
CMediaKeyCode::TrackPrevious => MediaKeyCode::TrackPrevious,
CMediaKeyCode::Record => MediaKeyCode::Record,
CMediaKeyCode::LowerVolume => MediaKeyCode::LowerVolume,
CMediaKeyCode::RaiseVolume => MediaKeyCode::RaiseVolume,
CMediaKeyCode::MuteVolume => MediaKeyCode::MuteVolume,
}
}
}
/// Represents a media key (as part of [`KeyCode::Modifier`]).
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
pub enum ModifierKeyCode {
/// Left Shift key.
LeftShift,
/// Left Control key.
LeftControl,
/// Left Alt key.
LeftAlt,
/// Left Super key.
LeftSuper,
/// Left Hyper key.
LeftHyper,
/// Left Meta key.
LeftMeta,
/// Right Shift key.
RightShift,
/// Right Control key.
RightControl,
/// Right Alt key.
RightAlt,
/// Right Super key.
RightSuper,
/// Right Hyper key.
RightHyper,
/// Right Meta key.
RightMeta,
/// Iso Level3 Shift key.
IsoLevel3Shift,
/// Iso Level5 Shift key.
IsoLevel5Shift,
}
#[cfg(feature = "term")]
impl From<ModifierKeyCode> for crossterm::event::ModifierKeyCode {
fn from(modifier_key_code: ModifierKeyCode) -> Self {
use crossterm::event::ModifierKeyCode as CModifierKeyCode;
match modifier_key_code {
ModifierKeyCode::LeftShift => CModifierKeyCode::LeftShift,
ModifierKeyCode::LeftControl => CModifierKeyCode::LeftControl,
ModifierKeyCode::LeftAlt => CModifierKeyCode::LeftAlt,
ModifierKeyCode::LeftSuper => CModifierKeyCode::LeftSuper,
ModifierKeyCode::LeftHyper => CModifierKeyCode::LeftHyper,
ModifierKeyCode::LeftMeta => CModifierKeyCode::LeftMeta,
ModifierKeyCode::RightShift => CModifierKeyCode::RightShift,
ModifierKeyCode::RightControl => CModifierKeyCode::RightControl,
ModifierKeyCode::RightAlt => CModifierKeyCode::RightAlt,
ModifierKeyCode::RightSuper => CModifierKeyCode::RightSuper,
ModifierKeyCode::RightHyper => CModifierKeyCode::RightHyper,
ModifierKeyCode::RightMeta => CModifierKeyCode::RightMeta,
ModifierKeyCode::IsoLevel3Shift => CModifierKeyCode::IsoLevel3Shift,
ModifierKeyCode::IsoLevel5Shift => CModifierKeyCode::IsoLevel5Shift,
}
}
}
#[cfg(feature = "term")]
impl From<crossterm::event::ModifierKeyCode> for ModifierKeyCode {
fn from(val: crossterm::event::ModifierKeyCode) -> Self {
use crossterm::event::ModifierKeyCode as CModifierKeyCode;
match val {
CModifierKeyCode::LeftShift => ModifierKeyCode::LeftShift,
CModifierKeyCode::LeftControl => ModifierKeyCode::LeftControl,
CModifierKeyCode::LeftAlt => ModifierKeyCode::LeftAlt,
CModifierKeyCode::LeftSuper => ModifierKeyCode::LeftSuper,
CModifierKeyCode::LeftHyper => ModifierKeyCode::LeftHyper,
CModifierKeyCode::LeftMeta => ModifierKeyCode::LeftMeta,
CModifierKeyCode::RightShift => ModifierKeyCode::RightShift,
CModifierKeyCode::RightControl => ModifierKeyCode::RightControl,
CModifierKeyCode::RightAlt => ModifierKeyCode::RightAlt,
CModifierKeyCode::RightSuper => ModifierKeyCode::RightSuper,
CModifierKeyCode::RightHyper => ModifierKeyCode::RightHyper,
CModifierKeyCode::RightMeta => ModifierKeyCode::RightMeta,
CModifierKeyCode::IsoLevel3Shift => ModifierKeyCode::IsoLevel3Shift,
CModifierKeyCode::IsoLevel5Shift => ModifierKeyCode::IsoLevel5Shift,
}
}
}
/// Represents a key. /// Represents a key.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
pub enum KeyCode { pub enum KeyCode {
@ -94,6 +252,24 @@ pub enum KeyCode {
Null, Null,
/// Escape key. /// Escape key.
Esc, Esc,
/// CapsLock key.
CapsLock,
/// ScrollLock key.
ScrollLock,
/// NumLock key.
NumLock,
/// PrintScreen key.
PrintScreen,
/// Pause key.
Pause,
/// Menu key.
Menu,
/// KeypadBegin key.
KeypadBegin,
/// A media key.
Media(MediaKeyCode),
/// A modifier key.
Modifier(ModifierKeyCode),
} }
#[cfg(feature = "term")] #[cfg(feature = "term")]
@ -119,6 +295,15 @@ impl From<KeyCode> for crossterm::event::KeyCode {
KeyCode::Char(character) => CKeyCode::Char(character), KeyCode::Char(character) => CKeyCode::Char(character),
KeyCode::Null => CKeyCode::Null, KeyCode::Null => CKeyCode::Null,
KeyCode::Esc => CKeyCode::Esc, KeyCode::Esc => CKeyCode::Esc,
KeyCode::CapsLock => CKeyCode::CapsLock,
KeyCode::ScrollLock => CKeyCode::ScrollLock,
KeyCode::NumLock => CKeyCode::NumLock,
KeyCode::PrintScreen => CKeyCode::PrintScreen,
KeyCode::Pause => CKeyCode::Pause,
KeyCode::Menu => CKeyCode::Menu,
KeyCode::KeypadBegin => CKeyCode::KeypadBegin,
KeyCode::Media(media_key_code) => CKeyCode::Media(media_key_code.into()),
KeyCode::Modifier(modifier_key_code) => CKeyCode::Modifier(modifier_key_code.into()),
} }
} }
} }
@ -147,17 +332,15 @@ impl From<crossterm::event::KeyCode> for KeyCode {
CKeyCode::Char(character) => KeyCode::Char(character), CKeyCode::Char(character) => KeyCode::Char(character),
CKeyCode::Null => KeyCode::Null, CKeyCode::Null => KeyCode::Null,
CKeyCode::Esc => KeyCode::Esc, CKeyCode::Esc => KeyCode::Esc,
CKeyCode::CapsLock CKeyCode::CapsLock => KeyCode::CapsLock,
| CKeyCode::ScrollLock CKeyCode::ScrollLock => KeyCode::ScrollLock,
| CKeyCode::NumLock CKeyCode::NumLock => KeyCode::NumLock,
| CKeyCode::PrintScreen CKeyCode::PrintScreen => KeyCode::PrintScreen,
| CKeyCode::Pause CKeyCode::Pause => KeyCode::Pause,
| CKeyCode::Menu CKeyCode::Menu => KeyCode::Menu,
| CKeyCode::KeypadBegin CKeyCode::KeypadBegin => KeyCode::KeypadBegin,
| CKeyCode::Media(_) CKeyCode::Media(media_key_code) => KeyCode::Media(media_key_code.into()),
| CKeyCode::Modifier(_) => unreachable!( CKeyCode::Modifier(modifier_key_code) => KeyCode::Modifier(modifier_key_code.into()),
"Shouldn't get this key without enabling DISAMBIGUATE_ESCAPE_CODES in crossterm"
),
} }
} }
} }

@ -136,8 +136,9 @@ impl Loader {
// Loads the theme data as `toml::Value` first from the user_dir then in default_dir // Loads the theme data as `toml::Value` first from the user_dir then in default_dir
fn load_toml(&self, path: PathBuf) -> Result<Value> { fn load_toml(&self, path: PathBuf) -> Result<Value> {
let data = std::fs::read(&path)?; let data = std::fs::read(&path)?;
let value = toml::from_slice(data.as_slice())?;
toml::from_slice(data.as_slice()).context("Failed to deserialize theme") Ok(value)
} }
// Returns the path to the theme with the name // Returns the path to the theme with the name

@ -1,9 +1,12 @@
use crate::{editor::GutterType, graphics::Rect, Document, DocumentId, ViewId}; use crate::{align_view, editor::GutterType, graphics::Rect, Align, 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::{collections::VecDeque, fmt}; use std::{
collections::{HashMap, VecDeque},
fmt,
};
const JUMP_LIST_CAPACITY: usize = 30; const JUMP_LIST_CAPACITY: usize = 30;
@ -102,6 +105,11 @@ pub struct View {
pub object_selections: Vec<Selection>, pub object_selections: Vec<Selection>,
/// GutterTypes used to fetch Gutter (constructor) and width for rendering /// GutterTypes used to fetch Gutter (constructor) and width for rendering
gutters: Vec<GutterType>, gutters: Vec<GutterType>,
/// A mapping between documents and the last history revision the view was updated at.
/// Changes between documents and views are synced lazily when switching windows. This
/// mapping keeps track of the last applied history revision so that only new changes
/// are applied.
doc_revisions: HashMap<DocumentId, usize>,
} }
impl fmt::Debug for View { impl fmt::Debug for View {
@ -126,6 +134,7 @@ impl View {
last_modified_docs: [None, None], last_modified_docs: [None, None],
object_selections: Vec::new(), object_selections: Vec::new(),
gutters: gutter_types, gutters: gutter_types,
doc_revisions: HashMap::new(),
} }
} }
@ -149,17 +158,10 @@ impl View {
} }
pub fn gutter_offset(&self, doc: &Document) -> u16 { pub fn gutter_offset(&self, doc: &Document) -> u16 {
let mut offset = self self.gutters
.gutters
.iter() .iter()
.map(|gutter| gutter.width(self, doc) as u16) .map(|gutter| gutter.width(self, doc) as u16)
.sum(); .sum()
if offset > 0 {
offset += 1
}
offset
} }
// //
@ -167,6 +169,15 @@ impl View {
&self, &self,
doc: &Document, doc: &Document,
scrolloff: usize, scrolloff: usize,
) -> Option<(usize, usize)> {
self.offset_coords_to_in_view_center(doc, scrolloff, false)
}
pub fn offset_coords_to_in_view_center(
&self,
doc: &Document,
scrolloff: usize,
centering: bool,
) -> Option<(usize, usize)> { ) -> Option<(usize, usize)> {
let cursor = doc let cursor = doc
.selection(self.id) .selection(self.id)
@ -178,44 +189,66 @@ impl View {
let inner_area = self.inner_area(doc); 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.
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth
// as we type
let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2);
let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize; let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
let row = if line > last_line.saturating_sub(scrolloff) { let new_offset = |scrolloff: usize| {
// scroll down // - 1 so we have at least one gap in the middle.
self.offset.row + line - (last_line.saturating_sub(scrolloff)) // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
} else if line < self.offset.row + scrolloff { // as we type
// scroll up let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2);
line.saturating_sub(scrolloff)
} else { let row = if line > last_line.saturating_sub(scrolloff) {
self.offset.row // scroll down
self.offset.row + line - (last_line.saturating_sub(scrolloff))
} else if line < self.offset.row + scrolloff {
// scroll up
line.saturating_sub(scrolloff)
} else {
self.offset.row
};
let col = if col > last_col.saturating_sub(scrolloff) {
// scroll right
self.offset.col + col - (last_col.saturating_sub(scrolloff))
} else if col < self.offset.col + scrolloff {
// scroll left
col.saturating_sub(scrolloff)
} else {
self.offset.col
};
(row, col)
}; };
let current_offset = (self.offset.row, self.offset.col);
let col = if col > last_col.saturating_sub(scrolloff) { if centering {
// scroll right // return None if cursor is out of view
self.offset.col + col - (last_col.saturating_sub(scrolloff)) let offset = new_offset(0);
} else if col < self.offset.col + scrolloff { (offset == current_offset).then(|| {
// scroll left if scrolloff == 0 {
col.saturating_sub(scrolloff) offset
} else {
new_offset(scrolloff)
}
})
} else { } else {
self.offset.col // return None if cursor is in (view - scrolloff)
}; let offset = new_offset(scrolloff);
if row == self.offset.row && col == self.offset.col { (offset != current_offset).then(|| offset) // TODO: use 'then_some' when 1.62 <= MSRV
None
} else {
Some((row, col))
} }
} }
pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
if let Some((row, col)) = self.offset_coords_to_in_view(doc, scrolloff) { if let Some((row, col)) = self.offset_coords_to_in_view_center(doc, scrolloff, false) {
self.offset.row = row;
self.offset.col = col;
}
}
pub fn ensure_cursor_in_view_center(&mut self, doc: &Document, scrolloff: usize) {
if let Some((row, col)) = self.offset_coords_to_in_view_center(doc, scrolloff, true) {
self.offset.row = row; self.offset.row = row;
self.offset.col = col; self.offset.col = col;
} else {
align_view(doc, self, Align::Center);
} }
} }
@ -349,10 +382,33 @@ impl View {
/// Applies a [`Transaction`] to the view. /// Applies a [`Transaction`] to the view.
/// Instead of calling this function directly, use [crate::apply_transaction] /// Instead of calling this function directly, use [crate::apply_transaction]
/// 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: &mut Document) {
self.jumps.apply(transaction, doc); self.jumps.apply(transaction, doc);
// TODO: remove the boolean return. This is unused. self.doc_revisions
true .insert(doc.id(), doc.get_current_revision());
}
pub fn sync_changes(&mut self, doc: &mut Document) {
let latest_revision = doc.get_current_revision();
let current_revision = *self
.doc_revisions
.entry(doc.id())
.or_insert(latest_revision);
if current_revision == latest_revision {
return;
}
log::debug!(
"Syncing view {:?} between {} and {}",
self.id,
current_revision,
latest_revision
);
if let Some(transaction) = doc.history.get_mut().changes_since(current_revision) {
self.apply(&transaction, doc);
}
} }
} }
@ -360,8 +416,8 @@ impl View {
mod tests { mod tests {
use super::*; use super::*;
use helix_core::Rope; use helix_core::Rope;
const OFFSET: u16 = 4; // 1 diagnostic + 2 linenr (< 100 lines) + 1 gutter const OFFSET: u16 = 3; // 1 diagnostic + 2 linenr (< 100 lines)
const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 1; // 1 diagnostic
// 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::document::Document;
use crate::editor::GutterType; use crate::editor::GutterType;

@ -190,7 +190,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd
name = "cpp" name = "cpp"
scope = "source.cpp" scope = "source.cpp"
injection-regex = "cpp" injection-regex = "cpp"
file-types = ["cc", "hh", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino"] file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino"]
roots = [] roots = []
comment-token = "//" comment-token = "//"
language-server = { command = "clangd" } language-server = { command = "clangd" }
@ -223,6 +223,18 @@ args = { console = "internalConsole", attachCommands = [ "platform select remote
name = "cpp" name = "cpp"
source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "d5e90fba898f320db48d81ddedd78d52c67c1fed" } source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "d5e90fba898f320db48d81ddedd78d52c67c1fed" }
[[language]]
name = "crystal"
scope = "source.cr"
file-types = ["cr"]
roots = ["shard.yml", "shard.lock"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "crystal"
source = { git = "https://github.com/will/tree-sitter-crystal", rev = "15597b307b18028b04d288561f9c29794621562b" }
[[language]] [[language]]
name = "c-sharp" name = "c-sharp"
scope = "source.csharp" scope = "source.csharp"
@ -301,7 +313,7 @@ args = { mode = "local", processId = "{0}" }
[[grammar]] [[grammar]]
name = "go" name = "go"
source = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "0fa917a7022d1cd2e9b779a6a8fc5dc7fad69c75" } source = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "05900faa3cdb5d2d8c8bd5e77ee698487e0a8611" }
[[language]] [[language]]
name = "gomod" name = "gomod"
@ -422,11 +434,13 @@ injection-regex = "css"
file-types = ["css", "scss"] file-types = ["css", "scss"]
roots = [] roots = []
language-server = { command = "vscode-css-language-server", args = ["--stdio"] } language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
auto-format = true
config = { "provideFormatter" = true }
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "css" name = "css"
source = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "94e10230939e702b4fa3fa2cb5c3bc7173b95d07" } source = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
[[language]] [[language]]
name = "scss" name = "scss"
@ -435,6 +449,8 @@ injection-regex = "scss"
file-types = ["scss"] file-types = ["scss"]
roots = [] roots = []
language-server = { command = "vscode-css-language-server", args = ["--stdio"] } language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
auto-format = true
config = { "provideFormatter" = true }
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
@ -572,6 +588,34 @@ indent = { tab-width = 4, unit = "\t" }
name = "latex" name = "latex"
source = { git = "https://github.com/latex-lsp/tree-sitter-latex", rev = "8c75e93cd08ccb7ce1ccab22c1fbd6360e3bcea6" } source = { git = "https://github.com/latex-lsp/tree-sitter-latex", rev = "8c75e93cd08ccb7ce1ccab22c1fbd6360e3bcea6" }
[[language]]
name = "bibtex"
scope = "source.bib"
injection-regex = "bib"
file-types = ["bib"]
roots = []
comment-token = "%"
language-server = { command = "texlab" }
indent = { tab-width = 4, unit = "\t" }
auto-format = true
[language.formatter]
command = 'bibtex-tidy'
args = [
"-",
"--curly",
"--drop-all-caps",
"--remove-empty-fields",
"--sort-fields",
"--sort=year,author,id",
"--strip-enclosing-braces",
"--trailing-commas",
]
[[grammar]]
name = "bibtex"
source = { git = "https://github.com/latex-lsp/tree-sitter-bibtex", rev = "ccfd77db0ed799b6c22c214fe9d2937f47bc8b34" }
[[language]] [[language]]
name = "lean" name = "lean"
scope = "source.lean" scope = "source.lean"
@ -874,12 +918,30 @@ source = { git = "https://github.com/ganezdragon/tree-sitter-perl", rev = "0ac2c
[[language]] [[language]]
name = "racket" name = "racket"
scope = "source.rkt" scope = "source.racket"
roots = [] roots = []
file-types = ["rkt"] file-types = ["rkt", "rktd", "rktl", "scrbl"]
shebangs = ["racket"] shebangs = ["racket"]
comment-token = ";" comment-token = ";"
language-server = { command = "racket", args = ["-l", "racket-langserver"] } language-server = { command = "racket", args = ["-l", "racket-langserver"] }
grammar = "scheme"
[[language]]
name = "common-lisp"
scope = "source.lisp"
roots = []
file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
comment-token = ";"
indent = { tab-width = 2, unit = " " }
language-server = { command = "cl-lsp", args = [ "stdio" ] }
grammar = "scheme"
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
[[language]] [[language]]
name = "comment" name = "comment"
@ -1039,7 +1101,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev
name = "diff" name = "diff"
scope = "source.diff" scope = "source.diff"
roots = [] roots = []
file-types = ["diff"] file-types = ["diff", "patch"]
injection-regex = "diff" injection-regex = "diff"
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
@ -1398,7 +1460,7 @@ file-types = ["tscn","tres"]
shebangs = [] shebangs = []
roots = ["project.godot"] roots = ["project.godot"]
auto-format = false auto-format = false
comment-token = "#" comment-token = ";"
indent = { tab-width = 4, unit = "\t" } indent = { tab-width = 4, unit = "\t" }
[[grammar]] [[grammar]]
@ -1526,14 +1588,14 @@ source = { git = "https://github.com/metio/tree-sitter-ssh-client-config", rev =
name = "scheme" name = "scheme"
scope = "source.scheme" scope = "source.scheme"
injection-regex = "scheme" injection-regex = "scheme"
file-types = ["ss", "rkt"] # "scm", file-types = ["ss"] # "scm",
roots = [] roots = []
comment-token = ";" comment-token = ";"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "scheme" name = "scheme"
source = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "27fb77db05f890c2823b4bd751c6420378df146b" } source = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "c0741320bfca6b7b5b7a13b5171275951e96a842" }
[[language]] [[language]]
name = "v" name = "v"
@ -1993,3 +2055,29 @@ grammar = "qmljs"
[[grammar]] [[grammar]]
name = "qmljs" name = "qmljs"
source = { git = "https://github.com/yuja/tree-sitter-qmljs", rev = "0b2b25bcaa7d4925d5f0dda16f6a99c588a437f1" } source = { git = "https://github.com/yuja/tree-sitter-qmljs", rev = "0b2b25bcaa7d4925d5f0dda16f6a99c588a437f1" }
[[language]]
name = "mermaid"
scope = "source.mermaid"
injection-regex = "mermaid"
file-types = ["mermaid"]
roots = []
comment-token = "%%"
indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "mermaid"
source = { git = "https://github.com/monaqa/tree-sitter-mermaid", rev = "d787c66276e7e95899230539f556e8b83ee16f6d" }
[[language]]
name = "matlab"
scope = "source.m"
file-types = ["m"]
comment-token = "%"
shebangs = ["octave-cli", "matlab"]
roots = []
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "matlab"
source = { git = "https://github.com/mstanciu552/tree-sitter-matlab", rev = "2d5d3d5193718a86477d4335aba5b34e79147326" }

@ -0,0 +1 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" viewBox="663.38 37.57 575.35 903.75"> <g transform="matrix(1,0,0,1,-31352.7,-1817.25)"> <g transform="matrix(1,0,0,1,31062.7,-20.8972)"> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1083.58,1875.72L1635.06,2194.12C1649.8,2202.63 1658.88,2218.37 1658.88,2235.39C1658.88,2264.98 1658.88,2311.74 1658.88,2341.33C1658.88,2349.84 1656.61,2358.03 1652.5,2365.16C1652.5,2365.16 1214.7,2112.4 1107.2,2050.33C1092.58,2041.89 1083.58,2026.29 1083.58,2009.41C1083.58,1963.5 1083.58,1875.72 1083.58,1875.72Z" style="fill:#706bc8;"></path> </g> <g transform="matrix(1,0,0,1,-130.173,0.00185558)"> <path d="M1635.26,2604.84C1649.88,2613.28 1658.88,2628.87 1658.88,2645.75C1658.88,2691.67 1658.88,2779.44 1658.88,2779.44L1107.41,2461.05C1092.66,2452.53 1083.58,2436.8 1083.58,2419.78C1083.58,2390.19 1083.58,2343.42 1083.58,2313.84C1083.58,2305.32 1085.85,2297.13 1089.96,2290.01C1089.96,2290.01 1527.76,2542.77 1635.26,2604.84Z" style="fill:#55c5e4;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1432.56C785.214,1435.55 780.717,1439.9 777.509,1445.46C767.862,1462.16 773.473,1483.76 790.004,1493.59L789.998,1493.59L761.173,1476.95C746.427,1468.44 737.344,1452.71 737.344,1435.68C737.344,1406.09 737.344,1359.33 737.344,1329.74C737.344,1312.71 746.427,1296.98 761.173,1288.47L1259.59,1000.74L1259.83,1000.6C1264.92,997.617 1269.33,993.314 1272.48,987.844C1282.13,971.136 1276.52,949.544 1259.99,939.707L1260,939.707L1288.82,956.349C1303.57,964.862 1312.65,980.595 1312.65,997.622C1312.65,1027.21 1312.65,1073.97 1312.65,1103.56C1312.65,1120.59 1303.57,1136.32 1288.82,1144.83L1259.19,1161.94L1259.59,1161.68L790.407,1432.56Z" style="fill:#84ddea;"></path> </g> <g transform="matrix(1,0,0,1,216.062,984.098)"> <path d="M790.407,1686.24C785.214,1689.23 780.717,1693.58 777.509,1699.13C767.862,1715.84 773.473,1737.43 790.004,1747.27L789.998,1747.27L761.173,1730.63C746.427,1722.12 737.344,1706.38 737.344,1689.36C737.344,1659.77 737.344,1613.01 737.344,1583.42C737.344,1566.39 746.427,1550.66 761.173,1542.15L1259.59,1254.42L1259.83,1254.28C1264.92,1251.29 1269.33,1246.99 1272.48,1241.52C1282.13,1224.81 1276.52,1203.22 1259.99,1193.38L1260,1193.38L1288.82,1210.03C1303.57,1218.54 1312.65,1234.27 1312.65,1251.3C1312.65,1280.89 1312.65,1327.65 1312.65,1357.24C1312.65,1374.26 1303.57,1390 1288.82,1398.51L1259.19,1415.61L1259.59,1415.36L790.407,1686.24Z" style="fill:#997bc8;"></path></g></g></g> </svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
xml:space="preserve"
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
viewBox="663.38 37.57 2087.006 903.71997"
id="svg22"
sodipodi:docname="logo_dark.svg"
width="2087.0059"
height="903.71997"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs26"><rect
x="713.02588"
y="-304.32538"
width="3615.2336"
height="1864.7544"
id="rect14663" /><rect
x="972.073"
y="151.15895"
width="2140.9646"
height="684.86273"
id="rect447" /><rect
x="897.0401"
y="217.45384"
width="837.72321"
height="631.59924"
id="rect435" /><rect
x="825.67834"
y="157.61452"
width="1496.2448"
height="861.45544"
id="rect429" /><rect
x="798.3819"
y="-42.157242"
width="2236.0837"
height="945.90723"
id="rect315" /><rect
x="661.30237"
y="48.087799"
width="769.15619"
height="828.46844"
id="rect309" /></defs><sodipodi:namedview
id="namedview24"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.28409405"
inkscape:cx="1904.2989"
inkscape:cy="633.59299"
inkscape:window-width="1908"
inkscape:window-height="2075"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="svg22" /> <g
transform="translate(-31352.726,-1817.2547)"
id="g20"> <g
transform="translate(31062.7,-20.8972)"
id="g18"> <g
transform="translate(-130.173,0.00185558)"
id="g4"> <path
d="m 1083.58,1875.72 551.48,318.4 c 14.74,8.51 23.82,24.25 23.82,41.27 0,29.59 0,76.35 0,105.94 0,8.51 -2.27,16.7 -6.38,23.83 0,0 -437.8,-252.76 -545.3,-314.83 -14.62,-8.44 -23.62,-24.04 -23.62,-40.92 0,-45.91 0,-133.69 0,-133.69 z"
style="fill:#706bc8"
id="path2" /> </g> <g
transform="translate(-130.173,0.00185558)"
id="g8"> <path
d="m 1635.26,2604.84 c 14.62,8.44 23.62,24.03 23.62,40.91 0,45.92 0,133.69 0,133.69 l -551.47,-318.39 c -14.75,-8.52 -23.83,-24.25 -23.83,-41.27 0,-29.59 0,-76.36 0,-105.94 0,-8.52 2.27,-16.71 6.38,-23.83 0,0 437.8,252.76 545.3,314.83 z"
style="fill:#55c5e4"
id="path6" /> </g> <g
transform="translate(216.062,984.098)"
id="g12"> <path
d="m 790.407,1432.56 c -5.193,2.99 -9.69,7.34 -12.898,12.9 -9.647,16.7 -4.036,38.3 12.495,48.13 h -0.006 l -28.825,-16.64 c -14.746,-8.51 -23.829,-24.24 -23.829,-41.27 0,-29.59 0,-76.35 0,-105.94 0,-17.03 9.083,-32.76 23.829,-41.27 l 498.417,-287.73 0.24,-0.14 c 5.09,-2.983 9.5,-7.286 12.65,-12.756 9.65,-16.708 4.04,-38.3 -12.49,-48.137 h 0.01 l 28.82,16.642 c 14.75,8.513 23.83,24.246 23.83,41.273 0,29.588 0,76.348 0,105.938 0,17.03 -9.08,32.76 -23.83,41.27 l -29.63,17.11 0.4,-0.26 z"
style="fill:#84ddea"
id="path10" /> </g> <g
transform="translate(216.062,984.098)"
id="g16"> <path
d="m 790.407,1686.24 c -5.193,2.99 -9.69,7.34 -12.898,12.89 -9.647,16.71 -4.036,38.3 12.495,48.14 h -0.006 l -28.825,-16.64 c -14.746,-8.51 -23.829,-24.25 -23.829,-41.27 0,-29.59 0,-76.35 0,-105.94 0,-17.03 9.083,-32.76 23.829,-41.27 l 498.417,-287.73 0.24,-0.14 c 5.09,-2.99 9.5,-7.29 12.65,-12.76 9.65,-16.71 4.04,-38.3 -12.49,-48.14 h 0.01 l 28.82,16.65 c 14.75,8.51 23.83,24.24 23.83,41.27 0,29.59 0,76.35 0,105.94 0,17.02 -9.08,32.76 -23.83,41.27 l -29.63,17.1 0.4,-0.25 z"
style="fill:#997bc8"
id="path14" /></g></g></g> <text
xml:space="preserve"
transform="translate(663.354,37.565425)"
id="text307"
style="font-size:4px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect309);display:inline;fill:#006400;stroke:#006400;stroke-width:2.66667" /><g
aria-label="Helix"
transform="matrix(1.3113898,0,0,1.3113898,142.0244,48.21073)"
id="text445"
style="font-size:4px;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect447);display:inline;fill:#f0f6fc;stroke:#f0f6fc;stroke-width:2.66687;stroke-opacity:1;fill-opacity:1"><path
d="m 1242.0723,515.10828 h -60.4 v -123.2 h -113.2 v 123.2 h -60.4 v -285.6 h 60.4 v 112 h 113.2 v -112 h 60.4 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
id="path14794" /><path
d="m 1399.272,292.70828 q 30.4,0 52,11.6 22,11.6 34,33.6 12,22 12,54 v 28.8 h -140.8 q 0.8,25.2 14.8,39.6 14.4,14.4 39.6,14.4 21.2,0 38.4,-4 17.2,-4.4 35.6,-13.2 v 46 q -16,8 -34,11.6 -17.6,4 -42.8,4 -32.8,0 -58,-12 -25.2,-12.4 -39.6,-37.2 -14.4,-24.8 -14.4,-62.4 0,-38.4 12.8,-63.6 13.2,-25.6 36.4,-38.4 23.2,-12.8 54,-12.8 z m 0.4,42.4 q -17.2,0 -28.8,11.2 -11.2,11.2 -13.2,34.8 h 83.6 q 0,-13.2 -4.8,-23.6 -4.4,-10.4 -13.6,-16.4 -9.2,-6 -23.2,-6 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
id="path14796" /><path
d="m 1605.2719,515.10828 h -59.6 v -304 h 59.6 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
id="path14798" /><path
d="m 1727.272,296.70828 v 218.4 h -59.6 v -218.4 z m -29.6,-85.6 q 13.2,0 22.8,6.4 9.6,6 9.6,22.8 0,16.4 -9.6,22.8 -9.6,6.4 -22.8,6.4 -13.6,0 -23.2,-6.4 -9.2,-6.4 -9.2,-22.8 0,-16.8 9.2,-22.8 9.6,-6.4 23.2,-6.4 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
id="path14800" /><path
d="m 1834.4721,403.50828 -70.4,-106.8 h 67.6 l 42.4,69.6 42.8,-69.6 h 67.6 l -71.2,106.8 74.4,111.6 h -67.6 l -46,-74.8 -46,74.8 h -67.6 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700;stroke:#f0f6fc;stroke-opacity:1;fill:#f0f6fc;fill-opacity:1"
id="path14802" /></g><text
xml:space="preserve"
transform="translate(663.38,37.570044)"
id="text14661"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:997.723px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, @wght=700';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 700;white-space:pre;shape-inside:url(#rect14663);display:inline;fill:#2a292f;fill-opacity:1;stroke:#2a292f;stroke-width:6.652;stroke-dasharray:none;stroke-opacity:1" /></svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
xml:space="preserve"
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
viewBox="663.38 37.57 2087.006 903.71997"
id="svg22"
sodipodi:docname="logo_light.svg"
width="2087.0059"
height="903.71997"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs26"><rect
x="713.02588"
y="-304.32538"
width="3615.2336"
height="1864.7544"
id="rect14663" /><rect
x="972.073"
y="151.15895"
width="2140.9646"
height="684.86273"
id="rect447" /><rect
x="897.0401"
y="217.45384"
width="837.72321"
height="631.59924"
id="rect435" /><rect
x="825.67834"
y="157.61452"
width="1496.2448"
height="861.45544"
id="rect429" /><rect
x="798.3819"
y="-42.157242"
width="2236.0837"
height="945.90723"
id="rect315" /><rect
x="661.30237"
y="48.087799"
width="769.15619"
height="828.46844"
id="rect309" /></defs><sodipodi:namedview
id="namedview24"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.28409405"
inkscape:cx="1904.2989"
inkscape:cy="633.59299"
inkscape:window-width="1908"
inkscape:window-height="2075"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="svg22" /> <g
transform="translate(-31352.726,-1817.2547)"
id="g20"> <g
transform="translate(31062.7,-20.8972)"
id="g18"> <g
transform="translate(-130.173,0.00185558)"
id="g4"> <path
d="m 1083.58,1875.72 551.48,318.4 c 14.74,8.51 23.82,24.25 23.82,41.27 0,29.59 0,76.35 0,105.94 0,8.51 -2.27,16.7 -6.38,23.83 0,0 -437.8,-252.76 -545.3,-314.83 -14.62,-8.44 -23.62,-24.04 -23.62,-40.92 0,-45.91 0,-133.69 0,-133.69 z"
style="fill:#706bc8"
id="path2" /> </g> <g
transform="translate(-130.173,0.00185558)"
id="g8"> <path
d="m 1635.26,2604.84 c 14.62,8.44 23.62,24.03 23.62,40.91 0,45.92 0,133.69 0,133.69 l -551.47,-318.39 c -14.75,-8.52 -23.83,-24.25 -23.83,-41.27 0,-29.59 0,-76.36 0,-105.94 0,-8.52 2.27,-16.71 6.38,-23.83 0,0 437.8,252.76 545.3,314.83 z"
style="fill:#55c5e4"
id="path6" /> </g> <g
transform="translate(216.062,984.098)"
id="g12"> <path
d="m 790.407,1432.56 c -5.193,2.99 -9.69,7.34 -12.898,12.9 -9.647,16.7 -4.036,38.3 12.495,48.13 h -0.006 l -28.825,-16.64 c -14.746,-8.51 -23.829,-24.24 -23.829,-41.27 0,-29.59 0,-76.35 0,-105.94 0,-17.03 9.083,-32.76 23.829,-41.27 l 498.417,-287.73 0.24,-0.14 c 5.09,-2.983 9.5,-7.286 12.65,-12.756 9.65,-16.708 4.04,-38.3 -12.49,-48.137 h 0.01 l 28.82,16.642 c 14.75,8.513 23.83,24.246 23.83,41.273 0,29.588 0,76.348 0,105.938 0,17.03 -9.08,32.76 -23.83,41.27 l -29.63,17.11 0.4,-0.26 z"
style="fill:#84ddea"
id="path10" /> </g> <g
transform="translate(216.062,984.098)"
id="g16"> <path
d="m 790.407,1686.24 c -5.193,2.99 -9.69,7.34 -12.898,12.89 -9.647,16.71 -4.036,38.3 12.495,48.14 h -0.006 l -28.825,-16.64 c -14.746,-8.51 -23.829,-24.25 -23.829,-41.27 0,-29.59 0,-76.35 0,-105.94 0,-17.03 9.083,-32.76 23.829,-41.27 l 498.417,-287.73 0.24,-0.14 c 5.09,-2.99 9.5,-7.29 12.65,-12.76 9.65,-16.71 4.04,-38.3 -12.49,-48.14 h 0.01 l 28.82,16.65 c 14.75,8.51 23.83,24.24 23.83,41.27 0,29.59 0,76.35 0,105.94 0,17.02 -9.08,32.76 -23.83,41.27 l -29.63,17.1 0.4,-0.25 z"
style="fill:#997bc8"
id="path14" /></g></g></g> <text
xml:space="preserve"
transform="translate(663.354,37.565425)"
id="text307"
style="font-size:4px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect309);display:inline;fill:#006400;stroke:#006400;stroke-width:2.66667" /><g
aria-label="Helix"
transform="matrix(1.3113898,0,0,1.3113898,142.0244,48.21073)"
id="text445"
style="font-size:4px;-inkscape-font-specification:'sans-serif, Normal';white-space:pre;shape-inside:url(#rect447);display:inline;fill:#2a292f;stroke:#2a292f;stroke-width:2.66687"><path
d="m 1242.0723,515.10828 h -60.4 v -123.2 h -113.2 v 123.2 h -60.4 v -285.6 h 60.4 v 112 h 113.2 v -112 h 60.4 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
id="path14794" /><path
d="m 1399.272,292.70828 q 30.4,0 52,11.6 22,11.6 34,33.6 12,22 12,54 v 28.8 h -140.8 q 0.8,25.2 14.8,39.6 14.4,14.4 39.6,14.4 21.2,0 38.4,-4 17.2,-4.4 35.6,-13.2 v 46 q -16,8 -34,11.6 -17.6,4 -42.8,4 -32.8,0 -58,-12 -25.2,-12.4 -39.6,-37.2 -14.4,-24.8 -14.4,-62.4 0,-38.4 12.8,-63.6 13.2,-25.6 36.4,-38.4 23.2,-12.8 54,-12.8 z m 0.4,42.4 q -17.2,0 -28.8,11.2 -11.2,11.2 -13.2,34.8 h 83.6 q 0,-13.2 -4.8,-23.6 -4.4,-10.4 -13.6,-16.4 -9.2,-6 -23.2,-6 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
id="path14796" /><path
d="m 1605.2719,515.10828 h -59.6 v -304 h 59.6 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
id="path14798" /><path
d="m 1727.272,296.70828 v 218.4 h -59.6 v -218.4 z m -29.6,-85.6 q 13.2,0 22.8,6.4 9.6,6 9.6,22.8 0,16.4 -9.6,22.8 -9.6,6.4 -22.8,6.4 -13.6,0 -23.2,-6.4 -9.2,-6.4 -9.2,-22.8 0,-16.8 9.2,-22.8 9.6,-6.4 23.2,-6.4 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
id="path14800" /><path
d="m 1834.4721,403.50828 -70.4,-106.8 h 67.6 l 42.4,69.6 42.8,-69.6 h 67.6 l -71.2,106.8 74.4,111.6 h -67.6 l -46,-74.8 -46,74.8 h -67.6 z"
style="font-size:400px;-inkscape-font-specification:'sans-serif, @wght=700';font-variation-settings:'wght' 700"
id="path14802" /></g><text
xml:space="preserve"
transform="translate(663.38,37.570044)"
id="text14661"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:997.723px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, @wght=700';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;font-variation-settings:'wght' 700;white-space:pre;shape-inside:url(#rect14663);display:inline;fill:#2a292f;fill-opacity:1;stroke:#2a292f;stroke-width:6.652;stroke-dasharray:none;stroke-opacity:1" /></svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

@ -0,0 +1,11 @@
[
(function_definition)
(if_statement)
(for_statement)
(case_statement)
(pipeline)
] @indent
[
"}"
] @outdent

@ -0,0 +1,47 @@
[
(string_type)
(preamble_type)
(entry_type)
] @keyword
[
(junk)
(comment)
] @comment
[
"="
"#"
] @operator
(command) @function.builtin
(number) @constant.numeric
(field
name: (identifier) @variable.builtin)
(token
(identifier) @variable.parameter)
[
(brace_word)
(quote_word)
] @string
[
(key_brace)
(key_paren)
] @attribute
(string
name: (identifier) @constant)
[
"{"
"}"
"("
")"
] @punctuation.bracket
"," @punctuation.delimiter

@ -0,0 +1,66 @@
[
"class"
"struct"
"module"
"def"
"alias"
"do"
"end"
"require"
"include"
"extend"
] @keyword
[
"[" "]"
"(" ")"
"{" "}"
] @punctuation.bracket
(operator) @operator
(comment) @comment
; literals
(nil) @constant.builtin
(bool) @constant.builtin.boolean
(integer) @constant.numeric.integer
(float) @constant.numeric.float
[
(string)
(char)
(commandLiteral)
] @string
(symbol) @string.special.symbol
(regex) @string.special.regex
; variables
(local_variable) @variable
[
(instance_variable)
(class_variable)
] @variable.other.member
(constant) @constant
; type defintitions
(type_identifier) @constructor
; method definition/call
(identifier) @function.method
; types
(generic_type) @type
(union_type) @type
(type_identifier) @type

@ -1,64 +1,85 @@
(comment) @comment (comment) @comment
(tag_name) @tag [
(nesting_selector) @tag (tag_name)
(universal_selector) @tag (nesting_selector)
(universal_selector)
] @tag
"~" @operator [
">" @operator "~"
"+" @operator ">"
"-" @operator "+"
"*" @operator "-"
"/" @operator "*"
"=" @operator "/"
"^=" @operator "="
"|=" @operator "^="
"~=" @operator "|="
"$=" @operator "~="
"*=" @operator "$="
"*="
] @operator
"and" @operator [
"or" @operator "and"
"not" @operator "not"
"only" @operator "only"
"or"
(attribute_selector (plain_value) @string) ] @keyword.operator
(pseudo_element_selector (tag_name) @attribute)
(pseudo_class_selector (class_name) @attribute)
(class_name) @variable.other.member
(id_name) @variable.other.member
(namespace_name) @variable.other.member
(property_name) @variable.other.member
(feature_name) @variable.other.member
(attribute_name) @attribute
(function_name) @function
((property_name) @variable ((property_name) @variable
(#match? @variable "^--")) (#match? @variable "^--"))
((plain_value) @variable ((plain_value) @variable
(#match? @variable "^--")) (#match? @variable "^--"))
"@media" @keyword (attribute_name) @attribute
"@import" @keyword (class_name) @label
"@charset" @keyword (feature_name) @variable.other.member
"@namespace" @keyword (function_name) @function
"@supports" @keyword (id_name) @label
"@keyframes" @keyword (namespace_name) @namespace
(at_keyword) @keyword (property_name) @variable.other.member
(to) @keyword
(from) @keyword [
(important) @keyword "@charset"
"@import"
"@keyframes"
"@media"
"@namespace"
"@supports"
(at_keyword)
(from)
(important)
(to)
] @keyword
[
"#"
"."
] @punctuation
(string_value) @string (string_value) @string
((color_value) "#") @string.special
(color_value) @string.special (color_value) @string.special
(integer_value) @constant.numeric.integer (integer_value) @constant.numeric.integer
(float_value) @constant.numeric.float (float_value) @constant.numeric.float
(unit) @type
"#" @punctuation.delimiter [
"," @punctuation.delimiter ")"
":" @punctuation.delimiter "("
"["
"]"
"{"
"}"
] @punctuation.bracket
[
","
";"
":"
"::"
] @punctuation.delimiter
(plain_value) @constant

@ -1,5 +1,9 @@
; Function calls ; Function calls
(call_expression
function: (identifier) @function.builtin
(match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$"))
(call_expression (call_expression
function: (identifier) @function) function: (identifier) @function)
@ -24,6 +28,9 @@
(parameter_declaration (identifier) @variable.parameter) (parameter_declaration (identifier) @variable.parameter)
(variadic_parameter_declaration (identifier) @variable.parameter) (variadic_parameter_declaration (identifier) @variable.parameter)
((type_identifier) @type.builtin
(match? @type.builtin "^(any|bool|byte|comparable|complex128|complex64|error|float32|float64|int|int16|int32|int64|int8|rune|string|uint|uint16|uint32|uint64|uint8|uintptr)$"))
(type_identifier) @type (type_identifier) @type
(field_identifier) @variable.other.member (field_identifier) @variable.other.member
(identifier) @variable (identifier) @variable

@ -5,7 +5,7 @@
(type_spec) (type_spec)
(func_literal) (func_literal)
(literal_value) (literal_value)
(element) (literal_element)
(keyed_element) (keyed_element)
(expression_case) (expression_case)
(default_case) (default_case)
@ -16,6 +16,7 @@
(block) (block)
(type_switch_statement) (type_switch_statement)
(expression_switch_statement) (expression_switch_statement)
(var_declaration)
] @indent ] @indent
[ [

@ -0,0 +1,13 @@
(comment) @comment.inside
[
(adt)
(decl_type)
(newtype)
] @class.around
((signature)? (function rhs:(_) @function.inside)) @function.around
(exp_lambda) @function.around
(adt (type_variable) @parameter.inside)
(patterns (_) @parameter.inside)

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

@ -5,7 +5,10 @@
(language) @injection.language) (language) @injection.language)
(code_fence_content) @injection.content (#set! injection.include-unnamed-children)) (code_fence_content) @injection.content (#set! injection.include-unnamed-children))
((html_block) @injection.content (#set! injection.language "html") (#set! injection.include-unnamed-children)) ((html_block) @injection.content
(#set! injection.language "html")
(#set! injection.include-unnamed-children)
(#set! injection.combined))
((pipe_table_cell) @injection.content (#set! injection.language "markdown.inline") (#set! injection.include-unnamed-children)) ((pipe_table_cell) @injection.content (#set! injection.language "markdown.inline") (#set! injection.include-unnamed-children))

@ -0,0 +1,97 @@
; highlights.scm
function_keyword: (function_keyword) @keyword.function
(function_definition
function_name: (identifier) @function
(end) @function)
(parameter_list (identifier) @variable.parameter)
[
"if"
"elseif"
"else"
"switch"
"case"
"otherwise"
] @keyword.control.conditional
(if_statement (end) @keyword.control.conditional)
(switch_statement (end) @keyword.control.conditional)
["for" "while"] @keyword.control.repeat
(for_statement (end) @keyword.control.repeat)
(while_statement (end) @keyword.control.repeat)
["try" "catch"] @keyword.control.exception
(try_statement (end) @keyword.control.exception)
(function_definition end: (end) @keyword)
["return" "break" "continue"] @keyword.return
(
(identifier) @constant.builtin
(#any-of? @constant.builtin "true" "false")
)
(
(identifier) @constant.builtin
(#eq? @constant.builtin "end")
)
;; Punctuations
[";" ","] @punctuation.special
(argument_list "," @punctuation.delimiter)
(vector_definition ["," ";"] @punctuation.delimiter)
(cell_definition ["," ";"] @punctuation.delimiter)
":" @punctuation.delimiter
(parameter_list "," @punctuation.delimiter)
(return_value "," @punctuation.delimiter)
; ;; Brackets
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
;; Operators
"=" @operator
(operation [ ">"
"<"
"=="
"<="
">="
"=<"
"=>"
"~="
"*"
".*"
"/"
"\\"
"./"
"^"
".^"
"+"] @operator)
;; boolean operator
[
"&&"
"||"
] @operator
;; Number
(number) @constant.numeric
;; String
(string) @string
;; Comment
(comment) @comment

@ -0,0 +1,187 @@
[
"sequenceDiagram"
"classDiagram"
"classDiagram-v2"
"stateDiagram"
"stateDiagram-v2"
"gantt"
"pie"
"flowchart"
"erdiagram"
"participant"
"as"
"activate"
"deactivate"
"note "
"over"
"link"
"links"
; "left of"
; "right of"
"properties"
"details"
"title"
"loop"
"rect"
"opt"
"alt"
"else"
"par"
"and"
"end"
(sequence_stmt_autonumber)
(note_placement_left)
(note_placement_right)
"class"
"state "
"dateformat"
"inclusiveenddates"
"topaxis"
"axisformat"
"includes"
"excludes"
"todaymarker"
"title"
"section"
"direction"
"subgraph"
] @keyword
[
(comment)
] @comment
(flow_vertex_id) @type
(flow_arrow_text) @label
(flow_text_literal) @string
[
":"
(sequence_signal_plus_sign)
(sequence_signal_minus_sign)
(class_visibility_public)
(class_visibility_private)
(class_visibility_protected)
(class_visibility_internal)
(state_division)
] @punctuation.delimiter
[
"("
")"
"{"
"}"
] @punctuation.bracket
[
"-->"
(solid_arrow)
(dotted_arrow)
(solid_open_arrow)
(dotted_open_arrow)
(solid_cross)
(dotted_cross)
(solid_point)
(dotted_point)
] @operator
[
(class_reltype_aggregation)
(class_reltype_extension)
(class_reltype_composition)
(class_reltype_dependency)
(class_linetype_solid)
(class_linetype_dotted)
"&"
] @operator
(sequence_actor) @variable
(sequence_text) @string
(class_name) @type
(class_label) @string
(class_method_line) @function.method
(state_name) @variable
(gantt_section) @markup.heading
(gantt_task_text) @variable.builtin
(gantt_task_data) @string
[
(class_annotation_line)
(class_stmt_annotation)
(class_generics)
(state_annotation_fork)
(state_annotation_join)
(state_annotation_choice)
] @type
(directive) @keyword.directive
(pie_label) @string
(pie_value) @constant.numeric
[
(flowchart_direction_lr)
(flowchart_direction_rl)
(flowchart_direction_tb)
(flowchart_direction_bt)
] @constant
(flow_vertex_id) @variable
[
(flow_link_arrow)
(flow_link_arrow_start)
] @operator
(flow_link_arrowtext "|" @punctuation.bracket)
(flow_vertex_square [ "[" "]" ] @punctuation.bracket )
(flow_vertex_circle ["((" "))"] @punctuation.bracket )
(flow_vertex_ellipse ["(-" "-)"] @punctuation.bracket )
(flow_vertex_stadium ["([" "])"] @punctuation.bracket )
(flow_vertex_subroutine ["[[" "]]"] @punctuation.bracket )
(flow_vertex_rect ["[|" "|]"] @punctuation.bracket )
(flow_vertex_cylinder ["[(" ")]"] @punctuation.bracket )
(flow_vertex_round ["(" ")"] @punctuation.bracket )
(flow_vertex_diamond ["{" "}"] @punctuation.bracket )
(flow_vertex_hexagon ["{{" "}}"] @punctuation.bracket )
(flow_vertex_odd [">" "]"] @punctuation.bracket )
(flow_vertex_trapezoid ["[/" "\\]"] @punctuation.bracket )
(flow_vertex_inv_trapezoid ["[\\" "/]"] @punctuation.bracket )
(flow_vertex_leanright ["[/" "/]"] @punctuation.bracket )
(flow_vertex_leanleft ["[\\" "\\]"] @punctuation.bracket )
(flow_stmt_subgraph ["[" "]"] @punctuation.bracket )
[
(er_cardinarity_zero_or_one)
(er_cardinarity_zero_or_more)
(er_cardinarity_one_or_more)
(er_cardinarity_only_one)
(er_reltype_non_identifying)
(er_reltype_identifying)
] @operator
(er_entity_name) @variable
(er_attribute_type) @type
(er_attribute_name) @variable
[
(er_attribute_key_type_pk)
(er_attribute_key_type_fk)
] @keyword
(er_attribute_comment) @string

@ -1,3 +1,6 @@
((comment) @injection.content
(#set! injection.language "comment"))
; mark arbitary languages with a comment ; mark arbitary languages with a comment
((((comment) @injection.language) . ((((comment) @injection.language) .
(indented_string_expression (string_fragment) @injection.content)) (indented_string_expression (string_fragment) @injection.content))

@ -5,8 +5,6 @@
; overrides are unnecessary. ; overrides are unnecessary.
; ------- ; -------
; ------- ; -------
; Types ; Types
; ------- ; -------
@ -47,7 +45,8 @@
"'" @label "'" @label
(identifier) @label) (identifier) @label)
(loop_label (loop_label
(identifier) @type) "'" @label
(identifier) @label)
; --- ; ---
; Punctuation ; Punctuation
@ -104,8 +103,6 @@
(closure_parameters (closure_parameters
(identifier) @variable.parameter) (identifier) @variable.parameter)
; ------- ; -------
; Keywords ; Keywords
; ------- ; -------
@ -131,9 +128,7 @@
[ [
"break" "break"
"continue" "continue"
"return" "return"
"await" "await"
] @keyword.control.return ] @keyword.control.return
@ -156,10 +151,7 @@
"trait" "trait"
"for" "for"
"unsafe"
"default" "default"
"macro_rules!"
"async" "async"
] @keyword ] @keyword
@ -167,13 +159,13 @@
"struct" "struct"
"enum" "enum"
"union" "union"
"type" "type"
] @keyword.storage.type ] @keyword.storage.type
"let" @keyword.storage "let" @keyword.storage
"fn" @keyword.function "fn" @keyword.function
"unsafe" @keyword.special
"macro_rules!" @function.macro
(mutable_specifier) @keyword.storage.modifier.mut (mutable_specifier) @keyword.storage.modifier.mut
@ -204,11 +196,11 @@
(call_expression (call_expression
function: [ function: [
((identifier) @type.variant ((identifier) @type.enum.variant
(#match? @type.variant "^[A-Z]")) (#match? @type.enum.variant "^[A-Z]"))
(scoped_identifier (scoped_identifier
name: ((identifier) @type.variant name: ((identifier) @type.enum.variant
(#match? @type.variant "^[A-Z]"))) (#match? @type.enum.variant "^[A-Z]")))
]) ])
; --- ; ---
@ -239,7 +231,12 @@
((identifier) @type ((identifier) @type
(#match? @type "^[A-Z]")) (#match? @type "^[A-Z]"))
(attribute
(identifier) @_macro
arguments: (token_tree (identifier) @constant.numeric.integer)
(#eq? @_macro "derive")
)
@special
; ------- ; -------
; Functions ; Functions
@ -271,6 +268,7 @@
; --- ; ---
; Macros ; Macros
; --- ; ---
(attribute (attribute
(identifier) @function.macro) (identifier) @function.macro)
(attribute (attribute
@ -296,8 +294,6 @@
(metavariable) @variable.parameter (metavariable) @variable.parameter
(fragment_specifier) @type (fragment_specifier) @type
; ------- ; -------
; Operators ; Operators
; ------- ; -------
@ -343,8 +339,6 @@
"'" "'"
] @operator ] @operator
; ------- ; -------
; Paths ; Paths
; ------- ; -------
@ -375,8 +369,6 @@
(scoped_type_identifier (scoped_type_identifier
path: (identifier) @namespace) path: (identifier) @namespace)
; ------- ; -------
; Remaining Identifiers ; Remaining Identifiers
; ------- ; -------

@ -2,29 +2,40 @@
(character) @constant.character (character) @constant.character
(boolean) @constant.builtin.boolean (boolean) @constant.builtin.boolean
[(string) (string) @string
(character)] @string
(escape_sequence) @constant.character.escape (escape_sequence) @constant.character.escape
[(comment) (comment) @comment.line
(block_comment) (block_comment) @comment.block
(directive)] @comment (directive) @keyword.directive
[(boolean) ; operators
(character)] @constant
((symbol) @function.builtin ((symbol) @operator
(#match? @function.builtin "^(eqv\\?|eq\\?|equal\\?)")) ; TODO (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
; keywords ; keywords
((symbol) @keyword.conditional (list
(#match? @keyword.conditional "^(if|cond|case|when|unless)$")) .
((symbol) @keyword.conditional
(#match? @keyword.conditional "^(if|cond|case|when|unless)$"
)))
((symbol) @keyword (list
(#match? @keyword .
"^(define|lambda|begin|do|define-syntax|and|or|if|cond|case|when|unless|else|=>|let|let*|let-syntax|let-values|let*-values|letrec|letrec*|letrec-syntax|set!|syntax-rules|identifier-syntax|quote|unquote|quote-splicing|quasiquote|unquote-splicing|delay|assert|library|export|import|rename|only|except|prefix)$")) (symbol) @keyword
(#match? @keyword
"^(define-syntax|let\\*|lambda|λ|case|=>|quote-splicing|unquote-splicing|set!|let|letrec|letrec-syntax|let-values|let\\*-values|do|else|define|cond|syntax-rules|unquote|begin|quote|let-syntax|and|if|quasiquote|letrec|delay|or|when|unless|identifier-syntax|assert|library|export|import|rename|only|except|prefix)$"
))
(list
.
(symbol) @function.builtin
(#match? @function.builtin
"^(caar|cadr|call-with-input-file|call-with-output-file|cdar|cddr|list|open-input-file|open-output-file|with-input-from-file|with-output-to-file|\\*|\\+|-|/|<|<=|=|>|>=|abs|acos|angle|append|apply|asin|assoc|assq|assv|atan|boolean\\?|caaaar|caaadr|caaar|caadar|caaddr|caadr|cadaar|cadadr|cadar|caddar|cadddr|caddr|call-with-current-continuation|call-with-values|car|cdaaar|cdaadr|cdaar|cdadar|cdaddr|cdadr|cddaar|cddadr|cddar|cdddar|cddddr|cdddr|cdr|ceiling|char->integer|char-alphabetic\\?|char-ci<=\\?|char-ci<\\?|char-ci=\\?|char-ci>=\\?|char-ci>\\?|char-downcase|char-lower-case\\?|char-numeric\\?|char-ready\\?|char-upcase|char-upper-case\\?|char-whitespace\\?|char<=\\?|char<\\?|char=\\?|char>=\\?|char>\\?|char\\?|close-input-port|close-output-port|complex\\?|cons|cos|current-error-port|current-input-port|current-output-port|denominator|display|dynamic-wind|eof-object\\?|eq\\?|equal\\?|eqv\\?|eval|even\\?|exact->inexact|exact\\?|exp|expt|floor|flush-output|for-each|force|gcd|imag-part|inexact->exact|inexact\\?|input-port\\?|integer->char|integer\\?|interaction-environment|lcm|length|list->string|list->vector|list-ref|list-tail|list\\?|load|log|magnitude|make-polar|make-rectangular|make-string|make-vector|map|max|member|memq|memv|min|modulo|negative\\?|newline|not|null-environment|null\\?|number->string|number\\?|numerator|odd\\?|output-port\\?|pair\\?|peek-char|positive\\?|procedure\\?|quotient|rational\\?|rationalize|read|read-char|real-part|real\\?|remainder|reverse|round|scheme-report-environment|set-car!|set-cdr!|sin|sqrt|string|string->list|string->number|string->symbol|string-append|string-ci<=\\?|string-ci<\\?|string-ci=\\?|string-ci>=\\?|string-ci>\\?|string-copy|string-fill!|string-length|string-ref|string-set!|string<=\\?|string<\\?|string=\\?|string>=\\?|string>\\?|string\\?|substring|symbol->string|symbol\\?|tan|transcript-off|transcript-on|truncate|values|vector|vector->list|vector-fill!|vector-length|vector-ref|vector-set!|vector\\?|write|write-char|zero\\?)$"
))
; special forms ; special forms
@ -47,26 +58,16 @@
. .
(list (list
(list (list
(symbol) @variable)) (symbol) @variable.parameter))
(#match? @_f (#match? @_f
"^(let|let\\*|let-syntax|let-values|let\\*-values|letrec|letrec\\*|letrec-syntax)$")) "^(let|let\\*|let-syntax|let-values|let\\*-values|letrec|letrec\\*|letrec-syntax)$"))
; operators
(list
.
(symbol) @operator
(#match? @operator "^([+*/<>=-]|(<=)|(>=))$"))
; quote ; quote
(abbreviation
"'" (symbol)) @constant
(list (list
. .
(symbol) @_f (symbol) @_f
(#eq? @_f "quote")) @symbol (#eq? @_f "quote")) @string.symbol
; library ; library
@ -89,12 +90,10 @@
((symbol) @variable.builtin ((symbol) @variable.builtin
(#eq? @variable.builtin "...")) (#eq? @variable.builtin "..."))
(symbol) @variable
((symbol) @variable.builtin ((symbol) @variable.builtin
(#eq? @variable.builtin ".")) (#eq? @variable.builtin "."))
(symbol) @variable (symbol) @variable
["(" ")" "[" "]" "{" "}"] @punctuation.bracket ["(" ")" "[" "]" "{" "}"] @punctuation.bracket

@ -1,4 +1,4 @@
(comment) @comment [(comment) (single_line_comment)] @comment
"~" @operator "~" @operator
">" @operator ">" @operator

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

@ -8,13 +8,37 @@
(raw_text) @injection.content) (raw_text) @injection.content)
(#set! injection.language "javascript")) (#set! injection.language "javascript"))
; <script>
((script_element ((script_element
(start_tag) @_no_lang
(raw_text) @injection.content)
(#not-match? @_no_lang "lang=")
(#set! injection.language "javascript"))
; <script lang="...">
((script_element
(start_tag
(attribute
(attribute_name) @_attr_name
(quoted_attribute_value (attribute_value) @injection.language)))
(raw_text) @injection.content) (raw_text) @injection.content)
(#set! injection.language "javascript")) (#eq? @_attr_name "lang"))
; <style>
((style_element ((style_element
(raw_text) @injection.content) (start_tag) @_no_lang
(#set! injection.language "css")) (raw_text) @injection.content)
(#not-match? @_no_lang "lang=")
(#set! injection.language "css"))
; <style lang="...">
((style_element
(start_tag
(attribute
(attribute_name) @_attr_name
(quoted_attribute_value (attribute_value) @injection.language)))
(raw_text) @injection.content)
(#eq? @_attr_name "lang"))
((comment) @injection.content ((comment) @injection.content
(#set! injection.language "comment")) (#set! injection.language "comment"))

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

Loading…
Cancel
Save