Compare commits

..

2 Commits

Author SHA1 Message Date
Blaž Hrastnik
ac1b7d8d0a Release 0.6 2022-01-04 18:54:05 +09:00
Blaž Hrastnik
6a0fcce010 Update changelog 2022-01-04 18:49:53 +09:00
952 changed files with 19785 additions and 90524 deletions

2
.cargo/config Normal file
View File

@@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

View File

@@ -1,3 +0,0 @@
[alias]
xtask = "run --package xtask --"
integration-test = "test --features integration --profile integration --workspace --test integration"

2
.envrc
View File

@@ -1,7 +1,5 @@
watch_file shell.nix
watch_file flake.lock
watch_file rust-toolchain.toml
# try to use flakes, if it fails use normal nix (ie. shell.nix)
use flake || use nix
eval "$shellHook"

11
.gitattributes vendored
View File

@@ -1,11 +0,0 @@
# Auto detect text files and perform normalization
* text=auto
*.rs text diff=rust
*.toml text diff=toml
*.scm text diff=scheme
*.md text diff=markdown
book/theme/highlight.js linguist-vendored
Cargo.lock text

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
open_collective: helix-editor

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: C-bug
assignees: ''
---
<!-- Your issue may already be reported!
Please search on the issue tracker before creating one. -->
### Reproduction steps
<!-- Ideally provide a key sequence and/or asciinema.org recording. -->
### Environment
- Platform: <!-- macOS / Windows / Linux -->
- Helix version: <!-- 'hx -V' if using a release, 'git describe' if building from master -->
<details><summary>~/.cache/helix/helix.log</summary>
```
please provide a copy of `~/.cache/helix/helix.log` here if possible, you may need to redact some of the lines
```
</details>

View File

@@ -1,78 +0,0 @@
name: Bug Report
description: Create a report to help us improve
labels: C-bug
body:
- type: markdown
attributes:
value: Thank you for filing a bug report! 🐛
- type: textarea
id: problem
attributes:
label: Summary
description: >
Please provide a short summary of the bug, along with any information
you feel relevant to replicate the bug.
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: Reproduction Steps
value: |
<!-- Ideally provide a key sequence and/or asciinema.org recording. -->
I tried this:
1. `hx`
I expected this to happen:
Instead, this happened:
- type: textarea
id: helix-log
attributes:
label: Helix log
description: See `hx -h` for log file path. If you can reproduce the issue run `RUST_BACKTRACE=1 hx -vv` to generate a more detailed log file.
value: |
<details><summary>~/.cache/helix/helix.log</summary>
```
please provide a copy of `~/.cache/helix/helix.log` here if possible, you may need to redact some of the lines
```
</details>
- type: input
id: platform
attributes:
label: Platform
placeholder: Linux / macOS / Windows
validations:
required: true
- type: input
id: terminal-emulator
attributes:
label: Terminal Emulator
placeholder: wezterm 20220101-133340-7edc5b5a
validations:
required: true
- type: input
id: installation-method
attributes:
label: Installation Method
description: >
How you installed Helix - from a package manager like Homebrew or the
AUR, built from source, downloaded a binary from the releases page, etc.
placeholder: "source / brew / nixpkgs / flake / releases page"
validations:
required: true
- type: input
id: helix-version
attributes:
label: Helix Version
description: >
Helix version (`hx -V` if using a release, `git describe` if building
from master).
**Make sure that you are using the [latest helix release](https://github.com/helix-editor/helix/releases) or a newer master build**
placeholder: "helix 22.12 (5eaa6d97)"
validations:
required: true

View File

@@ -1,13 +0,0 @@
---
name: Enhancement
about: Suggest an improvement
title: ''
labels: C-enhancement
assignees: ''
---
<!--
Your enhancement may already be reported!
Please search on the issue tracker before creating a new issue.
If this is an idea for a feature, please open an "Idea" Discussion instead.
-->

View File

@@ -0,0 +1,13 @@
---
name: Feature request
about: Suggest a new feature or improvement
title: ''
labels: C-enhancement
assignees: ''
---
<!-- Your feature may already be reported!
Please search on the issue tracker before creating one. -->
#### Describe your feature request

View File

@@ -4,110 +4,179 @@ on:
push:
branches:
- master
merge_group:
schedule:
- cron: '00 01 * * *'
jobs:
check:
name: Check (msrv)
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
submodules: true
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v2
- name: Cache cargo registry
uses: actions/cache@v2.1.7
with:
shared-key: "build"
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo check
run: cargo check
uses: actions-rs/cargo@v1
with:
command: check
test:
name: Test Suite
runs-on: ${{ matrix.os }}
env:
RUST_BACKTRACE: 1
HELIX_LOG_LEVEL: info
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
submodules: true
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.70
- uses: Swatinem/rust-cache@v2
uses: actions-rs/toolchain@v1
with:
shared-key: "build"
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
- name: Cache test tree-sitter grammar
uses: actions/cache@v3
- name: Cache cargo registry
uses: actions/cache@v2.1.7
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo test
run: cargo test --workspace
- name: Run cargo integration-test
run: cargo integration-test
uses: actions-rs/cargo@v1
with:
command: test
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
lints:
name: Lints
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
submodules: true
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.70
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Cache cargo registry
uses: actions/cache@v2.1.7
with:
shared-key: "build"
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo fmt
run: cargo fmt --all --check
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Run cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Run cargo doc
run: cargo doc --no-deps --workspace --document-private-items
env:
RUSTDOCFLAGS: -D warnings
uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
submodules: true
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.70
- uses: Swatinem/rust-cache@v2
uses: actions-rs/toolchain@v1
with:
shared-key: "build"
profile: minimal
toolchain: stable
override: true
- name: Validate queries
run: cargo xtask query-check
- name: Cache cargo registry
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Generate docs
run: cargo xtask docgen
uses: actions-rs/cargo@v1
with:
command: xtask
args: docgen
- name: Check uncommitted documentation changes
run: |

View File

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

View File

@@ -4,14 +4,12 @@ on:
push:
branches:
- master
tags:
- '*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
@@ -21,21 +19,9 @@ jobs:
- run: mdbook build book
- name: Set output directory
run: |
OUTDIR=$(basename ${{ github.ref }})
echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV
- name: Deploy stable
uses: peaceiris/actions-gh-pages@v3
if: startswith(github.ref, 'refs/tags/')
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if: github.ref == 'refs/heads/master'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book
destination_dir: ./${{ env.OUTDIR }}

View File

@@ -1,26 +0,0 @@
# This languages.toml is used for testing in CI.
[[language]]
name = "rust"
scope = "source.rust"
injection-regex = "rust"
file-types = ["rs"]
comment-token = "//"
roots = ["Cargo.toml", "Cargo.lock"]
indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "rust"
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
[[language]]
name = "nix"
scope = "source.nix"
injection-regex = "nix"
file-types = ["nix"]
shebangs = []
roots = []
comment-token = "#"
# A grammar entry is not necessary for this language - it is only used for
# testing TOML merging behavior.

View File

@@ -1,97 +1,46 @@
name: Release
on:
# schedule:
# - cron: '0 0 * * *' # midnight UTC
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/') || github.repository != 'helix-editor/helix' }}
- 'v[0-9]+.[0-9]+.[0-9]+'
## - release
jobs:
fetch-grammars:
name: Fetch Grammars
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- 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@v4
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
build: [x86_64-linux, aarch64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
include:
- build: x86_64-linux
os: ubuntu-latest
os: ubuntu-20.04
rust: stable
target: x86_64-unknown-linux-gnu
cross: false
- build: aarch64-linux
os: ubuntu-latest
os: ubuntu-20.04
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
os: windows-2019
rust: stable
target: x86_64-pc-windows-msvc
cross: false
# 23.03: build issues
- 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
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu
@@ -103,124 +52,57 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Download grammars
uses: actions/download-artifact@v4
- 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
# The rust-toolchain action ignores rust-toolchain.toml files.
# Removing this before building with cargo ensures that the rust-toolchain
# is considered the same between installation and usage.
- name: Remove the rust-toolchain.toml file
run: rm rust-toolchain.toml
uses: actions/checkout@v2
with:
submodules: true
- name: Install ${{ matrix.rust }} toolchain
uses: dtolnay/rust-toolchain@master
uses: actions-rs/toolchain@v1
with:
profile: minimal
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 }}"
override: true
- name: Run cargo test
if: "!matrix.skip_tests"
run: ${{ env.CARGO }} test --release --locked --target ${{ matrix.target }} --workspace
- name: Set profile.release.strip = true
shell: bash
run: |
cat >> .cargo/config.toml <<EOF
[profile.release]
strip = true
EOF
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.cross }}
command: test
args: --release --locked --target ${{ matrix.target }}
- name: Build release binary
run: ${{ env.CARGO }} build --release --locked --target ${{ matrix.target }}
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.cross }}
command: build
args: --release --locked --target ${{ matrix.target }}
- name: Build AppImage
shell: bash
if: matrix.build == 'x86_64-linux'
- name: Strip release binary (linux and macos)
if: matrix.build == 'x86_64-linux' || matrix.build == 'x86_64-macos'
run: strip "target/${{ matrix.target }}/release/hx"
- name: Strip release binary (arm)
if: matrix.build == 'aarch64-linux'
run: |
# Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE
sudo add-apt-repository universe
sudo apt install libfuse2
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
docker run --rm -v \
"$PWD/target:/target:Z" \
rustembedded/cross:${{ matrix.target }} \
aarch64-linux-gnu-strip \
/target/${{ matrix.target }}/release/hx
- name: Build archive
shell: bash
run: |
mkdir -p dist
mkdir 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@v4
- uses: actions/upload-artifact@v2.3.1
with:
name: bins-${{ matrix.build }}
path: dist
@@ -231,22 +113,33 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
submodules: false
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v2
# with:
# path: dist
# - run: ls -al ./dist
- run: ls -al bins-*
- name: Calculate tag name
run: |
name=dev
if [[ $GITHUB_REF == refs/tags/v* ]]; then
name=${GITHUB_REF:10}
fi
echo ::set-output name=val::$name
echo TAG=$name >> $GITHUB_ENV
id: tagname
- name: Build archive
shell: bash
run: |
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-* .
rm -rf tmp
mkdir tmp
mkdir dist
for dir in bins-* ; do
@@ -254,42 +147,25 @@ jobs:
if [[ $platform =~ "windows" ]]; then
exe=".exe"
fi
pkgname=helix-$GITHUB_REF_NAME-$platform
mkdir -p $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" = "x86_64-linux" ]]; then
mv bins-$platform/helix-*.AppImage* dist/
fi
pkgname=helix-$TAG-$platform
mkdir tmp/$pkgname
cp LICENSE README.md tmp/$pkgname
mv bins-$platform/runtime tmp/$pkgname/
mv bins-$platform/hx$exe tmp/$pkgname
chmod +x tmp/$pkgname/hx$exe
if [ "$exe" = "" ]; then
tar cJf dist/$pkgname.tar.xz $pkgname
tar cJf dist/$pkgname.tar.xz -C tmp $pkgname
else
7z a -r dist/$pkgname.zip $pkgname
(cd tmp && 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 }}
tag: ${{ steps.tagname.outputs.val }}
overwrite: true
- name: Upload binaries as artifact
uses: actions/upload-artifact@v4
if: env.preview == 'true'
with:
name: release
path: dist/*

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
target
.direnv
helix-term/rustfmt.toml
helix-syntax/languages/
result
runtime/grammars

188
.gitmodules vendored Normal file
View File

@@ -0,0 +1,188 @@
[submodule "helix-syntax/languages/tree-sitter-cpp"]
path = helix-syntax/languages/tree-sitter-cpp
url = https://github.com/tree-sitter/tree-sitter-cpp
shallow = true
[submodule "helix-syntax/languages/tree-sitter-javascript"]
path = helix-syntax/languages/tree-sitter-javascript
url = https://github.com/tree-sitter/tree-sitter-javascript
shallow = true
[submodule "helix-syntax/languages/tree-sitter-julia"]
path = helix-syntax/languages/tree-sitter-julia
url = https://github.com/tree-sitter/tree-sitter-julia
shallow = true
[submodule "helix-syntax/languages/tree-sitter-python"]
path = helix-syntax/languages/tree-sitter-python
url = https://github.com/tree-sitter/tree-sitter-python
shallow = true
[submodule "helix-syntax/languages/tree-sitter-typescript"]
path = helix-syntax/languages/tree-sitter-typescript
url = https://github.com/tree-sitter/tree-sitter-typescript
shallow = true
[submodule "helix-syntax/languages/tree-sitter-agda"]
path = helix-syntax/languages/tree-sitter-agda
url = https://github.com/tree-sitter/tree-sitter-agda
shallow = true
[submodule "helix-syntax/languages/tree-sitter-go"]
path = helix-syntax/languages/tree-sitter-go
url = https://github.com/tree-sitter/tree-sitter-go
shallow = true
[submodule "helix-syntax/languages/tree-sitter-ruby"]
path = helix-syntax/languages/tree-sitter-ruby
url = https://github.com/tree-sitter/tree-sitter-ruby
shallow = true
[submodule "helix-syntax/languages/tree-sitter-java"]
path = helix-syntax/languages/tree-sitter-java
url = https://github.com/tree-sitter/tree-sitter-java
shallow = true
[submodule "helix-syntax/languages/tree-sitter-php"]
path = helix-syntax/languages/tree-sitter-php
url = https://github.com/tree-sitter/tree-sitter-php
shallow = true
[submodule "helix-syntax/languages/tree-sitter-html"]
path = helix-syntax/languages/tree-sitter-html
url = https://github.com/tree-sitter/tree-sitter-html
shallow = true
[submodule "helix-syntax/languages/tree-sitter-scala"]
path = helix-syntax/languages/tree-sitter-scala
url = https://github.com/tree-sitter/tree-sitter-scala
shallow = true
[submodule "helix-syntax/languages/tree-sitter-bash"]
path = helix-syntax/languages/tree-sitter-bash
url = https://github.com/tree-sitter/tree-sitter-bash
shallow = true
[submodule "helix-syntax/languages/tree-sitter-rust"]
path = helix-syntax/languages/tree-sitter-rust
url = https://github.com/tree-sitter/tree-sitter-rust
shallow = true
[submodule "helix-syntax/languages/tree-sitter-json"]
path = helix-syntax/languages/tree-sitter-json
url = https://github.com/tree-sitter/tree-sitter-json
shallow = true
[submodule "helix-syntax/languages/tree-sitter-css"]
path = helix-syntax/languages/tree-sitter-css
url = https://github.com/tree-sitter/tree-sitter-css
shallow = true
[submodule "helix-syntax/languages/tree-sitter-c-sharp"]
path = helix-syntax/languages/tree-sitter-c-sharp
url = https://github.com/tree-sitter/tree-sitter-c-sharp
shallow = true
[submodule "helix-syntax/languages/tree-sitter-c"]
path = helix-syntax/languages/tree-sitter-c
url = https://github.com/tree-sitter/tree-sitter-c
shallow = true
[submodule "helix-syntax/languages/tree-sitter-haskell"]
path = helix-syntax/languages/tree-sitter-haskell
url = https://github.com/tree-sitter/tree-sitter-haskell
shallow = true
[submodule "helix-syntax/languages/tree-sitter-swift"]
path = helix-syntax/languages/tree-sitter-swift
url = https://github.com/tree-sitter/tree-sitter-swift
shallow = true
[submodule "helix-syntax/languages/tree-sitter-toml"]
path = helix-syntax/languages/tree-sitter-toml
url = https://github.com/ikatyang/tree-sitter-toml
shallow = true
[submodule "helix-syntax/languages/tree-sitter-elixir"]
path = helix-syntax/languages/tree-sitter-elixir
url = https://github.com/elixir-lang/tree-sitter-elixir
shallow = true
[submodule "helix-syntax/languages/tree-sitter-nix"]
path = helix-syntax/languages/tree-sitter-nix
url = https://github.com/cstrahan/tree-sitter-nix
shallow = true
[submodule "helix-syntax/languages/tree-sitter-latex"]
path = helix-syntax/languages/tree-sitter-latex
url = https://github.com/latex-lsp/tree-sitter-latex
shallow = true
[submodule "helix-syntax/languages/tree-sitter-ledger"]
path = helix-syntax/languages/tree-sitter-ledger
url = https://github.com/cbarrete/tree-sitter-ledger
shallow = true
[submodule "helix-syntax/languages/tree-sitter-protobuf"]
path = helix-syntax/languages/tree-sitter-protobuf
url = https://github.com/yusdacra/tree-sitter-protobuf.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-ocaml"]
path = helix-syntax/languages/tree-sitter-ocaml
url = https://github.com/tree-sitter/tree-sitter-ocaml
shallow = true
[submodule "helix-syntax/languages/tree-sitter-lua"]
path = helix-syntax/languages/tree-sitter-lua
url = https://github.com/nvim-treesitter/tree-sitter-lua
shallow = true
[submodule "helix-syntax/languages/tree-sitter-yaml"]
path = helix-syntax/languages/tree-sitter-yaml
url = https://github.com/ikatyang/tree-sitter-yaml
shallow = true
[submodule "helix-syntax/languages/tree-sitter-zig"]
path = helix-syntax/languages/tree-sitter-zig
url = https://github.com/maxxnino/tree-sitter-zig
shallow = true
[submodule "helix-syntax/languages/tree-sitter-svelte"]
path = helix-syntax/languages/tree-sitter-svelte
url = https://github.com/Himujjal/tree-sitter-svelte
shallow = true
[submodule "helix-syntax/languages/tree-sitter-vue"]
path = helix-syntax/languages/tree-sitter-vue
url = https://github.com/ikatyang/tree-sitter-vue
shallow = true
[submodule "helix-syntax/languages/tree-sitter-tsq"]
path = helix-syntax/languages/tree-sitter-tsq
url = https://github.com/tree-sitter/tree-sitter-tsq
shallow = true
[submodule "helix-syntax/languages/tree-sitter-cmake"]
path = helix-syntax/languages/tree-sitter-cmake
url = https://github.com/uyha/tree-sitter-cmake
shallow = true
[submodule "helix-syntax/languages/tree-sitter-glsl"]
path = helix-syntax/languages/tree-sitter-glsl
url = https://github.com/theHamsta/tree-sitter-glsl.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-perl"]
path = helix-syntax/languages/tree-sitter-perl
url = https://github.com/ganezdragon/tree-sitter-perl
shallow = true
[submodule "helix-syntax/languages/tree-sitter-comment"]
path = helix-syntax/languages/tree-sitter-comment
url = https://github.com/stsewd/tree-sitter-comment
shallow = true
[submodule "helix-syntax/languages/tree-sitter-wgsl"]
path = helix-syntax/languages/tree-sitter-wgsl
url = https://github.com/szebniok/tree-sitter-wgsl
shallow = true
[submodule "helix-syntax/languages/tree-sitter-llvm"]
path = helix-syntax/languages/tree-sitter-llvm
url = https://github.com/benwilliamgraham/tree-sitter-llvm
shallow = true
[submodule "helix-syntax/languages/tree-sitter-markdown"]
path = helix-syntax/languages/tree-sitter-markdown
url = https://github.com/MDeiml/tree-sitter-markdown
shallow = true
[submodule "helix-syntax/languages/tree-sitter-dart"]
path = helix-syntax/languages/tree-sitter-dart
url = https://github.com/UserNobody14/tree-sitter-dart.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-dockerfile"]
path = helix-syntax/languages/tree-sitter-dockerfile
url = https://github.com/camdencheek/tree-sitter-dockerfile.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-fish"]
path = helix-syntax/languages/tree-sitter-fish
url = https://github.com/ram02z/tree-sitter-fish
shallow = true
[submodule "helix-syntax/languages/tree-sitter-git-commit"]
path = helix-syntax/languages/tree-sitter-git-commit
url = https://github.com/the-mikedavis/tree-sitter-git-commit.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-git-diff"]
path = helix-syntax/languages/tree-sitter-git-diff
url = https://github.com/the-mikedavis/tree-sitter-git-diff.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-tablegen"]
path = helix-syntax/languages/tree-sitter-tablegen
url = https://github.com/Flakebi/tree-sitter-tablegen
shallow = true
[submodule "helix-syntax/languages/tree-sitter-git-rebase"]
path = helix-syntax/languages/tree-sitter-git-rebase
url = https://github.com/the-mikedavis/tree-sitter-git-rebase.git
shallow = true

View File

@@ -1,2 +0,0 @@
# Things that we don't want ripgrep to search that we do want in git
# https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering

File diff suppressed because it is too large Load Diff

2182
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,20 @@
[workspace]
resolver = "2"
members = [
"helix-core",
"helix-config",
"helix-view",
"helix-term",
"helix-tui",
"helix-syntax",
"helix-lsp",
"helix-event",
"helix-dap",
"helix-loader",
"helix-vcs",
"helix-parsec",
"xtask",
]
default-members = [
"helix-term"
]
# Build helix-syntax in release mode to make the code path faster in development.
# [profile.dev.package."helix-syntax"]
# opt-level = 3
[profile.dev]
split-debuginfo = "unpacked"
[profile.release]
lto = "thin"
# debug = true
[profile.opt]
inherits = "release"
lto = "fat"
codegen-units = 1
# strip = "debuginfo" # TODO: or strip = true
opt-level = 3
[profile.integration]
inherits = "test"
package.helix-core.opt-level = 2
package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
nucleo = "0.2.0"
[workspace.package]
version = "23.10.0"
edition = "2021"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
license = "MPL-2.0"
rust-version = "1.70"

View File

@@ -1,27 +1,14 @@
<div align="center">
# Helix
<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>
[![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](https://github.com/mawww/kakoune) / [Neovim](https://github.com/neovim/neovim) inspired editor, written in Rust.
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.
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/).
@@ -38,17 +25,44 @@ All shortcuts/keymaps can be found [in the documentation on the website](https:/
- 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.
(similar to emacs) in wgpu or skulpin.
Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.scm`.
`runtime/queries/<lang>/` for `indents.toml`.
# Installation
[Installation documentation](https://docs.helix-editor.com/install.html).
We provide packaging for various distributions, but here's a quick method to
build from source.
```
git clone --recurse-submodules --shallow-submodules -j8 https://github.com/helix-editor/helix
cd helix
cargo install --path helix-term
```
This will install the `hx` binary to `$HOME/.cargo/bin`.
Helix also 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).
This location can be overriden via the `HELIX_RUNTIME` environment variable.
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.
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## MacOS
Helix can be installed on MacOS through homebrew via:
```
brew tap helix-editor/helix
brew install helix
```
# Contributing
Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
@@ -58,7 +72,3 @@ Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
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 [@jakenvac](https://github.com/jakenvac) for designing the logo!

32
TODO.md Normal file
View File

@@ -0,0 +1,32 @@
- tree sitter:
- markdown
- regex
- kotlin
- clojure
- erlang
- [ ] completion isIncomplete support
1
- [ ] respect view fullscreen flag
- [ ] Implement marks (superset of Selection/Range)
- [ ] = for auto indent line/selection
- [ ] :x for closing buffers
- [ ] lsp: signature help
2
- [ ] macro recording
- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
- [ ] selection align
- [ ] store some state between restarts: file positions, prompt history
- [ ] highlight matched characters in picker
3
- [ ] diff mode with highlighting?
- [ ] snippet support (tab to jump between marks)
- [ ] gamelisp/wasm scripting
X
- [ ] rendering via skulpin/skia or raw wgpu

View File

@@ -1,17 +1,17 @@
# Author: NNB <nnbnh@protonmail.com>
"ui.menu" = { fg = "black", bg = "white" }
"ui.menu" = "black"
"ui.menu.selected" = { modifiers = ["reversed"] }
"ui.linenr" = { fg = "gray", bg = "black" }
"ui.popup" = { modifiers = ["reversed"] }
"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" }
"ui.cursor" = { fg = "white", modifiers = ["reversed"] }
"ui.help" = { modifiers = ["reversed"] }
"ui.cursor" = { modifiers = ["reversed"] }
"variable" = "red"
"constant.numeric" = "yellow"
"constant" = "yellow"
@@ -29,15 +29,6 @@
"namespace" = "magenta"
"ui.help" = { fg = "white", bg = "black" }
"markup.heading" = "blue"
"markup.list" = "red"
"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
"markup.link.text" = "red"
"markup.quote" = "cyan"
"markup.raw" = "green"
"diff.plus" = "green"
"diff.delta" = "yellow"
"diff.minus" = "red"

View File

@@ -3,14 +3,9 @@ authors = ["Blaž Hrastnik"]
language = "en"
multilingual = false
src = "src"
edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
[output.html]
cname = "docs.helix-editor.com"
default-theme = "colibri"
preferred-dark-theme = "colibri"
git-repository-url = "https://github.com/helix-editor/helix"
edit-url-template = "https://github.com/helix-editor/helix/edit/master/book/{path}"
additional-css = ["custom.css"]
[output.html.search]
use-boolean-and = true

View File

@@ -1,231 +0,0 @@
html {
font-family: "Inter", sans-serif;
}
.sidebar .sidebar-scrollbox {
padding: 0;
}
.chapter {
margin: 0.25rem 0;
}
.chapter li.chapter-item {
line-height: initial;
margin: 0;
padding: 1rem 1.5rem;
}
.chapter .section li.chapter-item {
line-height: inherit;
padding: .5rem .5rem 0 .5rem;
}
.content {
overflow-y: auto;
padding: 0 15px;
padding-bottom: 50px;
}
/* 2 1.75 1.5 1.25 1 .875 */
.content h1 { font-size: 2em }
.content h2 { font-size: 1.75em }
.content h3 { font-size: 1.5em }
.content h4 { font-size: 1.25em }
.content h5 { font-size: 1em }
.content h6 { font-size: .875em }
.content h1,
.content h2,
.content h3,
.content h4 {
font-weight: 500;
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p,
.content ol,
.content ul,
.content table {
margin-top: 0;
margin-bottom: .875em;
}
.content ul li {
margin-bottom: .25rem;
}
.content ul {
list-style-type: square;
}
.content ul ul,
.content ol ul {
margin-bottom: .5rem;
}
.content li p {
margin-bottom: .5em;
}
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-left: 4px solid var(--quote-border);
border-top: none;
border-bottom: none;
}
blockquote *:last-child {
margin-bottom: 0;
}
table {
width: 100%;
}
table thead th {
padding: .75rem;
text-align: left;
font-weight: 500;
line-height: 1.5;
width: auto;
}
table td {
padding: .75rem;
border: none;
}
table thead tr {
border: none;
border-bottom: 2px var(--table-border-color) solid;
}
table tbody tr {
border-bottom: 1px var(--table-border-line) solid;
}
table tbody tr:nth-child(2n) {
background: unset;
}
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
.colibri {
--bg: #3b224c;
--fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
/* --links: #a4a0e8; */
--links: #ECCDBA;
--inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0, 0, 0, .2);
--quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
}
.colibri .content .header {
color: #fff;
}
/* highlight.js theme, :where() is used to avoid increasing specificity */
:where(.colibri) .hljs {
background: #2f1e2e;
color: #a39e9b;
}
:where(.colibri) .hljs-comment,
:where(.colibri) .hljs-quote {
color: #8d8687;
}
:where(.colibri) .hljs-link,
:where(.colibri) .hljs-meta,
:where(.colibri) .hljs-name,
:where(.colibri) .hljs-regexp,
:where(.colibri) .hljs-selector-class,
:where(.colibri) .hljs-selector-id,
:where(.colibri) .hljs-tag,
:where(.colibri) .hljs-template-variable,
:where(.colibri) .hljs-variable {
color: #ef6155;
}
:where(.colibri) .hljs-built_in,
:where(.colibri) .hljs-deletion,
:where(.colibri) .hljs-literal,
:where(.colibri) .hljs-number,
:where(.colibri) .hljs-params,
:where(.colibri) .hljs-type {
color: #f99b15;
}
:where(.colibri) .hljs-attribute,
:where(.colibri) .hljs-section,
:where(.colibri) .hljs-title {
color: #fec418;
}
:where(.colibri) .hljs-addition,
:where(.colibri) .hljs-bullet,
:where(.colibri) .hljs-string,
:where(.colibri) .hljs-symbol {
color: #48b685;
}
:where(.colibri) .hljs-keyword,
:where(.colibri) .hljs-selector-tag {
color: #815ba4;
}
:where(.colibri) .hljs-emphasis {
font-style: italic;
}
:where(.colibri) .hljs-strong {
font-weight: 700;
}

View File

@@ -1,19 +1,16 @@
# Summary
[Helix](./title-page.md)
- [Installation](./install.md)
- [Usage](./usage.md)
- [Keymap](./keymap.md)
- [Commands](./commands.md)
- [Language support](./lang-support.md)
- [Language Support](./lang-support.md)
- [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md)
- [Themes](./themes.md)
- [Key remapping](./remapping.md)
- [Key Remapping](./remapping.md)
- [Hooks](./hooks.md)
- [Languages](./languages.md)
- [Guides](./guides/README.md)
- [Adding languages](./guides/adding_languages.md)
- [Adding textobject queries](./guides/textobject.md)
- [Adding indent queries](./guides/indent.md)
- [Adding injection queries](./guides/injection.md)
- [Adding Languages](./guides/adding_languages.md)
- [Adding Textobject Queries](./guides/textobject.md)

View File

@@ -1,5 +1,5 @@
# Commands
Command mode can be activated by pressing `:`. The built-in commands are:
Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
{{#include ./generated/typable-cmd.md}}

View File

@@ -2,375 +2,45 @@
To override global configuration parameters, create a `config.toml` file located in your config directory:
- Linux and Mac: `~/.config/helix/config.toml`
- Windows: `%AppData%\helix\config.toml`
> 💡 You can easily open the config file by typing `:config-open` within Helix normal mode.
Example config:
```toml
theme = "onedark"
[editor]
line-number = "relative"
mouse = false
[editor.cursor-shape]
insert = "bar"
normal = "block"
select = "underline"
[editor.file-picker]
hidden = false
```
You can use a custom configuration file by specifying it with the `-c` or
`--config` command line argument, for example `hx -c path/to/custom-config.toml`.
Additionally, you can reload the configuration file by sending the USR1
signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.
* Linux and Mac: `~/.config/helix/config.toml`
* Windows: `%AppData%\helix\config.toml`
## Editor
### `[editor]` Section
`[editor]` section of the config.
| Key | Description | Default |
|--|--|---------|
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling | `5` |
| `mouse` | Enable mouse mode | `true` |
| `middle-click-paste` | Middle click paste support | `true` |
| `scroll-lines` | Number of lines to scroll per scroll wheel step | `3` |
| `shell` | Shell to use when running external commands | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `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 `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 the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `250` |
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` |
| `mouse` | Enable mouse mode. | `true` |
| `middle-click-paste` | Middle click paste support. | `true` |
| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
| `auto-info` | Whether to display info boxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
| `undercurl` | Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative | `false` |
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
| `auto-info` | Whether to display infoboxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
### `[editor.statusline]` Section
Allows configuring the statusline at the bottom of the editor.
The configuration distinguishes between three areas of the status line:
`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]`
Statusline elements can be defined as follows:
```toml
[editor.statusline]
left = ["mode", "spinner"]
center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
separator = "│"
mode.normal = "NORMAL"
mode.insert = "INSERT"
mode.select = "SELECT"
```
The `[editor.statusline]` key takes the following sub-keys:
| Key | Description | Default |
| --- | --- | --- |
| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
| `center` | A list of elements aligned to the middle of the statusline | `[]` |
| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
| `separator` | The character used to separate elements in the statusline | `"│"` |
| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` |
The following statusline elements can be configured:
| Key | Description |
| ------ | ----------- |
| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) |
| `spinner` | A progress spinner indicating LSP activity |
| `file-name` | The path/name of the opened file |
| `file-base-name` | The basename of the opened file |
| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `file-line-ending` | The file line endings (CRLF or LF) |
| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written |
| `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections |
| `primary-selection-length` | The number of characters currently in primary selection |
| `position` | The cursor position |
| `position-percentage` | The cursor position as a percentage of the total number of lines |
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |
| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) |
| `version-control` | The current branch name or detached commit hash of the opened workspace |
| `register` | The current selected register |
### `[editor.lsp]` Section
| Key | Description | Default |
| --- | ----------- | ------- |
| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them!
### `[editor.cursor-shape]` Section
Defines the shape of cursor in each mode.
Valid values for these options are `block`, `bar`, `underline`, or `hidden`.
> 💡 Due to limitations of the terminal environment, only the primary cursor can
> change shape.
| Key | Description | Default |
| --- | ----------- | ------- |
| `normal` | Cursor shape in [normal mode][normal mode] | `block` |
| `insert` | Cursor shape in [insert mode][insert mode] | `block` |
| `select` | Cursor shape in [select mode][select mode] | `block` |
[normal mode]: ./keymap.md#normal-mode
[insert mode]: ./keymap.md#insert-mode
[select mode]: ./keymap.md#select--extend-mode
### `[editor.file-picker]` Section
Set options for file picker and global search. Ignoring a file means it is
not visible in the Helix file picker and global search.
All git related options are only enabled in a git repository.
`[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default.
| Key | Description | Default |
|--|--|---------|
|`hidden` | Enables ignoring hidden files | `true`
|`follow-symlinks` | Follow symlinks instead of ignoring them | `true`
|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `true`
|`parents` | Enables reading ignore files from parent directories | `true`
|`ignore` | Enables reading `.ignore` files | `true`
|`git-ignore` | Enables reading `.gitignore` files | `true`
|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true`
|`git-exclude` | Enables reading `.git/info/exclude` files | `true`
|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`.
|`hidden` | Enables ignoring hidden files. | true
|`parents` | Enables reading ignore files from parent directories. | true
|`ignore` | Enables reading `.ignore` files. | true
|`git-ignore` | Enables reading `.gitignore` files. | true
|`git-global` | Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option. | true
|`git-exclude` | Enables reading `.git/info/exclude` files. | true
|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`.
Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files.
Additionally, you can use Helix-specific ignore files by creating a local `.helix/ignore` file in the current workspace or a global `ignore` file located in your Helix config directory:
- Linux and Mac: `~/.config/helix/ignore`
- Windows: `%AppData%\helix\ignore`
Example:
```ini
# unignore in file picker and global search
!.github/
!.gitignore
!.gitattributes
```
### `[editor.auto-pairs]` Section
Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
simple boolean value, or a specific mapping of pairs of single characters.
To disable auto-pairs altogether, set `auto-pairs` to `false`:
## LSP
To display all language server messages in the status line add the following to your `config.toml`:
```toml
[editor]
auto-pairs = false # defaults to `true`
[lsp]
display-messages = true
```
The default pairs are <code>(){}[]''""``</code>, but these can be customized by
setting `auto-pairs` to a TOML table:
```toml
[editor.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'
```
Additionally, this setting can be used in a language config. Unless
the editor setting is `false`, this will override the editor config in
documents with this language.
Example `languages.toml` that adds <> and removes ''
```toml
[[language]]
name = "rust"
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'
```
### `[editor.search]` Section
Search specific options.
| Key | Description | Default |
|--|--|---------|
| `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` |
| `wrap-around`| Whether the search should wrap after depleting the matches | `true` |
### `[editor.whitespace]` Section
Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace.
| Key | Description | Default |
|-----|-------------|---------|
| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline` | `"none"` |
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below |
Example
```toml
[editor.whitespace]
render = "all"
# or control each character
[editor.whitespace.render]
space = "all"
tab = "all"
newline = "none"
[editor.whitespace.characters]
space = "·"
nbsp = "⍽"
tab = "→"
newline = "⏎"
tabpad = "·" # Tabs will look like "→···" (depending on tab width)
```
### `[editor.indent-guides]` Section
Options for rendering vertical indent guides.
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides | `false` |
| `character` | Literal character to use for rendering the indent guide | `` |
| `skip-levels` | Number of indent levels to skip | `0` |
Example:
```toml
[editor.indent-guides]
render = true
character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽"
skip-levels = 1
```
### `[editor.gutters]` Section
For simplicity, `editor.gutters` accepts an array of gutter types, which will
use default settings for all gutter components.
```toml
[editor]
gutters = ["diff", "diagnostics", "line-numbers", "spacer"]
```
To customize the behavior of gutters, the `[editor.gutters]` section must
be used. This section contains top level settings, as well as settings for
specific gutter components as subsections.
| Key | Description | Default |
| --- | --- | --- |
| `layout` | A vector of gutters to display | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
Example:
```toml
[editor.gutters]
layout = ["diff", "diagnostics", "line-numbers", "spacer"]
```
#### `[editor.gutters.line-numbers]` Section
Options for the line number gutter
| Key | Description | Default |
| --- | --- | --- |
| `min-width` | The minimum number of characters to use | `3` |
Example:
```toml
[editor.gutters.line-numbers]
min-width = 1
```
#### `[editor.gutters.diagnostics]` Section
Currently unused
#### `[editor.gutters.diff]` Section
Currently unused
#### `[editor.gutters.spacer]` Section
Currently unused
### `[editor.soft-wrap]` Section
Options for soft wrapping lines that exceed the view width:
| Key | Description | Default |
| --- | --- | --- |
| `enable` | Whether soft wrapping is enabled. | `false` |
| `max-wrap` | Maximum free space left at the end of the line. | `20` |
| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` |
| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `` |
| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` |
Example:
```toml
[editor.soft-wrap]
enable = true
max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it
```
### `[editor.smart-tab]` Section
| Key | Description | Default |
|------------|-------------|---------|
| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |

View File

@@ -1,12 +1,12 @@
# Migrating from Vim
Helix's editing model is strongly inspired from Vim and Kakoune, and a notable
difference from Vim (and the most striking similarity to Kakoune) is that Helix
follows the `selection → action` model. This means that whatever you are
going to act on (a word, a paragraph, a line, etc.) is selected first and the
action itself (delete, change, yank, etc.) comes second. A cursor is simply a
Helix's editing model is strongly inspired from vim and kakoune, and a notable
difference from vim (and the most striking similarity to kakoune) is that Helix
follows the `selection → action` model. This means that the whatever you are
going to act on (a word, a paragraph, a line, etc) is selected first and the
action itself (delete, change, yank, etc) comes second. A cursor is simply a
single width selection.
See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim) and Helix's [Migrating from Vim](https://github.com/helix-editor/helix/wiki/Migrating-from-Vim).
See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim).
> TODO: Mention textobjects, surround, registers
> TODO: Mention texobjects, surround, registers

View File

@@ -1,194 +1,51 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- |
| agda | ✓ | | | |
| astro | ✓ | | | |
| awk | ✓ | | | `awk-language-server` |
| bash | ✓ | ✓ | ✓ | `bash-language-server` |
| bass | ✓ | | | `bass` |
| beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` |
| blueprint | ✓ | | | `blueprint-compiler` |
| c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` |
| cabal | | | | `haskell-language-server-wrapper` |
| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
| capnp | ✓ | | ✓ | |
| clojure | ✓ | | | `clojure-lsp` |
| bash | ✓ | | | `bash-language-server` |
| c | ✓ | | | `clangd` |
| c-sharp | ✓ | | | |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
| comment | ✓ | | | |
| common-lisp | ✓ | | | `cl-lsp` |
| cpon | ✓ | | | |
| cpp | ✓ | ✓ | ✓ | `clangd` |
| crystal | ✓ | ✓ | | `crystalline` |
| css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` |
| d | ✓ | ✓ | ✓ | `serve-d` |
| cpp | ✓ | | | `clangd` |
| css | ✓ | | | |
| dart | ✓ | | ✓ | `dart` |
| dbml | ✓ | | | |
| devicetree | ✓ | | | |
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
| diff | ✓ | | | |
| dockerfile | ✓ | | | `docker-langserver` |
| dot | ✓ | | | `dot-language-server` |
| dtd | ✓ | | | |
| edoc | ✓ | | | |
| eex | ✓ | | | |
| ejs | ✓ | | | |
| elixir | ✓ | ✓ | ✓ | `elixir-ls` |
| elm | ✓ | ✓ | | `elm-language-server` |
| elvish | ✓ | | | `elvish` |
| env | ✓ | | | |
| erb | ✓ | | | |
| erlang | ✓ | ✓ | | `erlang_ls` |
| esdl | ✓ | | | |
| elixir | ✓ | | | `elixir-ls` |
| fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | `forth-lsp` |
| fortran | ✓ | | | `fortls` |
| fsharp | ✓ | | | `fsautocomplete` |
| gas | ✓ | ✓ | | |
| gdscript | ✓ | ✓ | ✓ | |
| gemini | ✓ | | | |
| git-attributes | ✓ | | | |
| git-commit | ✓ | ✓ | | |
| git-config | ✓ | | | |
| git-ignore | ✓ | | | |
| git-commit | ✓ | | | |
| git-diff | ✓ | | | |
| git-rebase | ✓ | | | |
| gleam | ✓ | ✓ | | `gleam` |
| glsl | ✓ | ✓ | ✓ | |
| gn | ✓ | | | |
| go | ✓ | | | `gopls`, `golangci-lint-langserver` |
| godot-resource | ✓ | | | |
| gomod | ✓ | | | `gopls` |
| gotmpl | ✓ | | | `gopls` |
| gowork | ✓ | | | `gopls` |
| graphql | ✓ | | | `graphql-lsp` |
| hare | ✓ | | | |
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | |
| hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` |
| hocon | ✓ | | ✓ | |
| hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server` |
| hurl | ✓ | | ✓ | |
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
| janet | ✓ | | | |
| java | ✓ | ✓ | ✓ | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | |
| jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` |
| json5 | ✓ | | | |
| jsonnet | ✓ | | | `jsonnet-language-server` |
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| julia | ✓ | ✓ | ✓ | `julia` |
| just | ✓ | ✓ | ✓ | |
| kdl | ✓ | ✓ | ✓ | |
| kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | ✓ | | `texlab` |
| lean | ✓ | | | `lean` |
| glsl | ✓ | | ✓ | |
| go | ✓ | ✓ | ✓ | `gopls` |
| html | ✓ | | | |
| java | ✓ | | | |
| javascript | ✓ | | | |
| json | ✓ | | | |
| julia | ✓ | | | `julia` |
| latex | ✓ | | | |
| ledger | ✓ | | | |
| llvm | ✓ | ✓ | ✓ | |
| llvm-mir | ✓ | | ✓ | |
| llvm-mir-yaml | ✓ | | | |
| log | ✓ | | | |
| lpf | ✓ | | | |
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
| make | ✓ | | | |
| markdoc | ✓ | | | `markdoc-ls` |
| markdown | ✓ | | | `marksman` |
| markdown.inline | ✓ | | | |
| matlab | ✓ | ✓ | ✓ | |
| mermaid | ✓ | | | |
| meson | ✓ | | ✓ | |
| lua | ✓ | | ✓ | |
| markdown | ✓ | | | |
| mint | | | | `mint` |
| msbuild | ✓ | | ✓ | |
| nasm | ✓ | | | |
| nickel | ✓ | | | `nls` |
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
| nix | ✓ | | | `nil` |
| nu | ✓ | | | `nu` |
| nunjucks | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | ✓ | `ols` |
| opencl | ✓ | ✓ | ✓ | `clangd` |
| openscad | ✓ | | | `openscad-lsp` |
| org | ✓ | | | |
| pascal | ✓ | ✓ | | `pasls` |
| passwd | ✓ | | | |
| pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
| php | ✓ | ✓ | ✓ | `intelephense` |
| po | ✓ | ✓ | | |
| pod | ✓ | | | |
| ponylang | ✓ | ✓ | ✓ | |
| prisma | ✓ | | | `prisma-language-server` |
| nix | ✓ | | ✓ | `rnix-lsp` |
| ocaml | ✓ | | | |
| ocaml-interface | ✓ | | | |
| perl | ✓ | ✓ | ✓ | |
| php | ✓ | | | |
| prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | `bufls`, `pb` |
| prql | ✓ | | | |
| purescript | ✓ | ✓ | | `purescript-language-server` |
| protobuf | ✓ | | ✓ | |
| python | ✓ | ✓ | ✓ | `pylsp` |
| qml | | | | `qmlls` |
| r | ✓ | | | `R` |
| racket | ✓ | | ✓ | `racket` |
| regex | ✓ | | | |
| rego | ✓ | | | `regols` |
| rescript | ✓ | ✓ | | `rescript-language-server` |
| rmarkdown | ✓ | | ✓ | `R` |
| robot | ✓ | | | `robotframework_ls` |
| ron | ✓ | | ✓ | |
| rst | ✓ | | | |
| ruby | ✓ | ✓ | ✓ | `solargraph` |
| racket | | | | `racket` |
| ruby | ✓ | | | `solargraph` |
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
| sage | ✓ | ✓ | | |
| scala | ✓ | ✓ | ✓ | `metals` |
| scheme | ✓ | | ✓ | |
| scss | ✓ | | | `vscode-css-language-server` |
| slint | ✓ | | ✓ | `slint-lsp` |
| smali | ✓ | | ✓ | |
| smithy | ✓ | | | `cs` |
| sml | ✓ | | | |
| solidity | ✓ | | | `solc` |
| sql | ✓ | | | |
| sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | |
| strace | ✓ | | | |
| scala | ✓ | | ✓ | `metals` |
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | | | `sourcekit-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | |
| task | ✓ | | | |
| templ | ✓ | | | `templ` |
| tfvars | ✓ | | ✓ | `terraform-ls` |
| todotxt | ✓ | | | |
| toml | ✓ | | | `taplo` |
| toml | ✓ | | | |
| tsq | ✓ | | | |
| tsx | ✓ | | | `typescript-language-server` |
| twig | ✓ | | | |
| typescript | ✓ | | | `typescript-language-server` |
| typst | ✓ | | | `typst-lsp` |
| ungrammar | ✓ | | | |
| unison | ✓ | | | |
| uxntal | ✓ | | | |
| v | ✓ | ✓ | ✓ | `v-analyzer` |
| vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | |
| vue | ✓ | | | `vue-language-server` |
| wast | ✓ | | | |
| wat | ✓ | | | |
| webc | ✓ | | | |
| wgsl | ✓ | | | `wgsl_analyzer` |
| wit | ✓ | | ✓ | |
| wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | |
| xml | ✓ | | ✓ | |
| yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` |
| yuck | ✓ | | | |
| zig | ✓ | ✓ | ✓ | `zls` |
| tsx | ✓ | | | `typescript-language-server` |
| typescript | ✓ | | | `typescript-language-server` |
| vue | ✓ | | | |
| wgsl | ✓ | | | |
| yaml | ✓ | | | |
| zig | ✓ | | | `zls` |

View File

@@ -1,38 +1,26 @@
| Name | Description |
| --- | --- |
| `:quit`, `:q` | Close the current view. |
| `:quit!`, `:q!` | Force close the current view, ignoring unsaved changes. |
| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). |
| `:open`, `:o` | Open a file from disk into the current view. |
| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully, ignoring unsaved changes. |
| `:buffer-close-others`, `:bco`, `:bcloseother` | Close all buffers but the currently focused one. |
| `:buffer-close-others!`, `:bco!`, `:bcloseother!` | 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. |
| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). |
| `:write`, `:w` | Write changes to disk. 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) |
| `:write-buffer-close`, `:wbc` | Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt) |
| `:write-buffer-close!`, `:wbc!` | Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! 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-16 for number of spaces.) |
| `:line-ending` | Set the document's default line ending. Options: crlf, lf. |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
| `:line-ending` | Set the document's default line ending. Options: crlf, lf, cr, ff, nel. |
| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
| `:write-all`, `:wa` | Write changes from all buffers to disk. |
| `:write-all!`, `:wa!` | Forcefully write changes from all buffers to disk creating necessary subdirectories. |
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. |
| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). |
| `:write-all`, `:wa` | Write changes from all views to disk. |
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all views to disk and close all views. |
| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all views to disk and close all views forcefully (ignoring unsaved changes). |
| `:quit-all`, `:qa` | Close all views. |
| `:quit-all!`, `:qa!` | Force close all views ignoring unsaved changes. |
| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). |
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:cquit!`, `:cq!` | 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). |
| `:yank-join` | Yank joined selections. A separator can be provided as first argument. Default value is newline. |
| `:theme` | Change the editor theme. |
| `:clipboard-yank` | Yank main selection into system clipboard. |
| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
@@ -46,43 +34,13 @@
| `: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`. |
| `:character-info`, `:char` | Get info about the character under the primary cursor. |
| `:reload`, `:rl` | Discard changes and reload from the source file. |
| `:reload-all`, `:rla` | Discard changes and reload all documents from the source files. |
| `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the language servers used by the current doc |
| `:lsp-stop` | Stops the language servers that are used by the current doc |
| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
| `:reload` | Discard changes and reload from the source file. |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:tree-sitter-highlight-name` | Display name of tree-sitter highlight scope under the cursor. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
| `:debug-eval` | Evaluate expression in current debug context. |
| `:vsplit`, `:vs` | Open the file in a vertical split. |
| `:vsplit-new`, `:vnew` | Open a scratch buffer in a vertical split. |
| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
| `:hsplit-new`, `:hnew` | Open a scratch buffer in a horizontal split. |
| `:tutor` | Open the tutorial. |
| `:goto`, `:g` | Goto line number. |
| `:set-language`, `:lang` | Set the language of current buffer (show current language if no value specified). |
| `:set-option`, `:set` | Set a config option at runtime.<br>For example to disable smart case search, use `:set search.smart-case false`. |
| `:toggle-option`, `:toggle` | Toggle a boolean config option at runtime.<br>For example to toggle smart case search, use `:toggle search.smart-case`. |
| `:get-option`, `:get` | Get the current value of a config option. |
| `:goto`, `:g` | Go to line number. |
| `:set-option`, `:set` | Set a config option at runtime |
| `:sort` | Sort ranges in selection. |
| `:rsort` | Sort ranges in selection in reverse order. |
| `:reflow` | Hard-wrap the current selection of lines to a given width. |
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
| `:config-reload` | Refresh user config. |
| `:config-open` | Open the user config.toml file. |
| `:config-open-workspace` | Open the workspace config.toml file. |
| `:log-open` | Open the helix log file. |
| `: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. |
| `:pipe-to` | Pipe each selection to the shell command, ignoring output. |
| `:run-shell-command`, `:sh` | Run a shell command |
| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
| `:redraw` | Clear and re-render the whole UI |
| `:move` | Move the current buffer and its corresponding file to a different path |

View File

@@ -1,4 +1,4 @@
# Guides
This section contains guides for adding new language server configurations,
tree-sitter grammars, textobject queries, and other similar items.
tree-sitter grammers, textobject queries, etc.

View File

@@ -1,55 +1,71 @@
# Adding new languages to Helix
# Adding languages
In order to add a new language to Helix, you will need to follow the steps
below.
## Submodules
## Language configuration
To add a new language, you should first add a tree-sitter submodule. To do this,
you can run the command
```sh
git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
```
For example, to add tree-sitter-ocaml you would run
```sh
git submodule add -f https://github.com/tree-sitter/tree-sitter-ocaml helix-syntax/languages/tree-sitter-ocaml
```
Make sure the submodule is shallow by doing
```sh
git config -f .gitmodules submodule.helix-syntax/languages/tree-sitter-<name>.shallow true
```
1. Add a new `[[language]]` entry in the `languages.toml` file and provide the
necessary configuration for the new language. For more information on
language configuration, refer to the
[language configuration section](../languages.md) of the documentation.
A new language server can be added by extending the `[language-server]` table in the same file.
2. If you are adding a new language or updating an existing language server
configuration, run the command `cargo xtask docgen` to update the
[Language Support](../lang-support.md) documentation.
or you can manually add `shallow = true` to `.gitmodules`.
> 💡 If you are adding a new Language Server configuration, make sure to update
> the
> [Language Server Wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
> with the installation instructions.
## languages.toml
## Grammar configuration
Next, you need to add the language to the [`languages.toml`][languages.toml] found in the root of
the repository; this `languages.toml` file is included at compilation time, and
is distinct from the `language.toml` file in the user's [configuration
directory](../configuration.md).
1. If a tree-sitter grammar is available for the new language, add a new
`[[grammar]]` entry to the `languages.toml` file.
2. If you are testing the grammar locally, you can use the `source.path` key
with an absolute path to the grammar. However, before submitting a pull
request, make sure to switch to using `source.git`.
These are the available keys and descriptions for the file.
| Key | Description |
| ---- | ----------- |
| name | The name of the language |
| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` |
| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
| auto-format | Whether to autoformat this language when saving |
| diagnostic-severity | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
| comment-token | The token to use as a comment-token |
| indent | The indent to use. Has sub keys `tab-width` and `unit` |
| config | Language server configuration |
## Queries
1. In order to provide syntax highlighting and indentation for the new language,
you will need to add queries.
2. Create a new directory for the language with the path
`runtime/queries/<name>/`.
3. Refer to the
[tree-sitter website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
for more information on writing queries.
4. A list of highlight captures can be found [on the themes page](https://docs.helix-editor.com/themes.html#scopes).
For a language to have syntax-highlighting and indentation among
other things, you have to add queries. Add a directory for your
language with the path `runtime/queries/<name>/`. The tree-sitter
[website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
gives more info on how to write queries.
> 💡 In Helix, the first matching query takes precedence when evaluating
> queries, which is different from other editors such as Neovim where the last
> matching query supersedes the ones before it. See
> [this issue](https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090)
> for an example.
> NOTE: When evaluating queries, the first matching query takes
precedence, which is different from other editors like neovim where
the last matching query supercedes the ones before it. See
[this issue][neovim-query-precedence] for an example.
## Common issues
## Common Issues
- If you encounter errors when running Helix after switching branches, you may
need to update the tree-sitter grammars. Run the command `hx --grammar fetch`
to fetch the grammars and `hx --grammar build` to build any out-of-date
grammars.
- If a parser is causing a segfault, or you want to remove it, make sure to
remove the compiled parser located at `runtime/grammars/<name>.so`.
- If you are attempting to add queries and Helix is unable to locate them, ensure that the environment variable `HELIX_RUNTIME` is set to the location of the `runtime` folder you're developing in.
- If you get errors when building after switching branches, you may have to remove or update tree-sitter submodules. You can update submodules by running
```sh
git submodule sync; git submodule update --init
```
- Make sure to not use the `--remote` flag. To remove submodules look inside the `.gitmodules` and remove directories that are not present inside of it.
- If a parser is segfaulting or you want to remove the parser, make sure to remove the submodule *and* the compiled parser in `runtime/grammar/<name>.so`
- The indents query is `indents.toml`, *not* `indents.scm`. See [this](https://github.com/helix-editor/helix/issues/114) issue for more information.
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
[neovim-query-precedence]: https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090

View File

@@ -1,369 +0,0 @@
# Adding indent queries
Helix uses tree-sitter to correctly indent new lines. This requires a tree-
sitter grammar and an `indent.scm` query file placed in `runtime/queries/
{language}/indents.scm`. The indentation for a line is calculated by traversing
the syntax tree from the lowest node at the beginning of the new line (see
[Indent queries](#indent-queries)). Each of these nodes contributes to the total
indent when it is captured by the query (in what way depends on the name of
the capture.
Note that it matters where these added indents begin. For example,
multiple indent level increases that start on the same line only increase
the total indent level by 1. See [Capture types](#capture-types).
By default, Helix uses the `hybrid` indentation heuristic. This means that
indent queries are not used to compute the expected absolute indentation of a
line but rather the expected difference in indentation between the new and an
already existing line. This difference is then added to the actual indentation
of the already existing line. Since this makes errors in the indent queries
harder to find, it is recommended to disable it when testing via
`:set indent-heuristic tree-sitter`. The rest of this guide assumes that
the `tree-sitter` heuristic is used.
## Indent queries
When Helix is inserting a new line through `o`, `O`, or `<ret>`, to determine
the indent level for the new line, the query in `indents.scm` is run on the
document. The starting position of the query is the end of the line above where
a new line will be inserted.
For `o`, the inserted line is the line below the cursor, so that starting
position of the query is the end of the current line.
```rust
fn need_hero(some_hero: Hero, life: Life) -> {
matches!(some_hero, Hero { // ←─────────────────╮
strong: true,//←╮ ↑ ↑ │
fast: true, // │ │ ╰── query start │
sure: true, // │ ╰───── cursor ├─ traversal
soon: true, // ╰──────── new line inserted │ start node
}) && // │
// ↑ │
// ╰───────────────────────────────────────────────╯
some_hero > life
}
```
For `O`, the newly inserted line is the *current* line, so the starting position
of the query is the end of the line above the cursor.
```rust
fn need_hero(some_hero: Hero, life: Life) -> { // ←─╮
matches!(some_hero, Hero { // ←╮ ↑ │
strong: true,// ↑ ╭───╯ │ │
fast: true, // │ │ query start ─╯ │
sure: true, // ╰───┼ cursor ├─ traversal
soon: true, // ╰ new line inserted │ start node
}) && // │
some_hero > life // │
} // ←──────────────────────────────────────────────╯
```
From this starting node, the syntax tree is traversed up until the root node.
Each indent capture is collected along the way, and then combined according to
their [capture types](#capture-types) and [scopes](#scopes) to a final indent
level for the line.
### Capture types
- `@indent` (default scope `tail`):
Increase the indent level by 1. Multiple occurrences in the same line *do not*
stack. If there is at least one `@indent` and one `@outdent` capture on the
same line, the indent level isn't changed at all.
- `@outdent` (default scope `all`):
Decrease the indent level by 1. The same rules as for `@indent` apply.
- `@indent.always` (default scope `tail`):
Increase the indent level by 1. Multiple occurrences on the same line *do*
stack. The final indent level is `@indent.always` `@outdent.always`. If
an `@indent` and an `@indent.always` are on the same line, the `@indent` is
ignored.
- `@outdent.always` (default scope `all`):
Decrease the indent level by 1. The same rules as for `@indent.always` apply.
- `@align` (default scope `all`):
Align everything inside this node to some anchor. The anchor is given
by the start of the node captured by `@anchor` in the same pattern.
Every pattern with an `@align` should contain exactly one `@anchor`.
Indent (and outdent) for nodes below (in terms of their starting line)
the `@align` node is added to the indentation required for alignment.
- `@extend`:
Extend the range of this node to the end of the line and to lines that are
indented more than the line that this node starts on. This is useful for
languages like Python, where for the purpose of indentation some nodes (like
functions or classes) should also contain indented lines that follow them.
- `@extend.prevent-once`:
Prevents the first extension of an ancestor of this node. For example, in Python
a return expression always ends the block that it is in. Note that this only
stops the extension of the next `@extend` capture. If multiple ancestors are
captured, only the extension of the innermost one is prevented. All other
ancestors are unaffected (regardless of whether the innermost ancestor would
actually have been extended).
#### `@indent` / `@outdent`
Consider this example:
```rust
fn shout(things: Vec<Thing>) {
// ↑
// ├───────────────────────╮ indent level
// @indent ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// │
let it_all = |out| { things.filter(|thing| { // │ 1
// ↑ ↑ │
// ├───────────────────────┼─────┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// @indent @indent │
// │ 2
thing.can_do_with(out) // │
})}; // ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄
//↑↑↑ │ 1
} //╰┼┴──────────────────────────────────────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// 3x @outdent
```
```scm
((block) @indent)
["}" ")"] @outdent
```
Note how on the second line, we have two blocks begin on the same line. In this
case, since both captures occur on the same line, they are combined and only
result in a net increase of 1. Also note that the closing `}`s are part of the
`@indent` captures, but the 3 `@outdent`s also combine into 1 and result in that
line losing one indent level.
#### `@extend` / `@extend.prevent-once`
For an example of where `@extend` can be useful, consider Python, which is
whitespace-sensitive.
```scm
]
(parenthesized_expression)
(function_definition)
(class_definition)
] @indent
```
```python
class Hero:
def __init__(self, strong, fast, sure, soon):# ←─╮
self.is_strong = strong # │
self.is_fast = fast # ╭─── query start │
self.is_sure = sure # │ ╭─ cursor │
self.is_soon = soon # │ │ │
# ↑ ↑ │ │ │
# │ ╰──────╯ │ │
# ╰─────────────────────╯ │
# ├─ traversal
def need_hero(self, life): # │ start node
return ( # │
self.is_strong # │
and self.is_fast # │
and self.is_sure # │
and self.is_soon # │
and self > life # │
) # ←─────────────────────────────────────────╯
```
Without braces to catch the scope of the function, the smallest descendant of
the cursor on a line feed ends up being the entire inside of the class. Because
of this, it will miss the entire function node and its indent capture, leading
to an indent level one too small.
To address this case, `@extend` tells helix to "extend" the captured node's span
to the line feed and every consecutive line that has a greater indent level than
the line of the node.
```scm
(parenthesized_expression) @indent
]
(function_definition)
(class_definition)
] @indent @extend
```
```python
class Hero:
def __init__(self, strong, fast, sure, soon):# ←─╮
self.is_strong = strong # │
self.is_fast = fast # ╭─── query start ├─ traversal
self.is_sure = sure # │ ╭─ cursor │ start node
self.is_soon = soon # │ │ ←───────────────╯
# ↑ ↑ │ │
# │ ╰──────╯ │
# ╰─────────────────────╯
def need_hero(self, life):
return (
self.is_strong
and self.is_fast
and self.is_sure
and self.is_soon
and self > life
)
```
Furthermore, there are some cases where extending to everything with a greater
indent level may not be desirable. Consider the `need_hero` function above. If
our cursor is on the last line of the returned expression.
```python
class Hero:
def __init__(self, strong, fast, sure, soon):
self.is_strong = strong
self.is_fast = fast
self.is_sure = sure
self.is_soon = soon
def need_hero(self, life):
return (
self.is_strong
and self.is_fast
and self.is_sure
and self.is_soon
and self > life
) # ←─── cursor
#←────────── where cursor should go on new line
```
In Python, the are a few tokens that will always end a scope, such as a return
statement. Since the scope ends, so should the indent level. But because the
function span is extended to every line with a greater indent level, a new line
would just continue on the same level. And an `@outdent` would not help us here
either, since it would cause everything in the parentheses to become outdented
as well.
To help, we need to signal an end to the extension. We can do this with
`@extend.prevent-once`.
```scm
(parenthesized_expression) @indent
]
(function_definition)
(class_definition)
] @indent @extend
(return_statement) @extend.prevent-once
```
#### `@indent.always` / `@outdent.always`
As mentioned before, normally if there is more than one `@indent` or `@outdent`
capture on the same line, they are combined.
Sometimes, there are cases when you may want to ensure that every indent capture
is additive, regardless of how many occur on the same line. Consider this
example in YAML.
```yaml
- foo: bar
# ↑ ↑
# │ ╰─────────────── start of map
# ╰───────────────── start of list element
baz: quux # ←─── cursor
# ←───────────── where the cursor should go on a new line
garply: waldo
- quux:
bar: baz
xyzzy: thud
fred: plugh
```
In YAML, you often have lists of maps. In these cases, the syntax is such that
the list element and the map both start on the same line. But we really do want
to start an indentation for each of these so that subsequent keys in the map
hang over the list and align properly. This is where `@indent.always` helps.
```scm
((block_sequence_item) @item @indent.always @extend
(#not-one-line? @item))
((block_mapping_pair
key: (_) @key
value: (_) @val
(#not-same-line? @key @val)
) @indent.always @extend
)
```
## Predicates
In some cases, an S-expression cannot express exactly what pattern should be matched.
For that, tree-sitter allows for predicates to appear anywhere within a pattern,
similar to how `#set!` declarations work:
```scm
(some_kind
(child_kind) @indent
(#predicate? arg1 arg2 ...)
)
```
The number of arguments depends on the predicate that's used.
Each argument is either a capture (`@name`) or a string (`"some string"`).
The following predicates are supported by tree-sitter:
- `#eq?`/`#not-eq?`:
The first argument (a capture) must/must not be equal to the second argument
(a capture or a string).
- `#match?`/`#not-match?`:
The first argument (a capture) must/must not match the regex given in the
second argument (a string).
Additionally, we support some custom predicates for indent queries:
- `#not-kind-eq?`:
The kind of the first argument (a capture) must not be equal to the second
argument (a string).
- `#same-line?`/`#not-same-line?`:
The captures given by the 2 arguments must/must not start on the same line.
- `#one-line?`/`#not-one-line?`:
The captures given by the fist argument must/must span a total of one line.
### Scopes
Added indents don't always apply to the whole node. For example, in most
cases when a node should be indented, we actually only want everything
except for its first line to be indented. For this, there are several
scopes (more scopes may be added in the future if required):
- `tail`:
This scope applies to everything except for the first line of the
captured node.
- `all`:
This scope applies to the whole captured node. This is only different from
`tail` when the captured node is the first node on its line.
For example, imagine we have the following function
```rust
fn aha() { // ←─────────────────────────────────────╮
let take = "on me"; // ←──────────────╮ scope: │
let take = "me on"; // ├─ "tail" ├─ (block) @indent
let ill = be_gone_days(1 || 2); // │ │
} // ←───────────────────────────────────┴──────────┴─ "}" @outdent
// scope: "all"
```
We can write the following query with the `#set!` declaration:
```scm
((block) @indent
(#set! "scope" "tail"))
("}" @outdent
(#set! "scope" "all"))
```
As we can see, the "tail" scope covers the node, except for the first line.
Everything up to and including the closing brace gets an indent level of 1.
Then, on the closing brace, we encounter an outdent with a scope of "all", which
means the first line is included, and the indent level is cancelled out on this
line. (Note these scopes are the defaults for `@indent` and `@outdent`—they are
written explicitly for demonstration.)

View File

@@ -1,57 +0,0 @@
# Adding Injection Queries
Writing language injection queries allows one to highlight a specific node as a different language.
In addition to the [standard][upstream-docs] language injection options used by tree-sitter, there
are a few Helix specific extensions that allow for more control.
And example of a simple query that would highlight all strings as bash in Nix:
```scm
((string_expression (string_fragment) @injection.content)
(#set! injection.language "bash"))
```
## Capture Types
- `@injection.language` (standard):
The captured node may contain the language name used to highlight the node captured by
`@injection.content`.
- `@injection.content` (standard):
Marks the content to be highlighted as the language captured with `@injection.language` _et al_.
- `@injection.filename` (extension):
The captured node may contain a filename with a file-extension known to Helix,
highlighting `@injection.content` as that language. This uses the language extensions defined in
both the default languages.toml distributed with Helix, as well as user defined languages.
- `@injection.shebang` (extension):
The captured node may contain a shebang used to choose a language to highlight as. This also uses
the shebangs defined in the default and user `languages.toml`.
## Settings
- `injection.combined` (standard):
Indicates that all the matching nodes in the tree should have their content parsed as one
nested document.
- `injection.language` (standard):
Forces the captured content to be highlighted as the given language
- `injection.include-children` (standard):
Indicates that the content nodes entire text should be re-parsed, including the text of its child
nodes. By default, child nodes text will be excluded from the injected document.
- `injection.include-unnamed-children` (extension):
Same as `injection.include-children` but only for unnamed child nodes.
## Predicates
- `#eq?` (standard):
The first argument (a capture) must be equal to the second argument
(a capture or a string).
- `#match?` (standard):
The first argument (a capture) must match the regex given in the
second argument (a string).
[upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection

View File

@@ -1,14 +1,14 @@
# Adding textobject queries
# Adding Textobject Queries
Helix supports textobjects that are language specific, such as functions, classes, etc.
These textobjects require an accompanying tree-sitter grammar and a `textobjects.scm` query file
Textobjects that are language specific ([like functions, classes, etc][textobjects])
require an accompanying tree-sitter grammar and a `textobjects.scm` query file
to work properly. Tree-sitter allows us to query the source code syntax tree
and capture specific parts of it. The queries are written in a lisp dialect.
More information on how to write queries can be found in the [official tree-sitter
documentation][tree-sitter-queries].
Query files should be placed in `runtime/queries/{language}/textobjects.scm`
when contributing to Helix. Note that to test the query files locally you should put
when contributing. Note that to test the query files locally you should put
them under your local runtime directory (`~/.config/helix/runtime` on Linux
for example).
@@ -20,28 +20,11 @@ The following [captures][tree-sitter-captures] are recognized:
| `function.around` |
| `class.inside` |
| `class.around` |
| `test.inside` |
| `test.around` |
| `parameter.inside` |
| `comment.inside` |
| `comment.around` |
[Example query files][textobject-examples] can be found in the helix GitHub repository.
## Queries for textobject based navigation
Tree-sitter based navigation in Helix is done using captures in the
following order:
- `object.movement`
- `object.around`
- `object.inside`
For example if a `function.around` capture has been already defined for a language
in its `textobjects.scm` file, function navigation should also work automatically.
`function.movement` should be defined only if the node captured by `function.around`
doesn't make sense in a navigation context.
[textobjects]: ../usage.md#textobjects
[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=

1
book/src/hooks.md Normal file
View File

@@ -0,0 +1 @@
# Hooks

View File

@@ -1,322 +1,51 @@
# Installing Helix
# Installation
<!--toc:start-->
- [Pre-built binaries](#pre-built-binaries)
- [Linux, macOS, Windows and OpenBSD packaging status](#linux-macos-windows-and-openbsd-packaging-status)
- [Linux](#linux)
- [Ubuntu](#ubuntu)
- [Fedora/RHEL](#fedorarhel)
- [Arch Linux extra](#arch-linux-extra)
- [NixOS](#nixos)
- [Flatpak](#flatpak)
- [Snap](#snap)
- [AppImage](#appimage)
- [macOS](#macos)
- [Homebrew Core](#homebrew-core)
- [MacPorts](#macports)
- [Windows](#windows)
- [Winget](#winget)
- [Scoop](#scoop)
- [Chocolatey](#chocolatey)
- [MSYS2](#msys2)
- [Building from source](#building-from-source)
- [Configuring Helix's runtime files](#configuring-helixs-runtime-files)
- [Linux and macOS](#linux-and-macos)
- [Windows](#windows)
- [Multiple runtime directories](#multiple-runtime-directories)
- [Validating the installation](#validating-the-installation)
- [Configure the desktop shortcut](#configure-the-desktop-shortcut)
<!--toc:end-->
To install Helix, follow the instructions specific to your operating system.
Note that:
- To get the latest nightly version of Helix, you need to
[build from source](#building-from-source).
- To take full advantage of Helix, install the language servers for your
preferred programming languages. See the
[wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
for instructions.
## Pre-built binaries
Download pre-built binaries from the
[GitHub Releases page](https://github.com/helix-editor/helix/releases). Add the binary to your system's `$PATH` to use it from the command
line.
## Linux, macOS, Windows and OpenBSD packaging status
We provide pre-built binaries on the [GitHub Releases page](https://github.com/helix-editor/helix/releases).
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## Linux
## OSX
The following third party repositories are available:
A Homebrew tap is available:
### Ubuntu
Add the `PPA` for Helix:
```sh
sudo add-apt-repository ppa:maveonair/helix-editor
sudo apt update
sudo apt install helix
```
### Fedora/RHEL
```sh
sudo dnf install helix
```
### Arch Linux extra
Releases are available in the `extra` repository:
```sh
sudo pacman -S helix
```
Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available
in the AUR, which builds the master branch.
### NixOS
Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute,
the unstable channel usually carries the latest release.
Helix is also available as a [flake](https://nixos.wiki/wiki/Flakes) in the project
root. Use `nix develop` to spin up a reproducible development shell. Outputs are
cached for each push to master using [Cachix](https://www.cachix.org/). The
flake is configured to automatically make use of this cache assuming the user
accepts the new settings on first use.
If you are using a version of Nix without flakes enabled,
[install Cachix CLI](https://docs.cachix.org/installation) and use
`cachix use helix` to configure Nix to use cached outputs when possible.
### Flatpak
Helix is available on [Flathub](https://flathub.org/en-GB/apps/com.helix_editor.Helix):
```sh
flatpak install flathub com.helix_editor.Helix
flatpak run com.helix_editor.Helix
```
### Snap
Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with:
```sh
snap install --classic helix
```
This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`.
### AppImage
Install Helix using the Linux [AppImage](https://appimage.org/) format.
Download the official Helix AppImage from the [latest releases](https://github.com/helix-editor/helix/releases/latest) page.
```sh
chmod +x helix-*.AppImage # change permission for executable mode
./helix-*.AppImage # run helix
```
## macOS
### Homebrew Core
```sh
brew tap helix-editor/helix
brew install helix
```
### MacPorts
## Linux
```sh
port install helix
### NixOS
A [flake](https://nixos.wiki/wiki/Flakes) containing the package is available in
the project root. The flake can also be used to spin up a reproducible development
shell for working on Helix.
### Arch Linux
Releases are available in the `community` repository.
A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
### Fedora Linux
You can install the COPR package for Helix via
```
sudo dnf copr enable varlad/helix
sudo dnf install helix
```
## Windows
## Build from source
Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
or [MSYS2](https://msys2.org/).
### Winget
Windows Package Manager winget command-line tool is by default available on Windows 11 and modern versions of Windows 10 as a part of the App Installer.
You can get [App Installer from the Microsoft Store](https://www.microsoft.com/p/app-installer/9nblggh4nns1#activetab=pivot:overviewtab). If it's already installed, make sure it is updated with the latest version.
```sh
winget install Helix.Helix
```
git clone --recurse-submodules --shallow-submodules -j8 https://github.com/helix-editor/helix
cd helix
cargo install --path helix-term
```
### Scoop
This will install the `hx` binary to `$HOME/.cargo/bin`.
```sh
scoop install helix
```
### Chocolatey
```sh
choco install helix
```
### MSYS2
For 64-bit Windows 8.1 or above:
```sh
pacman -S mingw-w64-ucrt-x86_64-helix
```
## Building from source
Requirements:
Clone the Helix GitHub repository into a directory of your choice. The
examples in this documentation assume installation into either `~/src/` on
Linux and macOS, or `%userprofile%\src\` on Windows.
- The [Rust toolchain](https://www.rust-lang.org/tools/install)
- The [Git version control system](https://git-scm.com/)
- A C++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang
If you are using the `musl-libc` standard library instead of `glibc` the following environment variable must be set during the build to ensure tree-sitter grammars can be loaded correctly:
```sh
RUSTFLAGS="-C target-feature=-crt-static"
```
1. Clone the repository:
```sh
git clone https://github.com/helix-editor/helix
cd helix
```
2. Compile from source:
```sh
cargo install --path helix-term --locked
```
This command will create the `hx` executable and construct the tree-sitter
grammars in the local `runtime` folder.
> 💡 If you do not want to fetch or build grammars, set an environment variable `HELIX_DISABLE_AUTO_GRAMMAR_BUILD`
> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
> grammars with `hx --grammar fetch` and compile them with
> `hx --grammar build`. This will install them in
> the `runtime` directory within the user's helix config directory (more
> [details below](#multiple-runtime-directories)).
### Configuring Helix's runtime files
#### Linux and macOS
The **runtime** directory is one below the Helix source, so either set a
`HELIX_RUNTIME` environment variable to point to that directory and add it to
your `~/.bashrc` or equivalent:
```sh
HELIX_RUNTIME=~/src/helix/runtime
```
Or, create a symbolic link:
```sh
ln -Ts $PWD/runtime ~/.config/helix/runtime
```
If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again.
#### Windows
Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for
`Edit environment variables for your account`) or use the `setx` command in
Cmd:
```sh
setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
```
> 💡 `%userprofile%` resolves to your user directory like
> `C:\Users\Your-Name\` for example.
Or, create a symlink in `%appdata%\helix\` that links to the source code directory:
| Method | Command |
| ---------- | -------------------------------------------------------------------------------------- |
| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
| Cmd | `cd %appdata%\helix` <br/> `mklink /D runtime "%userprofile%\src\helix\runtime"` |
> 💡 On Windows, creating a symbolic link may require running PowerShell or
> Cmd as an administrator.
#### Multiple runtime directories
When Helix finds multiple runtime directories it will search through them for files in the
following order:
1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
developing and testing helix only).
2. `runtime/` subdirectory of OS-dependent helix user config directory.
3. `$HELIX_RUNTIME`
4. Distribution-specific fallback directory (set at compile time—not run time—
with the `HELIX_DEFAULT_RUNTIME` environment variable)
5. `runtime/` subdirectory of path to Helix executable.
This order also sets the priority for selecting which file will be used if multiple runtime
directories have files with the same name.
#### Note to packagers
If you are making a package of Helix for end users, to provide a good out of
the box experience, you should set the `HELIX_DEFAULT_RUNTIME` environment
variable at build time (before invoking `cargo build`) to a directory which
will store the final runtime files after installation. For example, say you want
to package the runtime into `/usr/lib/helix/runtime`. The rough steps a build
script could follow are:
1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime`
1. `cargo build --profile opt --locked --path helix-term`
1. `cp -r runtime $BUILD_DIR/usr/lib/helix/`
1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx`
This way the resulting `hx` binary will always look for its runtime directory in
`/usr/lib/helix/runtime` if the user has no custom runtime in `~/.config/helix`
or `HELIX_RUNTIME`.
### Validating the installation
To make sure everything is set up as expected you should run the Helix health
check:
```sh
hx --health
```
For more information on the health check results refer to
[Health check](https://github.com/helix-editor/helix/wiki/Healthcheck).
### Configure the desktop shortcut
If your desktop environment supports the
[XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html)
you can configure Helix to show up in the application menu by copying the
provided `.desktop` and icon files to their correct folders:
```sh
cp contrib/Helix.desktop ~/.local/share/applications
cp contrib/helix.png ~/.icons # or ~/.local/share/icons
```
To use another terminal than the system default, you can modify the `.desktop`
file. For example, to use `kitty`:
```sh
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
```
Helix also needs it's runtime files so make sure to copy/symlink the `runtime/` directory into the
config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
via the `HELIX_RUNTIME` environment variable.

View File

@@ -1,42 +1,20 @@
# Keymap
- [Normal mode](#normal-mode)
- [Movement](#movement)
- [Changes](#changes)
- [Shell](#shell)
- [Selection manipulation](#selection-manipulation)
- [Search](#search)
- [Minor modes](#minor-modes)
- [View mode](#view-mode)
- [Goto mode](#goto-mode)
- [Match mode](#match-mode)
- [Window mode](#window-mode)
- [Space mode](#space-mode)
- [Popup](#popup)
- [Unimpaired](#unimpaired)
- [Insert mode](#insert-mode)
- [Select / extend mode](#select--extend-mode)
- [Picker](#picker)
- [Prompt](#prompt)
> 💡 Mappings marked (**LSP**) require an active language server for the file.
> 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type.
- Mappings marked (**LSP**) require an active language server for the file.
- Mappings marked (**TS**) require a tree-sitter grammar for the filetype.
## Normal mode
Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key.
### Movement
> NOTE: Unlike Vim, `f`, `F`, `t` and `T` are not confined to the current line.
> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `h`, `Left` | Move left | `move_char_left` |
| `j`, `Down` | Move down | `move_visual_line_down` |
| `k`, `Up` | Move up | `move_visual_line_up` |
| `l`, `Right` | Move right | `move_char_right` |
| `h`/`Left` | Move left | `move_char_left` |
| `j`/`Down` | Move down | `move_line_down` |
| `k`/`Up` | Move up | `move_line_up` |
| `l`/`Right` | Move right | `move_char_right` |
| `w` | Move next word start | `move_next_word_start` |
| `b` | Move previous word start | `move_prev_word_start` |
| `e` | Move next word end | `move_next_word_end` |
@@ -47,17 +25,24 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `f` | Find next char | `find_next_char` |
| `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
| `G` | Go to line number `<n>` | `goto_line` |
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
| `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` |
| `Ctrl-b`, `PageUp` | Move page up | `page_up` |
| `Ctrl-f`, `PageDown` | Move page down | `page_down` |
| `PageUp` | Move page up | `page_up` |
| `PageDown` | Move page down | `page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A |
| `:` | Enter command mode | `command_mode` |
| `z` | Enter [view mode](#view-mode) | N/A |
| `Z` | Enter sticky [view mode](#view-mode) | N/A |
| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
| `Space` | Enter [space mode](#space-mode) | N/A |
### Changes
@@ -70,11 +55,11 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
| `i` | Insert before selection | `insert_mode` |
| `a` | Insert after selection (append) | `append_mode` |
| `I` | Insert at the start of the line | `insert_at_line_start` |
| `A` | Insert at the end of the line | `insert_at_line_end` |
| `I` | Insert at the start of the line | `prepend_to_line` |
| `A` | Insert at the end of the line | `append_to_line` |
| `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` |
| `.` | Repeat last insert | N/A |
| `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` |
| `U` | Redo change | `redo` |
| `Alt-u` | Move backward in history | `earlier` |
@@ -103,7 +88,6 @@ Normal mode is the default mode when you launch helix. You can return to it from
| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
### Selection manipulation
@@ -111,15 +95,12 @@ Normal mode is the default mode when you launch helix. You can return to it from
| Key | Description | Command |
| ----- | ----------- | ------- |
| `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into sub selections on regex matches | `split_selection` |
| `S` | Split selection into subselections on regex matches | `split_selection` |
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `Alt-minus` | Merge selections | `merge_selections` |
| `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` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| `Alt-:` | Ensures the selection is in forward direction | `ensure_selections_forward` |
| `,` | Keep only the primary selection | `keep_primary_selection` |
| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` |
@@ -129,22 +110,17 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
| `Alt-)` | Rotate selection contents forward | `rotate_selection_contents_forward` |
| `%` | Select entire file | `select_all` |
| `x` | Select current line, if already selected, extend to next line | `extend_line_below` |
| `x` | Select current line, if already selected, extend to next line | `extend_line` |
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
| `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` |
| | Expand selection to parent syntax node TODO: pick a key (**TS**) | `expand_selection` |
| `J` | Join lines inside selection | `join_selections` |
| `Alt-J` | Join lines inside selection and select the inserted space | `join_selections_space` |
| `K` | Keep selections matching the regex | `keep_selections` |
| `Alt-K` | Remove selections matching the regex | `remove_selections` |
| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
| `Alt-o`, `Alt-up` | Expand selection to parent syntax node (**TS**) | `expand_selection` |
| `Alt-i`, `Alt-down` | Shrink syntax tree object selection (**TS**) | `shrink_selection` |
| `Alt-p`, `Alt-left` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` |
| `Alt-n`, `Alt-right` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` |
### Search
Search commands all operate on the `/` register by default. To use a different register, use `"<char>`.
| Key | Description | Command |
| ----- | ----------- | ------- |
@@ -158,54 +134,36 @@ Search commands all operate on the `/` register by default. To use a different r
These sub-modes are accessible from normal mode and typically switch back to normal mode after a command.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A |
| `:` | Enter command mode | `command_mode` |
| `z` | Enter [view mode](#view-mode) | N/A |
| `Z` | Enter sticky [view mode](#view-mode) | N/A |
| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
| `Space` | Enter [space mode](#space-mode) | N/A |
These modes (except command mode) can be configured by
[remapping keys](https://docs.helix-editor.com/remapping.html#minor-modes).
#### View mode
Accessed by typing `z` in [normal mode](#normal-mode).
View mode is intended for scrolling and manipulating the view without changing
the selection. The "sticky" variant of this mode (accessed by typing `Z` in
normal mode) is persistent and can be exited using the escape key. This is
useful when you're simply looking over text and not actively editing it.
the selection. The "sticky" variant of this mode is persistent; use the Escape
key to return to normal mode after usage (useful when you're simply looking
over text and not actively editing it).
| Key | Description | Command |
| ----- | ----------- | ------- |
| `z`, `c` | Vertically center the line | `align_view_center` |
| `z` , `c` | Vertically center the line | `align_view_center` |
| `t` | Align the line to the top of the screen | `align_view_top` |
| `b` | Align the line to the bottom of the screen | `align_view_bottom` |
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j`, `down` | Scroll the view downwards | `scroll_down` |
| `k`, `up` | Scroll the view upwards | `scroll_up` |
| `Ctrl-f`, `PageDown` | Move page down | `page_down` |
| `Ctrl-b`, `PageUp` | Move page up | `page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` |
| `j` , `down` | Scroll the view downwards | `scroll_down` |
| `k` , `up` | Scroll the view upwards | `scroll_up` |
| `f` | Move page down | `page_down` |
| `b` | Move page up | `page_up` |
| `d` | Move half page down | `half_page_down` |
| `u` | Move half page up | `half_page_up` |
#### Goto mode
Accessed by typing `g` in [normal mode](#normal-mode).
Jumps to various locations.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `g` | Go to line number `<n>` else start of file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` |
| `f` | Go to files in the selections | `goto_file` |
| `f` | Go to files in the selection | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
@@ -221,15 +179,12 @@ Jumps to various locations.
| `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | Go to last modification in current file | `goto_last_modification` |
| `j` | Move down textual (instead of visual) line | `move_line_down` |
| `k` | Move up textual (instead of visual) line | `move_line_up` |
#### Match mode
Accessed by typing `m` in [normal mode](#normal-mode).
See the relevant section in [Usage](./usage.md) for an explanation about
[surround](./usage.md#surround) and [textobject](./usage.md#navigating-using-tree-sitter-textobjects) usage.
Enter this mode using `m` from normal mode. See the relavant section
in [Usage](./usage.md) for an explanation about [surround](./usage.md#surround)
and [textobject](./usage.md#textobject) usage.
| Key | Description | Command |
| ----- | ----------- | ------- |
@@ -244,60 +199,46 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`).
#### Window mode
Accessed by typing `Ctrl-w` in [normal mode](#normal-mode).
This layer is similar to Vim keybindings as Kakoune does not support windows.
This layer is similar to vim keybindings as kakoune does not support window.
| Key | Description | Command |
| ----- | ------------- | ------- |
| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `f` | Go to files in the selections in horizontal splits | `goto_file` |
| `F` | Go to files in the selections in vertical splits | `goto_file` |
| `h`, `Ctrl-h`, `Left` | Move to left split | `jump_view_left` |
| `j`, `Ctrl-j`, `Down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `Up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `Right` | Move to right split | `jump_view_right` |
| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
| `f` | Go to files in the selection in horizontal splits | `goto_file` |
| `F` | Go to files in the selection in vertical splits | `goto_file` |
| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
| `q`, `Ctrl-q` | Close current window | `wclose` |
| `o`, `Ctrl-o` | Only keep the current window, closing all the others | `wonly` |
| `H` | Swap window to the left | `swap_view_left` |
| `J` | Swap window downwards | `swap_view_down` |
| `K` | Swap window upwards | `swap_view_up` |
| `L` | Swap window to the right | `swap_view_right` |
#### Space mode
Accessed by typing `Space` in [normal mode](#normal-mode).
This layer is a kludge of mappings, mostly pickers.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `f` | Open file picker | `file_picker` |
| `F` | Open file picker at current working directory | `file_picker_in_current_directory` |
| `b` | Open buffer picker | `buffer_picker` |
| `j` | Open jumplist picker | `jumplist_picker` |
| `g` | Debug (experimental) | N/A |
| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` |
| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` |
| `r` | Rename symbol (**LSP**) | `rename_symbol` |
| `a` | Apply code action (**LSP**) | `code_action` |
| `h` | Select symbol references (**LSP**) | `select_references_to_symbol_under_cursor` |
| `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Yank selections to clipboard | `yank_to_clipboard` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
| `?` | Open command palette | `command_palette` |
> 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file.
> TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
##### Popup
@@ -310,145 +251,84 @@ Displays documentation for item under cursor.
#### Unimpaired
These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
| Key | Description | Command |
| ----- | ----------- | ------- |
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
| `]f` | Go to next function (**TS**) | `goto_next_function` |
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
| `]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` |
| `]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 last change | `goto_last_change` |
| `[G` | Go to first change | `goto_first_change` |
| `]Space` | Add newline below | `add_newline_below` |
| `[Space` | Add newline above | `add_newline_above` |
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |
## Insert mode
Accessed by typing `i` in [normal mode](#normal-mode).
Insert mode bindings are minimal by default. Helix is designed to
be a modal editor, and this is reflected in the user experience and internal
mechanics. Changes to the text are only saved for undos when
escaping from insert mode to normal mode.
> 💡 New users are strongly encouraged to learn the modal editing paradigm
> to get the smoothest experience.
## Insert Mode
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-s` | Commit undo checkpoint | `commit_undo_checkpoint` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-r` | Insert a register content | `insert_register` |
| `Ctrl-w`, `Alt-Backspace` | Delete previous word | `delete_word_backward` |
| `Alt-d`, `Alt-Delete` | Delete next word | `delete_word_forward` |
| `Ctrl-u` | Delete to start of line | `kill_to_line_start` |
| `Ctrl-k` | Delete to end of line | `kill_to_line_end` |
| `Ctrl-h`, `Backspace`, `Shift-Backspace` | Delete previous char | `delete_char_backward` |
| `Ctrl-d`, `Delete` | Delete next char | `delete_char_forward` |
| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` |
These keys are not recommended, but are included for new users less familiar
with modal editors.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Up` | Move to previous line | `move_line_up` |
| `Down` | Move to next line | `move_line_down` |
| `Left` | Backward a char | `move_char_left` |
| `Right` | Forward a char | `move_char_right` |
| `PageUp` | Move one page up | `page_up` |
| `PageDown` | Move one page down | `page_down` |
| `Home` | Move to line start | `goto_line_start` |
| `End` | Move to line end | `goto_line_end_newline` |
As you become more comfortable with modal editing, you may want to disable some
insert mode bindings. You can do this by editing your `config.toml` file.
```toml
[keys.insert]
up = "no_op"
down = "no_op"
left = "no_op"
right = "no_op"
pageup = "no_op"
pagedown = "no_op"
home = "no_op"
end = "no_op"
```
| `Ctrl-w` | Delete previous word | `delete_word_backward` |
| `Alt-d` | Delete next word | `delete_word_forward` |
| `Alt-b`, `Alt-Left` | Backward a word | `move_prev_word_end` |
| `Ctrl-b`, `Left` | Backward a char | `move_char_left` |
| `Alt-f`, `Alt-Right` | Forward a word | `move_next_word_start` |
| `Ctrl-f`, `Right` | Forward a char | `move_char_right` |
| `Ctrl-e`, `End` | move to line end | `goto_line_end_newline` |
| `Ctrl-a`, `Home` | move to line start | `goto_line_start` |
| `Ctrl-u` | delete to start of line | `kill_to_line_start` |
| `Ctrl-k` | delete to end of line | `kill_to_line_end` |
| `backspace`, `Ctrl-h` | delete previous char | `delete_char_backward` |
| `delete`, `Ctrl-d` | delete previous char | `delete_char_forward` |
| `Ctrl-p`, `Up` | move to previous line | `move_line_up` |
| `Ctrl-n`, `Down` | move to next line | `move_line_down` |
## Select / extend mode
Accessed by typing `v` in [normal mode](#normal-mode).
I'm still pondering whether to keep this mode or not. It changes movement
commands (including goto) to extend the existing selection instead of replacing it.
Select mode echoes Normal mode, but changes any movements to extend
selections rather than replace them. Goto motions are also changed to
extend, so that `vgl`, for example, extends the selection to the end of
the line.
> NOTE: It's a bit confusing at the moment because extend hasn't been
> implemented for all movement commands yet.
Search is also affected. By default, `n` and `N` will remove the current
selection and select the next instance of the search term. Toggling this
mode before pressing `n` or `N` makes it possible to keep the current
selection. Toggling it on and off during your iterative searching allows
you to selectively add search terms to your selections.
## Picker
# Picker
Keys to use within picker. Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry |
| `Tab`, `Down`, `Ctrl-n` | Next entry |
| `PageUp`, `Ctrl-u` | Page up |
| `PageDown`, `Ctrl-d` | Page down |
| `Home` | Go to first entry |
| `End` | Go to last entry |
| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
| `Ctrl-space` | Filter options |
| `Enter` | Open selected |
| `Alt-Enter` | Open selected in the background without closing the picker |
| `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically |
| `Ctrl-t` | Toggle preview |
| `Escape`, `Ctrl-c` | Close picker |
## Prompt
# Prompt
Keys to use within prompt, Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Escape`, `Ctrl-c` | Close prompt |
| `Alt-b`, `Ctrl-Left` | Backward a word |
| `Alt-b`, `Alt-Left` | Backward a word |
| `Ctrl-b`, `Left` | Backward a char |
| `Alt-f`, `Ctrl-Right` | Forward a word |
| `Alt-f`, `Alt-Right` | Forward a word |
| `Ctrl-f`, `Right` | Forward a char |
| `Ctrl-e`, `End` | Move prompt end |
| `Ctrl-a`, `Home` | Move prompt start |
| `Ctrl-w`, `Alt-Backspace`, `Ctrl-Backspace` | Delete previous word |
| `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word |
| `Ctrl-w` | Delete previous word |
| `Alt-d` | Delete next word |
| `Ctrl-u` | Delete to start of line |
| `Ctrl-k` | Delete to end of line |
| `Backspace`, `Ctrl-h`, `Shift-Backspace` | Delete previous char |
| `Delete`, `Ctrl-d` | Delete next char |
| `backspace`, `Ctrl-h` | Delete previous char |
| `delete`, `Ctrl-d` | Delete next char |
| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next history |
| `Ctrl-r` | Insert the content of the register selected by following input char |
| `Tab` | Select next completion item |
| `BackTab` | Select previous completion item |
| `Enter` | Open selected |

View File

@@ -1,16 +1,10 @@
# Language Support
The following languages and Language Servers are supported. To use
Language Server features, you must first [install][lsp-install-wiki] the
appropriate Language Server.
You can check the language support in your installed helix version with `hx --health`.
Also see the [Language Configuration][lang-config] docs and the [Adding
Languages][adding-languages] guide for more language configuration information.
For more information like arguments passed to default LSP server,
extensions assosciated with a filetype, custom LSP settings, filetype
specific indent settings, etc see the default
[`languages.toml`][languages.toml] file.
{{#include ./generated/lang-support.md}}
[lsp-install-wiki]: https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers
[lang-config]: ./languages.md
[adding-languages]: ./guides/adding_languages.md
[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml

View File

@@ -1,237 +1,13 @@
# Languages
Language-specific settings and settings for language servers are configured
in `languages.toml` files.
Language-specific settings and settings for particular language servers can be configured in a `languages.toml` file placed in your [configuration directory](./configuration.md). Helix actually uses two `languages.toml` files, the [first one](https://github.com/helix-editor/helix/blob/master/languages.toml) is in the main helix repository; it contains the default settings for each language and is included in the helix binary at compile time. Users who want to see the available settings and options can either reference the helix repo's `languages.toml` file, or consult the table in the [adding languages](./guides/adding_languages.md) section.
## `languages.toml` files
Changes made to the `languages.toml` file in a user's [configuration directory](./configuration.md) are merged with helix's defaults on start-up, such that a user's settings will take precedence over defaults in the event of a collision. For example, the default `languages.toml` sets rust's `auto-format` to `true`. If a user wants to disable auto-format, they can change the `languages.toml` in their [configuration directory](./configuration.md) to make the rust entry read like the example below; the new key/value pair `auto-format = false` will override the default when the two sets of settings are merged on start-up:
There are three possible locations for a `languages.toml` file:
```
# in <config_dir>/helix/languages.toml
1. In the Helix source code, which lives in the
[Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml).
It provides the default configurations for languages and language servers.
2. In your [configuration directory](./configuration.md). This overrides values
from the built-in language configuration. For example, to disable
auto-LSP-formatting in Rust:
```toml
# in <config_dir>/helix/languages.toml
[language-server.mylang-lsp]
command = "mylang-lsp"
[[language]]
name = "rust"
auto-format = false
```
3. In a `.helix` folder in your project. Language configuration may also be
overridden local to a project by creating a `languages.toml` file in a
`.helix` folder. Its settings will be merged with the language configuration
in the configuration directory and the built-in configuration.
## Language configuration
Each language is configured by adding a `[[language]]` section to a
`languages.toml` file. For example:
```toml
[[language]]
name = "mylang"
scope = "source.mylang"
injection-regex = "mylang"
file-types = ["mylang", "myl"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
formatter = { command = "mylang-formatter" , args = ["--stdin"] }
language-servers = [ "mylang-lsp" ]
name = "rust"
auto-format = false
```
These configuration keys are available:
| Key | Description |
| ---- | ----------- |
| `name` | The name of the language |
| `language-id` | The language-id for language servers, checkout the table at [TextDocumentItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) for the right id |
| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
| `shebangs` | The interpreters from the shebang line, for example `["sh", "bash"]` |
| `roots` | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
| `auto-format` | Whether to autoformat this language when saving |
| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
| `comment-token` | The token to use as a comment-token |
| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section)
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
| `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.
### File-type detection and the `file-types` key
Helix determines which language configuration to use based on the `file-types` key
from the above section. `file-types` is a list of strings or tables, for
example:
```toml
file-types = ["Makefile", "toml", { suffix = ".git/config" }]
```
When determining a language configuration to use, Helix searches the file-types
with the following priorities:
1. Exact match: if the filename of a file is an exact match of a string in a
`file-types` list, that language wins. In the example above, `"Makefile"`
will match against `Makefile` files.
2. Extension: if there are no exact matches, any `file-types` string that
matches the file extension of a given file wins. In the example above, the
`"toml"` matches files like `Cargo.toml` or `languages.toml`.
3. Suffix: if there are still no matches, any values in `suffix` tables
are checked against the full path of the given file. In the example above,
the `{ suffix = ".git/config" }` would match against any `config` files
in `.git` directories. Note: `/` is used as the directory separator but is
replaced at runtime with the appropriate path separator for the operating
system, so this rule would match against `.git\config` files on Windows.
## Language Server configuration
Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`
For example:
```toml
[language-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }
environment = { "ENV1" = "value1", "ENV2" = "value2" }
[language-server.efm-lsp-prettier]
command = "efm-langserver"
[language-server.efm-lsp-prettier.config]
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
```
These are the available options for a language server.
| Key | Description |
| ---- | ----------- |
| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `config` | LSP initialization options |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
A `format` sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).
For example, with typescript:
```toml
[language-server.typescript-language-server]
# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
```
### Configuring Language Servers for a language
The `language-servers` attribute in a language tells helix which language servers are used for this language.
They have to be defined in the `[language-server]` table as described in the previous section.
Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.
In case multiple language servers are specified in the `language-servers` attribute of a `language`,
it's often useful to only enable/disable certain language-server features for these language servers.
As an example, `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`,
so everything else should be handled by the `typescript-language-server` (which is configured by default).
The language configuration for typescript could look like this:
```toml
[[language]]
name = "typescript"
language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]
```
or equivalent:
```toml
[[language]]
name = "typescript"
language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]
```
Each requested LSP feature is prioritized in the order of the `language-servers` array.
For example, the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).
The features `diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols` are an exception to that rule, as they are working for all language servers at the same time and are merged together, if enabled for the language.
If no `except-features` or `only-features` is given, all features for the language server are enabled.
If a language server itself doesn't support a feature, the next language server array entry will be tried (and so on).
The list of supported features is:
- `format`
- `goto-definition`
- `goto-declaration`
- `goto-type-definition`
- `goto-reference`
- `goto-implementation`
- `signature-help`
- `hover`
- `document-highlight`
- `completion`
- `code-action`
- `workspace-command`
- `document-symbols`
- `workspace-symbols`
- `diagnostics`
- `rename-symbol`
- `inlay-hints`
## Tree-sitter grammar configuration
The source for a language's tree-sitter grammar is specified in a `[[grammar]]`
section in `languages.toml`. For example:
```toml
[[grammar]]
name = "mylang"
source = { git = "https://github.com/example/mylang", rev = "a250c4582510ff34767ec3b7dcdd3c24e8c8aa68" }
```
Grammar configuration takes these keys:
| Key | Description |
| --- | ----------- |
| `name` | The name of the tree-sitter grammar |
| `source` | The method of fetching the grammar - a table with a schema defined below |
Where `source` is a table with either these keys when using a grammar from a
git repository:
| Key | Description |
| --- | ----------- |
| `git` | A git remote URL from which the grammar should be cloned |
| `rev` | The revision (commit hash or tag) which should be fetched |
| `subpath` | A path within the grammar directory which should be built. Some grammar repositories host multiple grammars (for example `tree-sitter-typescript` and `tree-sitter-ocaml`) in subdirectories. This key is used to point `hx --grammar build` to the correct path for compilation. When omitted, the root of repository is used |
### Choosing grammars
You may use a top-level `use-grammars` key to control which grammars are
fetched and built when using `hx --grammar fetch` and `hx --grammar build`.
```toml
# Note: this key must come **before** the [[language]] and [[grammar]] sections
use-grammars = { only = [ "rust", "c", "cpp" ] }
# or
use-grammars = { except = [ "yaml", "json" ] }
```
When omitted, all grammars are fetched and built.
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection

View File

@@ -1,21 +1,21 @@
# Key remapping
# Key Remapping
Helix currently supports one-way key remapping through a simple TOML configuration
One-way key remapping is temporarily supported via a simple TOML configuration
file. (More powerful solutions such as rebinding via commands will be
available in the future).
To remap keys, create a `config.toml` file in your `helix` configuration
directory (default `~/.config/helix` on Linux systems) with a structure like
To remap keys, write a `config.toml` file in your `helix` configuration
directory (default `~/.config/helix` in Linux systems) with a structure like
this:
```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal]
C-s = ":w" # Maps Ctrl-s to the typable command :w which is an alias for :write (save file)
C-o = ":open ~/.config/helix/config.toml" # Maps Ctrl-o to opening of the helix config file
C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file)
C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file
a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Ctrl-Shift-Escape to extend_line
"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
g = { a = "code_action" } # Maps `ga` to show possible code actions
"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode
@@ -23,34 +23,9 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
j = { k = "normal_mode" } # Maps `jk` to exit insert mode
```
> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command.
## Minor modes
Minor modes are accessed by pressing a key (usually from normal mode), giving access to dedicated bindings. Bindings
can be modified or added by nesting definitions.
```toml
[keys.insert.j]
k = "normal_mode" # Maps `jk` to exit insert mode
[keys.normal.g]
a = "code_action" # Maps `ga` to show possible code actions
# invert `j` and `k` in view mode
[keys.normal.z]
j = "scroll_up"
k = "scroll_down"
# create a new minor mode bound to `+`
[keys.normal."+"]
m = ":run-shell-command make"
c = ":run-shell-command cargo build"
t = ":run-shell-command cargo test"
```
## Special keys and modifiers
Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes
Control, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows:
| Key name | Representation |
@@ -58,7 +33,12 @@ Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes
| Backspace | `"backspace"` |
| Space | `"space"` |
| Return/Enter | `"ret"` |
| < | `"lt"` |
| \> | `"gt"` |
| \+ | `"plus"` |
| \- | `"minus"` |
| ; | `"semicolon"` |
| % | `"percent"` |
| Left | `"left"` |
| Right | `"right"` |
| Up | `"up"` |
@@ -75,5 +55,5 @@ Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes
Keys can be disabled by binding them to the `no_op` command.
A list of commands is available in the [Keymap](https://docs.helix-editor.com/keymap.html) documentation
and in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`.
Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands.
> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`.

View File

@@ -1,23 +1,22 @@
# Themes
To use a theme add `theme = "<name>"` to the top of your [`config.toml`](./configuration.md) file, or select it during runtime using `:theme <name>`.
First you'll need to place selected themes in your `themes` directory (i.e `~/.config/helix/themes`), the directory might have to be created beforehand.
To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`.
The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
## Creating a theme
Create a file with the name of your theme as the file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes` or `%AppData%\helix\themes` on Windows). The directory might have to be created beforehand.
> 💡 The names "default" and "base16_default" are reserved for built-in themes
> and cannot be overridden by user-defined themes.
### Overview
First create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`).
Each line in the theme file is specified as below:
```toml
key = { fg = "#ffffff", bg = "#000000", underline = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] }
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
```
Where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline `style`/`color`, and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults.
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
To specify only the foreground color:
@@ -25,44 +24,29 @@ To specify only the foreground color:
key = "#ffffff"
```
If the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
```toml
"key.key" = "#ffffff"
```
For inspiration, you can find the default `theme.toml`
[here](https://github.com/helix-editor/helix/blob/master/theme.toml) and
user-submitted themes
[here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
### Using the linter
Use the supplied linting tool to check for errors and missing scopes:
```sh
cargo xtask themelint onedark # replace onedark with <name>
```
## The details of theme creation
### Color palettes
It's recommended to define a palette of named colors, and refer to them in the
It's recommended define a palette of named colors, and refer to them from the
configuration values in your theme. To do this, add a table called
`palette` to your theme file:
```toml
"ui.background" = "white"
"ui.text" = "black"
ui.background = "white"
ui.text = "black"
[palette]
white = "#ffffff"
black = "#000000"
```
Keep in mind that the `[palette]` table includes all keys after its header,
so it should be defined after the normal theme options.
Remember that the `[palette]` table includes all keys after its header,
so you should define the palette after normal theme options.
The default palette uses the terminal's default 16 colors, and the colors names
are listed below. The `[palette]` section in the config file takes precedence
@@ -70,7 +54,6 @@ over it and is merged into the default palette.
| Color Name |
| --- |
| `default` |
| `black` |
| `red` |
| `green` |
@@ -90,8 +73,9 @@ over it and is merged into the default palette.
### Modifiers
The following values may be used as modifier, provided they are supported by
your terminal emulator.
The following values may be used as modifiers.
Less common modifiers might not be supported by your terminal emulator.
| Modifier |
| --- |
@@ -105,62 +89,25 @@ your terminal emulator.
| `hidden` |
| `crossed_out` |
> 💡 The `underlined` modifier is deprecated and only available for backwards compatibility.
> Its behavior is equivalent to setting `underline.style="line"`.
### Underline style
One of the following values may be used as a value for `underline.style`, providing it is
supported by your terminal emulator.
| Modifier |
| --- |
| `line` |
| `curl` |
| `dashed` |
| `dotted` |
| `double_line` |
### Inheritance
Extend other themes by setting the `inherits` property to an existing theme.
```toml
inherits = "boo_berry"
# Override the theming for "keyword"s:
"keyword" = { fg = "gold" }
# Override colors in the palette:
[palette]
berry = "#2A2A4D"
```
### Scopes
The following is a list of scopes available to use for styling:
The following is a list of scopes available to use for styling.
#### Syntax highlighting
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme).
When determining styling for a highlight, the longest matching theme key will be used. For example, if the highlight is `function.builtin.static`, the key `function.builtin` will be used instead of `function`.
For a given highlight produced, styling will be determined based on the longest matching theme key. For example, the highlight `function.builtin.static` would match the key `function.builtin` rather than `function`.
We use a similar set of scopes as
[Sublime Text](https://www.sublimetext.com/docs/scope_naming.html). See also
[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
- `attribute` - Class attributes, HTML tag attributes
- `type` - Types
- `builtin` - Primitive types provided by the language (`int`, `usize`)
- `parameter` - Generic type parameters (`T`)
- `enum`
- `variant`
- `constructor`
- `constant` (TODO: constant.other.placeholder for `%v`)
- `constant` (TODO: constant.other.placeholder for %v)
- `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
- `boolean`
- `character`
@@ -182,17 +129,17 @@ We use a similar set of scopes as
- `documentation` - Documentation comments (e.g. `///` in Rust)
- `variable` - Variables
- `builtin` - Reserved language variables (`self`, `this`, `super`, etc.)
- `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
- `parameter` - Function parameters
- `other`
- `member` - Fields of composite data types (e.g. structs, unions)
- `function` (TODO: ?)
- `label`
- `punctuation`
- `delimiter` - Commas, colons
- `bracket` - Parentheses, angle brackets, etc.
- `special` - String interpolation brackets.
- `keyword`
- `control`
@@ -200,13 +147,9 @@ We use a similar set of scopes as
- `repeat` - `for`, `while`, `loop`
- `import` - `import`, `export`
- `return`
- `exception`
- `operator` - `or`, `in`
- `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func`
- `storage` - Keywords describing how things are stored
- `type` - The type of something, `class`, `function`, `var`, `let`, etc.
- `modifier` - Storage modifiers like `static`, `mut`, `const`, `ref`, etc.
- `operator` - `||`, `+=`, `>`
@@ -214,31 +157,22 @@ We use a similar set of scopes as
- `builtin`
- `method`
- `macro`
- `special` (preprocessor in C)
- `special` (preprocesor in C)
- `tag` - Tags (e.g. `<body>` in HTML)
- `builtin`
- `namespace`
- `special`
- `markup`
- `heading`
- `marker`
- `1`, `2`, `3`, `4`, `5`, `6` - heading text for h1 through h6
- `list`
- `unnumbered`
- `numbered`
- `checked`
- `unchecked`
- `bold`
- `italic`
- `strikethrough`
- `link`
- `url` - URLs pointed to by links
- `label` - non-URL link references
- `text` - URL and image descriptions in links
- `url`
- `label`
- `quote`
- `raw`
- `inline`
@@ -246,92 +180,41 @@ We use a similar set of scopes as
- `diff` - version control changes
- `plus` - additions
- `gutter` - gutter indicator
- `minus` - deletions
- `gutter` - gutter indicator
- `delta` - modifications
- `moved` - renamed or moved files/changes
- `gutter` - gutter indicator
#### Interface
These scopes are used for theming the editor interface:
- `markup`
- `normal`
- `completion` - for completion doc popup UI
- `hover` - for hover popup UI
- `heading`
- `completion` - for completion doc popup UI
- `hover` - for hover popup UI
- `raw`
- `inline`
- `completion` - for completion doc popup UI
- `hover` - for hover popup UI
These scopes are used for theming the editor interface.
| Key | Notes |
| --- | --- |
| `ui.background` | |
| `ui.background.separator` | Picker separator below input line |
| `ui.cursor` | |
| `ui.cursor.normal` | |
| `ui.cursor.insert` | |
| `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection |
| `ui.cursor.primary.normal` | |
| `ui.cursor.primary.insert` | |
| `ui.cursor.primary.select` | |
| `ui.debug.breakpoint` | Breakpoint indicator, found in the gutter |
| `ui.debug.active` | Indicator for the line at which debugging execution is paused at, found in the gutter |
| `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.highlight.frameline` | Line at which debugging execution is paused at |
| `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.linenr` | |
| `ui.linenr.selected` | |
| `ui.statusline` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.statusline.normal` | Statusline mode during normal mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline |
| `ui.bufferline` | Style for the buffer line |
| `ui.bufferline.active` | Style for the active buffer in buffer line |
| `ui.bufferline.background` | Style for bufferline background |
| `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Default text style, command prompts, popup text, etc. |
| `ui.text.focus` | The currently selected line in the picker |
| `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 |
| `ui.virtual.indent-guide` | Vertical indent width guides |
| `ui.virtual.inlay-hint` | Default style for inlay hints of all kinds |
| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `ui.popup` | |
| `ui.window` | |
| `ui.help` | |
| `ui.text` | |
| `ui.text.focus` | |
| `ui.info` | |
| `ui.info.text` | |
| `ui.menu` | |
| `ui.menu.selected` | |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `ui.highlight` | Highlighted lines in the picker preview |
| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
| `ui.cursorcolumn.secondary` | The columns of any other cursors ([if cursorcolumn is enabled][editor-section]) |
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `diagnostic` | Diagnostics fallback style (editing area) |
| `diagnostic.hint` | Diagnostics hint (editing area) |
| `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
| `diagnostic` | For text in editing area |
[editor-section]: ./configuration.md#editor-section

View File

@@ -1,17 +0,0 @@
# Helix
Docs for bleeding edge master can be found at
[https://docs.helix-editor.com/master](https://docs.helix-editor.com/master).
See the [usage] section for a quick overview of the editor, [keymap]
section for all available keybindings and the [configuration] section
for defining custom keybindings, setting themes, etc.
For everything else (e.g., how to install supported language servers), see the [Helix Wiki].
Refer the [FAQ] for common questions.
[FAQ]: https://github.com/helix-editor/helix/wiki/FAQ
[usage]: ./usage.md
[keymap]: ./keymap.md
[configuration]: ./configuration.md
[Helix Wiki]: https://github.com/helix-editor/helix/wiki

View File

@@ -1,208 +1,76 @@
# Using Helix
# Usage
<!--toc:start-->
- [Registers](#registers)
- [User-defined registers](#user-defined-registers)
- [Special registers](#special-registers)
- [Surround](#surround)
- [Selecting and manipulating text with textobjects](#selecting-and-manipulating-text-with-textobjects)
- [Navigating using tree-sitter textobjects](#navigating-using-tree-sitter-textobjects)
- [Moving the selection with syntax-aware motions](#moving-the-selection-with-syntax-aware-motions)
<!--toc:end-->
(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
For a full interactive introduction to Helix, refer to the
[tutor](https://github.com/helix-editor/helix/blob/master/runtime/tutor) which
can be accessed via the command `hx --tutor` or `:tutor`.
> 💡 Currently, not all functionality is fully documented, please refer to the
> [key mappings](./keymap.md) list.
See [tutor.txt](https://github.com/helix-editor/helix/blob/master/runtime/tutor.txt) (accessible via `hx --tutor` or `:tutor`) for a vimtutor-like introduction.
## Registers
In Helix, registers are storage locations for text and other data, such as the
result of a search. Registers can be used to cut, copy, and paste text, similar
to the clipboard in other text editors. Usage is similar to Vim, with `"` being
used to select a register.
### User-defined registers
Helix allows you to create your own named registers for storing text, for
example:
Vim-like registers can be used to yank and store text to be pasted later. Usage is similar, with `"` being used to select a register:
- `"ay` - Yank the current selection to register `a`.
- `"op` - Paste the text in register `o` after the selection.
If a register is selected before invoking a change or delete command, the selection will be stored in the register and the action will be carried out:
If there is a selected register before invoking a change or delete command, the selection will be stored in the register and the action will be carried out:
- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
- `"md` - Store the selection in register `m` and delete it.
### Default registers
Commands that use registers, like yank (`y`), use a default register if none is specified.
These registers are used as defaults:
### Special Registers
| Register character | Contains |
| --- | --- |
| `/` | Last search |
| `:` | Last executed command |
| `"` | Last yanked text |
| `@` | Last recorded macro |
| `_` | Black hole |
### Special registers
Some registers have special behavior when read from and written to.
| Register character | When read | When written |
| --- | --- | --- |
| `_` | No values are returned | All values are discarded |
| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable |
| `.` | Contents of the current selections | This register is not writable |
| `%` | Name of the current file | This register is not writable |
| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard |
| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard |
When yanking multiple selections to the clipboard registers, the selections
are joined with newlines. Pasting from these registers will paste multiple
selections if the clipboard was last yanked to by the Helix session. Otherwise
the clipboard contents are pasted as one selection.
> There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics.
> The black hole register works as a no-op register, meaning no data will be written to / read from it.
## Surround
Helix includes built-in functionality similar to [vim-surround](https://github.com/tpope/vim-surround).
The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich):
Functionality similar to [vim-surround](https://github.com/tpope/vim-surround) is built into
helix. The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich):
![Surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif)
![surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif)
| Key Sequence | Action |
| --------------------------------- | --------------------------------------- |
| `ms<char>` (after selecting text) | Add surround characters to selection |
| `mr<char_to_replace><new_char>` | Replace the closest surround characters |
| `md<char_to_delete>` | Delete the closest surround characters |
- `ms` - Add surround characters
- `mr` - Replace surround characters
- `md` - Delete surround characters
You can use counts to act on outer pairs.
`ms` acts on a selection, so select the text first and use `ms<char>`. `mr` and `md` work
on the closest pairs found and selections are not required; use counts to act in outer pairs.
Surround can also act on multiple selections. For example, to change every occurrence of `(use)` to `[use]`:
It can also act on multiple seletions (yay!). For example, to change every occurance of `(use)` to `[use]`:
1. `%` to select the whole file
2. `s` to split the selections on a search term
3. Input `use` and hit Enter
4. `mr([` to replace the parentheses with square brackets
- `%` to select the whole file
- `s` to split the selections on a search term
- Input `use` and hit Enter
- `mr([` to replace the parens with square brackets
Multiple characters are currently not supported, but planned for future release.
Multiple characters are currently not supported, but planned.
## Selecting and manipulating text with textobjects
## Textobjects
In Helix, textobjects are a way to select, manipulate and operate on a piece of
text in a structured way. They allow you to refer to blocks of text based on
their structure or purpose, such as a word, sentence, paragraph, or even a
function or block of code.
Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
![Textobject demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
![Textobject tree-sitter demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
![textobject-treesitter-demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
- `ma` - Select around the object (`va` in Vim, `<alt-a>` in Kakoune)
- `mi` - Select inside the object (`vi` in Vim, `<alt-i>` in Kakoune)
- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
| Key after `mi` or `ma` | Textobject selected |
| --- | --- |
| `w` | Word |
| `W` | WORD |
| `p` | Paragraph |
| `(`, `[`, `'`, etc. | Specified surround pairs |
| `m` | The closest surround pair |
| `(`, `[`, `'`, etc | Specified surround pairs |
| `f` | Function |
| `t` | Type (or Class) |
| `a` | Argument/parameter |
| `c` | Comment |
| `T` | Test |
| `g` | Change |
| `c` | Class |
| `p` | Parameter |
> 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current
Note: `f`, `c`, etc need a tree-sitter grammar active for the current
document and a special tree-sitter query file to work properly. [Only
some grammars][lang-support] currently have the query file implemented.
Contributions are welcome!
## Navigating using tree-sitter textobjects
Navigating between functions, classes, parameters, and other elements is
possible using tree-sitter and textobject queries. For
example to move to the next function use `]f`, to move to previous
type use `[t`, and so on.
![Tree-sitter-nav-demo][tree-sitter-nav-demo]
For the full reference see the [unimpaired][unimpaired-keybinds] section of the key bind
documentation.
> 💡 This feature relies on tree-sitter textobjects
> and requires the corresponding query file to work properly.
## Moving the selection with syntax-aware motions
`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the
selection according to its location in the syntax tree. For example, many languages have the
following syntax for function calls:
```js
func(arg1, arg2, arg3);
```
A function call might be parsed by tree-sitter into a tree like the following.
```tsq
(call
function: (identifier) ; func
arguments:
(arguments ; (arg1, arg2, arg3)
(identifier) ; arg1
(identifier) ; arg2
(identifier))) ; arg3
```
Use `:tree-sitter-subtree` to view the syntax tree of the primary selection. In
a more intuitive tree format:
```
┌────┐
│call│
┌─────┴────┴─────┐
│ │
┌─────▼────┐ ┌────▼────┐
│identifier│ │arguments│
│ "func" │ ┌────┴───┬─────┴───┐
└──────────┘ │ │ │
│ │ │
┌─────────▼┐ ┌────▼─────┐ ┌▼─────────┐
│identifier│ │identifier│ │identifier│
│ "arg1" │ │ "arg2" │ │ "arg3" │
└──────────┘ └──────────┘ └──────────┘
```
If you have a selection that wraps `arg1` (see the tree above), and you use
`Alt-n`, it will select the next sibling in the syntax tree: `arg2`.
```js
// before
func([arg1], arg2, arg3)
// after
func(arg1, [arg2], arg3);
```
Similarly, `Alt-o` will expand the selection to the parent node, in this case, the
arguments node.
```js
func[(arg1, arg2, arg3)];
```
There is also some nuanced behavior that prevents you from getting stuck on a
node with no sibling. When using `Alt-p` with a selection on `arg1`, the previous
child node will be selected. In the event that `arg1` does not have a previous
sibling, the selection will move up the syntax tree and select the previous
element. As a result, using `Alt-p` with a selection on `arg1` will move the
selection to the "func" `identifier`.
[lang-support]: ./lang-support.md
[unimpaired-keybinds]: ./keymap.md#unimpaired
[tree-sitter-nav-demo]: https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif
some grammars](https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=)
currently have the query file implemented. Contributions are welcome !

660
book/theme/book.js Normal file
View File

@@ -0,0 +1,660 @@
"use strict";
// Fix back button cache problem
window.onunload = function () { };
// Global variable, shared between modules
function playground_text(playground) {
let code_block = playground.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
return editor.getValue();
} else {
return code_block.textContent;
}
}
(function codeSnippets() {
function fetch_with_timeout(url, options, timeout = 6000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
]);
}
var playgrounds = Array.from(document.querySelectorAll(".playground"));
if (playgrounds.length > 0) {
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
}
function handle_crate_list_update(playground_block, playground_crates) {
// update the play buttons after receiving the response
update_play_button(playground_block, playground_crates);
// and install on change listener to dynamically update ACE editors
if (window.ace) {
let code_block = playground_block.querySelector("code");
if (code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
editor.addEventListener("change", function (e) {
update_play_button(playground_block, playground_crates);
});
// add Ctrl-Enter command to execute rust code
editor.commands.addCommand({
name: "run",
bindKey: {
win: "Ctrl-Enter",
mac: "Ctrl-Enter"
},
exec: _editor => run_rust_code(playground_block)
});
}
}
}
// updates the visibility of play button based on `no_run` class and
// used crates vs ones available on http://play.rust-lang.org
function update_play_button(pre_block, playground_crates) {
var play_button = pre_block.querySelector(".play-button");
// skip if code is `no_run`
if (pre_block.querySelector('code').classList.contains("no_run")) {
play_button.classList.add("hidden");
return;
}
// get list of `extern crate`'s from snippet
var txt = playground_text(pre_block);
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
var snippet_crates = [];
var item;
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
// check if all used crates are available on play.rust-lang.org
var all_available = snippet_crates.every(function (elem) {
return playground_crates.indexOf(elem) > -1;
});
if (all_available) {
play_button.classList.remove("hidden");
} else {
play_button.classList.add("hidden");
}
}
function run_rust_code(code_block) {
var result_block = code_block.querySelector(".result");
if (!result_block) {
result_block = document.createElement('code');
result_block.className = 'result hljs language-bash';
code_block.append(result_block);
}
let text = playground_text(code_block);
let classes = code_block.querySelector('code').classList;
let has_2018 = classes.contains("edition2018");
let edition = has_2018 ? "2018" : "2015";
var params = {
version: "stable",
optimize: "0",
code: text,
edition: edition
};
if (text.indexOf("#![feature") !== -1) {
params.version = "nightly";
}
result_block.innerText = "Running...";
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params)
})
.then(response => response.json())
.then(response => result_block.innerText = response.result)
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
}
// Syntax highlighting Configuration
hljs.configure({
tabReplace: ' ', // 4 spaces
languages: [], // Languages used for auto-detection
});
let code_nodes = Array
.from(document.querySelectorAll('code'))
// Don't highlight `inline code` blocks in headers.
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
if (window.ace) {
// language-rust class needs to be removed for editable
// blocks or highlightjs will capture events
Array
.from(document.querySelectorAll('code.editable'))
.forEach(function (block) { block.classList.remove('language-rust'); });
Array
.from(document.querySelectorAll('code:not(.editable)'))
.forEach(function (block) { hljs.highlightBlock(block); });
} else {
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
}
// Adding the hljs class gives code blocks the color css
// even if highlighting doesn't apply
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
var lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return
if (!lines.length) { return; }
block.classList.add("hide-boring");
var buttons = document.createElement('div');
buttons.className = 'buttons';
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
// add expand button
var pre_block = block.parentNode;
pre_block.insertBefore(buttons, pre_block.firstChild);
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
if (e.target.classList.contains('fa-eye')) {
e.target.classList.remove('fa-eye');
e.target.classList.add('fa-eye-slash');
e.target.title = 'Hide lines';
e.target.setAttribute('aria-label', e.target.title);
block.classList.remove('hide-boring');
} else if (e.target.classList.contains('fa-eye-slash')) {
e.target.classList.remove('fa-eye-slash');
e.target.classList.add('fa-eye');
e.target.title = 'Show hidden lines';
e.target.setAttribute('aria-label', e.target.title);
block.classList.add('hide-boring');
}
});
});
if (window.playground_copyable) {
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
var pre_block = block.parentNode;
if (!pre_block.classList.contains('playground')) {
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var clipButton = document.createElement('button');
clipButton.className = 'fa fa-copy clip-button';
clipButton.title = 'Copy to clipboard';
clipButton.setAttribute('aria-label', clipButton.title);
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
}
});
}
// Process playground code blocks
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
// Add play button
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var runCodeButton = document.createElement('button');
runCodeButton.className = 'fa fa-play play-button';
runCodeButton.hidden = true;
runCodeButton.title = 'Run this code';
runCodeButton.setAttribute('aria-label', runCodeButton.title);
buttons.insertBefore(runCodeButton, buttons.firstChild);
runCodeButton.addEventListener('click', function (e) {
run_rust_code(pre_block);
});
if (window.playground_copyable) {
var copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
copyCodeClipboardButton.title = 'Copy to clipboard';
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
}
let code_block = pre_block.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
var undoChangesButton = document.createElement('button');
undoChangesButton.className = 'fa fa-history reset-button';
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
buttons.insertBefore(undoChangesButton, buttons.firstChild);
undoChangesButton.addEventListener('click', function () {
let editor = window.ace.edit(code_block);
editor.setValue(editor.originalCode);
editor.clearSelection();
});
}
});
})();
(function themes() {
var html = document.querySelector('html');
var themeToggleButton = document.getElementById('theme-toggle');
var themePopup = document.getElementById('theme-list');
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
var stylesheets = {
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
highlight: document.querySelector("[href$='highlight.css']"),
};
function showThemes() {
themePopup.style.display = 'block';
themeToggleButton.setAttribute('aria-expanded', true);
themePopup.querySelector("button#" + get_theme()).focus();
}
function hideThemes() {
themePopup.style.display = 'none';
themeToggleButton.setAttribute('aria-expanded', false);
themeToggleButton.focus();
}
function get_theme() {
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
if (theme === null || theme === undefined) {
return default_theme;
} else {
return theme;
}
}
function set_theme(theme, store = true) {
let ace_theme;
if (theme == 'coal' || theme == 'navy') {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = false;
stylesheets.highlight.disabled = true;
ace_theme = "ace/theme/tomorrow_night";
} else if (theme == 'ayu') {
stylesheets.ayuHighlight.disabled = false;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = true;
ace_theme = "ace/theme/tomorrow_night";
} else {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = false;
ace_theme = "ace/theme/dawn";
}
setTimeout(function () {
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
}, 1);
if (window.ace && window.editors) {
window.editors.forEach(function (editor) {
editor.setTheme(ace_theme);
});
}
var previousTheme = get_theme();
if (store) {
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
}
html.classList.remove(previousTheme);
html.classList.add(theme);
}
// Set theme
var theme = get_theme();
set_theme(theme, false);
themeToggleButton.addEventListener('click', function () {
if (themePopup.style.display === 'block') {
hideThemes();
} else {
showThemes();
}
});
themePopup.addEventListener('click', function (e) {
var theme = e.target.id || e.target.parentElement.id;
set_theme(theme);
});
themePopup.addEventListener('focusout', function(e) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
hideThemes();
}
});
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
document.addEventListener('click', function(e) {
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
hideThemes();
}
});
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (!themePopup.contains(e.target)) { return; }
switch (e.key) {
case 'Escape':
e.preventDefault();
hideThemes();
break;
case 'ArrowUp':
e.preventDefault();
var li = document.activeElement.parentElement;
if (li && li.previousElementSibling) {
li.previousElementSibling.querySelector('button').focus();
}
break;
case 'ArrowDown':
e.preventDefault();
var li = document.activeElement.parentElement;
if (li && li.nextElementSibling) {
li.nextElementSibling.querySelector('button').focus();
}
break;
case 'Home':
e.preventDefault();
themePopup.querySelector('li:first-child button').focus();
break;
case 'End':
e.preventDefault();
themePopup.querySelector('li:last-child button').focus();
break;
}
});
})();
(function sidebar() {
var html = document.querySelector("html");
var sidebar = document.getElementById("sidebar");
var sidebarLinks = document.querySelectorAll('#sidebar a');
var sidebarToggleButton = document.getElementById("sidebar-toggle");
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
var firstContact = null;
function showSidebar() {
html.classList.remove('sidebar-hidden')
html.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', 0);
});
sidebarToggleButton.setAttribute('aria-expanded', true);
sidebar.setAttribute('aria-hidden', false);
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
}
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
function toggleSection(ev) {
ev.currentTarget.parentElement.classList.toggle('expanded');
}
Array.from(sidebarAnchorToggles).forEach(function (el) {
el.addEventListener('click', toggleSection);
});
function hideSidebar() {
html.classList.remove('sidebar-visible')
html.classList.add('sidebar-hidden');
Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', -1);
});
sidebarToggleButton.setAttribute('aria-expanded', false);
sidebar.setAttribute('aria-hidden', true);
try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
}
// Toggle sidebar
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
if (html.classList.contains("sidebar-hidden")) {
var current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-width', '150px');
}
showSidebar();
} else if (html.classList.contains("sidebar-visible")) {
hideSidebar();
} else {
if (getComputedStyle(sidebar)['transform'] === 'none') {
hideSidebar();
} else {
showSidebar();
}
}
});
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
function initResize(e) {
window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false);
html.classList.add('sidebar-resizing');
}
function resize(e) {
var pos = (e.clientX - sidebar.offsetLeft);
if (pos < 20) {
hideSidebar();
} else {
if (html.classList.contains("sidebar-hidden")) {
showSidebar();
}
pos = Math.min(pos, window.innerWidth - 100);
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
}
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize(e) {
html.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}
document.addEventListener('touchstart', function (e) {
firstContact = {
x: e.touches[0].clientX,
time: Date.now()
};
}, { passive: true });
document.addEventListener('touchmove', function (e) {
if (!firstContact)
return;
var curX = e.touches[0].clientX;
var xDiff = curX - firstContact.x,
tDiff = Date.now() - firstContact.time;
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
showSidebar();
else if (xDiff < 0 && curX < 300)
hideSidebar();
firstContact = null;
}
}, { passive: true });
// Scroll sidebar to current active section
var activeSection = document.getElementById("sidebar").querySelector(".active");
if (activeSection) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
activeSection.scrollIntoView({ block: 'center' });
}
})();
(function chapterNavigation() {
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (window.search && window.search.hasFocus()) { return; }
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
var nextButton = document.querySelector('.nav-chapters.next');
if (nextButton) {
window.location.href = nextButton.href;
}
break;
case 'ArrowLeft':
e.preventDefault();
var previousButton = document.querySelector('.nav-chapters.previous');
if (previousButton) {
window.location.href = previousButton.href;
}
break;
}
});
})();
(function clipboard() {
var clipButtons = document.querySelectorAll('.clip-button');
function hideTooltip(elem) {
elem.firstChild.innerText = "";
elem.className = 'fa fa-copy clip-button';
}
function showTooltip(elem, msg) {
elem.firstChild.innerText = msg;
elem.className = 'fa fa-copy tooltipped';
}
var clipboardSnippets = new ClipboardJS('.clip-button', {
text: function (trigger) {
hideTooltip(trigger);
let playground = trigger.closest("pre");
return playground_text(playground);
}
});
Array.from(clipButtons).forEach(function (clipButton) {
clipButton.addEventListener('mouseout', function (e) {
hideTooltip(e.currentTarget);
});
});
clipboardSnippets.on('success', function (e) {
e.clearSelection();
showTooltip(e.trigger, "Copied!");
});
clipboardSnippets.on('error', function (e) {
showTooltip(e.trigger, "Clipboard error!");
});
})();
(function scrollToTop () {
var menuTitle = document.querySelector('.menu-title');
menuTitle.addEventListener('click', function () {
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
});
})();
(function controllMenu() {
var menu = document.getElementById('menu-bar');
(function controllPosition() {
var scrollTop = document.scrollingElement.scrollTop;
var prevScrollTop = scrollTop;
var minMenuY = -menu.clientHeight - 50;
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
menu.style.top = scrollTop + 'px';
// Same as parseInt(menu.style.top.slice(0, -2), but faster
var topCache = menu.style.top.slice(0, -2);
menu.classList.remove('sticky');
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
document.addEventListener('scroll', function () {
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
// `null` means that it doesn't need to be updated
var nextSticky = null;
var nextTop = null;
var scrollDown = scrollTop > prevScrollTop;
var menuPosAbsoluteY = topCache - scrollTop;
if (scrollDown) {
nextSticky = false;
if (menuPosAbsoluteY > 0) {
nextTop = prevScrollTop;
}
} else {
if (menuPosAbsoluteY > 0) {
nextSticky = true;
} else if (menuPosAbsoluteY < minMenuY) {
nextTop = prevScrollTop + minMenuY;
}
}
if (nextSticky === true && stickyCache === false) {
menu.classList.add('sticky');
stickyCache = true;
} else if (nextSticky === false && stickyCache === true) {
menu.classList.remove('sticky');
stickyCache = false;
}
if (nextTop !== null) {
menu.style.top = nextTop + 'px';
topCache = nextTop;
}
prevScrollTop = scrollTop;
}, { passive: true });
})();
(function controllBorder() {
menu.classList.remove('bordered');
document.addEventListener('scroll', function () {
if (menu.offsetTop === 0) {
menu.classList.remove('bordered');
} else {
menu.classList.add('bordered');
}
}, { passive: true });
})();
})();

499
book/theme/css/chrome.css Normal file
View File

@@ -0,0 +1,499 @@
/* CSS for UI elements (a.k.a. chrome) */
@import 'variables.css';
::-webkit-scrollbar {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
html {
scrollbar-color: var(--scrollbar) var(--bg);
}
#searchresults a,
.content a:link,
a:visited,
a > .hljs {
color: var(--links);
}
.content a:hover {
text-decoration: underline;
}
/* Menu Bar */
#menu-bar,
#menu-bar-hover-placeholder {
z-index: 101;
margin: auto calc(0px - var(--page-padding));
}
#menu-bar {
position: relative;
display: flex;
flex-wrap: wrap;
background-color: var(--bg);
border-bottom-color: var(--bg);
border-bottom-width: 1px;
border-bottom-style: solid;
}
#menu-bar.sticky,
.js #menu-bar-hover-placeholder:hover + #menu-bar,
.js #menu-bar:hover,
.js.sidebar-visible #menu-bar {
position: -webkit-sticky;
position: sticky;
top: 0 !important;
}
#menu-bar-hover-placeholder {
position: sticky;
position: -webkit-sticky;
top: 0;
height: var(--menu-bar-height);
}
#menu-bar.bordered {
border-bottom-color: var(--table-border-color);
}
#menu-bar i, #menu-bar .icon-button {
position: relative;
padding: 0 8px;
z-index: 10;
line-height: var(--menu-bar-height);
cursor: pointer;
transition: color 0.5s;
}
@media only screen and (max-width: 420px) {
#menu-bar i, #menu-bar .icon-button {
padding: 0 5px;
}
}
.icon-button {
border: none;
background: none;
padding: 0;
color: inherit;
}
.icon-button i {
margin: 0;
}
.right-buttons {
margin: 0 15px;
}
.right-buttons a {
text-decoration: none;
}
.left-buttons {
display: flex;
margin: 0 5px;
}
.no-js .left-buttons {
display: none;
}
.menu-title {
display: inline-block;
font-weight: 200;
font-size: 2.4rem;
line-height: var(--menu-bar-height);
text-align: center;
margin: 0;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.js .menu-title {
cursor: pointer;
}
.menu-bar,
.menu-bar:visited,
.nav-chapters,
.nav-chapters:visited,
.mobile-nav-chapters,
.mobile-nav-chapters:visited,
.menu-bar .icon-button,
.menu-bar a i {
color: var(--icons);
}
.menu-bar i:hover,
.menu-bar .icon-button:hover,
.nav-chapters:hover,
.mobile-nav-chapters i:hover {
color: var(--icons-hover);
}
/* Nav Icons */
.nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
position: fixed;
top: 0;
bottom: 0;
margin: 0;
max-width: 150px;
min-width: 90px;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
transition: color 0.5s, background-color 0.5s;
}
.nav-chapters:hover {
text-decoration: none;
background-color: var(--theme-hover);
transition: background-color 0.15s, color 0.15s;
}
.nav-wrapper {
margin-top: 50px;
display: none;
}
.mobile-nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
width: 90px;
border-radius: 5px;
background-color: var(--sidebar-bg);
}
.previous {
float: left;
}
.next {
float: right;
right: var(--page-padding);
}
@media only screen and (max-width: 1080px) {
.nav-wide-wrapper { display: none; }
.nav-wrapper { display: block; }
}
@media only screen and (max-width: 1380px) {
.sidebar-visible .nav-wide-wrapper { display: none; }
.sidebar-visible .nav-wrapper { display: block; }
}
/* Inline code */
:not(pre) > .hljs {
display: inline;
padding: 0.1em 0.3em;
border-radius: 3px;
}
:not(pre):not(a):not(td):not(p) > .hljs {
color: var(--inline-code-color);
overflow-x: initial;
}
a:hover > .hljs {
text-decoration: underline;
}
pre {
position: relative;
}
pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: var(--sidebar-fg);
cursor: pointer;
}
pre > .buttons :hover {
color: var(--sidebar-active);
}
pre > .buttons i {
margin-left: 8px;
}
pre > .buttons button {
color: inherit;
background: transparent;
border: none;
cursor: inherit;
}
pre > .result {
margin-top: 10px;
}
/* Search */
#searchresults a {
text-decoration: none;
}
mark {
border-radius: 2px;
padding: 0 3px 1px 3px;
margin: 0 -3px -1px -3px;
background-color: var(--search-mark-bg);
transition: background-color 300ms linear;
cursor: pointer;
}
mark.fade-out {
background-color: rgba(0,0,0,0) !important;
cursor: auto;
}
.searchbar-outer {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
}
#searchbar {
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
transition: box-shadow 300ms ease-in-out;
border: 1px solid var(--searchbar-border-color);
border-radius: 3px;
background-color: var(--searchbar-bg);
color: var(--searchbar-fg);
}
#searchbar:focus,
#searchbar.active {
box-shadow: 0 0 3px var(--searchbar-shadow-color);
}
.searchresults-header {
font-weight: bold;
font-size: 1em;
padding: 18px 0 0 5px;
color: var(--searchresults-header-fg);
}
.searchresults-outer {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
border-bottom: 1px dashed var(--searchresults-border-color);
}
ul#searchresults {
list-style: none;
padding-left: 20px;
}
ul#searchresults li {
margin: 10px 0px;
padding: 2px;
border-radius: 2px;
}
ul#searchresults li.focus {
background-color: var(--searchresults-li-bg);
}
ul#searchresults span.teaser {
display: block;
clear: both;
margin: 5px 0 0 20px;
font-size: 0.8em;
}
ul#searchresults span.teaser em {
font-weight: bold;
font-style: normal;
}
/* Sidebar */
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: var(--sidebar-width);
font-size: 0.875em;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.sidebar-resizing {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.js:not(.sidebar-resizing) .sidebar {
transition: transform 0.3s; /* Animation: slide away */
}
.sidebar code {
line-height: 2em;
}
.sidebar .sidebar-scrollbox {
overflow-y: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.sidebar .sidebar-resize-handle {
position: absolute;
cursor: col-resize;
width: 0;
right: 0;
top: 0;
bottom: 0;
}
.js .sidebar .sidebar-resize-handle {
cursor: col-resize;
width: 5px;
}
.sidebar-hidden .sidebar {
transform: translateX(calc(0px - var(--sidebar-width)));
}
.sidebar::-webkit-scrollbar {
background: var(--sidebar-bg);
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
.sidebar-visible .page-wrapper {
transform: translateX(var(--sidebar-width));
}
@media only screen and (min-width: 620px) {
.sidebar-visible .page-wrapper {
transform: none;
margin-left: var(--sidebar-width);
}
}
.chapter {
list-style: none outside none;
padding-left: 0;
margin: .25rem 0;
}
.chapter ol {
width: 100%;
}
.chapter li {
display: flex;
color: var(--sidebar-non-existant);
}
.chapter li a {
display: block;
text-decoration: none;
color: var(--sidebar-fg);
}
.chapter li a:hover {
color: var(--sidebar-active);
}
.chapter li a.active {
color: var(--sidebar-active);
}
.chapter li > a.toggle {
cursor: pointer;
display: block;
margin-left: auto;
padding: 0 10px;
user-select: none;
opacity: 0.68;
}
.chapter li > a.toggle div {
transition: transform 0.5s;
}
/* collapse the section */
.chapter li:not(.expanded) + li > ol {
display: none;
}
.chapter li.chapter-item {
padding: 1rem 1.5rem;
}
.chapter .section li.chapter-item {
padding: .5rem .5rem 0 .5rem;
}
.chapter li.expanded > a.toggle div {
transform: rotate(90deg);
}
.spacer {
width: 100%;
height: 3px;
margin: 5px 0px;
}
.chapter .spacer {
background-color: var(--sidebar-spacer);
}
@media (-moz-touch-enabled: 1), (pointer: coarse) {
.chapter li a { padding: 5px 0; }
.spacer { margin: 10px 0; }
}
.section {
list-style: none outside none;
padding-left: 2rem;
line-height: 1.9em;
}
/* Theme Menu Popup */
.theme-popup {
position: absolute;
left: 10px;
top: var(--menu-bar-height);
z-index: 1000;
border-radius: 4px;
font-size: 0.7em;
color: var(--fg);
background: var(--theme-popup-bg);
border: 1px solid var(--theme-popup-border);
margin: 0;
padding: 0;
list-style: none;
display: none;
}
.theme-popup .default {
color: var(--icons);
}
.theme-popup .theme {
width: 100%;
border: 0;
margin: 0;
padding: 2px 10px;
line-height: 25px;
white-space: nowrap;
text-align: left;
cursor: pointer;
color: inherit;
background: inherit;
font-size: inherit;
}
.theme-popup .theme:hover {
background-color: var(--theme-hover);
}
.theme-popup .theme:hover:first-child,
.theme-popup .theme:hover:last-child {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}

233
book/theme/css/general.css Normal file
View File

@@ -0,0 +1,233 @@
/* Base styles and content styles */
@import 'variables.css';
:root {
/* Browser default font-size is 16px, this way 1 rem = 10px */
font-size: 62.5%;
}
/* TODO: replace with self hosted fonts */
html {
font-family: "Inter", sans-serif;
color: var(--fg);
background-color: var(--bg);
text-size-adjust: none;
}
/* @supports (font-variation-settings: normal) { */
/* html { font-family: 'Inter var', sans-serif; } */
/* } */
body {
margin: 0;
font-size: 1.6rem;
overflow-x: hidden;
}
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
}
/* Don't change font size in headers. */
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
font-size: unset;
}
.left { float: left; }
.right { float: right; }
.boring { opacity: 0.6; }
.hide-boring .boring { display: none; }
.hidden { display: none !important; }
h2, h3 { margin-top: 2.5em; }
h4, h5 { margin-top: 2em; }
.header + .header h3,
.header + .header h4,
.header + .header h5 {
margin-top: 1em;
}
h1:target::before,
h2:target::before,
h3:target::before,
h4:target::before,
h5:target::before,
h6:target::before {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
/* This is broken on Safari as of version 14, but is fixed
in Safari Technology Preview 117 which I think will be Safari 14.2.
https://bugs.webkit.org/show_bug.cgi?id=218076
*/
:target {
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
}
.page {
outline: 0;
padding: 0 var(--page-padding);
margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
}
.page-wrapper {
box-sizing: border-box;
}
.js:not(.sidebar-resizing) .page-wrapper {
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
.content {
overflow-y: auto;
padding: 0 15px;
padding-bottom: 50px;
}
.content main {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
}
/* 2 1.75 1.5 1.25 1 .875 */
.content h1 { font-size: 2em }
.content h2 { font-size: 1.75em }
.content h3 { font-size: 1.5em }
.content h4 { font-size: 1.25em }
.content h5 { font-size: 1em }
.content h6 { font-size: .875em }
.content h1, .content h2, .content h3, .content h4 {
font-weight: 500;
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p, .content ol, .content ul, .content table {
margin-top: 0;
margin-bottom: .875em;
}
.content ul li {
margin-bottom: .25rem;
}
.content ul {
list-style-type: square;
}
.content ul ul, .content ol ul {
margin-bottom: .5rem;
}
.content li p {
margin-bottom: .5em;
}
.content p { line-height: 1.45em; }
.content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; }
.content a { text-decoration: none; }
.content a:hover { text-decoration: underline; }
.content img { max-width: 100%; }
.content .header:link,
.content .header:visited {
color: var(--fg);
color: var(--heading-fg);
}
.content .header:link,
.content .header:visited:hover {
text-decoration: none;
}
table {
margin: 0 auto;
border-collapse: collapse;
width: 100%;
}
table td {
padding: .75rem;
width: auto;
}
table thead {
background: var(--table-header-bg);
}
table thead td {
font-weight: 700;
border: none;
}
table thead th {
padding: .75rem;
text-align: left;
font-weight: 500;
line-height: 1.5;
width: auto;
}
table thead tr {
border-bottom: 2px var(--table-border-color) solid;
}
table tbody tr {
border-bottom: 1px var(--table-border-line) solid;
}
/* Alternate background colors for rows */
table tbody tr:nth-child(2n) {
/* background: var(--table-alternate-bg); */
}
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-left: 4px solid var(--quote-border);
}
blockquote *:last-child {
margin-bottom: 0;
}
:not(.footnote-definition) + .footnote-definition,
.footnote-definition + :not(.footnote-definition) {
margin-top: 2em;
}
.footnote-definition {
font-size: 0.9em;
margin: 0.5em 0;
}
.footnote-definition p {
display: inline;
}
.tooltiptext {
position: absolute;
visibility: hidden;
color: #fff;
background-color: #333;
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
left: -8px; /* Half of the width of the icon */
top: -35px;
font-size: 0.8em;
text-align: center;
border-radius: 6px;
padding: 5px 8px;
margin: 5px;
z-index: 1000;
}
.tooltipped .tooltiptext {
visibility: visible;
}
.chapter li.part-title {
color: var(--sidebar-fg);
margin: 5px 0px;
font-weight: bold;
}
.result-no-output {
font-style: italic;
}

54
book/theme/css/print.css Normal file
View File

@@ -0,0 +1,54 @@
#sidebar,
#menu-bar,
.nav-chapters,
.mobile-nav-chapters {
display: none;
}
#page-wrapper.page-wrapper {
transform: none;
margin-left: 0px;
overflow-y: initial;
}
#content {
max-width: none;
margin: 0;
padding: 0;
}
.page {
overflow-y: initial;
}
code {
background-color: #666666;
border-radius: 5px;
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact;
}
pre > .buttons {
z-index: 2;
}
a, a:visited, a:active, a:hover {
color: #4183c4;
text-decoration: none;
}
h1, h2, h3, h4, h5, h6 {
page-break-inside: avoid;
page-break-after: avoid;
}
pre, code {
page-break-inside: avoid;
white-space: pre-wrap;
}
.fa {
display: none !important;
}

View File

@@ -0,0 +1,342 @@
/* Globals */
:root {
--sidebar-width: 300px;
--page-padding: 15px;
--content-max-width: 750px;
--menu-bar-height: 50px;
}
/* Themes */
.ayu {
--bg: hsl(210, 25%, 8%);
--fg: #c5c5c5;
--sidebar-bg: #14191f;
--sidebar-fg: #c8c9db;
--sidebar-non-existant: #5c6773;
--sidebar-active: #ffb454;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #0096cf;
--inline-code-color: #ffb454;
--theme-popup-bg: #14191f;
--theme-popup-border: #5c6773;
--theme-hover: #191f26;
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(210, 25%, 13%);
--table-header-bg: hsl(210, 25%, 28%);
--table-alternate-bg: hsl(210, 25%, 11%);
--searchbar-border-color: #848484;
--searchbar-bg: #424242;
--searchbar-fg: #fff;
--searchbar-shadow-color: #d4c89f;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #252932;
--search-mark-bg: #e3b171;
}
.coal {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
--sidebar-non-existant: #505254;
--sidebar-active: #3473ad;
--sidebar-spacer: #393939;
--scrollbar: var(--sidebar-fg);
--icons: #43484d;
--icons-hover: #b3c0cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%);
--table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
}
.light {
--bg: hsl(0, 0%, 100%);
--fg: hsl(0, 0%, 0%);
--sidebar-bg: #fafafa;
--sidebar-fg: hsl(0, 0%, 0%);
--sidebar-non-existant: #aaaaaa;
--sidebar-active: #1f1fff;
--sidebar-spacer: #f4f4f4;
--scrollbar: #8F8F8F;
--icons: #747474;
--icons-hover: #000000;
--links: #20609f;
--inline-code-color: #301900;
--theme-popup-bg: #fafafa;
--theme-popup-border: #cccccc;
--theme-hover: #e6e6e6;
--quote-bg: hsl(197, 37%, 96%);
--quote-border: hsl(197, 37%, 91%);
--table-border-color: hsl(0, 0%, 95%);
--table-header-bg: hsl(0, 0%, 80%);
--table-alternate-bg: hsl(0, 0%, 97%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #e4f2fe;
--search-mark-bg: #a2cff5;
}
.navy {
--bg: hsl(226, 23%, 11%);
--fg: #bcbdd0;
--sidebar-bg: #282d3f;
--sidebar-fg: #c8c9db;
--sidebar-non-existant: #505274;
--sidebar-active: #2b79a2;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: #282e40;
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 16%);
--table-header-bg: hsl(226, 23%, 31%);
--table-alternate-bg: hsl(226, 23%, 14%);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
}
.rust {
--bg: hsl(60, 9%, 87%);
--fg: #262625;
--sidebar-bg: #3b2e2a;
--sidebar-fg: #c8c9db;
--sidebar-non-existant: #505254;
--sidebar-active: #e69f67;
--sidebar-spacer: #45373a;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #262625;
--links: #2b79a2;
--inline-code-color: #6e6b5e;
--theme-popup-bg: #e1e1db;
--theme-popup-border: #b38f6b;
--theme-hover: #99908a;
--quote-bg: hsl(60, 5%, 75%);
--quote-border: hsl(60, 5%, 70%);
--table-border-color: hsl(60, 9%, 82%);
--table-header-bg: #b3a497;
--table-alternate-bg: hsl(60, 9%, 84%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #dec2a2;
--search-mark-bg: #e69f67;
}
@media (prefers-color-scheme: dark) {
.light.no-js {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
--sidebar-non-existant: #505254;
--sidebar-active: #3473ad;
--sidebar-spacer: #393939;
--scrollbar: var(--sidebar-fg);
--icons: #43484d;
--icons-hover: #b3c0cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%);
--table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
}
}
.colibri {
--bg: #3b224c;
--fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existant: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
/* --links: #a4a0e8; */
--links: #ECCDBA;
--inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
}
.colibri {
/*
--bg: #ffffff;
--fg: #452859;
--fg: #5a5977;
--heading-fg: #281733;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existant: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #6F44F0;
--inline-code-color: #a39e9b;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: rgba(0, 0, 0, 0);
--quote-border: hsl(226, 15%, 75%);
--table-border-color: #5a5977;
--table-border-color: hsl(201deg 10% 67%);
--table-header-bg: hsl(0, 0%, 100%);
--table-alternate-bg: hsl(0, 0%, 97%);
--table-border-line: hsl(201deg, 20%, 92%);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
*/
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -1 +1,22 @@
<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>
<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 -->

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

56
book/theme/highlight.css Normal file
View File

@@ -0,0 +1,56 @@
pre code.hljs {
display:block;
overflow-x:auto;
padding:1em
}
code.hljs {
padding:3px 5px
}
.hljs {
background:#2f1e2e;
color:#a39e9b
}
.hljs-comment,
.hljs-quote {
color:#8d8687
}
.hljs-link,
.hljs-meta,
.hljs-name,
.hljs-regexp,
.hljs-selector-class,
.hljs-selector-id,
.hljs-tag,
.hljs-template-variable,
.hljs-variable {
color:#ef6155
}
.hljs-built_in,
.hljs-deletion,
.hljs-literal,
.hljs-number,
.hljs-params,
.hljs-type {
color:#f99b15
}
.hljs-attribute,
.hljs-section,
.hljs-title {
color:#fec418
}
.hljs-addition,
.hljs-bullet,
.hljs-string,
.hljs-symbol {
color:#48b685
}
.hljs-keyword,
.hljs-selector-tag {
color:#815ba4
}
.hljs-emphasis {
font-style:italic
}
.hljs-strong {
font-weight:700
}

6
book/theme/highlight.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,11 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>{{ title }}</title>
{{#if is_print }}
<meta name="robots" content="noindex">
<meta name="robots" content="noindex" />
{{/if}}
{{#if base_url}}
<base href="{{ base_url }}">
@@ -15,9 +15,10 @@
<!-- Custom HTML head -->
{{> head}}
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<meta name="theme-color" content="#ffffff" />
{{#if favicon_svg}}
<link rel="icon" href="{{ path_to_root }}favicon.svg">
@@ -34,6 +35,8 @@
<!-- Fonts -->
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;400;500;700&display=swap" rel="stylesheet">
{{#if copy_fonts}}
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
{{/if}}
@@ -50,19 +53,18 @@
{{#if mathjax_support}}
<!-- MathJax -->
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
</head>
<body class="sidebar-visible no-js">
<div id="body-container">
<body>
<!-- Provide site root to javascript -->
<script>
<script type="text/javascript">
var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
@@ -78,34 +80,27 @@
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('{{ default_theme }}')
html.classList.add(theme);
var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
html.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var body = document.querySelector('body');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
@@ -115,48 +110,26 @@
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
{{> header}}
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
<li role="none"><button role="menuitem" class="theme" id="colibri">Colibri</button></li>
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="colibri">{{ theme_option "Colibri" }}</button></li>
</ul>
{{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
@@ -178,19 +151,13 @@
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
</a>
{{/if}}
{{#if git_repository_edit_url}}
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
{{/if}}
</div>
</div>
{{#if search_enabled}}
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
@@ -201,7 +168,7 @@
{{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
@@ -223,7 +190,7 @@
{{/previous}}
{{#next}}
<a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
@@ -241,7 +208,7 @@
{{/previous}}
{{#next}}
<a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
@@ -249,12 +216,10 @@
</div>
{{#if live_reload_endpoint}}
{{#if livereload}}
<!-- Livereload script (if served using the cli tool) -->
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
<script type="text/javascript">
var socket = new WebSocket("{{{livereload}}}");
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
@@ -270,7 +235,7 @@
{{#if google_analytics}}
<!-- Google Analytics Tag -->
<script>
<script type="text/javascript">
var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is
@@ -288,43 +253,43 @@
{{/if}}
{{#if playground_line_numbers}}
<script>
<script type="text/javascript">
window.playground_line_numbers = true;
</script>
{{/if}}
{{#if playground_copyable}}
<script>
<script type="text/javascript">
window.playground_copyable = true;
</script>
{{/if}}
{{#if playground_js}}
<script src="{{ path_to_root }}ace.js"></script>
<script src="{{ path_to_root }}editor.js"></script>
<script src="{{ path_to_root }}mode-rust.js"></script>
<script src="{{ path_to_root }}theme-dawn.js"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js"></script>
<script src="{{ path_to_root }}mark.min.js"></script>
<script src="{{ path_to_root }}searcher.js"></script>
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
<script src="{{ path_to_root }}clipboard.min.js"></script>
<script src="{{ path_to_root }}highlight.js"></script>
<script src="{{ path_to_root }}book.js"></script>
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
{{#each additional_js}}
<script src="{{ ../path_to_root }}{{this}}"></script>
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
{{/each}}
{{#if is_print}}
{{#if mathjax_support}}
<script>
<script type="text/javascript">
window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100);
@@ -332,7 +297,7 @@
});
</script>
{{else}}
<script>
<script type="text/javascript">
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
@@ -340,6 +305,5 @@
{{/if}}
{{/if}}
</div>
</body>
</html>

View File

@@ -1,108 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.helix_editor.Helix</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MPL-2.0</project_license>
<name>Helix</name>
<summary>A post-modern text editor</summary>
<summary xml:lang="ar">مُحَرِّرُ نُصُوصٍ سَابِقٌ لِعَهدِه</summary>
<description>
<p>
Helix is a terminal-based text editor inspired by Kakoune / Neovim and written in Rust.
</p>
<ul>
<li>Vim-like modal editing</li>
<li>Multiple selections</li>
<li>Built-in language server support</li>
<li>Smart, incremental syntax highlighting and code editing via tree-sitter</li>
</ul>
</description>
<description xml:lang="ar">
<p>
مُحَرِّرُ نُصُوصٍ يَعمَلُ فِي الطَّرَفِيَّة، مُستَلهَمٌ مِن Kakoune وَ Neovim وَمَكتُوبٌ بِلُغَةِ رَست البَرمَجِيَّة.
</p>
<ul>
<li>تَحرِيرٌ وَضعِيٌّ شَبيهٌ بِـVim</li>
<li>تَحدِيدَاتٌ لِلنَّصِ مُتَعَدِّدَة</li>
<li>دَعْمٌ مُدمَجٌ لِخَوادِمِ اللُّغَات</li>
<li>تَحرِيرُ التَّعلِيمَاتِ البَّرمَجِيَّةِ مَعَ تَمييزٍ لِلتَّركِيبِ النَّحُويِّ بِواسِطَةِ tree-sitter</li>
</ul>
</description>
<launchable type="desktop-id">Helix.desktop</launchable>
<screenshots>
<screenshot type="default">
<caption>Helix with default theme</caption>
<image>https://github.com/helix-editor/helix/raw/d4565b4404cabc522bd60822abd374755581d751/screenshot.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://helix-editor.com/</url>
<url type="donation">https://opencollective.com/helix-editor</url>
<url type="help">https://docs.helix-editor.com/</url>
<url type="vcs-browser">https://github.com/helix-editor/helix</url>
<url type="bugtracker">https://github.com/helix-editor/helix/issues</url>
<content_rating type="oars-1.1" />
<releases>
<release version="23.10" date="2023-10-24">
<url>https://helix-editor.com/news/release-23-10-highlights/</url>
</release>
<release version="23.05" date="2023-05-18">
<url>https://github.com/helix-editor/helix/releases/tag/23.05</url>
</release>
<release version="23.03" date="2023-03-31">
<url>https://helix-editor.com/news/release-23-03-highlights/</url>
</release>
<release version="22.12" date="2022-12-6">
<url>https://helix-editor.com/news/release-22-12-highlights/</url>
</release>
<release version="22.08" date="2022-8-31">
<url>https://helix-editor.com/news/release-22-08-highlights/</url>
</release>
<release version="22.05" date="2022-5-28">
<url>https://helix-editor.com/news/release-22-05-highlights/</url>
</release>
<release version="22.03" date="2022-3-28">
<url>https://helix-editor.com/news/release-22-03-highlights/</url>
</release>
</releases>
<requires>
<control>keyboard</control>
</requires>
<categories>
<category>Utility</category>
<category>TextEditor</category>
</categories>
<keywords>
<keyword>text</keyword>
<keyword>editor</keyword>
<keyword>development</keyword>
<keyword>programming</keyword>
</keywords>
<provides>
<binary>hx</binary>
<mediatype>text/english</mediatype>
<mediatype>text/plain</mediatype>
<mediatype>text/x-makefile</mediatype>
<mediatype>text/x-c++hdr</mediatype>
<mediatype>text/x-c++src</mediatype>
<mediatype>text/x-chdr</mediatype>
<mediatype>text/x-csrc</mediatype>
<mediatype>text/x-java</mediatype>
<mediatype>text/x-moc</mediatype>
<mediatype>text/x-pascal</mediatype>
<mediatype>text/x-tcl</mediatype>
<mediatype>text/x-tex</mediatype>
<mediatype>application/x-shellscript</mediatype>
<mediatype>text/x-c</mediatype>
<mediatype>text/x-c++</mediatype>
</provides>
</component>

View File

@@ -1,91 +0,0 @@
[Desktop Entry]
Name=Helix
GenericName=Text Editor
GenericName[ar]=مُحَرِّرُ نُصُوص
GenericName[de]=Texteditor
GenericName[fr]=Éditeur de texte
GenericName[ru]=Текстовый редактор
GenericName[sr]=Едитор текст
GenericName[tr]=Metin Düzenleyici
Comment=Edit text files
Comment[af]=Redigeer tekslêers
Comment[am]=የጽሑፍ ፋይሎች ያስተካክሉ
Comment[ar]=مُحَرِّرُ مِلَفَّاتٍ نَصِّيَّة
Comment[az]=Mətn fayllarını redaktə edin
Comment[be]=Рэдагаваньне тэкставых файлаў
Comment[bg]=Редактиране на текстови файлове
Comment[bn]=টেক্স্ট ফাইল এডিট করুন
Comment[bs]=Izmijeni tekstualne datoteke
Comment[ca]=Edita fitxers de text
Comment[cs]=Úprava textových souborů
Comment[cy]=Golygu ffeiliau testun
Comment[da]=Redigér tekstfiler
Comment[de]=Textdateien bearbeiten
Comment[el]=Επεξεργασία αρχείων κειμένου
Comment[en_CA]=Edit text files
Comment[en_GB]=Edit text files
Comment[es]=Edita archivos de texto
Comment[et]=Redigeeri tekstifaile
Comment[eu]=Editatu testu-fitxategiak
Comment[fa]=ویرایش پرونده‌های متنی
Comment[fi]=Muokkaa tekstitiedostoja
Comment[fr]=Éditer des fichiers texte
Comment[ga]=Eagar comhad Téacs
Comment[gu]=લખાણ ફાઇલોમાં ફેરફાર કરો
Comment[he]=ערוך קבצי טקסט
Comment[hi]=पाठ फ़ाइलें संपादित करें
Comment[hr]=Uređivanje tekstualne datoteke
Comment[hu]=Szövegfájlok szerkesztése
Comment[id]=Edit file teks
Comment[it]=Modifica file di testo
Comment[ja]=テキストファイルを編集します
Comment[kn]=ಪಠ್ಯ ಕಡತಗಳನ್ನು ಸಂಪಾದಿಸು
Comment[ko]=텍스트 파일을 편집합니다
Comment[lt]=Redaguoti tekstines bylas
Comment[lv]=Rediģēt teksta failus
Comment[mk]=Уреди текстуални фајлови
Comment[ml]=വാചക രചനകള് തിരുത്തുക
Comment[mn]=Текст файл боловсруулах
Comment[mr]=गद्य फाइल संपादित करा
Comment[ms]=Edit fail teks
Comment[nb]=Rediger tekstfiler
Comment[ne]=पाठ फाइललाई संशोधन गर्नुहोस्
Comment[nl]=Tekstbestanden bewerken
Comment[nn]=Rediger tekstfiler
Comment[no]=Rediger tekstfiler
Comment[or]=ପାଠ୍ଯ ଫାଇଲଗୁଡ଼ିକୁ ସମ୍ପାଦନ କରନ୍ତୁ
Comment[pa]=ਪਾਠ ਫਾਇਲਾਂ ਸੰਪਾਦਨ
Comment[pl]=Edytor plików tekstowych
Comment[pt]=Editar ficheiros de texto
Comment[pt_BR]=Edite arquivos de texto
Comment[ro]=Editare fişiere text
Comment[ru]=Редактирование текстовых файлов
Comment[sk]=Úprava textových súborov
Comment[sl]=Urejanje datotek z besedili
Comment[sq]=Përpuno files teksti
Comment[sr]=Уређујте текст фајлове
Comment[sr@Latn]=Izmeni tekstualne datoteke
Comment[sv]=Redigera textfiler
Comment[ta]=உரை கோப்புகளை தொகுக்கவும்
Comment[th]=แก้ไขแฟ้มข้อความ
Comment[tk]=Metin faýllary editle
Comment[tr]=Metin dosyaları düzenleyin
Comment[uk]=Редактор текстових файлів
Comment[vi]=Soạn thảo tập tin văn bản
Comment[wa]=Asspougnî des fitchîs tecses
Comment[zh_CN]=编辑文本文件
Comment[zh_TW]=編輯文字檔
TryExec=hx
Exec=hx %F
Terminal=true
Type=Application
Keywords=Text;editor;
Keywords[ar]=نص;نصوص;محرر;
Keywords[fr]=Texte;éditeur;
Keywords[ru]=текст;текстовый редактор;
Keywords[sr]=Текст;едитор;
Keywords[tr]=Metin;düzenleyici;
Icon=helix
Categories=Utility;TextEditor;
StartupNotify=false
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
# Bash completion script for Helix editor
_hx() {
# $1 command name
# $2 word being completed
# $3 word preceding
COMPREPLY=()
case "$3" in
-g | --grammar)
COMPREPLY=($(compgen -W "fetch build" -- $2))
;;
--health)
local languages=$(hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g')
COMPREPLY=($(compgen -W "$languages" -- $2))
;;
*)
COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- $2))
;;
esac
} && complete -o filenames -F _hx hx

View File

@@ -1,55 +0,0 @@
# You can move it here ~/.config/elvish/lib/hx.elv
# Or add `eval (slurp < ~/$REPOS/helix/contrib/completion/hx.elv)`
# Be sure to replace `$REPOS` with something that makes sense for you!
### Renders a pretty completion candidate
var candidate = { | _stem _desc |
edit:complex-candidate $_stem &display=(styled $_stem bold)(styled " "$_desc dim)
}
### These commands will invalidate further input (i.e. not react to them)
var skips = [ "--tutor" "--help" "--version" "-V" "--health" ]
### Grammar commands
var grammar = [ "--grammar" "-g" ]
### Config commands
var config = [ "--config" "-c" ]
### Set an arg-completer for the `hx` binary
set edit:completion:arg-completer[hx] = {|@args|
var n = (count $args)
if (>= $n 3) {
# Stop completions if passed arg will take presedence
# and invalidate further input
if (has-value $skips $args[-2]) {
return
}
# If the previous arg == --grammar, then only suggest:
if (has-value $grammar $args[-2]) {
$candidate "fetch" "Fetch the tree-sitter grammars"
$candidate "build" "Build the tree-sitter grammars"
return
}
# When we have --config, we need a file
if (has-values $config $args[-2]) {
edit:complete-filename $args[-1] | each { |v| put $v[stem] }
return
}
# When we have --log, we need a file
if (has-values "log" $args[-2]) {
edit:complete-filename $args[-1] | each { |v| put $v[stem] }
return
}
}
edit:complete-filename $args[-1] | each { |v| put $v[stem]}
$candidate "--help" "(Prints help information)"
$candidate "--version" "(Prints version information)"
$candidate "--tutor" "(Loads the tutorial)"
$candidate "--health" "(Checks for errors in editor setup)"
$candidate "--grammar" "(Fetch or build the tree-sitter grammars)"
$candidate "--vsplit" "(Splits all given files vertically)"
$candidate "--hsplit" "(Splits all given files horizontally)"
$candidate "--config" "(Specifies a file to use for configuration)"
$candidate "--log" "(Specifies a file to write log data into)"
}

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env fish
# Fish completion script for Helix editor
set -l langs (hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g')
complete -c hx -s h -l help -d "Prints help information"
complete -c hx -l tutor -d "Loads the tutorial"
complete -c hx -l health -x -a "$langs" -d "Checks for errors in editor setup"
complete -c hx -s g -l grammar -x -a "fetch build" -d "Fetches or builds tree-sitter grammars"
complete -c hx -s v -o vv -o vvv -d "Increases logging verbosity"
complete -c hx -s V -l version -d "Prints version information"
complete -c hx -l vsplit -d "Splits all given files vertically into different windows"
complete -c hx -l hsplit -d "Splits all given files horizontally into different windows"
complete -c hx -s c -l config -r -d "Specifies a file to use for completion"
complete -c hx -l log -r -d "Specifies a file to write log data into"

View File

@@ -1,34 +0,0 @@
#compdef _hx hx
# Zsh completion script for Helix editor
_hx() {
_arguments -C \
"-h[Prints help information]" \
"--help[Prints help information]" \
"-v[Increase logging verbosity]" \
"-vv[Increase logging verbosity]" \
"-vvv[Increase logging verbosity]" \
"-V[Prints version information]" \
"--version[Prints version information]" \
"--tutor[Loads the tutorial]" \
"--health[Checks for errors in editor setup]:language:->health" \
"-g[Fetches or builds tree-sitter grammars]:action:->grammar" \
"--grammar[Fetches or builds tree-sitter grammars]:action:->grammar" \
"--vsplit[Splits all given files vertically into different windows]" \
"--hsplit[Splits all given files horizontally into different windows]" \
"-c[Specifies a file to use for configuration]" \
"--config[Specifies a file to use for configuration]" \
"--log[Specifies a file to write log data into]" \
"*:file:_files"
case "$state" in
health)
local languages=($(hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g'))
_values 'language' $languages
;;
grammar)
_values 'action' fetch build
;;
esac
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,8 +0,0 @@
# Flake's default package for non-flake-enabled nix instances
let
compat = builtins.fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz";
sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7";
};
in
(import compat {src = ./.;}).defaultNix.default

View File

@@ -8,10 +8,7 @@ Some suggestions to get started:
- Help with packaging on various distributions needed!
- To use print debugging to the [Helix log file][log-file], you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher verbosity)
* Want to display the logs in a separate file instead of using the `:log-open` command in your compiled Helix editor? Start your debug version with `cargo run -- --log foo.log` and in a new terminal use `tail -f foo.log`
- Instead of running a release version of Helix, while developing you may want to run in debug mode with `cargo run` which is way faster to compile
- Looking for even faster compile times? Give a try to [mold](https://github.com/rui314/mold)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
- If your preferred language is missing, integrating a tree-sitter grammar for
it and defining syntax highlight queries for it is straight forward and
doesn't require much knowledge of the internals.
@@ -29,40 +26,12 @@ files, run
cargo xtask docgen
```
inside the project. We use [xtask][xtask] as an ad-hoc task runner.
inside the project. We use [xtask][xtask] as an ad-hoc task runner and
thus do not require any dependencies other than `cargo` (You don't have
to `cargo install` anything either).
To preview the book itself, install [mdbook][mdbook]. Then, run
```shell
mdbook serve book
```
and visit [http://localhost:3000](http://localhost:3000).
# Testing
## Unit tests/Documentation tests
Run `cargo test --workspace` to run unit tests and documentation tests in all packages.
## Integration tests
Integration tests for helix-term can be run with `cargo integration-test`. Code
contributors are strongly encouraged to write integration tests for their code.
Existing tests can be used as examples. Helpers can be found in
[helpers.rs][helpers.rs]. The log level can be set with the `HELIX_LOG_LEVEL`
environment variable, e.g. `HELIX_LOG_LEVEL=debug cargo integration-test`.
## Minimum Stable Rust Version (MSRV) Policy
Helix follows the MSRV of Firefox.
The current MSRV and future changes to the MSRV are listed in the [Firefox documentation].
[Firefox documentation]: https://firefox-source-docs.mozilla.org/writing-rust-code/update-policy.html
[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy
[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file
[architecture.md]: ./architecture.md
[docs]: https://docs.helix-editor.com/
[xtask]: https://github.com/matklad/cargo-xtask
[mdbook]: https://rust-lang.github.io/mdBook/guide/installation.html
[helpers.rs]: ../helix-term/tests/test/helpers.rs

View File

@@ -2,9 +2,8 @@
| Crate | Description |
| ----------- | ----------- |
| helix-core | Core editing primitives, functional. |
| helix-syntax | Tree-sitter grammars |
| helix-lsp | Language server client |
| helix-dap | Debug Adapter Protocol (DAP) client |
| helix-loader | Functions for building, fetching, and loading external resources |
| helix-view | UI abstractions for use in backends, imperative shell. |
| helix-term | Terminal UI |
| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive |
@@ -32,7 +31,7 @@ represented by a `Selection`. Each `Range` in the selection consists of a moving
a selection with a single range, with the head and the anchor in the same
position.
Ropes are modified by constructing an OT-like `Transaction`. It represents
Ropes are modified by constructing an OT-like `Transaction`. It's represents
a single coherent change to the document and can be applied to the rope.
A transaction can be inverted to produce an undo. Selections and marks can be
mapped over a transaction to translate to a position in the new text state after
@@ -42,7 +41,7 @@ applying the transaction.
> interface used to generate text edits.
`Syntax` is the interface used to interact with tree-sitter ASTs for syntax
highlighting and other features.
highling and other features.
## View
@@ -55,40 +54,15 @@ A `Document` ties together the `Rope`, `Selection`(s), `Syntax`, document
file.
A `View` represents an open split in the UI. It holds the currently open
document ID and other related state. Views encapsulate the gutter, status line,
diagnostics, and the inner area where the code is displayed.
document ID and other related state.
> NOTE: Multiple views are able to display the same document, so the document
> contains selections for each view. To retrieve, `document.selection()` takes
> a `ViewId`.
`Info` is the autoinfo box that shows hints when awaiting another key with bindings
like `g` and `m`. It is attached to the viewport as a whole.
`Surface` is like a buffer to which widgets draw themselves to, and the
surface is then rendered on the screen on each cycle.
`Rect`s are areas (simply an x and y coordinate with the origin at the
screen top left and then a height and width) which are part of a
`Surface`. They can be used to limit the area to which a `Component` can
render. For example if we wrap a `Markdown` component in a `Popup`
(think the documentation popup with space+k), Markdown's render method
will get a Rect that is the exact size of the popup.
Widgets are called `Component`s internally, and you can see most of them
in `helix-term/src/ui`. Some components like `Popup` and `Overlay` can take
other components as children.
`Layer`s are how multiple components are displayed, and is simply a
`Vec<Component>`. Layers are managed by the `Compositor`. On each top
level render call, the compositor renders each component in the order
they were pushed into the stack. This makes multiple components "layer"
on top of one another. Hence we get a file picker displayed over the
editor, etc.
The `Editor` holds the global state: all the open documents, a tree
representation of all the view splits, the configuration, and a registry of
language servers. To open or close files, interact with the editor.
representation of all the view splits, and a registry of language servers. To
open or close files, interact with the editor.
## LSP

View File

@@ -1,67 +0,0 @@
## Checklist
Helix releases are versioned in the Calendar Versioning scheme:
`YY.0M(.MICRO)`, for example, `22.05` for May of 2022, or in a patch release,
`22.05.1`. In these instructions we'll use `<tag>` as a placeholder for the tag
being published.
* Merge the changelog PR
* Add new `<release>` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html)
* Tag and push
* `git tag -s -m "<tag>" -a <tag> && git push`
* Make sure to switch to master and pull first
* Edit the `Cargo.toml` file and change the date in the `version` field to the next planned release
* Due to Cargo having a strict requirement on SemVer with 3 or more version
numbers, a `0` is required in the micro version; however, unless we are
publishing a patch release after a major release, the `.0` is dropped in
the user facing version.
* Releases are planned to happen every two months, so `22.05.0` would change to `22.07.0`
* If we are pushing a patch/bugfix release in the same month as the previous
release, bump the micro version, e.g. `22.07.0` to `22.07.1`
* Wait for the Release CI to finish
* It will automatically turn the git tag into a GitHub release when it uploads artifacts
* Edit the new release
* Use `<tag>` as the title
* Link to the changelog and release notes
* Merge the release notes PR
* Download the macos and linux binaries and update the `sha256`s in the [homebrew formula]
* Use `sha256sum` on the downloaded `.tar.xz` files to determine the hash
* Link to the release notes in this-week-in-rust
* [Example PR](https://github.com/rust-lang/this-week-in-rust/pull/3300)
* Post to reddit
* [Example post](https://www.reddit.com/r/rust/comments/uzp5ze/helix_editor_2205_released/)
[homebrew formula]: https://github.com/Homebrew/homebrew-core/blob/master/Formula/helix.rb
## Changelog Curation
The changelog is currently created manually by reading through commits in the
log since the last release. GitHub's compare view is a nice way to approach
this. For example, when creating the 22.07 release notes, this compare link
may be used
```
https://github.com/helix-editor/helix/compare/22.05...master
```
Either side of the triple-dot may be replaced with an exact revision, so if
you wish to incrementally compile the changelog, you can tackle a weeks worth
or so, record the revision where you stopped, and use that as a starting point
next week:
```
https://github.com/helix-editor/helix/compare/7706a4a0d8b67b943c31d0c5f7b00d357b5d838d...master
```
A work-in-progress commit for a changelog might look like
[this example](https://github.com/helix-editor/helix/commit/831adfd4c709ca16b248799bfef19698d5175e55).
Not every PR or commit needs a blurb in the changelog. Each release section
tends to have a blurb that links to a GitHub comparison between release
versions for convenience:
> 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.
Typically, small changes like dependencies or documentation updates, refactors,
or meta changes like GitHub Actions work are left out.

View File

@@ -20,5 +20,5 @@ Vision statements are all well and good, but are also vague and subjective. Her
* **Built-in tools** for working with code bases efficiently. Most projects aren't a single file, and an editor should handle that as a first-class use case. In Helix's case, this means (among other things) a fuzzy-search file navigator and LSP support.
* **Edit anything** that comes up when coding, within reason. Whether it's a 200 MB XML file, a megabyte of minified javascript on a single line, or Japanese text encoded in ShiftJIS, you should be able to open it and edit it without problems. (Note: this doesn't mean handle every esoteric use case. Sometimes you do just need a specialized tool, and Helix isn't that.)
* **Configurable**, within reason. Although the defaults should be good, not everyone will agree on what "good" is. Within the bounds of Helix's core interaction models, it should be reasonably configurable so that it can be "good" for more people. This means, for example, custom key maps among other things.
* **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed.
* **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed. Right now we're thinking Wasm-based plugins.
* **Clean code base.** Sometimes other factors (e.g. significant performance gains, important features, correctness, etc.) will trump strict readability, but we nevertheless want to keep the code base straightforward and easy to understand to the extent we can.

108
flake.lock generated
View File

@@ -1,35 +1,27 @@
{
"nodes": {
"crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"devshell": {
"locked": {
"lastModified": 1701025348,
"narHash": "sha256-42GHmYH+GF7VjwGSt+fVT1CQuNpGanJbNgVHTAZppUM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "42afaeb1a0325194a7cdb526332d2cb92fddd07b",
"lastModified": 1639692811,
"narHash": "sha256-wOOBH0fVsfNqw/5ZWRoKspyesoXBgiwEOUBH4c7JKEo=",
"owner": "numtide",
"repo": "devshell",
"rev": "d3a1f5bec3632b33346865b1c165bf2420bb2f52",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"lastModified": 1637014545,
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github"
},
"original": {
@@ -38,13 +30,37 @@
"type": "github"
}
},
"nixCargoIntegration": {
"inputs": {
"devshell": "devshell",
"nixpkgs": [
"nixpkgs"
],
"rustOverlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1639807801,
"narHash": "sha256-y32tMq1LTRVbMW3QN5i98iOQjQt2QSsif3ayUkD1o3g=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "b5bbaa4f5239e6f0619846f9a5380f07baa853d3",
"type": "github"
},
"original": {
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1700794826,
"narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=",
"lastModified": 1639699734,
"narHash": "sha256-tlX6WebGmiHb2Hmniff+ltYp+7dRfdsBxw9YczLsP60=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8",
"rev": "03ec468b14067729a285c2c7cfa7b9434a04816c",
"type": "github"
},
"original": {
@@ -54,29 +70,40 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1637453606,
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1701137803,
"narHash": "sha256-0LcPAdql5IhQSUXJx3Zna0dYTgdIoYO7zUrsKgiBd04=",
"lastModified": 1639880499,
"narHash": "sha256-/BibDmFwgWuuTUkNVO6YlvuTSWM9dpBvlZoTAPs7ORI=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "9dd940c967502f844eacea52a61e9596268d4f70",
"rev": "c6c83589ae048af20d93d01eb07a4176012093d0",
"type": "github"
},
"original": {
@@ -84,21 +111,6 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

229
flake.nix
View File

@@ -3,198 +3,69 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};
crane = {
url = "github:ipetkov/crane";
rust-overlay.url = "github:oxalica/rust-overlay";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay";
};
};
outputs = {
self,
nixpkgs,
crane,
flake-utils,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
outputs = inputs@{ self, nixCargoIntegration, ... }:
nixCargoIntegration.lib.makeOutputs {
root = ./.;
buildPlatform = "crate2nix";
renameOutputs = { "helix-term" = "helix"; };
# Set default app to hx (binary is from helix-term release build)
# Set default package to helix-term release build
defaultOutputs = { app = "hx"; package = "helix"; };
overrides = {
crateOverrides = common: _: {
helix-term = prev: {
# link languages and theme toml files since helix-term expects them (for tests)
preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} ..";
buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
};
mkRootPath = rel:
builtins.path {
path = "${toString ./.}/${rel}";
name = rel;
# link languages and theme toml files since helix-view expects them
helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
helix-syntax = _prev: {
preConfigure = "mkdir -p ../runtime/grammars";
postInstall = "cp -r ../runtime $out/runtime";
};
filteredSource = let
pathsToIgnore = [
".envrc"
".ignore"
".github"
".gitignore"
"logo_dark.svg"
"logo_light.svg"
"rust-toolchain.toml"
"rustfmt.toml"
"runtime"
"screenshot.png"
"book"
"docs"
"README.md"
"CHANGELOG.md"
"shell.nix"
"default.nix"
"grammars.nix"
"flake.nix"
"flake.lock"
];
ignorePaths = path: type: let
inherit (nixpkgs) lib;
# split the nix store path into its components
components = lib.splitString "/" path;
# drop off the `/nix/hash-source` section from the path
relPathComponents = lib.drop 4 components;
# reassemble the path components
relPath = lib.concatStringsSep "/" relPathComponents;
in
lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
in
builtins.path {
name = "helix-source";
path = toString ./.;
# filter out unnecessary paths
filter = ignorePaths;
};
makeOverridableHelix = old: config: let
grammars = pkgs.callPackage ./grammars.nix config;
runtimeDir = pkgs.runCommand "helix-runtime" {} ''
mainBuild = common: prev:
let
inherit (common) pkgs lib;
helixSyntax = lib.buildCrate {
root = self;
memberName = "helix-syntax";
defaultCrateOverrides = {
helix-syntax = common.crateOverrides.helix-syntax;
};
release = false;
};
runtimeDir = pkgs.runCommand "helix-runtime" { } ''
mkdir -p $out
ln -s ${mkRootPath "runtime"}/* $out
rm -r $out/grammars
ln -s ${grammars} $out/grammars
'';
helix-wrapped =
pkgs.runCommand
old.name
{
inherit (old) pname version;
meta = old.meta or {};
passthru =
(old.passthru or {})
// {
unwrapped = old;
};
nativeBuildInputs = [pkgs.makeWrapper];
makeWrapperArgs = config.makeWrapperArgs or [];
}
''
cp -rs --no-preserve=mode,ownership ${old} $out
wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
ln -s ${common.root}/runtime/* $out
ln -sf ${helixSyntax}/runtime/grammars $out
'';
in
helix-wrapped
// {
override = makeOverridableHelix old;
passthru =
helix-wrapped.passthru
// {
wrapper = old: makeOverridableHelix old config;
};
};
stdenv =
if pkgs.stdenv.isLinux
then pkgs.stdenv
else pkgs.clangStdenv;
rustFlagsEnv =
if stdenv.isLinux
then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
else "$RUSTFLAGS";
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain;
craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default;
commonArgs = {
inherit stdenv;
inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;}) pname;
inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./Cargo.toml;}) version;
src = filteredSource;
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
buildInputs = [stdenv.cc.cc.lib];
# disable tests
doCheck = false;
meta.mainProgram = "hx";
};
cargoArtifacts = craneLibMSRV.buildDepsOnly commonArgs;
in {
packages = {
helix-unwrapped = craneLibStable.buildPackage (commonArgs
// {
cargoArtifacts = craneLibStable.buildDepsOnly commonArgs;
postInstall = ''
mkdir -p $out/share/applications $out/share/icons/hicolor/scalable/apps $out/share/icons/hicolor/256x256/apps
cp contrib/Helix.desktop $out/share/applications
cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg
cp contrib/helix.png $out/share/icons/hicolor/256x256/apps
'';
});
helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {};
default = self.packages.${system}.helix;
};
checks = {
# Build the crate itself
inherit (self.packages.${system}) helix;
clippy = craneLibMSRV.cargoClippy (commonArgs
// {
inherit cargoArtifacts;
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
});
fmt = craneLibMSRV.cargoFmt commonArgs;
doc = craneLibMSRV.cargoDoc (commonArgs
// {
inherit cargoArtifacts;
});
test = craneLibMSRV.cargoTest (commonArgs
// {
inherit cargoArtifacts;
});
};
devShells.default = pkgs.mkShell {
inputsFrom = builtins.attrValues self.checks.${system};
nativeBuildInputs = with pkgs;
[lld_13 cargo-flamegraph rust-analyzer]
++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
++ (lib.optional stdenv.isLinux pkgs.lldb)
++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
shellHook = ''
export HELIX_RUNTIME="$PWD/runtime"
export RUST_BACKTRACE="1"
export RUSTFLAGS="${rustFlagsEnv}"
lib.optionalAttrs (common.memberName == "helix-term") {
nativeBuildInputs = [ pkgs.makeWrapper ];
postFixup = ''
if [ -f "$out/bin/hx" ]; then
wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}"
fi
'';
};
})
// {
overlays.default = final: prev: {
inherit (self.packages.${final.system}) helix;
shell = common: prev: {
packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin ]);
env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; }
{ name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
];
};
};
nixConfig = {
extra-substituters = ["https://helix.cachix.org"];
extra-trusted-public-keys = ["helix.cachix.org-1:ejp9KQpR1FBI2onstMQ34yogDm4OgU2ru6lIwPvuCVs="];
};
}

View File

@@ -1,124 +0,0 @@
{
stdenv,
lib,
runCommandLocal,
runCommand,
yj,
includeGrammarIf ? _: true,
grammarOverlays ? [],
...
}: let
# HACK: nix < 2.6 has a bug in the toml parser, so we convert to JSON
# before parsing
languages-json = runCommandLocal "languages-toml-to-json" {} ''
${yj}/bin/yj -t < ${./languages.toml} > $out
'';
languagesConfig =
if lib.versionAtLeast builtins.nixVersion "2.6.0"
then builtins.fromTOML (builtins.readFile ./languages.toml)
else builtins.fromJSON (builtins.readFile (builtins.toPath languages-json));
isGitGrammar = grammar:
builtins.hasAttr "source" grammar
&& builtins.hasAttr "git" grammar.source
&& builtins.hasAttr "rev" grammar.source;
isGitHubGrammar = grammar: lib.hasPrefix "https://github.com" grammar.source.git;
toGitHubFetcher = url: let
match = builtins.match "https://github\.com/([^/]*)/([^/]*)/?" url;
in {
owner = builtins.elemAt match 0;
repo = builtins.elemAt match 1;
};
gitGrammars = builtins.filter isGitGrammar languagesConfig.grammar;
buildGrammar = grammar: let
gh = toGitHubFetcher grammar.source.git;
sourceGit = builtins.fetchTree {
type = "git";
url = grammar.source.git;
rev = grammar.source.rev;
ref = grammar.source.ref or "HEAD";
shallow = true;
};
sourceGitHub = builtins.fetchTree {
type = "github";
owner = gh.owner;
repo = gh.repo;
inherit (grammar.source) rev;
};
source =
if isGitHubGrammar grammar
then sourceGitHub
else sourceGit;
in
stdenv.mkDerivation {
# see https://github.com/NixOS/nixpkgs/blob/fbdd1a7c0bc29af5325e0d7dd70e804a972eb465/pkgs/development/tools/parsing/tree-sitter/grammar.nix
pname = "helix-tree-sitter-${grammar.name}";
version = grammar.source.rev;
src = source;
sourceRoot = if builtins.hasAttr "subpath" grammar.source then
"source/${grammar.source.subpath}"
else
"source";
dontConfigure = true;
FLAGS = [
"-Isrc"
"-g"
"-O3"
"-fPIC"
"-fno-exceptions"
"-Wl,-z,relro,-z,now"
];
NAME = grammar.name;
buildPhase = ''
runHook preBuild
if [[ -e src/scanner.cc ]]; then
$CXX -c src/scanner.cc -o scanner.o $FLAGS
elif [[ -e src/scanner.c ]]; then
$CC -c src/scanner.c -o scanner.o $FLAGS
fi
$CC -c src/parser.c -o parser.o $FLAGS
$CXX -shared -o $NAME.so *.o
ls -al
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir $out
mv $NAME.so $out/
runHook postInstall
'';
# Strip failed on darwin: strip: error: symbols referenced by indirect symbol table entries that can't be stripped
fixupPhase = lib.optionalString stdenv.isLinux ''
runHook preFixup
$STRIP $out/$NAME.so
runHook postFixup
'';
};
grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
builtGrammars = builtins.map (grammar: {
inherit (grammar) name;
value = buildGrammar grammar;
}) grammarsToBuild;
extensibleGrammars =
lib.makeExtensible (self: builtins.listToAttrs builtGrammars);
overlayedGrammars = lib.pipe extensibleGrammars
(builtins.map (overlay: grammar: grammar.extend overlay) grammarOverlays);
grammarLinks = lib.mapAttrsToList
(name: artifact: "ln -s ${artifact}/${name}.so $out/${name}.so")
(lib.filterAttrs (n: v: lib.isDerivation v) overlayedGrammars);
in
runCommand "consolidated-helix-grammars" {} ''
mkdir -p $out
${builtins.concatStringsSep "\n" grammarLinks}
''

View File

@@ -1,28 +0,0 @@
[package]
name = "helix-config"
description = "Helix editor core editing primitives"
include = ["src/**/*", "README.md"]
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
categories.workspace = true
repository.workspace = true
homepage.workspace = true
[dependencies]
ahash = "0.8.6"
hashbrown = { version = "0.14.3", features = ["raw"] }
parking_lot = "0.12"
anyhow = "1.0.79"
indexmap = { version = "2.1.0", features = ["serde"] }
serde = { version = "1.0" }
serde_json = "1.0"
globset = "0.4.14"
regex = "1.10.2"
regex-syntax = "0.8.2"
which = "5.0.0"
regex-syntax = "0.8.2"
which = "5.0.0"

View File

@@ -1,76 +0,0 @@
/// this is a reimplementation of dynamic dispatch that only stores the
/// information we need and stores everythin inline. Values that are smaller or
/// the same size as a slice (2 usize) are also stored inline. This avoids
/// significant overallocation when setting lots of simple config
/// options (integers, strings, lists, enums)
use std::any::{Any, TypeId};
use std::mem::{align_of, size_of, MaybeUninit};
pub struct ConfigData {
data: MaybeUninit<[usize; 2]>,
ty: TypeId,
drop_fn: unsafe fn(MaybeUninit<[usize; 2]>),
}
const fn store_inline<T>() -> bool {
size_of::<T>() <= size_of::<[usize; 2]>() && align_of::<T>() <= align_of::<[usize; 2]>()
}
impl ConfigData {
unsafe fn drop_impl<T: Any>(mut data: MaybeUninit<[usize; 2]>) {
if store_inline::<T>() {
data.as_mut_ptr().cast::<T>().drop_in_place();
} else {
let ptr = data.as_mut_ptr().cast::<*mut T>().read();
drop(Box::from_raw(ptr));
}
}
pub fn get<T: Any>(&self) -> &T {
assert_eq!(TypeId::of::<T>(), self.ty);
unsafe {
if store_inline::<T>() {
return &*self.data.as_ptr().cast();
}
let data: *const T = self.data.as_ptr().cast::<*const T>().read();
&*data
}
}
pub fn new<T: Any>(val: T) -> Self {
let mut data = MaybeUninit::uninit();
if store_inline::<T>() {
let data: *mut T = data.as_mut_ptr() as _;
unsafe {
data.write(val);
}
} else {
assert!(store_inline::<*const T>());
let data: *mut *const T = data.as_mut_ptr() as _;
unsafe {
data.write(Box::into_raw(Box::new(val)));
}
};
Self {
data,
ty: TypeId::of::<T>(),
drop_fn: ConfigData::drop_impl::<T>,
}
}
}
impl Drop for ConfigData {
fn drop(&mut self) {
unsafe {
(self.drop_fn)(self.data);
}
}
}
impl std::fmt::Debug for ConfigData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigData").finish_non_exhaustive()
}
}
unsafe impl Send for ConfigData {}
unsafe impl Sync for ConfigData {}

View File

@@ -1,42 +0,0 @@
use crate::any::ConfigData;
use crate::validator::Ty;
use crate::Value;
pub trait IntoTy: Clone {
type Ty: Ty;
fn into_ty(self) -> Self::Ty;
}
impl<T: Ty> IntoTy for T {
type Ty = Self;
fn into_ty(self) -> Self::Ty {
self
}
}
impl<T: IntoTy> IntoTy for &[T] {
type Ty = Box<[T::Ty]>;
fn into_ty(self) -> Self::Ty {
self.iter().cloned().map(T::into_ty).collect()
}
}
impl<T: IntoTy, const N: usize> IntoTy for &[T; N] {
type Ty = Box<[T::Ty]>;
fn into_ty(self) -> Self::Ty {
self.iter().cloned().map(T::into_ty).collect()
}
}
impl IntoTy for &str {
type Ty = Box<str>;
fn into_ty(self) -> Self::Ty {
self.into()
}
}
pub(super) fn ty_into_value<T: Ty>(val: &ConfigData) -> Value {
T::to_value(val.get())
}

View File

@@ -1,113 +0,0 @@
use std::time::Duration;
use crate::*;
mod language;
mod lsp;
mod ui;
pub use lsp::init_language_server_config;
options! {
use ui::*;
use lsp::*;
use language::*;
struct WrapConfig {
/// Soft wrap lines that exceed viewport width.
enable: bool = false,
/// Maximum free space left at the end of the line.
/// Automatically limited to a quarter of the viewport.
max_wrap: u16 = 20,
/// Maximum indentation to carry over when soft wrapping a line.
/// Automatically limited to a quarter of the viewport.
max_indent_retain: u16 = 40,
/// Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap`.
wrap_indicator: String = "",
/// Soft wrap at `text-width` instead of using the full viewport size.
wrap_at_text_width: bool = false,
/// Maximum line length. Used for the `:reflow` command and
/// soft-wrapping if `soft-wrap.wrap-at-text-width` is set
text_width: usize = 80,
}
struct MouseConfig {
/// Enable mouse mode
#[read = copy]
mouse: bool = true,
/// Number of lines to scroll per scroll wheel step.
#[read = copy]
scroll_lines: usize = 3,
/// Middle click paste support
#[read = copy]
middle_click_paste: bool = true,
}
struct SmartTabConfig {
/// If set to true, then when the cursor is in a position with
/// non-whitespace to its left, instead of inserting a tab, it will run
/// `move_parent_node_end`. If there is only whitespace to the left,
/// then it inserts a tab as normal. With the default bindings, to
/// explicitly insert a tab character, press Shift-tab.
#[name = "smart-tab.enable"]
#[read = copy]
enable: bool = true,
/// Normally, when a menu is on screen, such as when auto complete
/// is triggered, the tab key is bound to cycling through the items.
/// This means when menus are on screen, one cannot use the tab key
/// to trigger the `smart-tab` command. If this option is set to true,
/// the `smart-tab` command always takes precedence, which means one
/// cannot use the tab key to cycle through menu items. One of the other
/// bindings must be used instead, such as arrow keys or `C-n`/`C-p`.
#[name = "smart-tab.supersede-menu"]
#[read = copy]
supersede_menu: bool = false,
}
struct SearchConfig {
/// Enable smart case regex searching (case-insensitive unless pattern
/// contains upper case characters)
#[name = "search.smart-case"]
#[read = copy]
smart_case: bool = true,
/// Whether the search should wrap after depleting the matches
#[name = "search.wrap-round"]
#[read = copy]
wrap_round: bool = true,
}
struct MiscConfig {
/// Number of lines of padding around the edge of the screen when scrolling.
#[read = copy]
scrolloff: usize = 5,
/// Shell to use when running external commands
#[read = deref]
shell: List<String> = if cfg!(windows) {
&["cmd", "/C"]
} else {
&["sh", "-c"]
},
/// Enable automatic saving on the focus moving away from Helix.
/// Requires [focus event support](https://github.com/helix-editor/
/// helix/wiki/Terminal-Support) from your terminal
#[read = copy]
auto_save: bool = false,
/// Whether to automatically insert a trailing line-ending on write
/// if missing
#[read = copy]
insert_final_newline: bool = true,
/// Time in milliseconds since last keypress before idle timers trigger.
/// Used for autocompletion, set to 0 for instant
#[read = copy]
idle_timeout: Duration = Duration::from_millis(250),
}
}
impl Ty for Duration {
fn from_value(val: Value) -> anyhow::Result<Self> {
let val: usize = val.typed()?;
Ok(Duration::from_millis(val as _))
}
fn to_value(&self) -> Value {
Value::Int(self.as_millis().try_into().unwrap())
}
}

View File

@@ -1,27 +0,0 @@
use crate::*;
options! {
struct LanguageConfig {
/// regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site.
#[validator = regex_str_validator()]
injection_regex: Option<String> = None,
/// The interpreters from the shebang line, for example `["sh", "bash"]`
#[read = deref]
shebangs: List<String> = List::default(),
/// The token to use as a comment-token
#[read = deref]
comment_token: String = "//",
/// The tree-sitter grammar to use (defaults to the language name)
grammar: Option<String> = None,
}
struct FormatterConfiguration {
#[read = copy]
auto_format: bool = true,
#[name = "formatter.command"]
formatter_command: Option<String> = None,
#[name = "formatter.args"]
#[read = deref]
formatter_args: List<String> = List::default(),
}
}

View File

@@ -1,266 +0,0 @@
use std::fmt::{self, Display};
use serde::{Deserialize, Serialize};
use crate::*;
/// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
pub enum Severity {
Hint,
Info,
Warning,
Error,
}
impl Ty for Severity {
fn from_value(val: Value) -> anyhow::Result<Self> {
let val: String = val.typed()?;
match &*val {
"hint" => Ok(Severity::Hint),
"info" => Ok(Severity::Info),
"warning" => Ok(Severity::Warning),
"error" => Ok(Severity::Error),
_ => bail!("expected one of 'hint', 'info', 'warning' or 'error' (got {val:?})"),
}
}
fn to_value(&self) -> Value {
match self {
Severity::Hint => "hint".into(),
Severity::Info => "info".into(),
Severity::Warning => "warning".into(),
Severity::Error => "error".into(),
}
}
}
// TODO: move to stdx
/// Helper macro that automatically generates an array
/// that contains all variants of an enum
macro_rules! variant_list {
(
$(#[$outer:meta])*
$vis: vis enum $name: ident {
$($(#[$inner: meta])* $variant: ident $(= $_: literal)?),*$(,)?
}
) => {
$(#[$outer])*
$vis enum $name {
$($(#[$inner])* $variant),*
}
impl $name {
$vis const ALL: &[$name] = &[$(Self::$variant),*];
}
}
}
variant_list! {
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum LanguageServerFeature {
Format,
GotoDeclaration,
GotoDefinition,
GotoTypeDefinition,
GotoReference,
GotoImplementation,
// Goto, use bitflags, combining previous Goto members?
SignatureHelp,
Hover,
DocumentHighlight,
Completion,
CodeAction,
WorkspaceCommand,
DocumentSymbols,
WorkspaceSymbols,
// Symbols, use bitflags, see above?
Diagnostics,
RenameSymbol,
InlayHints,
}
}
impl LanguageServerFeature {
fn to_str(self) -> &'static str {
use LanguageServerFeature::*;
match self {
Format => "format",
GotoDeclaration => "goto-declaration",
GotoDefinition => "goto-definition",
GotoTypeDefinition => "goto-type-definition",
GotoReference => "goto-reference",
GotoImplementation => "goto-implementation",
SignatureHelp => "signature-help",
Hover => "hover",
DocumentHighlight => "document-highlight",
Completion => "completion",
CodeAction => "code-action",
WorkspaceCommand => "workspace-command",
DocumentSymbols => "document-symbols",
WorkspaceSymbols => "workspace-symbols",
Diagnostics => "diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
}
}
fn description(self) -> &'static str {
use LanguageServerFeature::*;
match self {
Format => "Use this language server for autoformatting.",
GotoDeclaration => "Use this language server for the goto_declaration command.",
GotoDefinition => "Use this language server for the goto_definition command.",
GotoTypeDefinition => "Use this language server for the goto_type_definition command.",
GotoReference => "Use this language server for the goto_reference command.",
GotoImplementation => "Use this language server for the goto_implementation command.",
SignatureHelp => "Use this language server to display signature help.",
Hover => "Use this language server to display hover information.",
DocumentHighlight => {
"Use this language server for the select_references_to_symbol_under_cursor command."
}
Completion => "Request completion items from this language server.",
CodeAction => "Use this language server for the code_action command.",
WorkspaceCommand => "Use this language server for :lsp-workspace-command.",
DocumentSymbols => "Use this language server for the symbol_picker command.",
WorkspaceSymbols => "Use this language server for the workspace_symbol_picker command.",
Diagnostics => "Display diagnostics emitted by this language server.",
RenameSymbol => "Use this language server for the rename_symbol command.",
InlayHints => "Display inlay hints form this language server.",
}
}
}
impl Display for LanguageServerFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let feature = self.to_str();
write!(f, "{feature}",)
}
}
impl Debug for LanguageServerFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl Ty for LanguageServerFeature {
fn from_value(val: Value) -> anyhow::Result<Self> {
let val: String = val.typed()?;
use LanguageServerFeature::*;
match &*val {
"format" => Ok(Format),
"goto-declaration" => Ok(GotoDeclaration),
"goto-definition" => Ok(GotoDefinition),
"goto-type-definition" => Ok(GotoTypeDefinition),
"goto-reference" => Ok(GotoReference),
"goto-implementation" => Ok(GotoImplementation),
"signature-help" => Ok(SignatureHelp),
"hover" => Ok(Hover),
"document-highlight" => Ok(DocumentHighlight),
"completion" => Ok(Completion),
"code-action" => Ok(CodeAction),
"workspace-command" => Ok(WorkspaceCommand),
"document-symbols" => Ok(DocumentSymbols),
"workspace-symbols" => Ok(WorkspaceSymbols),
"diagnostics" => Ok(Diagnostics),
"rename-symbol" => Ok(RenameSymbol),
"inlay-hints" => Ok(InlayHints),
_ => bail!("invalid language server feature {val}"),
}
}
fn to_value(&self) -> Value {
Value::String(self.to_str().into())
}
}
pub fn init_language_server_config(registry: &mut OptionRegistry, languag_server: &str) {
registry.register(
&format!("language-servers.{languag_server}.active"),
"Wether this language servers is used for a buffer",
false,
);
for &feature in LanguageServerFeature::ALL {
registry.register(
&format!("language-servers.{languag_server}.{feature}"),
feature.description(),
true,
);
}
}
options! {
struct LspConfig {
/// Enables LSP integration. Setting to false will completely disable language servers.
#[name = "lsp.enable"]
#[read = copy]
enable: bool = true,
/// Enables LSP integration. Setting to false will completely disable language servers.
#[name = "lsp.display-messages"]
#[read = copy]
display_messages: bool = false,
/// Enable automatic popup of signature help (parameter hints)
#[name = "lsp.auto-signature-help"]
#[read = copy]
auto_signature_help: bool = true,
/// Enable automatic popup of signature help (parameter hints)
#[name = "lsp.display-inlay-hints"]
#[read = copy]
display_inlay_hints: bool = false,
/// Display docs under signature help popup
#[name = "lsp.display-signature-help-docs"]
#[read = copy]
display_signature_help_docs: bool = true,
/// Enables snippet completions. Requires a server restart
/// (`:lsp-restart`) to take effect after `:config-reload`/`:set`.
#[name = "lsp.snippets"]
#[read = copy]
snippets: bool = true,
/// Include declaration in the goto references popup.
#[name = "lsp.goto-reference-include-declaration"]
#[read = copy]
goto_reference_include_declaration: bool = true,
// TODO(breaing): prefix all options below with `lsp.`
/// The language-id for language servers, checkout the
/// table at [TextDocumentItem](https://microsoft.github.io/
/// language-server-protocol/specifications/lsp/3.17/specification/
/// #textDocumentItem) for the right id
#[name = "languague-id"]
language_server_id: Option<String> = None,
// TODO(breaking): rename to root-markers to differentiate from workspace-roots
// TODO: also makes this setteble on the language server
/// A set of marker files to look for when trying to find the workspace
/// root. For example `Cargo.lock`, `yarn.lock`
roots: List<String> = List::default(),
// TODO: also makes this setteble on the language server
/// Directories relative to the workspace root that are treated as LSP
/// roots. The search for root markers (starting at the path of the
/// file) will stop at these paths.
#[name = "workspace-lsp-roots"]
workspace_roots: List<String> = List::default(),
/// An array of LSP diagnostic sources assumed unchanged when the
/// language server resends the same set of diagnostics. Helix can track
/// the position for these diagnostics internally instead. Useful for
/// diagnostics that are recomputed on save.
persistent_diagnostic_sources: List<String> = List::default(),
/// Minimal severity of diagnostic for it to be displayed. (Allowed
/// values: `error`, `warning`, `info`, `hint`)
diagnostic_severity: Severity = Severity::Hint,
}
struct CompletionConfig {
/// Automatic auto-completion, automatically pop up without user trigger.
#[read = copy]
auto_completion: bool = true,
/// Whether to apply completion item instantly when selected
#[read = copy]
preview_completion_insert: bool = true,
/// Whether to apply completion item instantly when selected
#[read = copy]
completion_replace: bool = false,
/// Whether to apply completion item instantly when selected
#[read = copy]
completion_trigger_len: u8 = 2,
}
}

View File

@@ -1,291 +0,0 @@
use serde::{Deserialize, Serialize};
use crate::*;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StatusLineElement {
/// The editor mode (Normal, Insert, Visual/Selection)
Mode,
/// The LSP activity spinner
Spinner,
/// The file basename (the leaf of the open file's path)
FileBaseName,
/// The relative file path
FileName,
// The file modification indicator
FileModificationIndicator,
/// An indicator that shows `"[readonly]"` when a file cannot be written
ReadOnlyIndicator,
/// The file encoding
FileEncoding,
/// The file line endings (CRLF or LF)
FileLineEnding,
/// The file type (language ID or "text")
FileType,
/// A summary of the number of errors and warnings
Diagnostics,
/// A summary of the number of errors and warnings on file and workspace
WorkspaceDiagnostics,
/// The number of selections (cursors)
Selections,
/// The number of characters currently in primary selection
PrimarySelectionLength,
/// The cursor position
Position,
/// The separator string
Separator,
/// The cursor position as a percent of the total file
PositionPercentage,
/// The total line numbers of the current file
TotalLineNumbers,
/// A single space
Spacer,
/// Current version control information
VersionControl,
/// Indicator for selected register
Register,
}
config_serde_adapter!(StatusLineElement);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
/// UNSTABLE
pub enum CursorKind {
/// █
Block,
/// |
Bar,
/// _
Underline,
/// Hidden cursor, can set cursor position with this to let IME have correct cursor position.
Hidden,
}
impl Default for CursorKind {
fn default() -> Self {
Self::Block
}
}
config_serde_adapter!(CursorKind);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WhitespaceRenderValue {
None,
// TODO
// Selection,
All,
}
config_serde_adapter!(WhitespaceRenderValue);
/// bufferline render modes
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BufferLine {
/// Don't render bufferline
Never,
/// Always render
Always,
/// Only if multiple buffers are open
Multiple,
}
config_serde_adapter!(BufferLine);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PopupBorderConfig {
None,
All,
Popup,
Menu,
}
config_serde_adapter!(PopupBorderConfig);
options! {
struct UiConfig {
/// Whether to display info boxes
#[read = copy]
auto_info: bool = true,
/// Renders a line at the top of the editor displaying open buffers.
/// Can be `always`, `never` or `multiple` (only shown if more than one
/// buffer is in use)
#[read = copy]
bufferline: BufferLine = BufferLine::Never,
/// Highlight all lines with a cursor
#[read = copy]
cursorline: bool = false,
/// Highlight all columns with a cursor
#[read = copy]
cursorcolumn: bool = false,
/// List of column positions at which to display the rulers.
#[read = deref]
rulers: List<u16> = List::default(),
/// Whether to color the mode indicator with different colors depending on the mode itself
#[read = copy]
popup_border: bool = false,
/// Whether to color the mode indicator with different colors depending on the mode itself
#[read = copy]
color_modes: bool = false,
}
struct WhiteSpaceRenderConfig {
#[name = "whitespace.characters.space"]
#[read = copy]
space_char: char = '·', // U+00B7
#[name = "whitespace.characters.nbsp"]
#[read = copy]
nbsp_char: char = '⍽', // U+237D
#[name = "whitespace.characters.tab"]
#[read = copy]
tab_char: char = '→', // U+2192
#[name = "whitespace.characters.tabpad"]
#[read = copy]
tabpad_char: char = '⏎', // U+23CE
#[name = "whitespace.characters.newline"]
#[read = copy]
newline_char: char = ' ',
#[name = "whitespace.render.default"]
#[read = copy]
render: WhitespaceRenderValue = WhitespaceRenderValue::None,
#[name = "whitespace.render.space"]
#[read = copy]
render_space: Option<WhitespaceRenderValue> = None,
#[name = "whitespace.render.nbsp"]
#[read = copy]
render_nbsp: Option<WhitespaceRenderValue> = None,
#[name = "whitespace.render.tab"]
#[read = copy]
render_tab: Option<WhitespaceRenderValue> = None,
#[name = "whitespace.render.newline"]
#[read = copy]
render_newline: Option<WhitespaceRenderValue> = None,
}
struct TerminfoConfig {
/// Set to `true` to override automatic detection of terminal truecolor
/// support in the event of a false negative
#[name = "true-color"]
#[read = copy]
force_true_color: bool = false,
/// Set to `true` to override automatic detection of terminal undercurl
/// support in the event of a false negative
#[name = "undercurl"]
#[read = copy]
force_undercurl: bool = false,
}
struct IndentGuidesConfig {
/// Whether to render indent guides
#[read = copy]
render: bool = false,
/// Character to use for rendering indent guides
#[read = copy]
character: char = '│',
/// Number of indent levels to skip
#[read = copy]
skip_levels: u8 = 0,
}
struct CursorShapeConfig {
/// Cursor shape in normal mode
#[name = "cursor-shape.normal"]
#[read = copy]
normal_mode_cursor: CursorKind = CursorKind::Block,
/// Cursor shape in select mode
#[name = "cursor-shape.select"]
#[read = copy]
select_mode_cursor: CursorKind = CursorKind::Block,
/// Cursor shape in insert mode
#[name = "cursor-shape.insert"]
#[read = copy]
insert_mode_cursor: CursorKind = CursorKind::Block,
}
struct FilePickerConfig {
/// Whether to exclude hidden files from any file pickers.
#[name = "file-picker.hidden"]
#[read = copy]
hidden: bool = true,
/// Follow symlinks instead of ignoring them
#[name = "file-picker.follow-symlinks"]
#[read = copy]
follow_symlinks: bool = true,
/// Ignore symlinks that point at files already shown in the picker
#[name = "file-picker.deduplicate-links"]
#[read = copy]
deduplicate_links: bool = true,
/// Enables reading ignore files from parent directories.
#[name = "file-picker.parents"]
#[read = copy]
parents: bool = true,
/// Enables reading `.ignore` files.
#[name = "file-picker.ignore"]
#[read = copy]
ignore: bool = true,
/// Enables reading `.gitignore` files.
#[name = "file-picker.git-ignore"]
#[read = copy]
git_ignore: bool = true,
/// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
#[name = "file-picker.git-global"]
#[read = copy]
git_global: bool = true,
/// Enables reading `.git/info/exclude` files.
#[name = "file-picker.git-exclude"]
#[read = copy]
git_exclude: bool = true,
/// Maximum Depth to recurse directories in file picker and global search.
#[name = "file-picker.max-depth"]
#[read = copy]
max_depth: Option<usize> = None,
}
struct StatusLineConfig{
/// A list of elements aligned to the left of the statusline
#[name = "statusline.left"]
#[read = deref]
left: List<StatusLineElement> = &[
StatusLineElement::Mode,
StatusLineElement::Spinner,
StatusLineElement::FileName,
StatusLineElement::ReadOnlyIndicator,
StatusLineElement::FileModificationIndicator,
],
/// A list of elements aligned to the middle of the statusline
#[name = "statusline.center"]
#[read = deref]
center: List<StatusLineElement> = List::default(),
/// A list of elements aligned to the right of the statusline
#[name = "statusline.right"]
#[read = deref]
right: List<StatusLineElement> = &[
StatusLineElement::Diagnostics,
StatusLineElement::Selections,
StatusLineElement::Register,
StatusLineElement::Position,
StatusLineElement::FileEncoding,
],
/// The character used to separate elements in the statusline
#[name = "statusline.seperator"]
#[read = deref]
seperator: String = "",
/// The text shown in the `mode` element for normal mode
#[name = "statusline.mode.normal"]
#[read = deref]
mode_indicator_normal: String = "NOR",
/// The text shown in the `mode` element for insert mode
#[name = "statusline.mode.insert"]
#[read = deref]
mode_indicator_insert: String = "INS",
/// The text shown in the `mode` element for select mode
#[name = "statusline.mode.select"]
#[read = deref]
mode_indicator_select: String = "SEL",
}
}

View File

@@ -1,10 +0,0 @@
// TOOD: move to stdx
pub fn binary_exists(binary_name: &str) -> bool {
which::which(binary_name).is_ok()
}
#[cfg(not(windows))]
pub fn env_var_is_set(env_var_name: &str) -> bool {
std::env::var_os(env_var_name).is_some()
}

View File

@@ -1,246 +0,0 @@
use std::any::Any;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::Arc;
use anyhow::bail;
use hashbrown::hash_map::Entry;
use hashbrown::HashMap;
use indexmap::IndexMap;
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
use any::ConfigData;
use convert::ty_into_value;
pub use convert::IntoTy;
pub use definition::{init_config, init_language_server_config};
pub use toml::read_toml_config;
use validator::StaticValidator;
pub use validator::{regex_str_validator, ty_validator, IntegerRangeValidator, Ty, Validator};
pub use value::{from_value, to_value, Value};
mod any;
mod convert;
mod definition;
pub mod env;
mod macros;
mod toml;
mod validator;
mod value;
pub type Guard<'a, T> = MappedRwLockReadGuard<'a, T>;
pub type Map<T> = IndexMap<Box<str>, T, ahash::RandomState>;
pub type String = Box<str>;
pub type List<T> = Box<[T]>;
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub struct OptionInfo {
pub name: Arc<str>,
pub description: Box<str>,
pub validator: Box<dyn Validator>,
pub into_value: fn(&ConfigData) -> Value,
}
#[derive(Debug)]
pub struct OptionManager {
vals: RwLock<HashMap<Arc<str>, ConfigData>>,
parent: Option<Arc<OptionManager>>,
}
impl OptionManager {
pub fn get<T: Any>(&self, option: &str) -> Guard<'_, T> {
Guard::map(self.get_data(option), ConfigData::get)
}
pub fn get_data(&self, option: &str) -> Guard<'_, ConfigData> {
let mut current_scope = self;
loop {
let lock = current_scope.vals.read();
if let Ok(res) = RwLockReadGuard::try_map(lock, |options| options.get(option)) {
return res;
}
let Some(new_scope) = current_scope.parent.as_deref() else{
unreachable!("option must be atleast defined in the global scope")
};
current_scope = new_scope;
}
}
pub fn get_deref<T: Deref + Any>(&self, option: &str) -> Guard<'_, T::Target> {
Guard::map(self.get::<T>(option), T::deref)
}
pub fn get_folded<T: Any, R>(
&self,
option: &str,
init: R,
mut fold: impl FnMut(&T, R) -> R,
) -> R {
let mut res = init;
let mut current_scope = self;
loop {
let options = current_scope.vals.read();
if let Some(option) = options.get(option).map(|val| val.get()) {
res = fold(option, res);
}
let Some(new_scope) = current_scope.parent.as_deref() else{
break
};
current_scope = new_scope;
}
res
}
pub fn get_value(
&self,
option: impl Into<Arc<str>>,
registry: &OptionRegistry,
) -> anyhow::Result<Value> {
let option: Arc<str> = option.into();
let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") };
let data = self.get_data(&option);
let val = (opt.into_value)(&data);
Ok(val)
}
pub fn create_scope(self: &Arc<OptionManager>) -> OptionManager {
OptionManager {
vals: RwLock::default(),
parent: Some(self.clone()),
}
}
pub fn set_parent_scope(&mut self, parent: Arc<OptionManager>) {
self.parent = Some(parent)
}
pub fn set_unchecked(&self, option: Arc<str>, val: ConfigData) {
self.vals.write().insert(option, val);
}
pub fn append(
&self,
option: impl Into<Arc<str>>,
val: impl Into<Value>,
registry: &OptionRegistry,
max_depth: usize,
) -> anyhow::Result<()> {
let val = val.into();
let option: Arc<str> = option.into();
let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") };
let old_data = self.get_data(&option);
let mut old = (opt.into_value)(&old_data);
old.append(val, max_depth);
let val = opt.validator.validate(old)?;
self.set_unchecked(option, val);
Ok(())
}
/// Sets the value of a config option. Returns an error if this config
/// option doesn't exist or the provided value is not valid.
pub fn set(
&self,
option: impl Into<Arc<str>>,
val: impl Into<Value>,
registry: &OptionRegistry,
) -> anyhow::Result<()> {
let option: Arc<str> = option.into();
let val = val.into();
let Some(opt) = registry.get(&option) else { bail!("unknown option {option:?}") };
let val = opt.validator.validate(val)?;
self.set_unchecked(option, val);
Ok(())
}
/// unsets an options so that its value will be read from
/// the parent scope instead
pub fn unset(&self, option: &str) {
self.vals.write().remove(option);
}
}
#[derive(Debug)]
pub struct OptionRegistry {
options: HashMap<Arc<str>, OptionInfo>,
defaults: Arc<OptionManager>,
}
impl OptionRegistry {
pub fn new() -> Self {
Self {
options: HashMap::with_capacity(1024),
defaults: Arc::new(OptionManager {
vals: RwLock::new(HashMap::with_capacity(1024)),
parent: None,
}),
}
}
pub fn register<T: IntoTy>(&mut self, name: &str, description: &str, default: T) {
self.register_with_validator(
name,
description,
default,
StaticValidator::<T::Ty> { ty: PhantomData },
);
}
pub fn register_with_validator<T: IntoTy>(
&mut self,
name: &str,
description: &str,
default: T,
validator: impl Validator,
) {
let mut name: Arc<str> = name.into();
// convert from snake case to kebab case in place without an additional
// allocation this is save since we only replace ascii with ascii in
// place std really ougth to have a function for this :/
// TODO: move to stdx as extension trait
for byte in unsafe { Arc::get_mut(&mut name).unwrap().as_bytes_mut() } {
if *byte == b'-' {
*byte = b'_';
}
}
let default = default.into_ty();
match self.options.entry(name.clone()) {
Entry::Vacant(e) => {
// make sure the validator is correct
if cfg!(debug_assertions) {
validator.validate(T::Ty::to_value(&default)).unwrap();
}
let opt = OptionInfo {
name: name.clone(),
description: description.into(),
validator: Box::new(validator),
into_value: ty_into_value::<T::Ty>,
};
e.insert(opt);
}
Entry::Occupied(ent) => {
ent.get()
.validator
.validate(T::Ty::to_value(&default))
.unwrap();
}
}
self.defaults.set_unchecked(name, ConfigData::new(default));
}
pub fn global_scope(&self) -> Arc<OptionManager> {
self.defaults.clone()
}
pub fn get(&self, name: &str) -> Option<&OptionInfo> {
self.options.get(name)
}
}
impl Default for OptionRegistry {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,130 +0,0 @@
/// This macro allows specifiying a trait of related config
/// options with a struct like syntax. From that information
/// two things are generated:
///
/// * A `init_config` function that registers the config options with the
/// `OptionRegistry` registry.
/// * A **trait** definition with an accessor for every config option that is
/// implemented for `OptionManager`.
///
/// The accessors on the trait allow convenient statically typed access to
/// config fields. The accessors return `Guard<T>` (which allows derferecning to
/// &T). Any type that implements copy can be returned as a copy instead by
/// specifying `#[read = copy]`. Collections like `List<T>` and `String` are not
/// copy However, they usually implement deref (to &[T] and &str respectively).
/// Working with the dereferneced &str/&[T] is more convenient then &String and &List<T>. The
/// accessor will return these if `#[read = deref]` is specified.
///
/// The doc comments will be retained for the accessors and also stored in the
/// option registrry for dispaly in the UI and documentation.
///
/// The name of a config option can be changed with #[name = "<name>"],
/// otherwise the name of the field is used directly. The OptionRegistry
/// automatically converts all names to kebab-case so a name attribute is only
/// required if the name is supposed to be significantly altered.
///
/// In some cases more complex validation may be necssary. In that case the
/// valiidtator can be provided with an exprission that implements the `Validator`
/// trait: `#[validator = create_validator()]`.
#[macro_export]
macro_rules! options {
(
$(use $use: ident::*;)*
$($(#[$($meta: tt)*])* struct $ident: ident {
$(
$(#[doc = $option_desc: literal])*
$(#[name = $option_name: literal])?
$(#[validator = $option_validator: expr])?
$(#[read = $($extra: tt)*])?
$option: ident: $ty: ty = $default: expr
),+$(,)?
})+
) => {
$(pub use $use::*;)*
$($(#[$($meta)*])* pub trait $ident {
$(
$(#[doc = $option_desc])*
fn $option(&self) -> $crate::options!(@ret_ty $($($extra)*)? $ty);
)+
})+
pub fn init_config(registry: &mut $crate::OptionRegistry) {
$($use::init_config(registry);)*
$($(
let name = $crate::options!(@name $option $($option_name)?);
let docs = concat!("" $(,$option_desc,)" "*);
$crate::options!(@register registry name docs $default, $ty $(,$option_validator)?);
)+)+
}
$(impl $ident for $crate::OptionManager {
$(
$(#[doc = $option_desc])*
fn $option(&self) -> $crate::options!(@ret_ty $($($extra)*)? $ty) {
let name = $crate::options!(@name $option $($option_name)?);
$crate::options!(@get $($($extra)*)? self, $ty, name)
}
)+
})+
};
(@register $registry: ident $name: ident $desc: ident $default: expr, $ty:ty) => {{
use $crate::IntoTy;
let val: $ty = $default.into_ty();
$registry.register($name, $desc, val);
}};
(@register $registry: ident $name: ident $desc: ident $default: expr, $ty:ty, $validator: expr) => {{
use $crate::IntoTy;
let val: $ty = $default.into_ty();
$registry.register_with_validator($name, $desc, val, $validator);
}};
(@name $ident: ident) => {
::std::stringify!($ident)
};
(@name $ident: ident $name: literal) => {
$name
};
(@ret_ty copy $ty: ty) => {
$ty
};
(@ret_ty map($fn: expr, $ret_ty: ty) $ty: ty) => {
$ret_ty
};
(@ret_ty fold($init: expr, $fn: expr, $ret_ty: ty) $ty: ty) => {
$ret_ty
};
(@ret_ty deref $ty: ty) => {
$crate::Guard<'_, <$ty as ::std::ops::Deref>::Target>
};
(@ret_ty $ty: ty) => {
$crate::Guard<'_, $ty>
};
(@get map($fn: expr, $ret_ty: ty) $config: ident, $ty: ty, $name: ident) => {
let val = $config.get::<$ty>($name);
$fn(val)
};
(@get fold($init: expr, $fn: expr, $ret_ty: ty) $config: ident, $ty: ty, $name: ident) => {
$config.get_folded::<$ty, $ret_ty>($name, $init, $fn)
};
(@get copy $config: ident, $ty: ty, $name: ident) => {
*$config.get::<$ty>($name)
};
(@get deref $config: ident, $ty: ty, $name: ident) => {
$config.get_deref::<$ty>($name)
};
(@get $config: ident, $ty: ty, $name: ident) => {
$config.get::<$ty>($name)
};
}
#[macro_export]
macro_rules! config_serde_adapter {
($ty: ident) => {
impl $crate::Ty for $ty {
fn to_value(&self) -> $crate::Value {
$crate::to_value(self).unwrap()
}
fn from_value(val: $crate::Value) -> ::anyhow::Result<Self> {
let val = $crate::from_value(val)?;
Ok(val)
}
}
};
}

View File

@@ -1,80 +0,0 @@
use std::ops::Deref;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::config_serde_adapter;
use crate::OptionRegistry;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
#[serde(alias = "abs")]
Absolute,
/// If focused and in normal/select mode, show relative line number to the primary cursor.
/// If unfocused or in insert mode, show absolute line number.
#[serde(alias = "rel")]
Relative,
}
config_serde_adapter!(LineNumber);
fn setup_registry() -> OptionRegistry {
let mut registry = OptionRegistry::new();
registry.register(
"scrolloff",
"Number of lines of padding around the edge of the screen when scrolling",
5usize,
);
registry.register(
"shell",
"Shell to use when running external commands",
&["sh", "-c"],
);
registry.register("mouse", "Enable mouse mode", true);
registry.register(
"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",
LineNumber::Absolute,
);
registry
}
#[test]
fn default_values() {
let registry = setup_registry();
let global_scope = registry.global_scope();
let scrolloff: usize = *global_scope.get("scrolloff");
let shell_ = global_scope.get_deref::<Box<[_]>>("shell");
let shell: &[Box<str>] = &shell_;
let mouse: bool = *global_scope.get("mouse");
let line_number: LineNumber = *global_scope.get("line-number");
assert_eq!(scrolloff, 5);
assert!(shell.iter().map(Box::deref).eq(["sh", "-c"]));
assert!(mouse);
assert_eq!(line_number, LineNumber::Absolute);
}
#[test]
fn scope_overwrite() {
let registry = setup_registry();
let global_scope = registry.global_scope();
let scope_1 = Arc::new(global_scope.create_scope());
let scope_2 = Arc::new(global_scope.create_scope());
let mut scope_3 = scope_1.create_scope();
scope_1.set("line-number", "rel", &registry).unwrap();
let line_number: LineNumber = *scope_3.get("line-number");
assert_eq!(line_number, LineNumber::Relative);
scope_3.set_parent_scope(scope_2.clone());
let line_number: LineNumber = *scope_3.get("line-number");
assert_eq!(line_number, LineNumber::Absolute);
scope_2.set("line-number", "rel", &registry).unwrap();
let line_number: LineNumber = *scope_3.get("line-number");
assert_eq!(line_number, LineNumber::Relative);
scope_2.set("line-number", "abs", &registry).unwrap();
let line_number: LineNumber = *scope_3.get("line-number");
assert_eq!(line_number, LineNumber::Absolute);
}

View File

@@ -1,69 +0,0 @@
use crate::{Map, OptionManager, OptionRegistry, Value};
/// Inserts the config declaration from a map deserialized from toml into
/// options manager. Returns an error if any of theu config options are
/// invalid The convresion may not be exactly one-to-one to retain backwards
/// compatibility
pub fn read_toml_config(
config_entries: Map<Value>,
options: &OptionManager,
registry: &OptionRegistry,
) -> anyhow::Result<()> {
let mut buf = String::new();
for (key, val) in config_entries {
if matches!(val, Value::Map(_)) {
buf.push_str(&key);
visit(&mut buf, val, options, registry)?;
buf.clear();
} else {
visit(&mut key.to_string(), val, options, registry)?;
}
}
Ok(())
}
fn visit(
path: &mut String,
val: Value,
options: &OptionManager,
registry: &OptionRegistry,
) -> anyhow::Result<()> {
match &**path {
// don't descend
"auto-format" => {
// treat as unset
if Value::Bool(true) == val {
return Ok(());
}
}
"auto-pairs" => return options.set("auto-pairs", val, registry),
"enviorment" => return options.set("enviorment", val, registry),
"config" => return options.set("config", val, registry),
"gutters" if matches!(val, Value::List(_)) => {
return options.set("gutters.layout", val, registry);
}
"gutters" if matches!(val, Value::List(_)) => {
return options.set("gutters.layout", val, registry);
}
"whitespace.render" if matches!(val, Value::String(_)) => {
return options.set("whitespace.render.default", val, registry);
}
"language-servers" => {
// merge list/map of language servers but if "only" and "except" are specified overwrite
return options.append("language-servers", val, registry, 0);
}
_ => (),
};
if let Value::Map(val) = val {
let old_path_len = path.len();
for (key, val) in val.into_iter() {
path.push('.');
path.push_str(&key);
visit(path, val, options, registry)?;
path.truncate(old_path_len);
}
Ok(())
} else {
options.set(&**path, val, registry)
}
}

View File

@@ -1,296 +0,0 @@
use std::any::{type_name, Any};
use std::error::Error;
use std::fmt::Debug;
use std::marker::PhantomData;
use anyhow::{bail, ensure, Result};
use crate::any::ConfigData;
use crate::Value;
pub trait Validator: 'static + Debug {
fn validate(&self, val: Value) -> Result<ConfigData>;
}
pub trait Ty: Sized + Clone + 'static {
fn from_value(val: Value) -> Result<Self>;
fn to_value(&self) -> Value;
}
#[derive(Clone, Copy)]
pub struct IntegerRangeValidator<T> {
pub min: isize,
pub max: isize,
ty: PhantomData<T>,
}
impl<E, T> IntegerRangeValidator<T>
where
E: Debug,
T: TryInto<isize, Error = E>,
{
pub fn new(min: T, max: T) -> Self {
Self {
min: min.try_into().unwrap(),
max: max.try_into().unwrap(),
ty: PhantomData,
}
}
}
impl<T> Debug for IntegerRangeValidator<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IntegerRangeValidator")
.field("min", &self.min)
.field("max", &self.max)
.field("ty", &type_name::<T>())
.finish()
}
}
impl<E, T> IntegerRangeValidator<T>
where
E: Error + Sync + Send + 'static,
T: Any + TryFrom<isize, Error = E>,
{
pub fn validate(&self, val: Value) -> Result<T> {
let IntegerRangeValidator { min, max, .. } = *self;
let Value::Int(val) = val else {
bail!("expected an integer")
};
ensure!(
min <= val && val <= max,
"expected an integer between {min} and {max} (got {val})",
);
Ok(T::try_from(val)?)
}
}
impl<E, T> Validator for IntegerRangeValidator<T>
where
E: Error + Sync + Send + 'static,
T: Any + TryFrom<isize, Error = E>,
{
fn validate(&self, val: Value) -> Result<ConfigData> {
Ok(ConfigData::new(self.validate(val)))
}
}
macro_rules! integer_tys {
($($ty: ident),*) => {
$(
impl Ty for $ty {
fn to_value(&self) -> Value {
Value::Int((*self).try_into().unwrap())
}
fn from_value(val: Value) -> Result<Self> {
IntegerRangeValidator::new($ty::MIN, $ty::MAX).validate(val)
}
}
)*
};
}
integer_tys! {
i8, i16, i32, isize,
u8, u16, u32
}
impl Ty for usize {
fn to_value(&self) -> Value {
Value::Int((*self).try_into().unwrap())
}
fn from_value(val: Value) -> Result<Self> {
IntegerRangeValidator::new(0usize, isize::MAX as usize).validate(val)
}
}
impl Ty for u64 {
fn to_value(&self) -> Value {
Value::Int((*self).try_into().unwrap())
}
fn from_value(val: Value) -> Result<Self> {
IntegerRangeValidator::new(0u64, isize::MAX as u64).validate(val)
}
}
impl Ty for bool {
fn to_value(&self) -> Value {
Value::Bool(*self)
}
fn from_value(val: Value) -> Result<Self> {
let Value::Bool(val) = val else {
bail!("expected a boolean")
};
Ok(val)
}
}
impl Ty for Box<str> {
fn to_value(&self) -> Value {
Value::String(self.clone().into_string())
}
fn from_value(val: Value) -> Result<Self> {
let Value::String(val) = val else {
bail!("expected a string")
};
Ok(val.into_boxed_str())
}
}
impl Ty for char {
fn to_value(&self) -> Value {
Value::String(self.to_string())
}
fn from_value(val: Value) -> Result<Self> {
let Value::String(val) = val else {
bail!("expected a string")
};
ensure!(
val.chars().count() == 1,
"expecet a single character (got {val:?})"
);
Ok(val.chars().next().unwrap())
}
}
impl Ty for std::string::String {
fn to_value(&self) -> Value {
Value::String(self.clone())
}
fn from_value(val: Value) -> Result<Self> {
let Value::String(val) = val else {
bail!("expected a string")
};
Ok(val)
}
}
impl<T: Ty> Ty for Option<T> {
fn to_value(&self) -> Value {
match self {
Some(_) => todo!(),
None => todo!(),
}
}
fn from_value(val: Value) -> Result<Self> {
if val == Value::Null {
return Ok(None);
}
Ok(Some(T::from_value(val)?))
}
}
impl<T: Ty> Ty for Box<T> {
fn from_value(val: Value) -> Result<Self> {
Ok(Box::new(T::from_value(val)?))
}
fn to_value(&self) -> Value {
T::to_value(self)
}
}
impl<T: Ty> Ty for indexmap::IndexMap<Box<str>, T, ahash::RandomState> {
fn from_value(val: Value) -> Result<Self> {
let Value::Map(map) = val else {
bail!("expected a map");
};
map.into_iter()
.map(|(k, v)| Ok((k, T::from_value(v)?)))
.collect()
}
fn to_value(&self) -> Value {
let map = self
.iter()
.map(|(k, v)| (k.clone(), v.to_value()))
.collect();
Value::Map(Box::new(map))
}
}
impl<T: Ty> Ty for Box<[T]> {
fn to_value(&self) -> Value {
Value::List(self.iter().map(T::to_value).collect())
}
fn from_value(val: Value) -> Result<Self> {
let Value::List(val) = val else {
bail!("expected a list")
};
val.iter().cloned().map(T::from_value).collect()
}
}
impl Ty for serde_json::Value {
fn from_value(val: Value) -> Result<Self> {
Ok(val.into())
}
fn to_value(&self) -> Value {
self.into()
}
}
pub(super) struct StaticValidator<T: Ty> {
pub(super) ty: PhantomData<fn(&T)>,
}
impl<T: Ty> Debug for StaticValidator<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticValidator")
.field("ty", &type_name::<T>())
.finish()
}
}
impl<T: Ty> Validator for StaticValidator<T> {
fn validate(&self, val: Value) -> Result<ConfigData> {
let val = <T as Ty>::from_value(val)?;
Ok(ConfigData::new(val))
}
}
pub struct TyValidator<F, T: Ty> {
pub(super) ty: PhantomData<fn(&T)>,
f: F,
}
impl<T: Ty, F> Debug for TyValidator<F, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TyValidator")
.field("ty", &type_name::<T>())
.finish()
}
}
impl<T, F> Validator for TyValidator<F, T>
where
T: Ty,
F: Fn(&T) -> anyhow::Result<()> + 'static,
{
fn validate(&self, val: Value) -> Result<ConfigData> {
let val = <T as Ty>::from_value(val)?;
(self.f)(&val)?;
Ok(ConfigData::new(val))
}
}
pub fn ty_validator<T, F>(f: F) -> impl Validator
where
T: Ty,
F: Fn(&T) -> anyhow::Result<()> + 'static,
{
TyValidator { ty: PhantomData, f }
}
pub fn regex_str_validator() -> impl Validator {
ty_validator(|val: &crate::String| {
regex_syntax::parse(val)?;
Ok(())
})
}

View File

@@ -1,448 +0,0 @@
use std::fmt::Display;
use indexmap::IndexMap;
use serde::de::DeserializeOwned;
use serde::ser::{Error as _, Impossible};
use serde::{Deserialize, Serialize};
use serde_json::{Error, Result};
use crate::Ty;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
List(Vec<Value>),
Map(Box<IndexMap<Box<str>, Value, ahash::RandomState>>),
Int(isize),
Float(f64),
Bool(bool),
String(String),
Null,
}
impl Value {
pub fn typed<T: Ty>(self) -> anyhow::Result<T> {
T::from_value(self)
}
pub fn append(&mut self, val: Value, depth: usize) {
match (self, val) {
(Value::List(dst), Value::List(ref mut val)) => dst.append(val),
(Value::Map(dst), Value::Map(val)) if depth == 0 || dst.is_empty() => {
dst.extend(val.into_iter())
}
(Value::Map(dst), Value::Map(val)) => {
dst.reserve(val.len());
for (k, v) in val.into_iter() {
// we don't use the entry api because we want
// to maintain thhe ordering
let merged = match dst.shift_remove(&k) {
Some(mut old) => {
old.append(v, depth - 1);
old
}
None => v,
};
dst.insert(k, merged);
}
}
(dst, val) => *dst = val,
}
}
}
impl From<&str> for Value {
fn from(value: &str) -> Self {
Value::String(value.to_owned())
}
}
macro_rules! from_int {
($($ty: ident),*) => {
$(
impl From<$ty> for Value {
fn from(value: $ty) -> Self {
Value::Int(value.try_into().unwrap())
}
}
)*
};
}
impl From<serde_json::Value> for Value {
fn from(value: serde_json::Value) -> Self {
to_value(value).unwrap()
}
}
impl From<&serde_json::Value> for Value {
fn from(value: &serde_json::Value) -> Self {
to_value(value).unwrap()
}
}
impl From<Value> for serde_json::Value {
fn from(value: Value) -> Self {
serde_json::to_value(value).unwrap()
}
}
from_int!(isize, usize, u32, i32, i16, u16, i8, u8);
pub fn to_value<T>(value: T) -> Result<Value>
where
T: Serialize,
{
value.serialize(Serializer)
}
pub fn from_value<T>(value: Value) -> Result<T>
where
T: DeserializeOwned,
{
// roundtripping trough json is very inefficient *and incorrect* (captures
// json semantics that don't apply to us)
// TODO: implement a custom deserializer just like serde_json does
serde_json::from_value(value.into())
}
// We only use our own error type; no need for From conversions provided by the
// standard library's try! macro. This reduces lines of LLVM IR by 4%.
macro_rules! tri {
($e:expr $(,)?) => {
match $e {
core::result::Result::Ok(val) => val,
core::result::Result::Err(err) => return core::result::Result::Err(err),
}
};
}
/// Serializer whose output is a `Value`.
///
/// This is the serializer that backs [`serde_json::to_value`][crate::to_value].
/// Unlike the main serde_json serializer which goes from some serializable
/// value of type `T` to JSON text, this one goes from `T` to
/// `serde_json::Value`.
///
/// The `to_value` function is implementable as:
///
/// ```
/// use serde::Serialize;
/// use serde_json::{Error, Value};
///
/// pub fn to_value<T>(input: T) -> Result<Value, Error>
/// where
/// T: Serialize,
/// {
/// input.serialize(serde_json::value::Serializer)
/// }
/// ```
pub struct Serializer;
impl serde::Serializer for Serializer {
type Ok = Value;
type Error = Error;
type SerializeSeq = SerializeVec;
type SerializeTuple = SerializeVec;
type SerializeTupleStruct = SerializeVec;
type SerializeTupleVariant = Impossible<Value, Error>;
type SerializeMap = SerializeMap;
type SerializeStruct = SerializeMap;
type SerializeStructVariant = Impossible<Value, Error>;
#[inline]
fn serialize_bool(self, value: bool) -> Result<Value> {
Ok(Value::Bool(value))
}
#[inline]
fn serialize_i8(self, value: i8) -> Result<Value> {
self.serialize_i64(value as i64)
}
#[inline]
fn serialize_i16(self, value: i16) -> Result<Value> {
self.serialize_i64(value as i64)
}
#[inline]
fn serialize_i32(self, value: i32) -> Result<Value> {
self.serialize_i64(value as i64)
}
fn serialize_i64(self, value: i64) -> Result<Value> {
Ok(Value::Int(value.try_into().unwrap()))
}
fn serialize_i128(self, _value: i128) -> Result<Value> {
unreachable!()
}
#[inline]
fn serialize_u8(self, value: u8) -> Result<Value> {
self.serialize_u64(value as u64)
}
#[inline]
fn serialize_u16(self, value: u16) -> Result<Value> {
self.serialize_u64(value as u64)
}
#[inline]
fn serialize_u32(self, value: u32) -> Result<Value> {
self.serialize_u64(value as u64)
}
#[inline]
fn serialize_u64(self, value: u64) -> Result<Value> {
Ok(Value::Int(value.try_into().unwrap()))
}
fn serialize_u128(self, _value: u128) -> Result<Value> {
unreachable!()
}
#[inline]
fn serialize_f32(self, float: f32) -> Result<Value> {
Ok(Value::Float(float as f64))
}
#[inline]
fn serialize_f64(self, float: f64) -> Result<Value> {
Ok(Value::Float(float))
}
#[inline]
fn serialize_char(self, value: char) -> Result<Value> {
let mut s = String::new();
s.push(value);
Ok(Value::String(s))
}
#[inline]
fn serialize_str(self, value: &str) -> Result<Value> {
Ok(Value::String(value.into()))
}
fn serialize_bytes(self, value: &[u8]) -> Result<Value> {
let vec = value.iter().map(|&b| Value::Int(b.into())).collect();
Ok(Value::List(vec))
}
#[inline]
fn serialize_unit(self) -> Result<Value> {
Ok(Value::Null)
}
#[inline]
fn serialize_unit_struct(self, _name: &'static str) -> Result<Value> {
unimplemented!()
}
#[inline]
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<Value> {
self.serialize_str(variant)
}
#[inline]
fn serialize_newtype_struct<T>(self, _name: &'static str, _value: &T) -> Result<Value>
where
T: ?Sized + Serialize,
{
unimplemented!()
}
fn serialize_newtype_variant<T>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_value: &T,
) -> Result<Value>
where
T: ?Sized + Serialize,
{
unimplemented!()
}
#[inline]
fn serialize_none(self) -> Result<Value> {
self.serialize_unit()
}
#[inline]
fn serialize_some<T>(self, value: &T) -> Result<Value>
where
T: ?Sized + Serialize,
{
value.serialize(self)
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
Ok(SerializeVec {
vec: Vec::with_capacity(len.unwrap_or(0)),
})
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_struct(
self,
_name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant> {
unimplemented!()
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
Ok(SerializeMap {
map: IndexMap::default(),
next_key: None,
})
}
fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
unreachable!()
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant> {
unreachable!()
}
fn collect_str<T>(self, value: &T) -> Result<Value>
where
T: ?Sized + Display,
{
Ok(Value::String(value.to_string()))
}
}
pub struct SerializeVec {
vec: Vec<Value>,
}
impl serde::ser::SerializeSeq for SerializeVec {
type Ok = Value;
type Error = Error;
fn serialize_element<T>(&mut self, value: &T) -> Result<()>
where
T: ?Sized + Serialize,
{
self.vec.push(tri!(to_value(value)));
Ok(())
}
fn end(self) -> Result<Value> {
Ok(Value::List(self.vec))
}
}
impl serde::ser::SerializeTuple for SerializeVec {
type Ok = Value;
type Error = Error;
fn serialize_element<T>(&mut self, value: &T) -> Result<()>
where
T: ?Sized + Serialize,
{
serde::ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<Value> {
serde::ser::SerializeSeq::end(self)
}
}
impl serde::ser::SerializeTupleStruct for SerializeVec {
type Ok = Value;
type Error = Error;
fn serialize_field<T>(&mut self, value: &T) -> Result<()>
where
T: ?Sized + Serialize,
{
serde::ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<Value> {
serde::ser::SerializeSeq::end(self)
}
}
pub struct SerializeMap {
map: IndexMap<Box<str>, Value, ahash::RandomState>,
next_key: Option<Box<str>>,
}
impl serde::ser::SerializeMap for SerializeMap {
type Ok = Value;
type Error = Error;
fn serialize_key<T>(&mut self, key: &T) -> Result<()>
where
T: ?Sized + Serialize,
{
let key = to_value(key)?;
let Value::String(val) = key else {
return Err(Error::custom("only string keys are supported"));
};
self.next_key = Some(val.into_boxed_str());
Ok(())
}
fn serialize_value<T>(&mut self, value: &T) -> Result<()>
where
T: ?Sized + Serialize,
{
let key = self.next_key.take();
// Panic because this indicates a bug in the program rather than an
// expected failure.
let key = key.expect("serialize_value called before serialize_key");
self.map.insert(key, tri!(to_value(value)));
Ok(())
}
fn end(self) -> Result<Value> {
Ok(Value::Map(Box::new(self.map)))
}
}
impl serde::ser::SerializeStruct for SerializeMap {
type Ok = Value;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
where
T: ?Sized + Serialize,
{
serde::ser::SerializeMap::serialize_entry(self, key, value)
}
fn end(self) -> Result<Value> {
serde::ser::SerializeMap::end(self)
}
}

View File

@@ -1,60 +1,43 @@
[package]
name = "helix-core"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
description = "Helix editor core editing primitives"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
categories.workspace = true
repository.workspace = true
homepage.workspace = true
[features]
unicode-lines = ["ropey/unicode_lines"]
integration = []
[dependencies]
helix-loader = { path = "../helix-loader" }
helix-config = { path = "../helix-config" }
helix-syntax = { version = "0.6", path = "../helix-syntax" }
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
smallvec = "1.11"
smartstring = "1.0.1"
unicode-segmentation = "1.10"
ropey = "1.3"
smallvec = "1.7"
tendril = "0.4.2"
unicode-segmentation = "1.8"
unicode-width = "0.1"
unicode-general-category = "0.6"
unicode-general-category = "0.4"
# slab = "0.4.2"
slotmap = "1.0"
tree-sitter.workspace = true
once_cell = "1.19"
tree-sitter = "0.20"
once_cell = "1.9"
arc-swap = "1"
regex = "1"
bitflags = "2.4"
ahash = "0.8.6"
hashbrown = { version = "0.14.3", features = ["raw"] }
dunce = "1.0"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.7"
toml = "0.5"
imara-diff = "0.1.0"
similar = "2.1"
etcetera = "0.3"
encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
etcetera = "0.8"
textwrap = "0.16.0"
nucleo.workspace = true
parking_lot = "0.12"
anyhow = "1.0.79"
indexmap = { version = "2.1.0", features = ["serde"] }
[dev-dependencies]
quickcheck = { version = "1", default-features = false }
indoc = "2.0.4"

View File

@@ -1,15 +1,13 @@
//! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character.
use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
use anyhow::{bail, ensure};
use helix_config::options;
use indexmap::IndexMap;
use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction};
use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
pub const DEFAULT_PAIRS: &[(char, char)] = &[
pub const PAIRS: &[(char, char)] = &[
('(', ')'),
('{', '}'),
('[', ']'),
@@ -18,95 +16,9 @@ pub const DEFAULT_PAIRS: &[(char, char)] = &[
('`', '`'),
];
/// The type that represents the collection of auto pairs,
/// keyed by both opener and closer.
#[derive(Debug, Clone)]
pub struct AutoPairs(IndexMap<char, Pair, ahash::RandomState>);
/// Represents the config for a particular pairing.
#[derive(Debug, Clone, Copy)]
pub struct Pair {
pub open: char,
pub close: char,
}
impl Pair {
/// true if open == close
pub fn same(&self) -> bool {
self.open == self.close
}
/// true if all of the pair's conditions hold for the given document and range
pub fn should_close(&self, doc: &Rope, range: &Range) -> bool {
let mut should_close = Self::next_is_not_alpha(doc, range);
if self.same() {
should_close &= Self::prev_is_not_alpha(doc, range);
}
should_close
}
pub fn next_is_not_alpha(doc: &Rope, range: &Range) -> bool {
let cursor = range.cursor(doc.slice(..));
let next_char = doc.get_char(cursor);
next_char.map(|c| !c.is_alphanumeric()).unwrap_or(true)
}
pub fn prev_is_not_alpha(doc: &Rope, range: &Range) -> bool {
let cursor = range.cursor(doc.slice(..));
let prev_char = prev_char(doc, cursor);
prev_char.map(|c| !c.is_alphanumeric()).unwrap_or(true)
}
}
impl From<&(char, char)> for Pair {
fn from(&(open, close): &(char, char)) -> Self {
Self { open, close }
}
}
impl From<(&char, &char)> for Pair {
fn from((open, close): (&char, &char)) -> Self {
Self {
open: *open,
close: *close,
}
}
}
impl AutoPairs {
/// Make a new AutoPairs set with the given pairs and default conditions.
pub fn new<'a, V: 'a, A>(pairs: V) -> anyhow::Result<Self>
where
V: IntoIterator<Item = anyhow::Result<A>>,
A: Into<Pair>,
{
let mut auto_pairs = IndexMap::default();
for pair in pairs.into_iter() {
let auto_pair = pair?.into();
auto_pairs.insert(auto_pair.open, auto_pair);
if auto_pair.open != auto_pair.close {
auto_pairs.insert(auto_pair.close, auto_pair);
}
}
Ok(Self(auto_pairs))
}
pub fn get(&self, ch: char) -> Option<&Pair> {
self.0.get(&ch)
}
}
impl Default for AutoPairs {
fn default() -> Self {
AutoPairs::new(DEFAULT_PAIRS.iter().map(Ok)).unwrap()
}
}
// [TODO] build this dynamically in language config. see #992
const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
@@ -122,17 +34,21 @@ impl Default for AutoPairs {
// middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
log::trace!("autopairs hook selection: {:#?}", selection);
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
debug!("autopairs hook selection: {:#?}", selection);
if let Some(pair) = pairs.get(ch) {
if pair.same() {
return Some(handle_same(doc, selection, pair));
} else if pair.open == ch {
return Some(handle_open(doc, selection, pair));
} else if pair.close == ch {
for &(open, close) in PAIRS {
if open == ch {
if open == close {
return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
} else {
return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
}
}
if close == ch {
// && char_at pos == close
return Some(handle_close(doc, selection, pair));
return Some(handle_close(doc, selection, open, close));
}
}
@@ -148,123 +64,42 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
}
/// calculate what the resulting range should be for an auto pair insertion
fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted: usize) -> Range {
// When the character under the cursor changes due to complete pair
// insertion, we must look backward a grapheme and then add the length
// of the insertion to put the resulting cursor in the right place, e.g.
//
// foo[\r\n] - anchor: 3, head: 5
// foo([)]\r\n - anchor: 4, head: 5
//
// foo[\r\n] - anchor: 3, head: 5
// foo'[\r\n] - anchor: 4, head: 6
//
// foo([)]\r\n - anchor: 4, head: 5
// foo()[\r\n] - anchor: 5, head: 7
//
// [foo]\r\n - anchor: 0, head: 3
// [foo(])\r\n - anchor: 0, head: 5
// inserting at the very end of the document after the last newline
if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
return Range::new(
start_range.anchor + offset + 1,
start_range.head + offset + 1,
);
}
let doc_slice = doc.slice(..);
let single_grapheme = start_range.is_single_grapheme(doc_slice);
// just skip over graphemes
if len_inserted == 0 {
let end_anchor = if single_grapheme {
graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
// even for backward inserts with multiple grapheme selections,
// we want the anchor to stay where it is so that the relative
// selection does not change, e.g.:
//
// foo([) wor]d -> insert ) -> foo()[ wor]d
} else {
start_range.anchor + offset
};
return Range::new(
end_anchor,
graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
);
}
// trivial case: only inserted a single-char opener, just move the selection
if len_inserted == 1 {
let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
start_range.anchor + offset + 1
} else {
start_range.anchor + offset
};
return Range::new(end_anchor, start_range.head + offset + 1);
}
// If the head = 0, then we must be in insert mode with a backward
// cursor, which implies the head will just move
let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
start_range.head + offset + 1
} else {
// We must have a forward cursor, which means we must move to the
// other end of the grapheme to get to where the new characters
// are inserted, then move the head to where it should be
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
log::trace!(
"prev_bound: {}, offset: {}, len_inserted: {}",
prev_bound,
offset,
len_inserted
);
prev_bound + offset + len_inserted
};
fn get_next_range(
start_range: &Range,
offset: usize,
typed_char: char,
len_inserted: usize,
) -> Range {
let end_head = start_range.head + offset + typed_char.len_utf8();
let end_anchor = match (start_range.len(), start_range.direction()) {
// if we have a zero width cursor, it shifts to the same number
(0, _) => end_head,
// If we are inserting for a regular one-width cursor, the anchor
// moves with the head. This is the fast path for ASCII.
// if we are inserting for a regular one-width cursor, the anchor
// moves with the head
(1, Direction::Forward) => end_head - 1,
(1, Direction::Backward) => end_head + 1,
(_, Direction::Forward) => {
if single_grapheme {
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head) + 1
// if we are appending, the anchor stays where it is; only offset
// for multiple range insertions
} else {
start_range.anchor + offset
}
}
(_, Direction::Forward) => start_range.anchor + offset,
(_, Direction::Backward) => {
if single_grapheme {
// if we're backward, then the head is at the first char
// of the typed char, so we need to add the length of
// the closing char
graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor)
+ len_inserted
+ offset
} else {
// when we are inserting in front of a selection, we need to move
// the anchor over by however many characters were inserted overall
start_range.anchor + offset + len_inserted
}
}
(_, Direction::Backward) => start_range.anchor + offset + len_inserted,
};
Range::new(end_anchor, end_head)
}
fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
fn handle_open(
doc: &Rope,
selection: &Selection,
open: char,
close: char,
close_before: &str,
) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
@@ -273,25 +108,21 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let next_char = doc.get_char(cursor);
let len_inserted;
// Since auto pairs are currently limited to single chars, we're either
// inserting exactly one or two chars. When arbitrary length pairs are
// added, these will need to be changed.
let change = match next_char {
Some(_) if !pair.should_close(doc, start_range) => {
len_inserted = 1;
let mut tendril = Tendril::new();
tendril.push(pair.open);
(cursor, cursor, Some(tendril))
Some(ch) if !close_before.contains(ch) => {
len_inserted = open.len_utf8();
(cursor, cursor, Some(Tendril::from_char(open)))
}
// None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
let pair_str = Tendril::from_iter([pair.open, pair.close]);
len_inserted = 2;
(cursor, cursor, Some(pair_str))
let pair = Tendril::from_iter([open, close]);
len_inserted = open.len_utf8() + close.len_utf8();
(cursor, cursor, Some(pair))
}
};
let next_range = get_next_range(doc, start_range, offs, len_inserted);
let next_range = get_next_range(start_range, offs, open, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -299,12 +130,13 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
log::debug!("auto pair transaction: {:#?}", t);
debug!("auto pair transaction: {:#?}", t);
t
}
fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
@@ -312,17 +144,15 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let next_char = doc.get_char(cursor);
let mut len_inserted = 0;
let change = if next_char == Some(pair.close) {
let change = if next_char == Some(close) {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
len_inserted = 1;
let mut tendril = Tendril::new();
tendril.push(pair.close);
(cursor, cursor, Some(tendril))
len_inserted += close.len_utf8();
(cursor, cursor, Some(Tendril::from_char(close)))
};
let next_range = get_next_range(doc, start_range, offs, len_inserted);
let next_range = get_next_range(start_range, offs, close, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -330,12 +160,18 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
log::debug!("auto pair transaction: {:#?}", t);
debug!("auto pair transaction: {:#?}", t);
t
}
/// handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
fn handle_same(
doc: &Rope,
selection: &Selection,
token: char,
close_before: &str,
open_before: &str,
) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
@@ -343,26 +179,30 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let cursor = start_range.cursor(doc.slice(..));
let mut len_inserted = 0;
let next_char = doc.get_char(cursor);
let change = if next_char == Some(pair.open) {
let next_char = doc.get_char(cursor);
let prev_char = prev_char(doc, cursor);
let change = if next_char == Some(token) {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
let mut pair_str = Tendril::new();
pair_str.push(pair.open);
let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
pair.push_char(token);
// for equal pairs, don't insert both open and close if either
// side has a non-pair char
if pair.should_close(doc, start_range) {
pair_str.push(pair.close);
if (next_char.is_none() || close_before.contains(next_char.unwrap()))
&& (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
{
pair.push_char(token);
}
len_inserted += pair_str.chars().count();
(cursor, cursor, Some(pair_str))
len_inserted += pair.len();
(cursor, cursor, Some(pair))
};
let next_range = get_next_range(doc, start_range, offs, len_inserted);
let next_range = get_next_range(start_range, offs, token, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -370,46 +210,400 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
log::debug!("auto pair transaction: {:#?}", t);
debug!("auto pair transaction: {:#?}", t);
t
}
options! {
struct AutopairConfig {
/// Mapping of character pairs like `{ '(' = ')', '`' = '`' }` that are
/// automatically closed by the editor when typed.
auto_pairs: AutoPairs = AutoPairs::default(),
}
}
#[cfg(test)]
mod test {
use super::*;
use smallvec::smallvec;
impl helix_config::Ty for AutoPairs {
fn from_value(val: helix_config::Value) -> anyhow::Result<Self> {
let map = match val {
helix_config::Value::Map(map) => map,
helix_config::Value::Bool(false) => return Ok(Self(IndexMap::default())),
_ => bail!("expect 'false' or a map of pairs"),
};
let pairs = map.into_iter().map(|(open, close)| {
let open = helix_config::Value::String(open.into_string());
Ok(Pair {
open: open.typed()?,
close: close.typed()?,
})
});
AutoPairs::new(pairs)
fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
PAIRS.iter().filter(|(open, close)| open != close)
}
fn to_value(&self) -> helix_config::Value {
let map = self
.0
.values()
.map(|pair| {
(
pair.open.to_string().into(),
helix_config::Value::String(pair.close.into()),
fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
PAIRS.iter().filter(|(open, close)| open == close)
}
fn test_hooks(
in_doc: &Rope,
in_sel: &Selection,
ch: char,
expected_doc: &Rope,
expected_sel: &Selection,
) {
let trans = hook(&in_doc, &in_sel, ch).unwrap();
let mut actual_doc = in_doc.clone();
assert!(trans.apply(&mut actual_doc));
assert_eq!(expected_doc, &actual_doc);
assert_eq!(expected_sel, trans.selection().unwrap());
}
fn test_hooks_with_pairs<I, F, R>(
in_doc: &Rope,
in_sel: &Selection,
pairs: I,
get_expected_doc: F,
actual_sel: &Selection,
) where
I: IntoIterator<Item = &'static (char, char)>,
F: Fn(char, char) -> R,
R: Into<Rope>,
Rope: From<R>,
{
pairs.into_iter().for_each(|(open, close)| {
test_hooks(
in_doc,
in_sel,
*open,
&Rope::from(get_expected_doc(*open, *close)),
actual_sel,
)
})
.collect();
helix_config::Value::Map(Box::new(map))
});
}
// [] indicates range
/// [] -> insert ( -> ([])
#[test]
fn test_insert_blank() {
test_hooks_with_pairs(
&Rope::new(),
&Selection::single(1, 0),
PAIRS,
|open, close| format!("{}{}", open, close),
&Selection::single(2, 1),
);
}
/// [] -> append ( -> ([])
#[test]
fn test_append_blank() {
test_hooks_with_pairs(
// this is what happens when you have a totally blank document and then append
&Rope::from("\n\n"),
&Selection::single(0, 2),
PAIRS,
|open, close| format!("\n{}{}\n", open, close),
&Selection::single(0, 3),
);
}
/// [] ([])
/// [] -> insert -> ([])
/// [] ([])
#[test]
fn test_insert_blank_multi_cursor() {
test_hooks_with_pairs(
&Rope::from("\n\n\n"),
&Selection::new(
smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
0,
),
PAIRS,
|open, close| {
format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
)
},
&Selection::new(
smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
0,
),
);
}
/// fo[o] -> append ( -> fo[o(])
#[test]
fn test_append() {
test_hooks_with_pairs(
&Rope::from("foo\n"),
&Selection::single(2, 4),
differing_pairs(),
|open, close| format!("foo{}{}\n", open, close),
&Selection::single(2, 5),
);
}
/// fo[o] fo[o(])
/// fo[o] -> append ( -> fo[o(])
/// fo[o] fo[o(])
#[test]
fn test_append_multi() {
test_hooks_with_pairs(
&Rope::from("foo\nfoo\nfoo\n"),
&Selection::new(
smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)),
0,
),
differing_pairs(),
|open, close| {
format!(
"foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
open = open,
close = close
)
},
&Selection::new(
smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)),
0,
),
);
}
/// ([]) -> insert ) -> ()[]
#[test]
fn test_insert_close_inside_pair() {
for (open, close) in PAIRS {
let doc = Rope::from(format!("{}{}", open, close));
test_hooks(
&doc,
&Selection::single(2, 1),
*close,
&doc,
&Selection::single(3, 2),
);
}
}
/// [(]) -> append ) -> [()]
#[test]
fn test_append_close_inside_pair() {
for (open, close) in PAIRS {
let doc = Rope::from(format!("{}{}\n", open, close));
test_hooks(
&doc,
&Selection::single(0, 2),
*close,
&doc,
&Selection::single(0, 3),
);
}
}
/// ([]) ()[]
/// ([]) -> insert ) -> ()[]
/// ([]) ()[]
#[test]
fn test_insert_close_inside_pair_multi_cursor() {
let sel = Selection::new(
smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
0,
);
let expected_sel = Selection::new(
smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
0,
);
for (open, close) in PAIRS {
let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
));
test_hooks(&doc, &sel, *close, &doc, &expected_sel);
}
}
/// [(]) [()]
/// [(]) -> append ) -> [()]
/// [(]) [()]
#[test]
fn test_append_close_inside_pair_multi_cursor() {
let sel = Selection::new(
smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),),
0,
);
let expected_sel = Selection::new(
smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),),
0,
);
for (open, close) in PAIRS {
let doc = Rope::from(format!(
"{open}{close}\n{open}{close}\n{open}{close}\n",
open = open,
close = close
));
test_hooks(&doc, &sel, *close, &doc, &expected_sel);
}
}
/// ([]) -> insert ( -> (([]))
#[test]
fn test_insert_open_inside_pair() {
let sel = Selection::single(2, 1);
let expected_sel = Selection::single(3, 2);
for (open, close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", open, close));
let expected_doc = Rope::from(format!(
"{open}{open}{close}{close}",
open = open,
close = close
));
test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
}
}
/// [word(]) -> append ( -> [word((]))
#[test]
fn test_append_open_inside_pair() {
let sel = Selection::single(0, 6);
let expected_sel = Selection::single(0, 7);
for (open, close) in differing_pairs() {
let doc = Rope::from(format!("word{}{}", open, close));
let expected_doc = Rope::from(format!(
"word{open}{open}{close}{close}",
open = open,
close = close
));
test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
}
}
/// ([]) -> insert " -> ("[]")
#[test]
fn test_insert_nested_open_inside_pair() {
let sel = Selection::single(2, 1);
let expected_sel = Selection::single(3, 2);
for (outer_open, outer_close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
for (inner_open, inner_close) in matching_pairs() {
let expected_doc = Rope::from(format!(
"{}{}{}{}",
outer_open, inner_open, inner_close, outer_close
));
test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
}
}
}
/// [(]) -> append " -> [("]")
#[test]
fn test_append_nested_open_inside_pair() {
let sel = Selection::single(0, 2);
let expected_sel = Selection::single(0, 3);
for (outer_open, outer_close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
for (inner_open, inner_close) in matching_pairs() {
let expected_doc = Rope::from(format!(
"{}{}{}{}",
outer_open, inner_open, inner_close, outer_close
));
test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
}
}
}
/// []word -> insert ( -> ([]word
#[test]
fn test_insert_open_before_non_pair() {
test_hooks_with_pairs(
&Rope::from("word"),
&Selection::single(1, 0),
PAIRS,
|open, _| format!("{}word", open),
&Selection::single(2, 1),
)
}
/// [wor]d -> insert ( -> ([wor]d
#[test]
fn test_insert_open_with_selection() {
test_hooks_with_pairs(
&Rope::from("word"),
&Selection::single(3, 0),
PAIRS,
|open, _| format!("{}word", open),
&Selection::single(4, 1),
)
}
/// [wor]d -> append ) -> [wor)]d
#[test]
fn test_append_close_inside_non_pair_with_selection() {
let sel = Selection::single(0, 4);
let expected_sel = Selection::single(0, 5);
for (_, close) in PAIRS {
let doc = Rope::from("word");
let expected_doc = Rope::from(format!("wor{}d", close));
test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
}
}
/// foo[ wor]d -> insert ( -> foo([) wor]d
#[test]
fn test_insert_open_trailing_word_with_selection() {
test_hooks_with_pairs(
&Rope::from("foo word"),
&Selection::single(7, 3),
differing_pairs(),
|open, close| format!("foo{}{} word", open, close),
&Selection::single(9, 4),
)
}
/// we want pairs that are *not* the same char to be inserted after
/// a non-pair char, for cases like functions, but for pairs that are
/// the same char, we want to *not* insert a pair to handle cases like "I'm"
///
/// word[] -> insert ( -> word([])
/// word[] -> insert ' -> word'[]
#[test]
fn test_insert_open_after_non_pair() {
let doc = Rope::from("word");
let sel = Selection::single(5, 4);
let expected_sel = Selection::single(6, 5);
test_hooks_with_pairs(
&doc,
&sel,
differing_pairs(),
|open, close| format!("word{}{}", open, close),
&expected_sel,
);
test_hooks_with_pairs(
&doc,
&sel,
matching_pairs(),
|open, _| format!("word{}", open),
&expected_sel,
);
}
/// appending with only a cursor should stay a cursor
///
/// [] -> append to end "foo -> "foo[]"
#[test]
fn test_append_single_cursor() {
test_hooks_with_pairs(
&Rope::from("\n"),
&Selection::single(0, 1),
PAIRS,
|open, close| format!("{}{}\n", open, close),
&Selection::single(1, 2),
);
}
}

View File

@@ -91,14 +91,12 @@ mod test {
#[test]
fn test_categorize() {
#[cfg(not(feature = "unicode-lines"))]
const EOL_TEST_CASE: &str = "\n";
#[cfg(feature = "unicode-lines")]
const EOL_TEST_CASE: &str = "\n\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
const WORD_TEST_CASE: &str = "_hello_world_あいうえおー1234567890";
const PUNCTUATION_TEST_CASE: &str =
const EOL_TEST_CASE: &'static str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
const WORD_TEST_CASE: &'static str =
"_hello_world_あいうえおー1234567890";
const PUNCTUATION_TEST_CASE: &'static str =
"!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
const WHITESPACE_TEST_CASE: &str = "  ";
const WHITESPACE_TEST_CASE: &'static str = "  ";
for ch in EOL_TEST_CASE.chars() {
assert_eq!(CharCategory::Eol, categorize_char(ch));

View File

@@ -45,7 +45,7 @@ fn find_line_comment(
// determine margin of 0 or 1 for uncommenting; if any comment token is not followed by a space,
// a margin of 0 is used for all lines.
if !matches!(line_slice.get_char(pos + token_len), Some(c) if c == ' ') {
if matches!(line_slice.get_char(pos + token_len), Some(c) if c != ' ') {
margin = 0;
}
@@ -68,11 +68,11 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
let mut min_next_line = 0;
for selection in selection {
let (start, end) = selection.line_range(text);
let start = start.clamp(min_next_line, text.len_lines());
let start = start.max(min_next_line).min(text.len_lines());
let end = (end + 1).min(text.len_lines());
lines.extend(start..end);
min_next_line = end;
min_next_line = end + 1;
}
let (commented, to_change, min, margin) = find_line_comment(token, text, lines);
@@ -100,52 +100,43 @@ mod test {
#[test]
fn test_find_line_comment() {
// four lines, two space indented, except for line 1 which is blank.
let mut doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
let mut selection = Selection::single(0, doc.len_chars() - 1);
use crate::State;
let text = doc.slice(..);
// four lines, two space indented, except for line 1 which is blank.
let doc = Rope::from(" 1\n\n 2\n 3");
let mut state = State::new(doc);
// select whole document
state.selection = Selection::single(0, state.doc.len_chars() - 1);
let text = state.doc.slice(..);
let res = find_line_comment("//", text, 0..3);
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 0)
assert_eq!(res, (false, vec![0, 2], 2, 0));
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1)
assert_eq!(res, (false, vec![0, 2], 2, 1));
// comment
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.map(transaction.changes());
assert_eq!(doc, " // 1\n\n // 2\n // 3");
assert_eq!(state.doc, " // 1\n\n // 2\n // 3");
// uncomment
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3");
// 0 margin comments
doc = Rope::from(" //1\n\n //2\n //3");
state.doc = Rope::from(" //1\n\n //2\n //3");
// reset the selection.
selection = Selection::single(0, doc.len_chars() - 1);
state.selection = Selection::single(0, state.doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
// 0 margin comments, with no space
doc = Rope::from("//");
// reset the selection.
selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, "");
assert!(selection.len() == 1); // to ignore the selection unused warning
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3");
// TODO: account for uncommenting with uneven comment indentation
}

View File

@@ -23,32 +23,11 @@ pub struct Range {
pub end: usize,
}
#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)]
pub enum NumberOrString {
Number(i32),
String(String),
}
#[derive(Debug, Clone)]
pub enum DiagnosticTag {
Unnecessary,
Deprecated,
}
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.94.0/lsp_types/struct.Diagnostic.html)
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub range: Range,
// whether this diagnostic ends at the end of(or inside) a word
pub ends_at_word: bool,
pub starts_at_word: bool,
pub zero_width: bool,
pub line: usize,
pub message: String,
pub severity: Option<Severity>,
pub code: Option<NumberOrString>,
pub language_server_id: usize,
pub tags: Vec<DiagnosticTag>,
pub source: Option<String>,
pub data: Option<serde_json::Value>,
}

View File

@@ -1,221 +1,68 @@
use std::ops::Range;
use std::time::Instant;
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(|&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();
}
} 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
}
}
use crate::{Rope, Transaction};
/// 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(),
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.
//
// Note: Ignore the clippy warning, as the trait bounds of
// `Transaction::change()` require an iterator implementing
// `ExactIterator`.
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()
.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]
};
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
Some((old_pos, pos, Some(text.into())))
}
similar::DiffTag::Delete => Some((old_pos, pos, None)),
similar::DiffTag::Equal => None,
}
}),
)
}
#[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);
let new = Rope::from(b);
compare_ropes(&old, &new).apply(&mut old);
old == new
old.to_string() == new.to_string()
}
}
#[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", "");
}
}

View File

@@ -1,384 +0,0 @@
//! The `DocumentFormatter` forms the bridge between the raw document text
//! and onscreen positioning. It yields the text graphemes as an iterator
//! and traverses (part) of the document text. During that traversal it
//! handles grapheme detection, softwrapping and annotations.
//! It yields `FormattedGrapheme`s and their corresponding visual coordinates.
//!
//! As both virtual text and softwrapping can insert additional lines into the document
//! it is generally not possible to find the start of the previous visual line.
//! Instead the `DocumentFormatter` starts at the last "checkpoint" (usually a linebreak)
//! called a "block" and the caller must advance it as needed.
use std::borrow::Cow;
use std::fmt::Debug;
use std::mem::{replace, take};
#[cfg(test)]
mod test;
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
use crate::graphemes::{Grapheme, GraphemeStr};
use crate::syntax::Highlight;
use crate::text_annotations::TextAnnotations;
use crate::{Position, RopeGraphemes, RopeSlice};
/// TODO make Highlight a u32 to reduce the size of this enum to a single word.
#[derive(Debug, Clone, Copy)]
pub enum GraphemeSource {
Document {
codepoints: u32,
},
/// Inline virtual text can not be highlighted with a `Highlight` iterator
/// because it's not part of the document. Instead the `Highlight`
/// is emitted right by the document formatter
VirtualText {
highlight: Option<Highlight>,
},
}
#[derive(Debug, Clone)]
pub struct FormattedGrapheme<'a> {
pub grapheme: Grapheme<'a>,
pub source: GraphemeSource,
}
impl<'a> FormattedGrapheme<'a> {
pub fn new(
g: GraphemeStr<'a>,
visual_x: usize,
tab_width: u16,
source: GraphemeSource,
) -> FormattedGrapheme<'a> {
FormattedGrapheme {
grapheme: Grapheme::new(g, visual_x, tab_width),
source,
}
}
/// Returns whether this grapheme is virtual inline text
pub fn is_virtual(&self) -> bool {
matches!(self.source, GraphemeSource::VirtualText { .. })
}
pub fn placeholder() -> Self {
FormattedGrapheme {
grapheme: Grapheme::Other { g: " ".into() },
source: GraphemeSource::Document { codepoints: 0 },
}
}
pub fn doc_chars(&self) -> usize {
match self.source {
GraphemeSource::Document { codepoints } => codepoints as usize,
GraphemeSource::VirtualText { .. } => 0,
}
}
pub fn is_whitespace(&self) -> bool {
self.grapheme.is_whitespace()
}
pub fn width(&self) -> usize {
self.grapheme.width()
}
pub fn is_word_boundary(&self) -> bool {
self.grapheme.is_word_boundary()
}
}
#[derive(Debug, Clone)]
pub struct TextFormat {
pub soft_wrap: bool,
pub tab_width: u16,
pub max_wrap: u16,
pub max_indent_retain: u16,
pub wrap_indicator: Box<str>,
pub wrap_indicator_highlight: Option<Highlight>,
pub viewport_width: u16,
}
// test implementation is basically only used for testing or when softwrap is always disabled
impl Default for TextFormat {
fn default() -> Self {
TextFormat {
soft_wrap: false,
tab_width: 4,
max_wrap: 3,
max_indent_retain: 4,
wrap_indicator: Box::from(" "),
viewport_width: 17,
wrap_indicator_highlight: None,
}
}
}
#[derive(Debug)]
pub struct DocumentFormatter<'t> {
text_fmt: &'t TextFormat,
annotations: &'t TextAnnotations,
/// The visual position at the end of the last yielded word boundary
visual_pos: Position,
graphemes: RopeGraphemes<'t>,
/// The character pos of the `graphemes` iter used for inserting annotations
char_pos: usize,
/// The line pos of the `graphemes` iter used for inserting annotations
line_pos: usize,
exhausted: bool,
/// Line breaks to be reserved for virtual text
/// at the next line break
virtual_lines: usize,
inline_anntoation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
// softwrap specific
/// The indentation of the current line
/// Is set to `None` if the indentation level is not yet known
/// because no non-whitespace graphemes have been encountered yet
indent_level: Option<usize>,
/// In case a long word needs to be split a single grapheme might need to be wrapped
/// while the rest of the word stays on the same line
peeked_grapheme: Option<(FormattedGrapheme<'t>, usize)>,
/// A first-in first-out (fifo) buffer for the Graphemes of any given word
word_buf: Vec<FormattedGrapheme<'t>>,
/// The index of the next grapheme that will be yielded from the `word_buf`
word_i: usize,
}
impl<'t> DocumentFormatter<'t> {
/// Creates a new formatter at the last block before `char_idx`.
/// A block is a chunk which always ends with a linebreak.
/// This is usually just a normal line break.
/// However very long lines are always wrapped at constant intervals that can be cheaply calculated
/// to avoid pathological behaviour.
pub fn new_at_prev_checkpoint(
text: RopeSlice<'t>,
text_fmt: &'t TextFormat,
annotations: &'t TextAnnotations,
char_idx: usize,
) -> (Self, usize) {
// TODO divide long lines into blocks to avoid bad performance for long lines
let block_line_idx = text.char_to_line(char_idx.min(text.len_chars()));
let block_char_idx = text.line_to_char(block_line_idx);
annotations.reset_pos(block_char_idx);
(
DocumentFormatter {
text_fmt,
annotations,
visual_pos: Position { row: 0, col: 0 },
graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
char_pos: block_char_idx,
exhausted: false,
virtual_lines: 0,
indent_level: None,
peeked_grapheme: None,
word_buf: Vec::with_capacity(64),
word_i: 0,
line_pos: block_line_idx,
inline_anntoation_graphemes: None,
},
block_char_idx,
)
}
fn next_inline_annotation_grapheme(&mut self) -> Option<(&'t str, Option<Highlight>)> {
loop {
if let Some(&mut (ref mut annotation, highlight)) =
self.inline_anntoation_graphemes.as_mut()
{
if let Some(grapheme) = annotation.next() {
return Some((grapheme, highlight));
}
}
if let Some((annotation, highlight)) =
self.annotations.next_inline_annotation_at(self.char_pos)
{
self.inline_anntoation_graphemes = Some((
UnicodeSegmentation::graphemes(&*annotation.text, true),
highlight,
))
} else {
return None;
}
}
}
fn advance_grapheme(&mut self, col: usize) -> Option<FormattedGrapheme<'t>> {
let (grapheme, source) =
if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme() {
(grapheme.into(), GraphemeSource::VirtualText { highlight })
} else if let Some(grapheme) = self.graphemes.next() {
self.virtual_lines += self.annotations.annotation_lines_at(self.char_pos);
let codepoints = grapheme.len_chars() as u32;
let overlay = self.annotations.overlay_at(self.char_pos);
let grapheme = match overlay {
Some((overlay, _)) => overlay.grapheme.as_str().into(),
None => Cow::from(grapheme).into(),
};
self.char_pos += codepoints as usize;
(grapheme, GraphemeSource::Document { codepoints })
} else {
if self.exhausted {
return None;
}
self.exhausted = true;
// EOF grapheme is required for rendering
// and correct position computations
return Some(FormattedGrapheme {
grapheme: Grapheme::Other { g: " ".into() },
source: GraphemeSource::Document { codepoints: 0 },
});
};
let grapheme = FormattedGrapheme::new(grapheme, col, self.text_fmt.tab_width, source);
Some(grapheme)
}
/// Move a word to the next visual line
fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize {
// softwrap this word to the next line
let indent_carry_over = if let Some(indent) = self.indent_level {
if indent as u16 <= self.text_fmt.max_indent_retain {
indent as u16
} else {
0
}
} else {
// ensure the indent stays 0
self.indent_level = Some(0);
0
};
self.visual_pos.col = indent_carry_over as usize;
self.virtual_lines -= virtual_lines_before_word;
self.visual_pos.row += 1 + virtual_lines_before_word;
let mut i = 0;
let mut word_width = 0;
let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true)
.map(|g| {
i += 1;
let grapheme = FormattedGrapheme::new(
g.into(),
self.visual_pos.col + word_width,
self.text_fmt.tab_width,
GraphemeSource::VirtualText {
highlight: self.text_fmt.wrap_indicator_highlight,
},
);
word_width += grapheme.width();
grapheme
});
self.word_buf.splice(0..0, wrap_indicator);
for grapheme in &mut self.word_buf[i..] {
let visual_x = self.visual_pos.col + word_width;
grapheme
.grapheme
.change_position(visual_x, self.text_fmt.tab_width);
word_width += grapheme.width();
}
word_width
}
fn advance_to_next_word(&mut self) {
self.word_buf.clear();
let mut word_width = 0;
let virtual_lines_before_word = self.virtual_lines;
let mut virtual_lines_before_grapheme = self.virtual_lines;
loop {
// softwrap word if necessary
if word_width + self.visual_pos.col >= self.text_fmt.viewport_width as usize {
// wrapping this word would move too much text to the next line
// split the word at the line end instead
if word_width > self.text_fmt.max_wrap as usize {
// Usually we stop accomulating graphemes as soon as softwrapping becomes necessary.
// However if the last grapheme is multiple columns wide it might extend beyond the EOL.
// The condition below ensures that this grapheme is not cutoff and instead wrapped to the next line
if word_width + self.visual_pos.col > self.text_fmt.viewport_width as usize {
self.peeked_grapheme = self.word_buf.pop().map(|grapheme| {
(grapheme, self.virtual_lines - virtual_lines_before_grapheme)
});
self.virtual_lines = virtual_lines_before_grapheme;
}
return;
}
word_width = self.wrap_word(virtual_lines_before_word);
}
virtual_lines_before_grapheme = self.virtual_lines;
let grapheme = if let Some((grapheme, virtual_lines)) = self.peeked_grapheme.take() {
self.virtual_lines += virtual_lines;
grapheme
} else if let Some(grapheme) = self.advance_grapheme(self.visual_pos.col + word_width) {
grapheme
} else {
return;
};
// Track indentation
if !grapheme.is_whitespace() && self.indent_level.is_none() {
self.indent_level = Some(self.visual_pos.col);
} else if grapheme.grapheme == Grapheme::Newline {
self.indent_level = None;
}
let is_word_boundary = grapheme.is_word_boundary();
word_width += grapheme.width();
self.word_buf.push(grapheme);
if is_word_boundary {
return;
}
}
}
/// returns the document line pos of the **next** grapheme that will be yielded
pub fn line_pos(&self) -> usize {
self.line_pos
}
/// returns the visual pos of the **next** grapheme that will be yielded
pub fn visual_pos(&self) -> Position {
self.visual_pos
}
}
impl<'t> Iterator for DocumentFormatter<'t> {
type Item = (FormattedGrapheme<'t>, Position);
fn next(&mut self) -> Option<Self::Item> {
let grapheme = if self.text_fmt.soft_wrap {
if self.word_i >= self.word_buf.len() {
self.advance_to_next_word();
self.word_i = 0;
}
let grapheme = replace(
self.word_buf.get_mut(self.word_i)?,
FormattedGrapheme::placeholder(),
);
self.word_i += 1;
grapheme
} else {
self.advance_grapheme(self.visual_pos.col)?
};
let pos = self.visual_pos;
if grapheme.grapheme == Grapheme::Newline {
self.visual_pos.row += 1;
self.visual_pos.row += take(&mut self.virtual_lines);
self.visual_pos.col = 0;
self.line_pos += 1;
} else {
self.visual_pos.col += grapheme.width();
}
Some((grapheme, pos))
}
}

View File

@@ -1,182 +0,0 @@
use std::rc::Rc;
use crate::doc_formatter::{DocumentFormatter, TextFormat};
use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations};
impl TextFormat {
fn new_test(softwrap: bool) -> Self {
TextFormat {
soft_wrap: softwrap,
tab_width: 2,
max_wrap: 3,
max_indent_retain: 4,
wrap_indicator: ".".into(),
wrap_indicator_highlight: None,
// use a prime number to allow lining up too often with repeat
viewport_width: 17,
}
}
}
impl<'t> DocumentFormatter<'t> {
fn collect_to_str(&mut self) -> String {
use std::fmt::Write;
let mut res = String::new();
let viewport_width = self.text_fmt.viewport_width;
let mut line = 0;
for (grapheme, pos) in self {
if pos.row != line {
line += 1;
assert_eq!(pos.row, line);
write!(res, "\n{}", ".".repeat(pos.col)).unwrap();
assert!(
pos.col <= viewport_width as usize,
"softwrapped failed {}<={viewport_width}",
pos.col
);
}
write!(res, "{}", grapheme.grapheme).unwrap();
}
res
}
}
fn softwrap_text(text: &str) -> String {
DocumentFormatter::new_at_prev_checkpoint(
text.into(),
&TextFormat::new_test(true),
&TextAnnotations::default(),
0,
)
.0
.collect_to_str()
}
#[test]
fn basic_softwrap() {
assert_eq!(
softwrap_text(&"foo ".repeat(10)),
"foo foo foo foo \n.foo foo foo foo \n.foo foo "
);
assert_eq!(
softwrap_text(&"fooo ".repeat(10)),
"fooo fooo fooo \n.fooo fooo fooo \n.fooo fooo fooo \n.fooo "
);
// check that we don't wrap unnecessarily
assert_eq!(softwrap_text("\t\txxxx1xxxx2xx\n"), " xxxx1xxxx2xx \n ");
}
#[test]
fn softwrap_indentation() {
assert_eq!(
softwrap_text("\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"),
" foo1 foo2 \n.....foo3 foo4 \n.....foo5 foo6 \n "
);
assert_eq!(
softwrap_text("\t\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"),
" foo1 foo2 \n.foo3 foo4 foo5 \n.foo6 \n "
);
}
#[test]
fn long_word_softwrap() {
assert_eq!(
softwrap_text("\t\txxxx1xxxx2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
" xxxx1xxxx2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n "
);
assert_eq!(
softwrap_text("xxxxxxxx1xxxx2xxx\n"),
"xxxxxxxx1xxxx2xxx\n. \n "
);
assert_eq!(
softwrap_text("\t\txxxx1xxxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
" xxxx1xxxx \n.....2xxxx3xxxx4x\n.....xxx5xxxx6xxx\n.....x7xxxx8xxxx9\n.....xxx \n "
);
assert_eq!(
softwrap_text("\t\txxxx1xxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
" xxxx1xxx 2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n "
);
}
fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String {
DocumentFormatter::new_at_prev_checkpoint(
text.into(),
&TextFormat::new_test(softwrap),
TextAnnotations::default().add_overlay(overlays.into(), None),
char_pos,
)
.0
.collect_to_str()
}
#[test]
fn overlay() {
assert_eq!(
overlay_text(
"foobar",
0,
false,
&[Overlay::new(0, "X"), Overlay::new(2, "\t")],
),
"Xo bar "
);
assert_eq!(
overlay_text(
&"foo ".repeat(10),
0,
true,
&[
Overlay::new(2, "\t"),
Overlay::new(5, "\t"),
Overlay::new(16, "X"),
]
),
"fo f o foo \n.foo Xoo foo foo \n.foo foo foo "
);
}
fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -> String {
DocumentFormatter::new_at_prev_checkpoint(
text.into(),
&TextFormat::new_test(softwrap),
TextAnnotations::default().add_inline_annotations(annotations.into(), None),
0,
)
.0
.collect_to_str()
}
#[test]
fn annotation() {
assert_eq!(
annotate_text("bar", false, &[InlineAnnotation::new(0, "foo")]),
"foobar "
);
assert_eq!(
annotate_text(
&"foo ".repeat(10),
true,
&[InlineAnnotation::new(0, "foo ")]
),
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
);
}
#[test]
fn annotation_and_overlay() {
assert_eq!(
DocumentFormatter::new_at_prev_checkpoint(
"bbar".into(),
&TextFormat::new_test(false),
TextAnnotations::default()
.add_inline_annotations(Rc::new([InlineAnnotation::new(0, "fooo")]), None)
.add_overlay(Rc::new([Overlay::new(0, "\t")]), None),
0,
)
.0
.collect_to_str(),
"fooo bar "
);
}

View File

@@ -1,43 +0,0 @@
use std::ops::DerefMut;
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
use nucleo::Config;
use parking_lot::Mutex;
pub struct LazyMutex<T> {
inner: Mutex<Option<T>>,
init: fn() -> T,
}
impl<T> LazyMutex<T> {
pub const fn new(init: fn() -> T) -> Self {
Self {
inner: Mutex::new(None),
init,
}
}
pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
parking_lot::MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
}
}
pub static MATCHER: LazyMutex<nucleo::Matcher> = LazyMutex::new(nucleo::Matcher::default);
/// convenience function to easily fuzzy match
/// on a (relatively small list of inputs). This is not recommended for building a full tui
/// application that can match large numbers of matches as all matching is done on the current
/// thread, effectively blocking the UI
pub fn fuzzy_match<T: AsRef<str>>(
pattern: &str,
items: impl IntoIterator<Item = T>,
path: bool,
) -> Vec<(T, u16)> {
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
if path {
matcher.config.set_match_paths();
}
let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false);
pattern.match_list(items, &mut matcher)
}

View File

@@ -5,88 +5,7 @@ use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
use std::borrow::Cow;
use std::fmt::{self, Debug, Display};
use std::marker::PhantomData;
use std::ops::Deref;
use std::ptr::NonNull;
use std::{slice, str};
use crate::chars::{char_is_whitespace, char_is_word};
use crate::LineEnding;
#[inline]
pub fn tab_width_at(visual_x: usize, tab_width: u16) -> usize {
tab_width as usize - (visual_x % tab_width as usize)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Grapheme<'a> {
Newline,
Tab { width: usize },
Other { g: GraphemeStr<'a> },
}
impl<'a> Grapheme<'a> {
pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> {
match g {
g if g == "\t" => Grapheme::Tab {
width: tab_width_at(visual_x, tab_width),
},
_ if LineEnding::from_str(&g).is_some() => Grapheme::Newline,
_ => Grapheme::Other { g },
}
}
pub fn change_position(&mut self, visual_x: usize, tab_width: u16) {
if let Grapheme::Tab { width } = self {
*width = tab_width_at(visual_x, tab_width)
}
}
/// Returns the a visual width of this grapheme,
#[inline]
pub fn width(&self) -> usize {
match *self {
// width is not cached because we are dealing with
// ASCII almost all the time which already has a fastpath
// it's okay to convert to u16 here because no codepoint has a width larger
// than 2 and graphemes are usually atmost two visible codepoints wide
Grapheme::Other { ref g } => grapheme_width(g),
Grapheme::Tab { width } => width,
Grapheme::Newline => 1,
}
}
pub fn is_whitespace(&self) -> bool {
!matches!(&self, Grapheme::Other { g } if !g.chars().all(char_is_whitespace))
}
// TODO currently word boundaries are used for softwrapping.
// This works best for programming languages and well for prose.
// This could however be improved in the future by considering unicode
// character classes but
pub fn is_word_boundary(&self) -> bool {
!matches!(&self, Grapheme::Other { g,.. } if g.chars().all(char_is_word))
}
}
impl Display for Grapheme<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Grapheme::Newline => write!(f, " "),
Grapheme::Tab { width } => {
for _ in 0..width {
write!(f, " ")?;
}
Ok(())
}
Grapheme::Other { ref g } => {
write!(f, "{g}")
}
}
}
}
use std::fmt;
#[must_use]
pub fn grapheme_width(g: &str) -> usize {
@@ -95,7 +14,7 @@ pub fn grapheme_width(g: &str) -> usize {
// Point 1: theoretically, ascii control characters should have zero
// width, but in our case we actually want them to have width: if they
// show up in text, we want to treat them as textual elements that can
// be edited. So we can get away with making all ascii single width
// be editied. So we can get away with making all ascii single width
// here.
// Point 2: we're only examining the first codepoint here, which means
// we're ignoring graphemes formed with combining characters. However,
@@ -108,8 +27,6 @@ pub fn grapheme_width(g: &str) -> usize {
// We use max(1) here because all grapeheme clusters--even illformed
// ones--should have at least some width so they can be edited
// properly.
// TODO properly handle unicode width for all codepoints
// example of where unicode width is currently wrong: 🤦🏼‍♂️ (taken from https://hsivonen.fi/string-length/)
UnicodeWidthStr::width(g).max(1)
}
}
@@ -203,43 +120,6 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
chunk_char_idx + tmp
}
#[must_use]
pub fn nth_next_grapheme_boundary_byte(slice: RopeSlice, mut byte_idx: usize, n: usize) -> usize {
// Bounds check
debug_assert!(byte_idx <= slice.len_bytes());
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, mut _chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Find the nth next grapheme cluster boundary.
for _ in 0..n {
loop {
match gc.next_boundary(chunk, chunk_byte_idx) {
Ok(None) => return slice.len_bytes(),
Ok(Some(n)) => {
byte_idx = n;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
chunk_byte_idx += chunk.len();
let (a, _, _c, _) = slice.chunk_at_byte(chunk_byte_idx);
chunk = a;
// chunk_char_idx = c;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = slice.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
byte_idx
}
/// Finds the next grapheme boundary after the given char position.
#[must_use]
#[inline(always)]
@@ -247,13 +127,6 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
nth_next_grapheme_boundary(slice, char_idx, 1)
}
/// Finds the next grapheme boundary after the given byte position.
#[must_use]
#[inline(always)]
pub fn next_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> usize {
nth_next_grapheme_boundary_byte(slice, byte_idx, 1)
}
/// Returns the passed char index if it's already a grapheme boundary,
/// or the next grapheme boundary char index if not.
#[must_use]
@@ -278,23 +151,6 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize
}
}
/// Returns the passed byte index if it's already a grapheme boundary,
/// or the next grapheme boundary byte index if not.
#[must_use]
#[inline]
pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize {
if byte_idx == 0 {
byte_idx
} else {
// TODO: optimize so we're not constructing grapheme cursor twice
if is_grapheme_boundary_byte(slice, byte_idx) {
byte_idx
} else {
next_grapheme_boundary_byte(slice, byte_idx)
}
}
}
/// Returns whether the given char position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
@@ -323,31 +179,6 @@ pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
}
}
/// Returns whether the given byte position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> bool {
// Bounds check
debug_assert!(byte_idx <= slice.len_bytes());
// Get the chunk with our byte index in it.
let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Determine if the given position is a grapheme cluster boundary.
loop {
match gc.is_boundary(chunk, chunk_byte_idx) {
Ok(n) => return n,
Err(GraphemeIncomplete::PreContext(n)) => {
let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
gc.provide_context(ctx_chunk, ctx_byte_start);
}
Err(_) => unreachable!(),
}
}
}
/// An iterator over the graphemes of a `RopeSlice`.
#[derive(Clone)]
pub struct RopeGraphemes<'a> {
@@ -416,7 +247,10 @@ impl<'a> Iterator for RopeGraphemes<'a> {
}
if a < self.cur_chunk_start {
Some(self.text.byte_slice(a..b))
let a_char = self.text.byte_to_char(a);
let b_char = self.text.byte_to_char(b);
Some(self.text.slice(a_char..b_char))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
@@ -424,101 +258,3 @@ impl<'a> Iterator for RopeGraphemes<'a> {
}
}
}
/// A highly compressed Cow<'a, str> that holds
/// atmost u31::MAX bytes and is readonly
pub struct GraphemeStr<'a> {
ptr: NonNull<u8>,
len: u32,
phantom: PhantomData<&'a str>,
}
impl GraphemeStr<'_> {
const MASK_OWNED: u32 = 1 << 31;
fn compute_len(&self) -> usize {
(self.len & !Self::MASK_OWNED) as usize
}
}
impl Deref for GraphemeStr<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
unsafe {
let bytes = slice::from_raw_parts(self.ptr.as_ptr(), self.compute_len());
str::from_utf8_unchecked(bytes)
}
}
}
impl Drop for GraphemeStr<'_> {
fn drop(&mut self) {
if self.len & Self::MASK_OWNED != 0 {
// free allocation
unsafe {
drop(Box::from_raw(slice::from_raw_parts_mut(
self.ptr.as_ptr(),
self.compute_len(),
)));
}
}
}
}
impl<'a> From<&'a str> for GraphemeStr<'a> {
fn from(g: &'a str) -> Self {
GraphemeStr {
ptr: unsafe { NonNull::new_unchecked(g.as_bytes().as_ptr() as *mut u8) },
len: i32::try_from(g.len()).unwrap() as u32,
phantom: PhantomData,
}
}
}
impl<'a> From<String> for GraphemeStr<'a> {
fn from(g: String) -> Self {
let len = g.len();
let ptr = Box::into_raw(g.into_bytes().into_boxed_slice()) as *mut u8;
GraphemeStr {
ptr: unsafe { NonNull::new_unchecked(ptr) },
len: (i32::try_from(len).unwrap() as u32) | Self::MASK_OWNED,
phantom: PhantomData,
}
}
}
impl<'a> From<Cow<'a, str>> for GraphemeStr<'a> {
fn from(g: Cow<'a, str>) -> Self {
match g {
Cow::Borrowed(g) => g.into(),
Cow::Owned(g) => g.into(),
}
}
}
impl<T: Deref<Target = str>> PartialEq<T> for GraphemeStr<'_> {
fn eq(&self, other: &T) -> bool {
self.deref() == other.deref()
}
}
impl PartialEq<str> for GraphemeStr<'_> {
fn eq(&self, other: &str) -> bool {
self.deref() == other
}
}
impl Eq for GraphemeStr<'_> {}
impl Debug for GraphemeStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Debug::fmt(self.deref(), f)
}
}
impl Display for GraphemeStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(self.deref(), f)
}
}
impl Clone for GraphemeStr<'_> {
fn clone(&self) -> Self {
self.deref().to_owned().into()
}
}

View File

@@ -1,15 +1,9 @@
use crate::{Assoc, ChangeSet, Range, Rope, Selection, Transaction};
use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction};
use once_cell::sync::Lazy;
use regex::Regex;
use std::num::NonZeroUsize;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct State {
pub doc: Rope,
pub selection: Selection,
}
/// Stores the history of changes to a buffer.
///
/// Currently the history is represented as a vector of revisions. The vector
@@ -28,10 +22,10 @@ pub struct State {
///
/// The current revision is the one currently displayed in the buffer.
///
/// Committing a new revision to the history will update the last child of the
/// Commiting a new revision to the history will update the last child of the
/// current revision, and push a new revision to the end of the vector.
///
/// Revisions are committed with a timestamp. :earlier and :later can be used
/// Revisions are commited with a timestamp. :earlier and :later can be used
/// to jump to the closest revision to a moment in time relative to the timestamp
/// of the current revision plus (:later) or minus (:earlier) the duration
/// given to the command. If a single integer is given, the editor will instead
@@ -39,7 +33,7 @@ pub struct State {
///
/// Limitations:
/// * Changes in selections currently don't commit history changes. The selection
/// will only be updated to the state after a committed buffer change.
/// will only be updated to the state after a commited buffer change.
/// * The vector of history revisions is currently unbounded. This might
/// cause the memory consumption to grow significantly large during long
/// editing sessions.
@@ -54,7 +48,7 @@ pub struct History {
}
/// A single point in history. See [History] for more information.
#[derive(Debug, Clone)]
#[derive(Debug)]
struct Revision {
parent: usize,
last_child: Option<NonZeroUsize>,
@@ -72,8 +66,8 @@ impl Default for History {
revisions: vec![Revision {
parent: 0,
last_child: None,
transaction: Transaction::from(ChangeSet::new("".into())),
inversion: Transaction::from(ChangeSet::new("".into())),
transaction: Transaction::from(ChangeSet::new(&Rope::new())),
inversion: Transaction::from(ChangeSet::new(&Rope::new())),
timestamp: Instant::now(),
}],
current: 0,
@@ -119,21 +113,6 @@ impl History {
self.current == 0
}
/// Returns the changes since the given revision composed into a transaction.
/// Returns None if there are no changes between the current and given revisions.
pub fn changes_since(&self, revision: usize) -> Option<Transaction> {
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());
down_txns.chain(up_txns).reduce(|acc, tx| tx.compose(acc))
}
/// Undo the last edit.
pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() {
@@ -198,7 +177,7 @@ impl History {
}
}
/// List of nodes on the way from `n` to 'a`. Doesn't include `a`.
/// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
/// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
let mut path = Vec::new();
@@ -303,13 +282,13 @@ impl History {
}
/// Whether to undo by a number of edits or a duration of time.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum UndoKind {
Steps(usize),
TimePeriod(std::time::Duration),
}
/// A subset of systemd.time time span syntax units.
/// A subset of sytemd.time time span syntax units.
const TIME_UNITS: &[(&[&str], &str, u64)] = &[
(&["seconds", "second", "sec", "s"], "seconds", 1),
(&["minutes", "minute", "min", "m"], "minutes", 60),
@@ -387,16 +366,12 @@ impl std::str::FromStr for UndoKind {
#[cfg(test)]
mod test {
use super::*;
use crate::Selection;
#[test]
fn test_undo_redo() {
let mut history = History::default();
let doc = Rope::from("hello");
let mut state = State {
doc,
selection: Selection::point(0),
};
let mut state = State::new(doc);
let transaction1 =
Transaction::change(&state.doc, vec![(5, 5, Some(" world!".into()))].into_iter());
@@ -445,10 +420,7 @@ mod test {
fn test_earlier_later() {
let mut history = History::default();
let doc = Rope::from("a\n");
let mut state = State {
doc,
selection: Selection::point(0),
};
let mut state = State::new(doc);
fn undo(history: &mut History, state: &mut State) {
if let Some(transaction) = history.undo() {
@@ -476,8 +448,8 @@ mod test {
change: crate::transaction::Change,
instant: Instant,
) {
let txn = Transaction::change(&state.doc, vec![change].into_iter());
history.commit_revision_at_timestamp(&txn, state, instant);
let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter());
history.commit_revision_at_timestamp(&txn, &state, instant);
txn.apply(&mut state.doc);
}
@@ -574,8 +546,8 @@ mod test {
// Units are validated.
assert_eq!(
"1 millennium".parse::<UndoKind>(),
Err("incorrect time unit: millennium".to_string())
"1 millenium".parse::<UndoKind>(),
Err("incorrect time unit: millenium".to_string())
);
// Units can't be specified twice.

View File

@@ -1,53 +1,113 @@
use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use once_cell::sync::Lazy;
use regex::Regex;
use std::fmt::Write;
use ropey::RopeSlice;
/// Increment a Date or DateTime
///
/// If just a Date is selected the day will be incremented.
/// If a DateTime is selected the second will be incremented.
pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
if selected_text.is_empty() {
use std::borrow::Cow;
use std::cmp;
use super::Increment;
use crate::{Range, Tendril};
#[derive(Debug, PartialEq, Eq)]
pub struct DateTimeIncrementor {
date_time: NaiveDateTime,
range: Range,
fmt: &'static str,
field: DateField,
}
impl DateTimeIncrementor {
pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> {
let range = if range.is_empty() {
if range.anchor < text.len_chars() {
// Treat empty range as a cursor range.
range.put_cursor(text, range.anchor + 1, true)
} else {
// The range is empty and at the end of the text.
return None;
}
} else {
range
};
FORMATS.iter().find_map(|format| {
let captures = format.regex.captures(selected_text)?;
let from = range.from().saturating_sub(format.max_len);
let to = (range.from() + format.max_len).min(text.len_chars());
let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
let text: Cow<str> = text.slice(from..to).into();
let captures = format.regex.captures(&text)?;
if captures.len() - 1 != format.fields.len() {
return None;
}
let date_time = captures.get(0)?;
let offset = range.from() - from_in_text;
let range = Range::new(date_time.start() + offset, date_time.end() + offset);
let field = captures
.iter()
.skip(1)
.enumerate()
.find_map(|(i, capture)| {
let capture = capture?;
let capture_range = capture.range();
if capture_range.contains(&from_in_text)
&& capture_range.contains(&(to_in_text - 1))
{
Some(format.fields[i])
} else {
None
}
})?;
let has_date = format.fields.iter().any(|f| f.unit.is_date());
let has_time = format.fields.iter().any(|f| f.unit.is_time());
let date_time = &selected_text[date_time.start()..date_time.end()];
match (has_date, has_time) {
(true, true) => {
let date_time = NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?;
Some(
date_time
.checked_add_signed(Duration::minutes(amount))?
.format(format.fmt)
.to_string(),
)
}
let date_time = &text[date_time.start()..date_time.end()];
let date_time = match (has_date, has_time) {
(true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?,
(true, false) => {
let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
Some(
date.checked_add_signed(Duration::days(amount))?
.format(format.fmt)
.to_string(),
)
date.and_hms(0, 0, 0)
}
(false, true) => {
let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
let (adjusted_time, _) = time.overflowing_add_signed(Duration::minutes(amount));
Some(adjusted_time.format(format.fmt).to_string())
}
(false, false) => None,
NaiveDate::from_ymd(0, 1, 1).and_time(time)
}
(false, false) => return None,
};
Some(DateTimeIncrementor {
date_time,
range,
fmt: format.fmt,
field,
})
})
}
}
impl Increment for DateTimeIncrementor {
fn increment(&self, amount: i64) -> (Range, Tendril) {
let date_time = match self.field.unit {
DateUnit::Years => add_years(self.date_time, amount),
DateUnit::Months => add_months(self.date_time, amount),
DateUnit::Days => add_duration(self.date_time, Duration::days(amount)),
DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)),
DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)),
DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)),
DateUnit::AmPm => toggle_am_pm(self.date_time),
}
.unwrap_or(self.date_time);
(self.range, date_time.format(self.fmt).to_string().into())
}
}
static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| {
@@ -83,7 +143,7 @@ impl Format {
fn new(fmt: &'static str) -> Self {
let mut remaining = fmt;
let mut fields = Vec::new();
let mut regex = "^".to_string();
let mut regex = String::new();
let mut max_len = 0;
while let Some(i) = remaining.find('%') {
@@ -102,10 +162,9 @@ impl Format {
fields.push(field);
max_len += field.max_len + remaining[..i].len();
regex += &remaining[..i];
write!(regex, "({})", field.regex).unwrap();
regex += &format!("({})", field.regex);
remaining = &after[spec_len..];
}
regex += "$";
let regex = Regex::new(&regex).unwrap();
@@ -136,82 +195,82 @@ struct DateField {
impl DateField {
fn from_specifier(specifier: &str) -> Option<Self> {
match specifier {
"Y" => Some(Self {
"Y" => Some(DateField {
regex: r"\d{4}",
unit: DateUnit::Years,
max_len: 5,
}),
"y" => Some(Self {
"y" => Some(DateField {
regex: r"\d\d",
unit: DateUnit::Years,
max_len: 2,
}),
"m" => Some(Self {
"m" => Some(DateField {
regex: r"[0-1]\d",
unit: DateUnit::Months,
max_len: 2,
}),
"d" => Some(Self {
"d" => Some(DateField {
regex: r"[0-3]\d",
unit: DateUnit::Days,
max_len: 2,
}),
"-d" => Some(Self {
"-d" => Some(DateField {
regex: r"[1-3]?\d",
unit: DateUnit::Days,
max_len: 2,
}),
"a" => Some(Self {
"a" => Some(DateField {
regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
unit: DateUnit::Days,
max_len: 3,
}),
"A" => Some(Self {
"A" => Some(DateField {
regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
unit: DateUnit::Days,
max_len: 9,
}),
"b" | "h" => Some(Self {
"b" | "h" => Some(DateField {
regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
unit: DateUnit::Months,
max_len: 3,
}),
"B" => Some(Self {
"B" => Some(DateField {
regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
unit: DateUnit::Months,
max_len: 9,
}),
"H" => Some(Self {
"H" => Some(DateField {
regex: r"[0-2]\d",
unit: DateUnit::Hours,
max_len: 2,
}),
"M" => Some(Self {
"M" => Some(DateField {
regex: r"[0-5]\d",
unit: DateUnit::Minutes,
max_len: 2,
}),
"S" => Some(Self {
"S" => Some(DateField {
regex: r"[0-5]\d",
unit: DateUnit::Seconds,
max_len: 2,
}),
"I" => Some(Self {
"I" => Some(DateField {
regex: r"[0-1]\d",
unit: DateUnit::Hours,
max_len: 2,
}),
"-I" => Some(Self {
"-I" => Some(DateField {
regex: r"1?\d",
unit: DateUnit::Hours,
max_len: 2,
}),
"P" => Some(Self {
"P" => Some(DateField {
regex: r"am|pm",
unit: DateUnit::AmPm,
max_len: 2,
}),
"p" => Some(Self {
"p" => Some(DateField {
regex: r"AM|PM",
unit: DateUnit::AmPm,
max_len: 2,
@@ -245,47 +304,155 @@ impl DateUnit {
}
}
fn ndays_in_month(year: i32, month: u32) -> u32 {
// The first day of the next month...
let (y, m) = if month == 12 {
(year + 1, 1)
} else {
(year, month + 1)
};
let d = NaiveDate::from_ymd(y, m, 1);
// ...is preceded by the last day of the original month.
d.pred().day()
}
fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
let month = (date_time.month0() as i64).checked_add(amount)?;
let year = date_time.year() + i32::try_from(month / 12).ok()?;
let year = if month.is_negative() { year - 1 } else { year };
// Normalize month
let month = month % 12;
let month = if month.is_negative() {
month + 12
} else {
month
} as u32
+ 1;
let day = cmp::min(date_time.day(), ndays_in_month(year, month));
Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time()))
}
fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?;
let ndays = ndays_in_month(year, date_time.month());
if date_time.day() > ndays {
let d = NaiveDate::from_ymd(year, date_time.month(), ndays);
Some(d.succ().and_time(date_time.time()))
} else {
date_time.with_year(year)
}
}
fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> {
date_time.checked_add_signed(duration)
}
fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> {
if date_time.hour() < 12 {
add_duration(date_time, Duration::hours(12))
} else {
add_duration(date_time, Duration::hours(-12))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Rope;
#[test]
fn test_increment_date_times() {
let tests = [
// (original, cursor, amount, expected)
("2020-02-28", 1, "2020-02-29"),
("2020-02-29", 1, "2020-03-01"),
("2020-01-31", 1, "2020-02-01"),
("2020-01-20", 1, "2020-01-21"),
("2021-01-01", -1, "2020-12-31"),
("2021-01-31", -2, "2021-01-29"),
("2020-02-28", 1, "2020-02-29"),
("2021-02-28", 1, "2021-03-01"),
("2021-03-01", -1, "2021-02-28"),
("2020-02-29", -1, "2020-02-28"),
("2020-02-20", -1, "2020-02-19"),
("2021-03-01", -1, "2021-02-28"),
("1980/12/21", 100, "1981/03/31"),
("1980/12/21", -100, "1980/09/12"),
("1980/12/21", 1000, "1983/09/17"),
("1980/12/21", -1000, "1978/03/27"),
("2021-11-24 07:12:23", 1, "2021-11-24 07:13:23"),
("2021-11-24 07:12", 1, "2021-11-24 07:13"),
("Wed Nov 24 2021", 1, "Thu Nov 25 2021"),
("24-Nov-2021", 1, "25-Nov-2021"),
("2021 Nov 24", 1, "2021 Nov 25"),
("Nov 24, 2021", 1, "Nov 25, 2021"),
("7:21:53 am", 1, "7:22:53 am"),
("7:21:53 AM", 1, "7:22:53 AM"),
("7:21 am", 1, "7:22 am"),
("23:24:23", 1, "23:25:23"),
("23:24", 1, "23:25"),
("23:59", 1, "00:00"),
("23:59:59", 1, "00:00:59"),
("2020-02-28", 0, 1, "2021-02-28"),
("2020-02-29", 0, 1, "2021-03-01"),
("2020-01-31", 5, 1, "2020-02-29"),
("2020-01-20", 5, 1, "2020-02-20"),
("2021-01-01", 5, -1, "2020-12-01"),
("2021-01-31", 5, -2, "2020-11-30"),
("2020-02-28", 8, 1, "2020-02-29"),
("2021-02-28", 8, 1, "2021-03-01"),
("2021-02-28", 0, -1, "2020-02-28"),
("2021-03-01", 0, -1, "2020-03-01"),
("2020-02-29", 5, -1, "2020-01-29"),
("2020-02-20", 5, -1, "2020-01-20"),
("2020-02-29", 8, -1, "2020-02-28"),
("2021-03-01", 8, -1, "2021-02-28"),
("1980/12/21", 8, 100, "1981/03/31"),
("1980/12/21", 8, -100, "1980/09/12"),
("1980/12/21", 8, 1000, "1983/09/17"),
("1980/12/21", 8, -1000, "1978/03/27"),
("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"),
("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"),
("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"),
("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"),
("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"),
("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"),
("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"),
("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"),
("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"),
("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"),
("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"),
("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"),
("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"),
("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"),
("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"),
("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"),
("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"),
("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"),
("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"),
("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"),
("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"),
("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"),
("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"),
("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"),
("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"),
("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"),
("24-Nov-2021", 0, 1, "25-Nov-2021"),
("24-Nov-2021", 3, 1, "24-Dec-2021"),
("24-Nov-2021", 7, 1, "24-Nov-2022"),
("2021 Nov 24", 0, 1, "2022 Nov 24"),
("2021 Nov 24", 5, 1, "2021 Dec 24"),
("2021 Nov 24", 9, 1, "2021 Nov 25"),
("Nov 24, 2021", 0, 1, "Dec 24, 2021"),
("Nov 24, 2021", 4, 1, "Nov 25, 2021"),
("Nov 24, 2021", 8, 1, "Nov 24, 2022"),
("7:21:53 am", 0, 1, "8:21:53 am"),
("7:21:53 am", 3, 1, "7:22:53 am"),
("7:21:53 am", 5, 1, "7:21:54 am"),
("7:21:53 am", 8, 1, "7:21:53 pm"),
("7:21:53 AM", 0, 1, "8:21:53 AM"),
("7:21:53 AM", 3, 1, "7:22:53 AM"),
("7:21:53 AM", 5, 1, "7:21:54 AM"),
("7:21:53 AM", 8, 1, "7:21:53 PM"),
("7:21 am", 0, 1, "8:21 am"),
("7:21 am", 3, 1, "7:22 am"),
("7:21 am", 5, 1, "7:21 pm"),
("7:21 AM", 0, 1, "8:21 AM"),
("7:21 AM", 3, 1, "7:22 AM"),
("7:21 AM", 5, 1, "7:21 PM"),
("23:24:23", 1, 1, "00:24:23"),
("23:24:23", 3, 1, "23:25:23"),
("23:24:23", 6, 1, "23:24:24"),
("23:24", 1, 1, "00:24"),
("23:24", 3, 1, "23:25"),
];
for (original, amount, expected) in tests {
assert_eq!(increment(original, amount).unwrap(), expected);
for (original, cursor, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::new(cursor, cursor + 1);
assert_eq!(
DateTimeIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
@@ -314,7 +481,10 @@ mod test {
];
for invalid in tests {
assert_eq!(increment(invalid, 1), None)
let rope = Rope::from_str(invalid);
let range = Range::new(0, 1);
assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None)
}
}
}

View File

@@ -1,235 +0,0 @@
const SEPARATOR: char = '_';
/// Increment an integer.
///
/// Supported bases:
/// 2 with prefix 0b
/// 8 with prefix 0o
/// 10 with no prefix
/// 16 with prefix 0x
///
/// An integer can contain `_` as a separator but may not start or end with a separator.
/// Base 10 integers can go negative, but bases 2, 8, and 16 cannot.
/// All addition and subtraction is saturating.
pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
if selected_text.is_empty()
|| selected_text.ends_with(SEPARATOR)
|| selected_text.starts_with(SEPARATOR)
{
return None;
}
let radix = if selected_text.starts_with("0x") {
16
} else if selected_text.starts_with("0o") {
8
} else if selected_text.starts_with("0b") {
2
} else {
10
};
// Get separator indexes from right to left.
let separator_rtl_indexes: Vec<usize> = selected_text
.chars()
.rev()
.enumerate()
.filter_map(|(i, c)| if c == SEPARATOR { Some(i) } else { None })
.collect();
let word: String = selected_text.chars().filter(|&c| c != SEPARATOR).collect();
let mut new_text = if radix == 10 {
let number = &word;
let value = i128::from_str_radix(number, radix).ok()?;
let new_value = value.saturating_add(amount as i128);
let format_length = match (value.is_negative(), new_value.is_negative()) {
(true, false) => number.len() - 1,
(false, true) => number.len() + 1,
_ => number.len(),
} - separator_rtl_indexes.len();
if number.starts_with('0') || number.starts_with("-0") {
format!("{:01$}", new_value, format_length)
} else {
format!("{}", new_value)
}
} else {
let number = &word[2..];
let value = u128::from_str_radix(number, radix).ok()?;
let new_value = (value as i128).saturating_add(amount as i128);
let new_value = if new_value < 0 { 0 } else { new_value };
let format_length = selected_text.len() - 2 - separator_rtl_indexes.len();
match radix {
2 => format!("0b{:01$b}", new_value, format_length),
8 => format!("0o{:01$o}", new_value, format_length),
16 => {
let (lower_count, upper_count): (usize, usize) =
number.chars().fold((0, 0), |(lower, upper), c| {
(
lower + c.is_ascii_lowercase() as usize,
upper + c.is_ascii_uppercase() as usize,
)
});
if upper_count > lower_count {
format!("0x{:01$X}", new_value, format_length)
} else {
format!("0x{:01$x}", new_value, format_length)
}
}
_ => unimplemented!("radix not supported: {}", radix),
}
};
// Add separators from original number.
for &rtl_index in &separator_rtl_indexes {
if rtl_index < new_text.len() {
let new_index = new_text.len().saturating_sub(rtl_index);
if new_index > 0 {
new_text.insert(new_index, SEPARATOR);
}
}
}
// Add in additional separators if necessary.
if new_text.len() > selected_text.len() && !separator_rtl_indexes.is_empty() {
let spacing = match separator_rtl_indexes.as_slice() {
[.., b, a] => a - b - 1,
_ => separator_rtl_indexes[0],
};
let prefix_length = if radix == 10 { 0 } else { 2 };
if let Some(mut index) = new_text.find(SEPARATOR) {
while index - prefix_length > spacing {
index -= spacing;
new_text.insert(index, SEPARATOR);
}
}
}
Some(new_text)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_increment_basic_decimal_numbers() {
let tests = [
("100", 1, "101"),
("100", -1, "99"),
("99", 1, "100"),
("100", 1000, "1100"),
("100", -1000, "-900"),
("-1", 1, "0"),
("-1", 2, "1"),
("1", -1, "0"),
("1", -2, "-1"),
];
for (original, amount, expected) in tests {
assert_eq!(increment(original, amount).unwrap(), expected);
}
}
#[test]
fn test_increment_basic_hexadecimal_numbers() {
let tests = [
("0x0100", 1, "0x0101"),
("0x0100", -1, "0x00ff"),
("0x0001", -1, "0x0000"),
("0x0000", -1, "0x0000"),
("0xffffffffffffffff", 1, "0x10000000000000000"),
("0xffffffffffffffff", 2, "0x10000000000000001"),
("0xffffffffffffffff", -1, "0xfffffffffffffffe"),
("0xABCDEF1234567890", 1, "0xABCDEF1234567891"),
("0xabcdef1234567890", 1, "0xabcdef1234567891"),
];
for (original, amount, expected) in tests {
assert_eq!(increment(original, amount).unwrap(), expected);
}
}
#[test]
fn test_increment_basic_octal_numbers() {
let tests = [
("0o0107", 1, "0o0110"),
("0o0110", -1, "0o0107"),
("0o0001", -1, "0o0000"),
("0o7777", 1, "0o10000"),
("0o1000", -1, "0o0777"),
("0o0107", 10, "0o0121"),
("0o0000", -1, "0o0000"),
("0o1777777777777777777777", 1, "0o2000000000000000000000"),
("0o1777777777777777777777", 2, "0o2000000000000000000001"),
("0o1777777777777777777777", -1, "0o1777777777777777777776"),
];
for (original, amount, expected) in tests {
assert_eq!(increment(original, amount).unwrap(), expected);
}
}
#[test]
fn test_increment_basic_binary_numbers() {
let tests = [
("0b00000100", 1, "0b00000101"),
("0b00000100", -1, "0b00000011"),
("0b00000100", 2, "0b00000110"),
("0b00000100", -2, "0b00000010"),
("0b00000001", -1, "0b00000000"),
("0b00111111", 10, "0b01001001"),
("0b11111111", 1, "0b100000000"),
("0b10000000", -1, "0b01111111"),
("0b0000", -1, "0b0000"),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
1,
"0b10000000000000000000000000000000000000000000000000000000000000000",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
2,
"0b10000000000000000000000000000000000000000000000000000000000000001",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
-1,
"0b1111111111111111111111111111111111111111111111111111111111111110",
),
];
for (original, amount, expected) in tests {
assert_eq!(increment(original, amount).unwrap(), expected);
}
}
#[test]
fn test_increment_with_separators() {
let tests = [
("999_999", 1, "1_000_000"),
("1_000_000", -1, "999_999"),
("-999_999", -1, "-1_000_000"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000", -1, "0x0000_0000"),
("0x0000_0000_0000", -1, "0x0000_0000_0000"),
("0b01111111_11111111", 1, "0b10000000_00000000"),
("0b11111111_11111111", 1, "0b1_00000000_00000000"),
];
for (original, amount, expected) in tests {
assert_eq!(increment(original, amount).unwrap(), expected);
}
}
#[test]
fn test_leading_and_trailing_separators_arent_a_match() {
assert_eq!(increment("9_", 1), None);
assert_eq!(increment("_9", 1), None);
assert_eq!(increment("_9_", 1), None);
}
}

View File

@@ -1,10 +1,8 @@
mod date_time;
mod integer;
pub mod date_time;
pub mod number;
pub fn integer(selected_text: &str, amount: i64) -> Option<String> {
integer::increment(selected_text, amount)
}
use crate::{Range, Tendril};
pub fn date_time(selected_text: &str, amount: i64) -> Option<String> {
date_time::increment(selected_text, amount)
pub trait Increment {
fn increment(&self, amount: i64) -> (Range, Tendril);
}

View File

@@ -0,0 +1,507 @@
use std::borrow::Cow;
use ropey::RopeSlice;
use super::Increment;
use crate::{
textobject::{textobject_word, TextObject},
Range, Tendril,
};
#[derive(Debug, PartialEq, Eq)]
pub struct NumberIncrementor<'a> {
value: i64,
radix: u32,
range: Range,
text: RopeSlice<'a>,
}
impl<'a> NumberIncrementor<'a> {
/// Return information about number under rang if there is one.
pub fn from_range(text: RopeSlice, range: Range) -> Option<NumberIncrementor> {
// If the cursor is on the minus sign of a number we want to get the word textobject to the
// right of it.
let range = if range.to() < text.len_chars()
&& range.to() - range.from() <= 1
&& text.char(range.from()) == '-'
{
Range::new(range.from() + 1, range.to() + 1)
} else {
range
};
let range = textobject_word(text, range, TextObject::Inside, 1, false);
// If there is a minus sign to the left of the word object, we want to include it in the range.
let range = if range.from() > 0 && text.char(range.from() - 1) == '-' {
range.extend(range.from() - 1, range.from())
} else {
range
};
let word: String = text
.slice(range.from()..range.to())
.chars()
.filter(|&c| c != '_')
.collect();
let (radix, prefixed) = if word.starts_with("0x") {
(16, true)
} else if word.starts_with("0o") {
(8, true)
} else if word.starts_with("0b") {
(2, true)
} else {
(10, false)
};
let number = if prefixed { &word[2..] } else { &word };
let value = i128::from_str_radix(number, radix).ok()?;
if (value.is_positive() && value.leading_zeros() < 64)
|| (value.is_negative() && value.leading_ones() < 64)
{
return None;
}
let value = value as i64;
Some(NumberIncrementor {
range,
value,
radix,
text,
})
}
}
impl<'a> Increment for NumberIncrementor<'a> {
fn increment(&self, amount: i64) -> (Range, Tendril) {
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
let old_length = old_text.len();
let new_value = self.value.wrapping_add(amount);
// Get separator indexes from right to left.
let separator_rtl_indexes: Vec<usize> = old_text
.chars()
.rev()
.enumerate()
.filter_map(|(i, c)| if c == '_' { Some(i) } else { None })
.collect();
let format_length = if self.radix == 10 {
match (self.value.is_negative(), new_value.is_negative()) {
(true, false) => old_length - 1,
(false, true) => old_length + 1,
_ => old_text.len(),
}
} else {
old_text.len() - 2
} - separator_rtl_indexes.len();
let mut new_text = match self.radix {
2 => format!("0b{:01$b}", new_value, format_length),
8 => format!("0o{:01$o}", new_value, format_length),
10 if old_text.starts_with('0') || old_text.starts_with("-0") => {
format!("{:01$}", new_value, format_length)
}
10 => format!("{}", new_value),
16 => {
let (lower_count, upper_count): (usize, usize) =
old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| {
(
lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0),
upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0),
)
});
if upper_count > lower_count {
format!("0x{:01$X}", new_value, format_length)
} else {
format!("0x{:01$x}", new_value, format_length)
}
}
_ => unimplemented!("radix not supported: {}", self.radix),
};
// Add separators from original number.
for &rtl_index in &separator_rtl_indexes {
if rtl_index < new_text.len() {
let new_index = new_text.len() - rtl_index;
new_text.insert(new_index, '_');
}
}
// Add in additional separators if necessary.
if new_text.len() > old_length && !separator_rtl_indexes.is_empty() {
let spacing = match separator_rtl_indexes.as_slice() {
[.., b, a] => a - b - 1,
_ => separator_rtl_indexes[0],
};
let prefix_length = if self.radix == 10 { 0 } else { 2 };
if let Some(mut index) = new_text.find('_') {
while index - prefix_length > spacing {
index -= spacing;
new_text.insert(index, '_');
}
}
}
(self.range, new_text.into())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Rope;
#[test]
fn test_decimal_at_point() {
let rope = Rope::from_str("Test text 12345 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 15),
value: 12345,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_uppercase_hexadecimal_at_point() {
let rope = Rope::from_str("Test text 0x123ABCDEF more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 21),
value: 0x123ABCDEF,
radix: 16,
text: rope.slice(..),
})
);
}
#[test]
fn test_lowercase_hexadecimal_at_point() {
let rope = Rope::from_str("Test text 0xfa3b4e more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 18),
value: 0xfa3b4e,
radix: 16,
text: rope.slice(..),
})
);
}
#[test]
fn test_octal_at_point() {
let rope = Rope::from_str("Test text 0o1074312 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 19),
value: 0o1074312,
radix: 8,
text: rope.slice(..),
})
);
}
#[test]
fn test_binary_at_point() {
let rope = Rope::from_str("Test text 0b10111010010101 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 26),
value: 0b10111010010101,
radix: 2,
text: rope.slice(..),
})
);
}
#[test]
fn test_negative_decimal_at_point() {
let rope = Rope::from_str("Test text -54321 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 16),
value: -54321,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_decimal_with_leading_zeroes_at_point() {
let rope = Rope::from_str("Test text 000045326 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 19),
value: 45326,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_negative_decimal_cursor_on_minus_sign() {
let rope = Rope::from_str("Test text -54321 more text.");
let range = Range::point(10);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 16),
value: -54321,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_under_range_start_of_rope() {
let rope = Rope::from_str("100");
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(0, 3),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_under_range_end_of_rope() {
let rope = Rope::from_str("100");
let range = Range::point(2);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(0, 3),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_surrounded_by_punctuation() {
let rope = Rope::from_str(",100;");
let range = Range::point(1);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(1, 4),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_not_a_number_point() {
let rope = Rope::from_str("Test text 45326 more text.");
let range = Range::point(6);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_too_large_at_point() {
let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text.");
let range = Range::point(12);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_cursor_one_right_of_number() {
let rope = Rope::from_str("100 ");
let range = Range::point(3);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_cursor_one_left_of_number() {
let rope = Rope::from_str(" 100");
let range = Range::point(0);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_increment_basic_decimal_numbers() {
let tests = [
("100", 1, "101"),
("100", -1, "99"),
("99", 1, "100"),
("100", 1000, "1100"),
("100", -1000, "-900"),
("-1", 1, "0"),
("-1", 2, "1"),
("1", -1, "0"),
("1", -2, "-1"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_increment_basic_hexadedimal_numbers() {
let tests = [
("0x0100", 1, "0x0101"),
("0x0100", -1, "0x00ff"),
("0x0001", -1, "0x0000"),
("0x0000", -1, "0xffffffffffffffff"),
("0xffffffffffffffff", 1, "0x0000000000000000"),
("0xffffffffffffffff", 2, "0x0000000000000001"),
("0xffffffffffffffff", -1, "0xfffffffffffffffe"),
("0xABCDEF1234567890", 1, "0xABCDEF1234567891"),
("0xabcdef1234567890", 1, "0xabcdef1234567891"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_increment_basic_octal_numbers() {
let tests = [
("0o0107", 1, "0o0110"),
("0o0110", -1, "0o0107"),
("0o0001", -1, "0o0000"),
("0o7777", 1, "0o10000"),
("0o1000", -1, "0o0777"),
("0o0107", 10, "0o0121"),
("0o0000", -1, "0o1777777777777777777777"),
("0o1777777777777777777777", 1, "0o0000000000000000000000"),
("0o1777777777777777777777", 2, "0o0000000000000000000001"),
("0o1777777777777777777777", -1, "0o1777777777777777777776"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_increment_basic_binary_numbers() {
let tests = [
("0b00000100", 1, "0b00000101"),
("0b00000100", -1, "0b00000011"),
("0b00000100", 2, "0b00000110"),
("0b00000100", -2, "0b00000010"),
("0b00000001", -1, "0b00000000"),
("0b00111111", 10, "0b01001001"),
("0b11111111", 1, "0b100000000"),
("0b10000000", -1, "0b01111111"),
(
"0b0000",
-1,
"0b1111111111111111111111111111111111111111111111111111111111111111",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
1,
"0b0000000000000000000000000000000000000000000000000000000000000000",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
2,
"0b0000000000000000000000000000000000000000000000000000000000000001",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
-1,
"0b1111111111111111111111111111111111111111111111111111111111111110",
),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
#[test]
fn test_increment_with_separators() {
let tests = [
("999_999", 1, "1_000_000"),
("1_000_000", -1, "999_999"),
("-999_999", -1, "-1_000_000"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"),
("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"),
("0b01111111_11111111", 1, "0b10000000_00000000"),
("0b11111111_11111111", 1, "0b1_00000000_00000000"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.increment(amount)
.1,
expected.into()
);
}
}
}

File diff suppressed because it is too large Load Diff

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