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

@ -2,13 +2,20 @@ name: Release
on:
push:
tags:
- '[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+'
branches:
- 'patch/ci-release-*'
pull_request:
paths:
- '.github/workflows/release.yml'
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/')}}
preview: ${{ !startsWith(github.ref, 'refs/tags/') || github.repository != 'helix-editor/helix' }}
jobs:
fetch-grammars:
@ -19,10 +26,7 @@ jobs:
uses: actions/checkout@v3
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
profile: minimal
override: true
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
@ -40,6 +44,16 @@ jobs:
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
@ -47,17 +61,17 @@ jobs:
build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
include:
- build: x86_64-linux
os: ubuntu-20.04
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-gnu
cross: false
- build: aarch64-linux
os: ubuntu-20.04
os: ubuntu-latest
rust: stable
target: aarch64-unknown-linux-gnu
cross: true
- build: riscv64-linux
os: ubuntu-20.04
os: ubuntu-latest
rust: stable
target: riscv64gc-unknown-linux-gnu
cross: true
@ -67,7 +81,7 @@ jobs:
target: x86_64-apple-darwin
cross: false
- build: x86_64-windows
os: windows-2019
os: windows-latest
rust: stable
target: x86_64-pc-windows-msvc
cross: false
@ -77,6 +91,14 @@ jobs:
target: aarch64-apple-darwin
cross: false
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:
- name: Checkout sources
@ -92,7 +114,7 @@ jobs:
tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources
- name: Install ${{ matrix.rust }} toolchain
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ matrix.rust }}
@ -105,7 +127,24 @@ jobs:
# 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
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
shell: bash
@ -116,11 +155,7 @@ jobs:
EOF
- name: Build release binary
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.cross }}
command: build
args: --release --locked --target ${{ matrix.target }}
run: ${{ env.CARGO }} build --release --locked --target ${{ matrix.target }}
- name: Build AppImage
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)
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-dap",
"helix-loader",
"helix-vcs",
"xtask",
]

@ -1,30 +1,4 @@
# 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
- - -
# Helix
[![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.selection" = { fg = "black", bg = "blue" }
"ui.selection.primary" = { fg = "white", bg = "blue" }
"ui.text.inactive" = { fg = "gray" }
"comment" = { fg = "gray" }
"ui.statusline" = { fg = "black", 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"
default-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` |
| `cursorline` | Highlight all lines with a cursor. | `false` |
| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "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-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` |

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

@ -71,9 +71,6 @@
| `:insert-output` | Run shell command, inserting output before each selection. |
| `:append-output` | Run shell command, appending output after each selection. |
| `: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 |
||||||| 4b1fe367
=======
| `:lsp-restart` | Restarts the LSP server of the current buffer |
>>>>>>> lsp-restart

@ -1,18 +1,18 @@
| Name | Description |
| --- | --- |
| `: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. |
| `: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-all`, `:bca`, `:bcloseall` | Close all buffers, without quitting. |
| `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Close all buffers forcefully (ignoring unsaved changes), without quitting. |
| `:buffer-next`, `:bn`, `:bnext` | Go to next buffer. |
| `:buffer-previous`, `:bp`, `:bprev` | Go to previous buffer. |
| `: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!` | Force close all buffers ignoring unsaved changes without quitting. |
| `:buffer-next`, `:bn`, `:bnext` | Goto next 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 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. |
| `: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.) |
@ -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 forcefully (ignoring unsaved changes). |
| `: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) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme. |
| `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme (show current theme if no name specified). |
| `:clipboard-yank` | Yank main selection into system clipboard. |
| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
@ -42,8 +42,12 @@
| `:show-clipboard-provider` | Show clipboard provider name in status bar. |
| `:change-current-directory`, `:cd` | Change the current working directory. |
| `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
| `:reload` | Discard changes and reload from the source file. |
| `:reload-all` | Discard changes and reload all documents from the source files. |
| `:update` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
| `: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-new`, `:hnew` | Open a scratch buffer in a horizontal split. |
| `: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-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. |
@ -61,13 +65,18 @@
| `:rsort` | Sort ranges in selection in reverse order. |
| `:reflow` | Hard-wrap the current selection of lines to a given width. |
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
| `:config-reload` | Refreshes helix's config. |
| `:config-open` | Open the helix config.toml file. |
| `:config-reload` | Refresh user config. |
| `:config-open` | Open the user config.toml 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. |
| `:pipe` | Pipe each selection to the shell command. |
<<<<<<< HEAD
<<<<<<< HEAD
||||||| f0f295a6
=======
| `:pipe-to` | Pipe each selection to the shell command, ignoring output. |
>>>>>>> origin/master
| `:run-shell-command`, `:sh` | Run a shell command |
||||||| 4b1fe367
=======

@ -111,6 +111,7 @@
| `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into subselections on regex matches | `split_selection` |
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `Alt-_ ` | Merge consecutive selections | `merge_consecutive_selections` |
| `&` | Align selection in columns | `align_selections` |
| `_` | Trim whitespace from the selection | `trim_selections` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
@ -299,7 +300,7 @@ Displays documentation for item under cursor.
| ---- | ----------- |
| `Ctrl-u` | Scroll up |
| `Ctrl-d` | Scroll down |
#### 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` |
| `]f` | Go to next function (**TS**) | `goto_next_function` |
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
| `]c` | Go to next class (**TS**) | `goto_next_class` |
| `[c` | Go to previous class (**TS**) | `goto_prev_class` |
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
| `]o` | Go to next comment (**TS**) | `goto_next_comment` |
| `[o` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]t` | Go to next test (**TS**) | `goto_next_test` |
| `]t` | Go to previous test (**TS**) | `goto_prev_test` |
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]T` | Go to next test (**TS**) | `goto_next_test` |
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
| `]p` | Go to next paragraph | `goto_next_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 below | `add_newline_below` |

@ -39,7 +39,7 @@ injection-regex = "^mylang$"
file-types = ["mylang", "myl"]
comment-token = "#"
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"] }
```
@ -99,6 +99,7 @@ The `language-server` field takes the following keys:
| `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` |
| `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`
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` |
| `curl` |
| `dashed` |
| `dot` |
| `dotted` |
| `double_line` |
@ -313,6 +313,7 @@ These scopes are used for theming the editor interface.
| `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. |
| `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.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |
| `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 |
| `o` | Comment |
| `t` | Test |
| `g` | Change |
> 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

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">
<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 -->
<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>

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

@ -17,7 +17,7 @@ integration = []
[dependencies]
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"
smartstring = "1.0.1"
unicode-segmentation = "1.10"
@ -38,7 +38,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5"
similar = "2.2"
imara-diff = "0.1.0"
encoding_rs = "0.8"

@ -45,4 +45,5 @@ pub struct Diagnostic {
pub code: Option<NumberOrString>,
pub tags: Vec<DiagnosticTag>,
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
/// the steps required to get from `old` to `new`.
pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction {
// `similar` only works on contiguous data, so a `Rope` has
// to be temporarily converted into a `String`.
let old_converted = old.to_string();
let new_converted = new.to_string();
// A timeout is set so after 1 seconds, the algorithm will start
// approximating. This is especially important for big `Rope`s or
// `Rope`s that are extremely dissimilar to each other.
let mut config = similar::TextDiff::configure();
config.timeout(std::time::Duration::from_secs(1));
let diff = config.diff_chars(&old_converted, &new_converted);
// The current position of the change needs to be tracked to
// construct the `Change`s.
let mut pos = 0;
Transaction::change(
old,
diff.ops()
use imara_diff::intern::InternedInput;
use imara_diff::Algorithm;
use ropey::RopeSlice;
use crate::{ChangeSet, Rope, Tendril, Transaction};
/// A `imara_diff::Sink` that builds a `ChangeSet` for a character diff of a hunk
struct CharChangeSetBuilder<'a> {
res: &'a mut ChangeSet,
hunk: &'a InternedInput<char>,
pos: u32,
}
impl imara_diff::Sink for CharChangeSetBuilder<'_> {
type Out = ();
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
self.res.retain((before.start - self.pos) as usize);
self.res.delete(before.len());
self.pos = before.end;
let res = self.hunk.after[after.start as usize..after.end as usize]
.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()
.map(|op| op.as_tag_tuple())
.filter_map(|(tag, old_range, new_range)| {
// `old_pos..pos` is equivalent to `start..end` for where
// the change should be applied.
let old_pos = pos;
pos += old_range.end - old_range.start;
match tag {
// Semantically, inserts and replacements are the same thing.
similar::DiffTag::Insert | similar::DiffTag::Replace => {
// This is the text from the `new` rope that should be
// inserted into `old`.
let text: &str = {
let start = new.char_to_byte(new_range.start);
let end = new.char_to_byte(new_range.end);
&new_converted[start..end]
};
Some((old_pos, pos, Some(text.into())))
.map(|&it| self.file.interner[it].len_chars())
.sum();
self.res.retain(len);
self.pos = before.end;
// do not perform diffs on large hunks
let len_before = before.end - before.start;
let len_after = after.end - after.start;
// Pure insertions/removals do not require a character diff.
// Very large changes are ignored because their character diff is expensive to compute
// TODO adjust heuristic to detect large changes?
if len_before == 0
|| len_after == 0
|| len_after > 5 * len_before
|| 5 * len_after < len_before && len_before > 10
|| len_before + len_after > 200
{
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)),
similar::DiffTag::Equal => None,
} else if after.start == 0 {
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)]
mod tests {
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! {
fn test_compare_ropes(a: String, b: String) -> bool {
let mut old = Rope::from(a);
@ -61,4 +197,25 @@ mod tests {
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 None if there are no changes between the current and given revisions.
pub fn changes_since(&self, revision: usize) -> Option<Transaction> {
use std::cmp::Ordering::*;
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) {
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)
}
}
down_txns.chain(up_txns).reduce(|acc, tx| tx.compose(acc))
}
/// 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);
}
if ancestor.join(".git").is_dir() {
if ancestor.join(".git").exists() {
// Top marker is repo root if not root marker was detected yet
if top_marker.is_none() {
top_marker = Some(ancestor);
@ -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())
}
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 smartstring::SmartString;

@ -495,28 +495,53 @@ impl Selection {
/// Normalizes a `Selection`.
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.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
.ranges
.iter()
.position(|&range| range == primary)
.unwrap();
let mut prev_i = 0;
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]);
self
}
// 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 {
prev_i += 1;
self.ranges[prev_i] = self.ranges[i];
false
}
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
}
@ -1132,6 +1157,52 @@ mod test {
&["", "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]
fn test_selection_contains() {
fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool {

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

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

@ -1,6 +1,26 @@
use std::borrow::Cow;
use std::process::Command;
const VERSION: &str = include_str!("../VERSION");
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!(
"cargo:rustc-env=BUILD_TARGET={}",
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
if !grammar_dir.join(".git").is_dir() {
if !grammar_dir.join(".git").exists() {
git(&grammar_dir, ["init"])?;
}

@ -4,6 +4,8 @@ pub mod grammar;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
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);
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();
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").is_dir() {
if ancestor.join(".git").exists() {
directories.push(ancestor.to_path_buf());
// Don't go higher than repo if we're in one
break;

@ -13,6 +13,7 @@ homepage = "https://helix-editor.com"
[dependencies]
helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" }
anyhow = "1.0"
futures-executor = "0.3"
@ -22,6 +23,6 @@ lsp-types = { version = "0.93", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "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"
which = "4.2"

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

@ -60,7 +60,7 @@ pub enum OffsetEncoding {
pub mod util {
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`].
///
@ -105,16 +105,17 @@ pub mod util {
None
};
// TODO: add support for Diagnostic.data
lsp::Diagnostic::new(
range_to_lsp_range(doc, range, offset_encoding),
lsp::Diagnostic {
range: range_to_lsp_range(doc, range, offset_encoding),
severity,
code,
diag.source.clone(),
diag.message.to_owned(),
None,
source: diag.source.clone(),
message: diag.message.to_owned(),
related_information: None,
tags,
)
data: diag.data.to_owned(),
..Default::default()
}
}
/// Converts [`lsp::Position`] to a position in the document.
@ -198,6 +199,42 @@ pub mod util {
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(
doc: &Rope,
mut edits: Vec<lsp::TextEdit>,
@ -516,6 +553,7 @@ fn start_client(
&ls_config.command,
&ls_config.args,
config.config.clone(),
ls_config.environment.clone(),
&config.roots,
id,
ls_config.timeout,

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

@ -1,30 +1,9 @@
use helix_loader::grammar::{build_grammars, fetch_grammars};
use std::borrow::Cow;
use std::process::Command;
const VERSION: &str = include_str!("../VERSION");
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() {
fetch_grammars().expect("Failed to fetch tree-sitter grammars");
build_grammars(Some(std::env::var("TARGET").unwrap()))
.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);
}
}
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,
// does not affect views without pos since it is at the top
let (view, doc) = current!(editor);
@ -286,16 +290,27 @@ impl Application {
}
#[cfg(feature = "integration")]
fn render(&mut self) {}
async fn render(&mut self) {}
#[cfg(not(feature = "integration"))]
fn render(&mut self) {
async fn render(&mut self) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
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
.terminal
.autoresize()
@ -316,7 +331,7 @@ impl Application {
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
{
self.render();
self.render().await;
self.last_render = Instant::now();
loop {
@ -341,18 +356,18 @@ impl Application {
biased;
Some(event) = input_stream.next() => {
self.handle_terminal_events(event);
self.handle_terminal_events(event).await;
}
Some(signal) = self.signals.next() => {
self.handle_signals(signal).await;
}
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
self.render().await;
}
Some(callback) = self.jobs.wait_futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
self.render().await;
}
event = self.editor.wait_event() => {
let _idle_handled = self.handle_editor_event(event).await;
@ -457,25 +472,25 @@ impl Application {
self.compositor.resize(area);
self.terminal.clear().expect("couldn't clear terminal");
self.render();
self.render().await;
}
signal::SIGUSR1 => {
self.refresh_config();
self.render();
self.render().await;
}
_ => unreachable!(),
}
}
pub fn handle_idle_timeout(&mut self) {
pub async fn handle_idle_timeout(&mut self) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
scroll: None,
};
let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
if should_render {
self.render();
if should_render || self.editor.needs_redraw {
self.render().await;
}
}
@ -548,11 +563,11 @@ impl Application {
match event {
EditorEvent::DocumentSaved(event) => {
self.handle_document_write(event);
self.render();
self.render().await;
}
EditorEvent::ConfigEvent(event) => {
self.handle_config_events(event);
self.render();
self.render().await;
}
EditorEvent::LanguageServerMessage((id, call)) => {
self.handle_language_server_message(call, id).await;
@ -560,19 +575,19 @@ impl Application {
let last = self.editor.language_servers.incoming.is_empty();
if last || self.last_render.elapsed() > LSP_DEADLINE {
self.render();
self.render().await;
self.last_render = Instant::now();
}
}
EditorEvent::DebuggerEvent(payload) => {
let needs_render = self.editor.handle_debugger_message(payload).await;
if needs_render {
self.render();
self.render().await;
}
}
EditorEvent::IdleTimer => {
self.editor.clear_idle_timer();
self.handle_idle_timeout();
self.handle_idle_timeout().await;
#[cfg(feature = "integration")]
{
@ -584,7 +599,10 @@ impl Application {
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 {
editor: &mut self.editor,
jobs: &mut self.jobs,
@ -608,7 +626,7 @@ impl Application {
};
if should_redraw && !self.editor.should_close() {
self.render();
self.render().await;
}
}
@ -770,7 +788,8 @@ impl Application {
severity,
code,
tags,
source: diagnostic.source.clone()
source: diagnostic.source.clone(),
data: diagnostic.data.clone(),
})
})
.collect();

@ -3,6 +3,7 @@ pub(crate) mod lsp;
pub(crate) mod typed;
pub use dap::*;
use helix_vcs::Hunk;
pub use lsp::*;
use tui::text::Spans;
pub use typed::*;
@ -243,6 +244,7 @@ impl MappableCommand {
select_regex, "Select all regex matches inside selections",
split_selection, "Split selections on regex matches",
split_selection_on_newline, "Split selection on newlines",
merge_consecutive_selections, "Merge consecutive selections",
search, "Search for regex pattern",
rsearch, "Reverse search for regex pattern",
search_next, "Select next search match",
@ -309,6 +311,10 @@ impl MappableCommand {
goto_last_diag, "Goto last diagnostic",
goto_next_diag, "Goto next 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_end, "Goto line end",
goto_next_buffer, "Goto next buffer",
@ -403,8 +409,8 @@ impl MappableCommand {
select_textobject_inner, "Select inside object",
goto_next_function, "Goto next function",
goto_prev_function, "Goto previous function",
goto_next_class, "Goto next class",
goto_prev_class, "Goto previous class",
goto_next_class, "Goto next type definition",
goto_prev_class, "Goto previous type definition",
goto_next_parameter, "Goto next parameter",
goto_prev_parameter, "Goto previous parameter",
goto_next_comment, "Goto next comment",
@ -1588,6 +1594,12 @@ fn split_selection_on_newline(cx: &mut Context) {
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)]
fn search_impl(
editor: &mut Editor,
@ -1671,12 +1683,7 @@ fn search_impl(
};
doc.set_selection(view.id, selection);
// TODO: is_cursor_in_view does the same calculation as ensure_cursor_in_view
if view.is_cursor_in_view(doc, 0) {
view.ensure_cursor_in_view(doc, scrolloff);
} else {
align_view(doc, view, Align::Center)
}
view.ensure_cursor_in_view_center(doc, scrolloff);
};
}
@ -2387,8 +2394,8 @@ fn buffer_picker(cx: &mut Context) {
let picker = FilePicker::new(
cx.editor
.documents
.iter()
.map(|(_, doc)| new_meta(doc))
.values()
.map(|doc| new_meta(doc))
.collect(),
(),
|cx, meta, action| {
@ -2475,8 +2482,10 @@ fn jumplist_picker(cx: &mut Context) {
(),
|cx, meta, action| {
cx.editor.switch(meta.id, action);
let config = cx.editor.config();
let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, meta.selection.clone());
view.ensure_cursor_in_view_center(doc, config.scrolloff);
},
|editor, meta| {
let doc = &editor.documents.get(&meta.id)?;
@ -2605,7 +2614,7 @@ async fn make_format_callback(
if let Ok(format) = format {
if doc.version() == doc_version {
apply_transaction(&format, doc, view);
doc.append_changes_to_history(view.id);
doc.append_changes_to_history(view);
doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff);
} else {
@ -2711,62 +2720,7 @@ fn open_above(cx: &mut Context) {
}
fn normal_mode(cx: &mut Context) {
if cx.editor.mode == Mode::Normal {
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);
}
cx.editor.enter_normal_mode();
}
// 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) {
let doc = doc!(cx.editor);
let pos = match doc.diagnostics().first() {
Some(diag) => diag.range.start,
let (view, doc) = current!(cx.editor);
let selection = match doc.diagnostics().first() {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
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) {
let doc = doc!(cx.editor);
let pos = match doc.diagnostics().last() {
Some(diag) => diag.range.start,
let (view, doc) = current!(cx.editor);
let selection = match doc.diagnostics().last() {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
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) {
let editor = &mut cx.editor;
let (view, doc) = current!(editor);
let (view, doc) = current!(cx.editor);
let cursor_pos = doc
.selection(view.id)
@ -2924,17 +2879,16 @@ fn goto_next_diag(cx: &mut Context) {
.find(|diag| diag.range.start > cursor_pos)
.or_else(|| doc.diagnostics().first());
let pos = match diag {
Some(diag) => diag.range.start,
let selection = match diag {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
goto_pos(editor, pos);
doc.set_selection(view.id, selection);
align_view(doc, view, Align::Center);
}
fn goto_prev_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (view, doc) = current!(editor);
let (view, doc) = current!(cx.editor);
let cursor_pos = doc
.selection(view.id)
@ -2948,12 +2902,108 @@ fn goto_prev_diag(cx: &mut Context) {
.find(|diag| diag.range.start < cursor_pos)
.or_else(|| doc.diagnostics().last());
let pos = match diag {
Some(diag) => diag.range.start,
let selection = match diag {
// 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,
};
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 {
@ -2994,6 +3044,11 @@ pub mod insert {
}
fn language_server_completion(cx: &mut Context, ch: char) {
let config = cx.editor.config();
if !config.auto_completion {
return;
}
use helix_lsp::lsp;
// if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor);
@ -3362,7 +3417,7 @@ fn undo(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
for _ in 0..count {
if !doc.undo(view.id) {
if !doc.undo(view) {
cx.editor.set_status("Already at oldest change");
break;
}
@ -3373,7 +3428,7 @@ fn redo(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
for _ in 0..count {
if !doc.redo(view.id) {
if !doc.redo(view) {
cx.editor.set_status("Already at newest change");
break;
}
@ -3385,7 +3440,7 @@ fn earlier(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
for _ in 0..count {
// rather than doing in batch we do this so get error halfway
if !doc.earlier(view.id, UndoKind::Steps(1)) {
if !doc.earlier(view, UndoKind::Steps(1)) {
cx.editor.set_status("Already at oldest change");
break;
}
@ -3397,7 +3452,7 @@ fn later(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
for _ in 0..count {
// rather than doing in batch we do this so get error halfway
if !doc.later(view.id, UndoKind::Steps(1)) {
if !doc.later(view, UndoKind::Steps(1)) {
cx.editor.set_status("Already at newest change");
break;
}
@ -3406,7 +3461,7 @@ fn later(cx: &mut Context) {
fn commit_undo_checkpoint(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
doc.append_changes_to_history(view.id);
doc.append_changes_to_history(view);
}
// Yank / Paste
@ -3718,7 +3773,7 @@ fn replace_selections_with_clipboard_impl(
});
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")),
}
@ -4246,6 +4301,7 @@ fn match_brackets(cx: &mut Context) {
fn jump_forward(cx: &mut Context) {
let count = cx.count();
let config = cx.editor.config();
let view = view_mut!(cx.editor);
let doc_id = view.doc;
@ -4259,12 +4315,13 @@ fn jump_forward(cx: &mut Context) {
}
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) {
let count = cx.count();
let config = cx.editor.config();
let (view, doc) = current!(cx.editor);
let doc_id = doc.id();
@ -4278,7 +4335,7 @@ fn jump_backward(cx: &mut Context) {
}
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| {
match ch {
'w' => textobject::textobject_word(text, range, objtype, count, false),
'W' => textobject::textobject_word(text, range, objtype, count, true),
'c' => textobject_treesitter("class", range),
't' => textobject_treesitter("class", range),
'f' => textobject_treesitter("function", range),
'a' => textobject_treesitter("parameter", range),
'o' => textobject_treesitter("comment", range),
't' => textobject_treesitter("test", range),
'c' => textobject_treesitter("comment", range),
'T' => textobject_treesitter("test", range),
'p' => textobject::textobject_paragraph(text, range, objtype, count),
'm' => textobject::textobject_pair_surround_closest(
text, range, objtype, count,
),
'g' => textobject_change(range),
// TODO: cancel new ranges if inconsistent surround matches across lines
ch if !ch.is_ascii_alphanumeric() => {
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"),
("p", "Paragraph"),
("c", "Class (tree-sitter)"),
("t", "Type definition (tree-sitter)"),
("f", "Function (tree-sitter)"),
("a", "Argument/parameter (tree-sitter)"),
("o", "Comment (tree-sitter)"),
("t", "Test (tree-sitter)"),
("c", "Comment (tree-sitter)"),
("T", "Test (tree-sitter)"),
("m", "Closest surrounding pair to cursor"),
(" ", "... 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())
.with_selection(Selection::new(ranges, selection.primary_index()));
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

@ -1,3 +1,4 @@
use futures_util::FutureExt;
use helix_lsp::{
block_on,
lsp::{self, CodeAction, CodeActionOrCommand, DiagnosticSeverity, NumberOrString},
@ -14,7 +15,8 @@ use helix_view::{apply_transaction, document::Mode, editor::Action, theme::Style
use crate::{
compositor::{self, Compositor},
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(
future,
move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
if let Some(symbols) = response {
let picker = sym_picker(symbols, current_url, offset_encoding);
compositor.push(Box::new(overlayed(picker)))
}
let symbols = response.unwrap_or_default();
let picker = sym_picker(symbols, current_url, offset_encoding);
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() {
Ok(())
} else {
fs::rename(&from, &to)
fs::rename(from, &to)
}
}
}
@ -787,8 +822,9 @@ pub fn apply_workspace_edit(
text_edits,
offset_encoding,
);
apply_transaction(&transaction, doc, view_mut!(editor, view_id));
doc.append_changes_to_history(view_id);
let view = view_mut!(editor, view_id);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view);
};
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");
for arg in args {
let (path, pos) = args::parse_file(arg);
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);
// If the path is a directory, open a file picker on that directory and update the status
// message
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path, &editor.config());
compositor.push(Box::new(overlayed(picker)));
},
));
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(())
}
@ -489,7 +505,7 @@ fn set_line_ending(
}),
);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
doc.append_changes_to_history(view);
Ok(())
}
@ -506,7 +522,7 @@ fn earlier(
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
let success = doc.earlier(view.id, uk);
let success = doc.earlier(view, uk);
if !success {
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 (view, doc) = current!(cx.editor);
let success = doc.later(view.id, uk);
let success = doc.later(view, uk);
if !success {
cx.editor.set_status("Already at newest change");
}
@ -802,7 +818,7 @@ fn theme(
.editor
.theme_loader
.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()) {
bail!("Unsupported theme: theme requires true color support");
}
@ -934,7 +950,7 @@ fn replace_selections_with_clipboard_impl(
});
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
doc.append_changes_to_history(view);
Ok(())
}
Err(e) => Err(e.context("Couldn't get system clipboard contents")),
@ -1053,10 +1069,12 @@ fn reload(
}
let scrolloff = cx.editor.config().scrolloff;
let redraw_handle = cx.editor.redraw_handle.clone();
let (view, doc) = current!(cx.editor);
doc.reload(view).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})
doc.reload(view, &cx.editor.diff_providers, redraw_handle)
.map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})
}
fn reload_all(
@ -1091,7 +1109,12 @@ fn reload_all(
// Every doc is guaranteed to have at least 1 view at this point.
let view = view_mut!(cx.editor, view_ids[0]);
doc.reload(view)?;
// 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 {
let view = view_mut!(cx.editor, view_id);
@ -1598,7 +1621,7 @@ fn sort_impl(
);
apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
doc.append_changes_to_history(view);
Ok(())
}
@ -1642,7 +1665,7 @@ fn reflow(
});
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);
Ok(())
@ -1759,13 +1782,30 @@ fn insert_output(
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<()> {
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 {
return Ok(());
}
ensure!(!args.is_empty(), "Shell command required");
shell(cx, &args.join(" "), &ShellBehavior::Replace);
shell(cx, &args.join(" "), behavior);
Ok(())
}
@ -2317,6 +2357,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: pipe,
completer: None,
},
TypableCommand {
name: "pipe-to",
aliases: &[],
doc: "Pipe each selection to the shell command, ignoring output.",
fun: pipe_to,
completer: None,
},
TypableCommand {
name: "run-shell-command",
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 {
let path = match which::which(&cmd) {
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)?;
}

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

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

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

@ -1,5 +1,5 @@
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::text::Spans;
@ -107,6 +107,7 @@ impl Completion {
let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction(
doc: &Document,
view_id: ViewId,
item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
@ -121,9 +122,10 @@ impl Completion {
}
};
util::generate_transaction_from_edits(
util::generate_transaction_from_completion_edit(
doc.text(),
vec![edit],
doc.selection(view_id),
edit,
offset_encoding, // TODO: should probably transcode in Client
)
} else {
@ -132,10 +134,23 @@ impl Completion {
// 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 text = text.trim_start_matches::<&str>(&prefix);
Transaction::change(
doc.text(),
vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
)
// TODO: this needs to be true for the numbers to work out correctly
// 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
@ -164,6 +179,7 @@ impl Completion {
let transaction = item_to_transaction(
doc,
view.id,
item,
offset_encoding,
start_offset,
@ -185,6 +201,7 @@ impl Completion {
let transaction = item_to_transaction(
doc,
view.id,
item,
offset_encoding,
start_offset,
@ -394,7 +411,7 @@ impl Component for Completion {
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
contents
),
cx.editor.syn_loader.clone(),
)
@ -404,15 +421,14 @@ impl Component for Completion {
value: contents,
})) => {
// TODO: set language based on doc scope
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
if let Some(detail) = &option.detail.as_deref() {
Markdown::new(
format!("```{}\n{}\n```\n{}", language, detail, contents),
cx.editor.syn_loader.clone(),
)
} else {
Markdown::new(contents.to_string(), cx.editor.syn_loader.clone())
}
}
None if option.detail.is_some() => {
// TODO: copied from above

@ -539,8 +539,8 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = offset.col > (visual_x as usize)
|| (visual_x as usize) >= viewport.width as usize + offset.col;
let out_of_bounds = offset.col > visual_x
|| visual_x >= viewport.width as usize + offset.col;
if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds {
@ -570,7 +570,7 @@ impl EditorView {
let (display_grapheme, width) = if grapheme == "\t" {
is_whitespace = true;
// 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 =
helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width);
@ -589,7 +589,7 @@ impl EditorView {
(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 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 {
// partially on screen
let rect = Rect::new(
viewport.x as u16,
viewport.x,
viewport.y + line,
(width - cut_off_start) as u16,
1,
@ -753,7 +753,7 @@ impl EditorView {
let mut text = String::with_capacity(8);
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);
text.reserve(width); // ensure there's enough space for the gutter
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
@ -1183,6 +1183,7 @@ impl EditorView {
}
editor.focus(view_id);
editor.ensure_cursor_in_view(view_id);
return EventResult::Consumed(None);
}
@ -1219,7 +1220,8 @@ impl EditorView {
let primary = selection.primary_mut();
*primary = primary.put_cursor(doc.text().slice(..), pos, true);
doc.set_selection(view.id, selection);
let view_id = view.id;
cxt.editor.ensure_cursor_in_view(view_id);
EventResult::Consumed(None)
}
@ -1241,6 +1243,7 @@ impl EditorView {
commands::scroll(cxt, offset, direction);
cxt.editor.tree.focus = current_view;
cxt.editor.ensure_cursor_in_view(current_view);
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
// to include any edits to the paste in the history state.
if mode != Mode::Insert {
doc.append_changes_to_history(view.id);
doc.append_changes_to_history(view);
}
EventResult::Consumed(None)
@ -1370,9 +1373,7 @@ impl Component for EditorView {
cx.editor.status_msg = None;
let mode = cx.editor.mode();
let (view, doc) = current!(cx.editor);
let original_doc_id = doc.id();
let original_doc_revision = doc.get_current_revision();
let (view, _) = current!(cx.editor);
let focus = view.id;
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 doc = doc_mut!(cx.editor, &view.doc);
view.ensure_cursor_in_view(doc, config.scrolloff);
// Store a history state if not in insert mode. This also takes care of
// committing changes when leaving insert mode.
if mode != Mode::Insert {
doc.append_changes_to_history(view.id);
}
// If the current document has been changed, apply the changes to all views.
// This ensures that selections in jumplists follow changes.
if doc.id() == original_doc_id
&& doc.get_current_revision() != original_doc_revision
{
if let Some(transaction) =
doc.history.get_mut().changes_since(original_doc_revision)
{
let doc = doc!(cx.editor, &original_doc_id);
for (view, _focused) in cx.editor.tree.views_mut() {
view.apply(&transaction, doc);
}
}
doc.append_changes_to_history(view);
}
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)

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

@ -22,7 +22,7 @@ pub use editor::EditorView;
pub use explore::Explorer;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{FileLocation, FilePicker, Picker};
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
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
// 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()
} else {
// const MAX: usize = 8192;
const MAX: usize = 100_000;
files.take(MAX).collect()
};
files.sort();
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> {
let mut names: Vec<_> = editor
.documents
.iter()
.map(|(_id, doc)| {
.values()
.map(|doc| {
let name = doc
.relative_path()
.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);
self.content.cursor(dimensions, ctx)
}
fn id(&self) -> Option<&'static str> {
self.content.id()
}
}

@ -3,6 +3,7 @@ use crate::{
ctrl, key, shift,
ui::{self, fuzzy_match::FuzzyQuery, EditorView},
};
use futures_util::future::BoxFuture;
use tui::{
buffer::Buffer as Surface,
widgets::{Block, BorderType, Borders},
@ -11,7 +12,10 @@ use tui::{
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
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 crate::ui::{Prompt, PromptEvent};
@ -22,7 +26,7 @@ use helix_view::{
Document, DocumentId, Editor,
};
use super::menu::Item;
use super::{menu::Item, overlay::Overlay};
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes
@ -343,11 +347,17 @@ impl<T: Item + 'static> Component for FilePicker<T> {
#[derive(PartialEq, Eq, Debug)]
struct PickerMatch {
index: usize,
score: i64,
index: usize,
len: usize,
}
impl PickerMatch {
fn key(&self) -> impl Ord {
(cmp::Reverse(self.score), self.len, self.index)
}
}
impl PartialOrd for PickerMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
@ -356,10 +366,7 @@ impl PartialOrd for PickerMatch {
impl Ord for PickerMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.cmp(&other.score)
.reverse()
.then_with(|| self.len.cmp(&other.len))
self.key().cmp(&other.key())
}
}
@ -469,34 +476,42 @@ impl<T: Item> Picker<T> {
self.matches.sort_unstable();
} else {
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();
self.force_score();
}
log::debug!("picker score {:?}", Instant::now().duration_since(now));
// reset cursor position
self.cursor = 0;
let pattern = self.prompt.line();
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`)
pub fn move_by(&mut self, amount: usize, direction: Direction) {
let len = self.matches.len();
@ -744,3 +759,78 @@ impl<T: Item + 'static> Component for Picker<T> {
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 completion_color = theme.get("ui.menu");
let selected_color = theme.get("ui.menu.selected");
let suggestion_color = theme.get("ui.text.inactive");
// completion
let max_len = self
@ -450,21 +451,29 @@ impl Prompt {
// render buffer text
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
self.history_register
match self
.history_register
.and_then(|reg| cx.editor.registers.last(reg))
.map(|entry| entry.into())
.unwrap_or_else(|| Cow::from(""))
{
Some(value) => (value, true),
None => (Cow::from(""), false),
}
} else {
self.line.as_str().into()
(self.line.as_str().into(), false)
};
surface.set_string(
area.x + self.prompt.len() as u16,
area.y + line,
&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.
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(())
}

@ -14,6 +14,10 @@ use std::{
fmt,
io::{self, Write},
};
fn term_program() -> Option<String> {
std::env::var("TERM_PROGRAM").ok()
}
fn vte_version() -> Option<usize> {
std::env::var("VTE_VERSION").ok()?.parse().ok()
}
@ -35,9 +39,11 @@ impl Capabilities {
Ok(t) => Capabilities {
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
// 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()
|| 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-dap = { version = "0.6", path = "../helix-dap" }
crossterm = { version = "0.25", optional = true }
helix-vcs = { version = "0.6", path = "../helix-vcs" }
# Conversion traits
once_cell = "1.16"
@ -43,6 +44,7 @@ log = "~0.4"
which = "4.2"
[target.'cfg(windows)'.dependencies]
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") {
command_provider! {
paste => "tmux", "save-buffer", "-";
copy => "tmux", "load-buffer", "-";
copy => "tmux", "load-buffer", "-w", "-";
}
} else {
Box::new(provider::FallbackProvider::new())

@ -3,6 +3,8 @@ use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs;
use helix_core::Range;
use helix_vcs::{DiffHandle, DiffProviderRegistry};
use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize;
use std::borrow::Cow;
@ -24,6 +26,7 @@ use helix_core::{
DEFAULT_LINE_ENDING,
};
use crate::editor::RedrawHandle;
use crate::{apply_transaction, DocumentId, Editor, View, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
@ -133,6 +136,8 @@ pub struct Document {
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
diff_handle: Option<DiffHandle>,
}
use std::{fmt, mem};
@ -371,6 +376,7 @@ impl Document {
last_saved_revision: 0,
modified_since_accessed: false,
language_server: None,
diff_handle: None,
}
}
@ -639,16 +645,20 @@ impl Document {
}
/// 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 path = self.path().filter(|path| path.exists());
// If there is no path or the path no longer exists.
if path.is_none() {
bail!("can't find file to reload from");
}
let path = self
.path()
.filter(|path| path.exists())
.ok_or_else(|| anyhow!("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))?;
// Calculate the difference between the buffer and source text, and apply it.
@ -656,11 +666,16 @@ impl Document {
// of the encoding.
let transaction = helix_core::diff::compare_ropes(self.text(), &rope);
apply_transaction(&transaction, self, view);
self.append_changes_to_history(view.id);
self.append_changes_to_history(view);
self.reset_modified();
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(())
}
@ -802,6 +817,10 @@ impl Document {
if !transaction.changes().is_empty() {
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
if self.savepoint.is_some() {
@ -873,11 +892,11 @@ impl Document {
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 txn = if undo { history.undo() } else { history.redo() };
let success = if let Some(txn) = txn {
self.apply_impl(txn, view_id)
self.apply_impl(txn, view.id)
} else {
false
};
@ -886,18 +905,20 @@ impl Document {
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
// Sync with changes with the jumplist selections.
view.sync_changes(self);
}
success
}
/// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
pub fn undo(&mut self, view_id: ViewId) -> bool {
self.undo_redo_impl(view_id, true)
pub fn undo(&mut self, view: &mut View) -> bool {
self.undo_redo_impl(view, true)
}
/// Redo the last modification to the [`Document`]. Returns whether the redo was successful.
pub fn redo(&mut self, view_id: ViewId) -> bool {
self.undo_redo_impl(view_id, false)
pub fn redo(&mut self, view: &mut View) -> bool {
self.undo_redo_impl(view, false)
}
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 {
self.history.get_mut().earlier(uk)
} else {
@ -918,29 +939,31 @@ impl Document {
};
let mut success = false;
for txn in txns {
if self.apply_impl(&txn, view_id) {
if self.apply_impl(&txn, view.id) {
success = true;
}
}
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
// Sync with changes with the jumplist selections.
view.sync_changes(self);
}
success
}
/// Undo modifications to the [`Document`] according to `uk`.
pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
self.earlier_later_impl(view_id, uk, true)
pub fn earlier(&mut self, view: &mut View, uk: UndoKind) -> bool {
self.earlier_later_impl(view, uk, true)
}
/// Redo modifications to the [`Document`] according to `uk`.
pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
self.earlier_later_impl(view_id, uk, false)
pub fn later(&mut self, view: &mut View, uk: UndoKind) -> bool {
self.earlier_later_impl(view, uk, false)
}
/// 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() {
return;
}
@ -950,7 +973,7 @@ impl Document {
// 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.
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..
let old_state = self.old_state.take().expect("no old_state available");
@ -958,6 +981,9 @@ impl Document {
let mut history = self.history.take();
history.commit_revision(&transaction, &old_state);
self.history.set(history);
// Update jumplist entries in the view.
view.apply(&transaction, self);
}
pub fn id(&self) -> DocumentId {
@ -1055,6 +1081,23 @@ impl Document {
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]
/// Tree-sitter AST tree
pub fn syntax(&self) -> Option<&Syntax> {

@ -9,6 +9,7 @@ use crate::{
tree::{self, Tree},
Align, Document, DocumentId, View, ViewId,
};
use helix_vcs::DiffProviderRegistry;
use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt};
@ -26,7 +27,10 @@ use std::{
};
use tokio::{
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
Notify, RwLock,
},
time::{sleep, Duration, Instant, Sleep},
};
@ -517,6 +521,8 @@ pub enum GutterType {
LineNumbers,
/// Show one blank space
Spacer,
/// Highlight local changes
Diff,
}
impl std::str::FromStr for GutterType {
@ -527,6 +533,7 @@ impl std::str::FromStr for GutterType {
"diagnostics" => Ok(Self::Diagnostics),
"spacer" => Ok(Self::Spacer),
"line-numbers" => Ok(Self::LineNumbers),
"diff" => Ok(Self::Diff),
_ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
}
}
@ -673,6 +680,8 @@ impl Default for Config {
GutterType::Diagnostics,
GutterType::Spacer,
GutterType::LineNumbers,
GutterType::Spacer,
GutterType::Diff,
],
middle_click_paste: true,
auto_pairs: AutoPairConfig::default(),
@ -756,6 +765,7 @@ pub struct Editor {
pub macro_replaying: Vec<char>,
pub language_servers: helix_lsp::Registry,
pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>,
pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
@ -786,8 +796,15 @@ pub struct Editor {
pub exit_code: i32,
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)]
pub enum EditorEvent {
DocumentSaved(DocumentSavedEventResult),
@ -873,6 +890,7 @@ impl Editor {
theme: theme_loader.default(),
language_servers: helix_lsp::Registry::new(),
diagnostics: BTreeMap::new(),
diff_providers: DiffProviderRegistry::default(),
debugger: None,
debugger_events: SelectAll::new(),
breakpoints: HashMap::new(),
@ -891,6 +909,8 @@ impl Editor {
auto_pairs,
exit_code: 0,
config_events: unbounded_channel(),
redraw_handle: Default::default(),
needs_redraw: false,
}
}
@ -1072,7 +1092,8 @@ impl Editor {
fn _refresh(&mut self) {
let config = self.config();
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)
}
}
@ -1084,6 +1105,7 @@ impl Editor {
let doc = doc_mut!(self, &doc_id);
doc.ensure_view_init(view.id);
view.sync_changes(doc);
align_view(doc, view, Align::Center);
}
@ -1096,6 +1118,8 @@ impl Editor {
return;
}
self.enter_normal_mode();
match action {
Action::Replace => {
let (view, doc) = current_ref!(self);
@ -1116,6 +1140,9 @@ impl Editor {
let (view, doc) = current!(self);
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 {
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
// 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 _ = 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)
};
@ -1351,8 +1380,14 @@ impl Editor {
// if leaving the view: mode should reset and the cursor should be
// within view
if prev_id != view_id {
self.mode = Mode::Normal;
self.enter_normal_mode();
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 {
tokio::select! {
biased;
// the loop only runs once or twice and would be better implemented with a recursion + const generic
// 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.write_count -= 1;
EditorEvent::DocumentSaved(event)
}
Some(config_event) = self.config_events.1.recv() => {
EditorEvent::ConfigEvent(config_event)
}
Some(message) = self.language_servers.incoming.next() => {
EditorEvent::LanguageServerMessage(message)
}
Some(event) = self.debugger_events.next() => {
EditorEvent::DebuggerEvent(event)
}
_ = &mut self.idle_timer => {
EditorEvent::IdleTimer
_ = self.redraw_handle.0.notified() => {
if !self.needs_redraw{
self.needs_redraw = true;
let timeout = Instant::now() + Duration::from_millis(96);
if timeout < self.idle_timer.deadline(){
self.idle_timer.as_mut().reset(timeout)
}
}
}
_ = &mut self.idle_timer => {
return EditorEvent::IdleTimer
}
}
}
}
@ -1495,4 +1545,67 @@ impl Editor {
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()
}
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 =
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::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::LineNumbers => line_numbers_width(_view, doc),
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>(
editor: &'doc Editor,
doc: &'doc Document,
@ -226,8 +275,8 @@ pub fn diagnostics_or_breakpoints<'doc>(
theme: &Theme,
is_focused: bool,
) -> GutterFn<'doc> {
let diagnostics = diagnostic(editor, doc, view, theme, is_focused);
let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
Box::new(move |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 std::fmt;
pub use crate::keyboard::{KeyCode, KeyModifiers};
pub use crate::keyboard::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode};
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)]
pub enum Event {
@ -119,6 +119,40 @@ pub(crate) mod keys {
pub(crate) const MINUS: &str = "minus";
pub(crate) const LESS_THAN: &str = "lt";
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 {
@ -163,6 +197,44 @@ impl fmt::Display for KeyEvent {
KeyCode::Char('>') => f.write_str(keys::GREATER_THAN)?,
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
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(())
}
@ -192,6 +264,40 @@ impl UnicodeWidthStr for KeyEvent {
KeyCode::F(1..=9) => 2,
KeyCode::F(_) => 3,
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) {
width += 2;
@ -235,6 +341,40 @@ impl std::str::FromStr for KeyEvent {
keys::MINUS => KeyCode::Char('-'),
keys::LESS_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()),
function if function.len() > 1 && function.starts_with('F') => {
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.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
pub enum KeyCode {
@ -94,6 +252,24 @@ pub enum KeyCode {
Null,
/// Escape key.
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")]
@ -119,6 +295,15 @@ impl From<KeyCode> for crossterm::event::KeyCode {
KeyCode::Char(character) => CKeyCode::Char(character),
KeyCode::Null => CKeyCode::Null,
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::Null => KeyCode::Null,
CKeyCode::Esc => KeyCode::Esc,
CKeyCode::CapsLock
| CKeyCode::ScrollLock
| CKeyCode::NumLock
| CKeyCode::PrintScreen
| CKeyCode::Pause
| CKeyCode::Menu
| CKeyCode::KeypadBegin
| CKeyCode::Media(_)
| CKeyCode::Modifier(_) => unreachable!(
"Shouldn't get this key without enabling DISAMBIGUATE_ESCAPE_CODES in crossterm"
),
CKeyCode::CapsLock => KeyCode::CapsLock,
CKeyCode::ScrollLock => KeyCode::ScrollLock,
CKeyCode::NumLock => KeyCode::NumLock,
CKeyCode::PrintScreen => KeyCode::PrintScreen,
CKeyCode::Pause => KeyCode::Pause,
CKeyCode::Menu => KeyCode::Menu,
CKeyCode::KeypadBegin => KeyCode::KeypadBegin,
CKeyCode::Media(media_key_code) => KeyCode::Media(media_key_code.into()),
CKeyCode::Modifier(modifier_key_code) => KeyCode::Modifier(modifier_key_code.into()),
}
}
}

@ -136,8 +136,9 @@ impl Loader {
// Loads the theme data as `toml::Value` first from the user_dir then in default_dir
fn load_toml(&self, path: PathBuf) -> Result<Value> {
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

@ -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::{
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;
@ -102,6 +105,11 @@ pub struct View {
pub object_selections: Vec<Selection>,
/// GutterTypes used to fetch Gutter (constructor) and width for rendering
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 {
@ -126,6 +134,7 @@ impl View {
last_modified_docs: [None, None],
object_selections: Vec::new(),
gutters: gutter_types,
doc_revisions: HashMap::new(),
}
}
@ -149,17 +158,10 @@ impl View {
}
pub fn gutter_offset(&self, doc: &Document) -> u16 {
let mut offset = self
.gutters
self.gutters
.iter()
.map(|gutter| gutter.width(self, doc) as u16)
.sum();
if offset > 0 {
offset += 1
}
offset
.sum()
}
//
@ -167,6 +169,15 @@ impl View {
&self,
doc: &Document,
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)> {
let cursor = doc
.selection(self.id)
@ -178,44 +189,66 @@ impl View {
let inner_area = self.inner_area(doc);
let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
// - 1 so we have at least one gap in the middle.
// 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 row = if line > last_line.saturating_sub(scrolloff) {
// 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 new_offset = |scrolloff: usize| {
// - 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 row = if line > last_line.saturating_sub(scrolloff) {
// 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 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)
let current_offset = (self.offset.row, self.offset.col);
if centering {
// return None if cursor is out of view
let offset = new_offset(0);
(offset == current_offset).then(|| {
if scrolloff == 0 {
offset
} else {
new_offset(scrolloff)
}
})
} else {
self.offset.col
};
if row == self.offset.row && col == self.offset.col {
None
} else {
Some((row, col))
// return None if cursor is in (view - scrolloff)
let offset = new_offset(scrolloff);
(offset != current_offset).then(|| offset) // TODO: use 'then_some' when 1.62 <= MSRV
}
}
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.col = col;
} else {
align_view(doc, self, Align::Center);
}
}
@ -349,10 +382,33 @@ impl View {
/// Applies a [`Transaction`] to the view.
/// Instead of calling this function directly, use [crate::apply_transaction]
/// 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);
// TODO: remove the boolean return. This is unused.
true
self.doc_revisions
.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 {
use super::*;
use helix_core::Rope;
const OFFSET: u16 = 4; // 1 diagnostic + 2 linenr (< 100 lines) + 1 gutter
const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
const OFFSET: u16 = 3; // 1 diagnostic + 2 linenr (< 100 lines)
const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 1; // 1 diagnostic
// const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
use crate::document::Document;
use crate::editor::GutterType;

@ -190,7 +190,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd
name = "cpp"
scope = "source.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 = []
comment-token = "//"
language-server = { command = "clangd" }
@ -223,6 +223,18 @@ args = { console = "internalConsole", attachCommands = [ "platform select remote
name = "cpp"
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]]
name = "c-sharp"
scope = "source.csharp"
@ -301,7 +313,7 @@ args = { mode = "local", processId = "{0}" }
[[grammar]]
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]]
name = "gomod"
@ -422,11 +434,13 @@ injection-regex = "css"
file-types = ["css", "scss"]
roots = []
language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
auto-format = true
config = { "provideFormatter" = true }
indent = { tab-width = 2, unit = " " }
[[grammar]]
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]]
name = "scss"
@ -435,6 +449,8 @@ injection-regex = "scss"
file-types = ["scss"]
roots = []
language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
auto-format = true
config = { "provideFormatter" = true }
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -572,6 +588,34 @@ indent = { tab-width = 4, unit = "\t" }
name = "latex"
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]]
name = "lean"
scope = "source.lean"
@ -874,12 +918,30 @@ source = { git = "https://github.com/ganezdragon/tree-sitter-perl", rev = "0ac2c
[[language]]
name = "racket"
scope = "source.rkt"
scope = "source.racket"
roots = []
file-types = ["rkt"]
file-types = ["rkt", "rktd", "rktl", "scrbl"]
shebangs = ["racket"]
comment-token = ";"
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]]
name = "comment"
@ -1039,7 +1101,7 @@ source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev
name = "diff"
scope = "source.diff"
roots = []
file-types = ["diff"]
file-types = ["diff", "patch"]
injection-regex = "diff"
comment-token = "#"
indent = { tab-width = 2, unit = " " }
@ -1398,7 +1460,7 @@ file-types = ["tscn","tres"]
shebangs = []
roots = ["project.godot"]
auto-format = false
comment-token = "#"
comment-token = ";"
indent = { tab-width = 4, unit = "\t" }
[[grammar]]
@ -1526,14 +1588,14 @@ source = { git = "https://github.com/metio/tree-sitter-ssh-client-config", rev =
name = "scheme"
scope = "source.scheme"
injection-regex = "scheme"
file-types = ["ss", "rkt"] # "scm",
file-types = ["ss"] # "scm",
roots = []
comment-token = ";"
indent = { tab-width = 2, unit = " " }
[[grammar]]
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]]
name = "v"
@ -1993,3 +2055,29 @@ grammar = "qmljs"
[[grammar]]
name = "qmljs"
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
(tag_name) @tag
(nesting_selector) @tag
(universal_selector) @tag
[
(tag_name)
(nesting_selector)
(universal_selector)
] @tag
"~" @operator
">" @operator
"+" @operator
"-" @operator
"*" @operator
"/" @operator
"=" @operator
"^=" @operator
"|=" @operator
"~=" @operator
"$=" @operator
"*=" @operator
[
"~"
">"
"+"
"-"
"*"
"/"
"="
"^="
"|="
"~="
"$="
"*="
] @operator
"and" @operator
"or" @operator
"not" @operator
"only" @operator
(attribute_selector (plain_value) @string)
(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
[
"and"
"not"
"only"
"or"
] @keyword.operator
((property_name) @variable
(#match? @variable "^--"))
((plain_value) @variable
(#match? @variable "^--"))
"@media" @keyword
"@import" @keyword
"@charset" @keyword
"@namespace" @keyword
"@supports" @keyword
"@keyframes" @keyword
(at_keyword) @keyword
(to) @keyword
(from) @keyword
(important) @keyword
(attribute_name) @attribute
(class_name) @label
(feature_name) @variable.other.member
(function_name) @function
(id_name) @label
(namespace_name) @namespace
(property_name) @variable.other.member
[
"@charset"
"@import"
"@keyframes"
"@media"
"@namespace"
"@supports"
(at_keyword)
(from)
(important)
(to)
] @keyword
[
"#"
"."
] @punctuation
(string_value) @string
((color_value) "#") @string.special
(color_value) @string.special
(integer_value) @constant.numeric.integer
(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
(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
function: (identifier) @function)
@ -24,6 +28,9 @@
(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
(field_identifier) @variable.other.member
(identifier) @variable

@ -5,7 +5,7 @@
(type_spec)
(func_literal)
(literal_value)
(element)
(literal_element)
(keyed_element)
(expression_case)
(default_case)
@ -16,6 +16,7 @@
(block)
(type_switch_statement)
(expression_switch_statement)
(var_declaration)
] @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)
(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))

@ -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
((((comment) @injection.language) .
(indented_string_expression (string_fragment) @injection.content))

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

@ -2,29 +2,40 @@
(character) @constant.character
(boolean) @constant.builtin.boolean
[(string)
(character)] @string
(string) @string
(escape_sequence) @constant.character.escape
[(comment)
(block_comment)
(directive)] @comment
(comment) @comment.line
(block_comment) @comment.block
(directive) @keyword.directive
[(boolean)
(character)] @constant
; operators
((symbol) @function.builtin
(#match? @function.builtin "^(eqv\\?|eq\\?|equal\\?)")) ; TODO
((symbol) @operator
(#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
; keywords
((symbol) @keyword.conditional
(#match? @keyword.conditional "^(if|cond|case|when|unless)$"))
(list
.
((symbol) @keyword.conditional
(#match? @keyword.conditional "^(if|cond|case|when|unless)$"
)))
((symbol) @keyword
(#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)$"))
(list
.
(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
@ -47,26 +58,16 @@
.
(list
(list
(symbol) @variable))
(symbol) @variable.parameter))
(#match? @_f
"^(let|let\\*|let-syntax|let-values|let\\*-values|letrec|letrec\\*|letrec-syntax)$"))
; operators
(list
.
(symbol) @operator
(#match? @operator "^([+*/<>=-]|(<=)|(>=))$"))
; quote
(abbreviation
"'" (symbol)) @constant
(list
.
(symbol) @_f
(#eq? @_f "quote")) @symbol
(#eq? @_f "quote")) @string.symbol
; library
@ -89,12 +90,10 @@
((symbol) @variable.builtin
(#eq? @variable.builtin "..."))
(symbol) @variable
((symbol) @variable.builtin
(#eq? @variable.builtin "."))
(symbol) @variable
["(" ")" "[" "]" "{" "}"] @punctuation.bracket

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

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

@ -8,13 +8,37 @@
(raw_text) @injection.content)
(#set! injection.language "javascript"))
; <script>
((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)
(#set! injection.language "javascript"))
(#eq? @_attr_name "lang"))
; <style>
((style_element
(raw_text) @injection.content)
(#set! injection.language "css"))
(start_tag) @_no_lang
(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
(#set! injection.language "comment"))

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

Loading…
Cancel
Save