Merge branch 'master' of https://github.com/helix-editor/helix into tree_explore

pull/9/head
wongjiahau 1 year ago
commit 88ac941407

128
Cargo.lock generated

@ -73,6 +73,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
[[package]]
name = "bstr"
version = "1.3.0"
@ -143,9 +149,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.24"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"iana-time-zone",
"num-integer",
@ -210,7 +216,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"crossterm_winapi",
"futures-core",
"libc",
@ -254,7 +260,7 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn",
"syn 1.0.104",
]
[[package]]
@ -271,7 +277,7 @@ checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.104",
]
[[package]]
@ -444,15 +450,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
[[package]]
name = "futures-executor"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
dependencies = [
"futures-core",
"futures-task",
@ -461,15 +467,15 @@ dependencies = [
[[package]]
name = "futures-task"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879"
[[package]]
name = "futures-util"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
dependencies = [
"futures-core",
"futures-task",
@ -623,7 +629,7 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d4a4ba0531e46fe558459557a5b29fb86c3e4b2666c1c0861d93c7c678331"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"bstr",
"gix-path",
"libc",
@ -708,7 +714,7 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e43efd776bc543f46f0fd0ca3d920c37af71a764a16f2aebd89765e9ff2993"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"bstr",
]
@ -739,7 +745,7 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "546ee7855d5d8731288f05a63c07ab41b59cb406660a825ed3fe89d7223823df"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"bstr",
"btoi",
"filetime",
@ -923,7 +929,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8ffa5bf0772f9b01de501c035b6b084cf9b8bb07dec41e3afc6a17336a65f47"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"dirs",
"gix-path",
"libc",
@ -932,9 +938,9 @@ dependencies = [
[[package]]
name = "gix-tempfile"
version = "5.0.0"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bae41b5db7f085dc7acc54ed72c98853a6e5dabb355e95caa7b534f21b35c"
checksum = "aed73ef9642f779d609fd19acc332ac1597b978ee87ec11743a68eefaed65bfa"
dependencies = [
"libc",
"once_cell",
@ -1074,13 +1080,14 @@ version = "0.6.0"
dependencies = [
"ahash 0.8.3",
"arc-swap",
"bitflags",
"bitflags 2.0.2",
"chrono",
"encoding_rs",
"etcetera",
"hashbrown 0.13.2",
"helix-loader",
"imara-diff",
"indoc",
"log",
"once_cell",
"quickcheck",
@ -1197,7 +1204,7 @@ dependencies = [
name = "helix-tui"
version = "0.6.0"
dependencies = [
"bitflags",
"bitflags 2.0.2",
"cassowary",
"crossterm",
"helix-core",
@ -1229,7 +1236,7 @@ version = "0.6.0"
dependencies = [
"anyhow",
"arc-swap",
"bitflags",
"bitflags 2.0.2",
"chardetng",
"clipboard-win",
"crossterm",
@ -1461,7 +1468,7 @@ version = "0.94.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"serde",
"serde_json",
"serde_repr",
@ -1516,7 +1523,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"cfg-if",
"libc",
"static_assertions",
@ -1532,6 +1539,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
dependencies = [
"memchr",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@ -1619,9 +1635,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.47"
version = "1.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224"
dependencies = [
"unicode-ident",
]
@ -1638,7 +1654,7 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"memchr",
"unicase",
]
@ -1654,9 +1670,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.21"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [
"proc-macro2",
]
@ -1685,7 +1701,7 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
@ -1738,7 +1754,7 @@ version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
@ -1775,22 +1791,22 @@ checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "serde"
version = "1.0.155"
version = "1.0.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8"
checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.155"
version = "1.0.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630"
checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.4",
]
[[package]]
@ -1812,7 +1828,7 @@ checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.104",
]
[[package]]
@ -1952,6 +1968,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.4.0"
@ -2011,7 +2038,7 @@ checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.104",
]
[[package]]
@ -2104,7 +2131,7 @@ checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.104",
]
[[package]]
@ -2120,9 +2147,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6"
dependencies = [
"serde",
"serde_spanned",
@ -2141,15 +2168,15 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.6"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08de71aa0d6e348f070457f85af8bd566e2bc452156a423ddf22861b3a953fae"
checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5"
dependencies = [
"indexmap",
"nom8",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
@ -2281,7 +2308,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
"syn 1.0.104",
"wasm-bindgen-shared",
]
@ -2303,7 +2330,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 1.0.104",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2452,15 +2479,6 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winnow"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee7b2c67f962bf5042bfd8b6a916178df33a26eec343ae064cb8e069f638fa6f"
dependencies = [
"memchr",
]
[[package]]
name = "xtask"
version = "0.6.0"

@ -307,7 +307,7 @@ Example:
min-width = 1
```
#### `[editor.gutters.diagnotics]` Section
#### `[editor.gutters.diagnostics]` Section
Currently unused

@ -10,6 +10,7 @@
| c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` |
| cairo | ✓ | | | |
| capnp | ✓ | | ✓ | |
| clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
| comment | ✓ | | | |
@ -122,6 +123,7 @@
| scheme | ✓ | | | |
| scss | ✓ | | | `vscode-css-language-server` |
| slint | ✓ | | ✓ | `slint-lsp` |
| smithy | ✓ | | | `cs` |
| sml | ✓ | | | |
| solidity | ✓ | | | `solc` |
| sql | ✓ | | | |
@ -140,7 +142,7 @@
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
| ungrammar | ✓ | | | |
| uxntal | ✓ | | | |
| v | ✓ | | | `v` |
| v | ✓ | | | `v` |
| vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` |
| vhs | ✓ | | | |

@ -8,6 +8,7 @@
- [Fedora/RHEL](#fedorarhel)
- [Arch Linux community](#arch-linux-community)
- [NixOS](#nixos)
- [AppImage](#appimage)
- [macOS](#macos)
- [Homebrew Core](#homebrew-core)
- [Windows](#windows)
@ -87,6 +88,16 @@ 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.
### AppImage
Install Helix using [AppImage](https://appimage.org/).
Download 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

@ -306,6 +306,7 @@ These scopes are used for theming the editor interface:
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `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]) |

@ -29,7 +29,7 @@ tree-sitter = "0.20"
once_cell = "1.17"
arc-swap = "1"
regex = "1"
bitflags = "1.3"
bitflags = "2.0"
ahash = "0.8.3"
hashbrown = { version = "0.13.2", features = ["raw"] }
@ -49,3 +49,4 @@ textwrap = "0.16.0"
[dev-dependencies]
quickcheck = { version = "1", default-features = false }
indoc = "2.0.1"

@ -1474,7 +1474,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection =
selection.transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Move));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
@ -1497,7 +1497,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection =
selection.transform(|r| move_prev_paragraph(text.slice(..), r, 2, Movement::Move));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
@ -1520,7 +1520,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Extend));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
@ -1540,7 +1540,7 @@ mod test {
"a\nb\n\n#[goto\nthird\n\n|]#paragraph",
),
(
"a\nb#[\n|]#\ngoto\nsecond\n\nparagraph",
"a\nb#[\n|]#\n\ngoto\nsecond\n\nparagraph",
"a\nb#[\n\n|]#goto\nsecond\n\nparagraph",
),
(
@ -1562,7 +1562,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection =
selection.transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Move));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
@ -1585,7 +1585,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection =
selection.transform(|r| move_next_paragraph(text.slice(..), r, 2, Movement::Move));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
@ -1608,7 +1608,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Extend));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}

@ -1157,6 +1157,7 @@ impl Syntax {
bitflags! {
/// Flags that track the status of a layer
/// in the `Sytaxn::update` function
#[derive(Debug)]
struct LayerUpdateFlags : u32{
const MODIFIED = 0b001;
const MOVED = 0b010;

@ -1,7 +1,9 @@
//! Test helpers.
use crate::{Range, Selection};
use ropey::Rope;
use smallvec::SmallVec;
use std::cmp::Reverse;
use unicode_segmentation::UnicodeSegmentation;
/// Convert annotated test string to test string and selection.
///
@ -10,6 +12,10 @@ use std::cmp::Reverse;
/// `#[` for primary selection with head after anchor followed by `|]#`.
/// `#(` for secondary selection with head after anchor followed by `|)#`.
///
/// If the selection contains any LF or CRLF sequences, which are immediately
/// followed by the same grapheme, then the subsequent one is removed. This is
/// to allow representing having the cursor over the end of the line.
///
/// # Examples
///
/// ```
@ -30,23 +36,23 @@ use std::cmp::Reverse;
pub fn print(s: &str) -> (String, Selection) {
let mut primary_idx = None;
let mut ranges = SmallVec::new();
let mut iter = s.chars().peekable();
let mut iter = UnicodeSegmentation::graphemes(s, true).peekable();
let mut left = String::with_capacity(s.len());
'outer: while let Some(c) = iter.next() {
let start = left.chars().count();
if c != '#' {
left.push(c);
if c != "#" {
left.push_str(c);
continue;
}
let (is_primary, close_pair) = match iter.next() {
Some('[') => (true, ']'),
Some('(') => (false, ')'),
Some("[") => (true, "]"),
Some("(") => (false, ")"),
Some(ch) => {
left.push('#');
left.push(ch);
left.push_str(ch);
continue;
}
None => break,
@ -56,24 +62,45 @@ pub fn print(s: &str) -> (String, Selection) {
panic!("primary `#[` already appeared {:?} {:?}", left, s);
}
let head_at_beg = iter.next_if_eq(&'|').is_some();
let head_at_beg = iter.next_if_eq(&"|").is_some();
let last_grapheme = |s: &str| {
UnicodeSegmentation::graphemes(s, true)
.last()
.map(String::from)
};
while let Some(c) = iter.next() {
if !(c == close_pair && iter.peek() == Some(&'#')) {
left.push(c);
let next = iter.peek();
let mut prev = last_grapheme(left.as_str());
if !(c == close_pair && next == Some(&"#")) {
left.push_str(c);
continue;
}
if !head_at_beg {
let prev = left.pop().unwrap();
if prev != '|' {
left.push(prev);
left.push(c);
continue;
match &prev {
Some(p) if p != "|" => {
left.push_str(c);
continue;
}
Some(p) if p == "|" => {
left.pop().unwrap(); // pop the |
prev = last_grapheme(left.as_str());
}
_ => (),
}
}
iter.next(); // skip "#"
let next = iter.peek();
// skip explicit line end inside selection
if (prev == Some(String::from("\r\n")) || prev == Some(String::from("\n")))
&& next.map(|n| String::from(*n)) == prev
{
iter.next();
}
if is_primary {
primary_idx = Some(ranges.len());
@ -118,14 +145,16 @@ pub fn print(s: &str) -> (String, Selection) {
/// use smallvec::smallvec;
///
/// assert_eq!(
/// plain("abc", Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)),
/// plain("abc", &Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)),
/// "#[a|]#b#(|c)#".to_owned()
/// );
/// ```
pub fn plain(s: &str, selection: Selection) -> String {
pub fn plain<R: Into<Rope>>(s: R, selection: &Selection) -> String {
let s = s.into();
let primary = selection.primary_index();
let mut out = String::with_capacity(s.len() + 5 * selection.len());
out.push_str(s);
let mut out = String::with_capacity(s.len_bytes() + 5 * selection.len());
out.push_str(&s.to_string());
let mut insertion: Vec<_> = selection
.iter()
.enumerate()
@ -138,7 +167,9 @@ pub fn plain(s: &str, selection: Selection) -> String {
(false, false) => [(range.anchor, ")#"), (range.head, "#(|")],
}
})
.map(|(char_idx, marker)| (s.char_to_byte(char_idx), marker))
.collect();
// insert in reverse order
insertion.sort_unstable_by_key(|k| Reverse(k.0));
for (i, s) in insertion {
@ -262,4 +293,94 @@ mod test {
print("hello #[|👨‍👩‍👧‍👦]# goodbye")
);
}
#[test]
fn plain_single() {
assert_eq!("#[|h]#ello", plain("hello", &Selection::single(1, 0)));
assert_eq!("#[h|]#ello", plain("hello", &Selection::single(0, 1)));
assert_eq!("#[|hell]#o", plain("hello", &Selection::single(4, 0)));
assert_eq!("#[hell|]#o", plain("hello", &Selection::single(0, 4)));
assert_eq!("#[|hello]#", plain("hello", &Selection::single(5, 0)));
assert_eq!("#[hello|]#", plain("hello", &Selection::single(0, 5)));
}
#[test]
fn plain_multi() {
assert_eq!(
plain(
"hello",
&Selection::new(
SmallVec::from_slice(&[Range::new(1, 0), Range::new(5, 4)]),
0
)
),
String::from("#[|h]#ell#(|o)#")
);
assert_eq!(
plain(
"hello",
&Selection::new(
SmallVec::from_slice(&[Range::new(0, 1), Range::new(4, 5)]),
0
)
),
String::from("#[h|]#ell#(o|)#")
);
assert_eq!(
plain(
"hello",
&Selection::new(
SmallVec::from_slice(&[Range::new(2, 0), Range::new(5, 3)]),
0
)
),
String::from("#[|he]#l#(|lo)#")
);
assert_eq!(
plain(
"hello\r\nhello\r\nhello\r\n",
&Selection::new(
SmallVec::from_slice(&[
Range::new(7, 5),
Range::new(21, 19),
Range::new(14, 12)
]),
0
)
),
String::from("hello#[|\r\n]#hello#(|\r\n)#hello#(|\r\n)#")
);
}
#[test]
fn plain_multi_byte_code_point() {
assert_eq!(
plain("„“", &Selection::single(1, 0)),
String::from("#[|„]#“")
);
assert_eq!(
plain("„“", &Selection::single(2, 1)),
String::from("„#[|“]#")
);
assert_eq!(
plain("„“", &Selection::single(0, 1)),
String::from("#[„|]#“")
);
assert_eq!(
plain("„“", &Selection::single(1, 2)),
String::from("„#[“|]#")
);
assert_eq!(
plain("they said „hello“", &Selection::single(11, 10)),
String::from("they said #[|„]#hello“")
);
}
#[test]
fn plain_multi_code_point_grapheme() {
assert_eq!(
plain("hello 👨‍👩‍👧‍👦 goodbye", &Selection::single(13, 6)),
String::from("hello #[|👨‍👩‍👧‍👦]# goodbye")
);
}
}

@ -437,7 +437,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 1));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
@ -460,7 +460,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 2));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
@ -491,7 +491,7 @@ mod test {
let text = Rope::from(s.as_str());
let selection = selection
.transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Around, 1));
let actual = crate::test::plain(&s, selection);
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}

@ -318,6 +318,17 @@ impl Client {
inlay_hint: Some(lsp::InlayHintWorkspaceClientCapabilities {
refresh_support: Some(false),
}),
workspace_edit: Some(lsp::WorkspaceEditClientCapabilities {
document_changes: Some(true),
resource_operations: Some(vec![
lsp::ResourceOperationKind::Create,
lsp::ResourceOperationKind::Rename,
lsp::ResourceOperationKind::Delete,
]),
failure_handling: Some(lsp::FailureHandlingKind::Abort),
normalizes_line_endings: Some(false),
change_annotation_support: None,
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@ -387,6 +398,7 @@ impl Client {
..Default::default()
}),
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
version_support: Some(true),
..Default::default()
}),
inlay_hint: Some(lsp::InlayHintClientCapabilities {

@ -11,18 +11,18 @@ pub enum CaseChange {
}
#[derive(Debug, PartialEq, Eq)]
pub enum FormatItem<'a> {
Text(&'a str),
pub enum FormatItem {
Text(Tendril),
Capture(usize),
CaseChange(usize, CaseChange),
Conditional(usize, Option<&'a str>, Option<&'a str>),
Conditional(usize, Option<Tendril>, Option<Tendril>),
}
#[derive(Debug, PartialEq, Eq)]
pub struct Regex<'a> {
value: &'a str,
replacement: Vec<FormatItem<'a>>,
options: Option<&'a str>,
pub struct Regex {
value: Tendril,
replacement: Vec<FormatItem>,
options: Tendril,
}
#[derive(Debug, PartialEq, Eq)]
@ -36,14 +36,14 @@ pub enum SnippetElement<'a> {
},
Choice {
tabstop: usize,
choices: Vec<&'a str>,
choices: Vec<Tendril>,
},
Variable {
name: &'a str,
default: Option<&'a str>,
regex: Option<Regex<'a>>,
default: Option<Vec<SnippetElement<'a>>>,
regex: Option<Regex>,
},
Text(&'a str),
Text(Tendril),
}
#[derive(Debug, PartialEq, Eq)]
@ -67,25 +67,30 @@ fn render_elements(
for element in snippet_elements {
match element {
&Text(text) => {
Text(text) => {
// small optimization to avoid calling replace when it's unnecessary
let text = if text.contains('\n') {
Cow::Owned(text.replace('\n', newline_with_offset))
} else {
Cow::Borrowed(text)
Cow::Borrowed(text.as_str())
};
*offset += text.chars().count();
insert.push_str(&text);
}
&Variable {
Variable {
name: _,
regex: _,
r#default,
} => {
// TODO: variables. For now, fall back to the default, which defaults to "".
let text = r#default.unwrap_or_default();
*offset += text.chars().count();
insert.push_str(text);
render_elements(
r#default.as_deref().unwrap_or_default(),
insert,
offset,
tabstops,
newline_with_offset,
include_placeholer,
);
}
&Tabstop { tabstop } => {
tabstops.push((tabstop, (*offset, *offset)));
@ -160,6 +165,7 @@ pub fn render(
}
mod parser {
use helix_core::Tendril;
use helix_parsec::*;
use super::{CaseChange, FormatItem, Regex, Snippet, SnippetElement};
@ -190,28 +196,55 @@ mod parser {
fn var<'a>() -> impl Parser<'a, Output = &'a str> {
// var = [_a-zA-Z][_a-zA-Z0-9]*
move |input: &'a str| match input
.char_indices()
.take_while(|(p, c)| {
*c == '_'
|| if *p == 0 {
c.is_ascii_alphabetic()
} else {
c.is_ascii_alphanumeric()
}
})
.last()
{
Some((index, c)) if index >= 1 => {
let index = index + c.len_utf8();
Ok((&input[index..], &input[0..index]))
}
_ => Err(input),
move |input: &'a str| {
input
.char_indices()
.take_while(|(p, c)| {
*c == '_'
|| if *p == 0 {
c.is_ascii_alphabetic()
} else {
c.is_ascii_alphanumeric()
}
})
.last()
.map(|(index, c)| {
let index = index + c.len_utf8();
(&input[index..], &input[0..index])
})
.ok_or(input)
}
}
fn text<'a, const SIZE: usize>(cs: [char; SIZE]) -> impl Parser<'a, Output = &'a str> {
take_while(move |c| cs.into_iter().all(|c1| c != c1))
const TEXT_ESCAPE_CHARS: &[char] = &['\\', '}', '$'];
const CHOICE_TEXT_ESCAPE_CHARS: &[char] = &['\\', '|', ','];
fn text<'a>(
escape_chars: &'static [char],
term_chars: &'static [char],
) -> impl Parser<'a, Output = Tendril> {
move |input: &'a str| {
let mut chars = input.char_indices().peekable();
let mut res = Tendril::new();
while let Some((i, c)) = chars.next() {
match c {
'\\' => {
if let Some(&(_, c)) = chars.peek() {
if escape_chars.contains(&c) {
chars.next();
res.push(c);
continue;
}
}
res.push('\\');
}
c if term_chars.contains(&c) => return Ok((&input[i..], res)),
c => res.push(c),
}
}
Ok(("", res))
}
}
fn digit<'a>() -> impl Parser<'a, Output = usize> {
@ -228,7 +261,7 @@ mod parser {
)
}
fn format<'a>() -> impl Parser<'a, Output = FormatItem<'a>> {
fn format<'a>() -> impl Parser<'a, Output = FormatItem> {
use FormatItem::*;
choice!(
@ -242,7 +275,7 @@ mod parser {
}),
// '${' int ':+' if '}'
map(
seq!("${", digit(), ":+", take_until(|c| c == '}'), "}"),
seq!("${", digit(), ":+", text(TEXT_ESCAPE_CHARS, &['}']), "}"),
|seq| { Conditional(seq.1, Some(seq.3), None) }
),
// '${' int ':?' if ':' else '}'
@ -251,9 +284,9 @@ mod parser {
"${",
digit(),
":?",
take_until(|c| c == ':'),
text(TEXT_ESCAPE_CHARS, &[':']),
":",
take_until(|c| c == '}'),
text(TEXT_ESCAPE_CHARS, &['}']),
"}"
),
|seq| { Conditional(seq.1, Some(seq.3), Some(seq.5)) }
@ -265,7 +298,7 @@ mod parser {
digit(),
":",
optional("-"),
take_until(|c| c == '}'),
text(TEXT_ESCAPE_CHARS, &['}']),
"}"
),
|seq| { Conditional(seq.1, None, Some(seq.4)) }
@ -273,21 +306,24 @@ mod parser {
)
}
fn regex<'a>() -> impl Parser<'a, Output = Regex<'a>> {
let text = map(text(['$', '/']), FormatItem::Text);
let replacement = reparse_as(
take_until(|c| c == '/'),
one_or_more(choice!(format(), text)),
);
fn regex<'a>() -> impl Parser<'a, Output = Regex> {
map(
seq!(
"/",
take_until(|c| c == '/'),
// TODO parse as ECMAScript and convert to rust regex
text(&['/'], &['/']),
"/",
replacement,
zero_or_more(choice!(
format(),
// text doesn't parse $, if format fails we just accept the $ as text
map("$", |_| FormatItem::Text("$".into())),
map(text(&['\\', '/'], &['/', '$']), FormatItem::Text),
)),
"/",
optional(take_until(|c| c == '}')),
// vscode really doesn't allow escaping } here
// so it's impossible to write a regex escape containing a }
// we can consider deviating here and allowing the escape
text(&[], &['}']),
),
|(_, value, _, replacement, _, options)| Regex {
value,
@ -308,13 +344,17 @@ mod parser {
}
fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
let text = map(text(['$', '}']), SnippetElement::Text);
map(
seq!(
"${",
digit(),
":",
one_or_more(choice!(anything(), text)),
// according to the grammar there is just a single anything here.
// However in the prose it is explained that placeholders can be nested.
// The example there contains both a placeholder text and a nested placeholder
// which indicates a list. Looking at the VSCode sourcecode, the placeholder
// is indeed parsed as zero_or_more so the grammar is simply incorrect here
zero_or_more(anything(TEXT_ESCAPE_CHARS, true)),
"}"
),
|seq| SnippetElement::Placeholder {
@ -330,7 +370,7 @@ mod parser {
"${",
digit(),
"|",
sep(take_until(|c| c == ',' || c == '|'), ","),
sep(text(CHOICE_TEXT_ESCAPE_CHARS, &['|', ',']), ","),
"|}",
),
|seq| SnippetElement::Choice {
@ -348,9 +388,21 @@ mod parser {
default: None,
regex: None,
}),
// ${var}
map(seq!("${", var(), "}",), |values| SnippetElement::Variable {
name: values.1,
default: None,
regex: None,
}),
// ${var:default}
map(
seq!("${", var(), ":", take_until(|c| c == '}'), "}",),
seq!(
"${",
var(),
":",
zero_or_more(anything(TEXT_ESCAPE_CHARS, true)),
"}",
),
|values| SnippetElement::Variable {
name: values.1,
default: Some(values.3),
@ -368,23 +420,38 @@ mod parser {
)
}
fn anything<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
// The parser has to be constructed lazily to avoid infinite opaque type recursion
|input: &'a str| {
let parser = choice!(tabstop(), placeholder(), choice(), variable());
fn anything<'a>(
escape_chars: &'static [char],
end_at_brace: bool,
) -> impl Parser<'a, Output = SnippetElement<'a>> {
let term_chars: &[_] = if end_at_brace { &['$', '}'] } else { &['$'] };
move |input: &'a str| {
let parser = choice!(
tabstop(),
placeholder(),
choice(),
variable(),
map("$", |_| SnippetElement::Text("$".into())),
map(text(escape_chars, term_chars), SnippetElement::Text),
);
parser.parse(input)
}
}
fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> {
let text = map(text(['$']), SnippetElement::Text);
map(one_or_more(choice!(anything(), text)), |parts| Snippet {
elements: parts,
map(one_or_more(anything(TEXT_ESCAPE_CHARS, false)), |parts| {
Snippet { elements: parts }
})
}
pub fn parse(s: &str) -> Result<Snippet, &str> {
snippet().parse(s).map(|(_input, elements)| elements)
snippet().parse(s).and_then(|(remainder, snippet)| {
if remainder.is_empty() {
Ok(snippet)
} else {
Err(remainder)
}
})
}
#[cfg(test)]
@ -402,32 +469,59 @@ mod parser {
assert_eq!(
Ok(Snippet {
elements: vec![
Text("match("),
Text("match(".into()),
Placeholder {
tabstop: 1,
value: vec!(Text("Arg1")),
value: vec!(Text("Arg1".into())),
},
Text(")")
Text(")".into())
]
}),
parse("match(${1:Arg1})")
)
}
#[test]
fn unterminated_placeholder() {
assert_eq!(
Ok(Snippet {
elements: vec![Text("match(".into()), Text("$".into()), Text("{1:)".into())]
}),
parse("match(${1:)")
)
}
#[test]
fn parse_empty_placeholder() {
assert_eq!(
Ok(Snippet {
elements: vec![
Text("match(".into()),
Placeholder {
tabstop: 1,
value: vec![],
},
Text(")".into())
]
}),
parse("match(${1:})")
)
}
#[test]
fn parse_placeholders_in_statement() {
assert_eq!(
Ok(Snippet {
elements: vec![
Text("local "),
Text("local ".into()),
Placeholder {
tabstop: 1,
value: vec!(Text("var")),
value: vec!(Text("var".into())),
},
Text(" = "),
Text(" = ".into()),
Placeholder {
tabstop: 1,
value: vec!(Text("value")),
value: vec!(Text("value".into())),
},
]
}),
@ -441,7 +535,7 @@ mod parser {
Ok(Snippet {
elements: vec![Placeholder {
tabstop: 1,
value: vec!(Text("var, "), Tabstop { tabstop: 2 },),
value: vec!(Text("var, ".into()), Tabstop { tabstop: 2 },),
},]
}),
parse("${1:var, $2}")
@ -455,10 +549,10 @@ mod parser {
elements: vec![Placeholder {
tabstop: 1,
value: vec!(
Text("foo "),
Text("foo ".into()),
Placeholder {
tabstop: 2,
value: vec!(Text("bar")),
value: vec!(Text("bar".into())),
},
),
},]
@ -472,27 +566,27 @@ mod parser {
assert_eq!(
Ok(Snippet {
elements: vec![
Text("hello "),
Text("hello ".into()),
Tabstop { tabstop: 1 },
Tabstop { tabstop: 2 },
Text(" "),
Text(" ".into()),
Choice {
tabstop: 1,
choices: vec!["one", "two", "three"]
choices: vec!["one".into(), "two".into(), "three".into()]
},
Text(" "),
Text(" ".into()),
Variable {
name: "name",
default: Some("foo"),
default: Some(vec![Text("foo".into())]),
regex: None
},
Text(" "),
Text(" ".into()),
Variable {
name: "var",
default: None,
regex: None
},
Text(" "),
Text(" ".into()),
Variable {
name: "TM",
default: None,
@ -512,14 +606,405 @@ mod parser {
name: "TM_FILENAME",
default: None,
regex: Some(Regex {
value: "(.*).+$",
replacement: vec![FormatItem::Capture(1)],
options: None,
value: "(.*).+$".into(),
replacement: vec![FormatItem::Capture(1), FormatItem::Text("$".into())],
options: Tendril::new(),
}),
}]
}),
parse("${TM_FILENAME/(.*).+$/$1/}")
parse("${TM_FILENAME/(.*).+$/$1$/}")
);
}
#[test]
fn rust_macro() {
assert_eq!(
Ok(Snippet {
elements: vec![
Text("macro_rules! ".into()),
Tabstop { tabstop: 1 },
Text(" {\n (".into()),
Tabstop { tabstop: 2 },
Text(") => {\n ".into()),
Tabstop { tabstop: 0 },
Text("\n };\n}".into())
]
}),
parse("macro_rules! $1 {\n ($2) => {\n $0\n };\n}")
);
}
fn assert_text(snippet: &str, parsed_text: &str) {
let res = parse(snippet).unwrap();
let text = crate::snippet::render(&res, "\n", true).0;
assert_eq!(text, parsed_text)
}
#[test]
fn robust_parsing() {
assert_text("$", "$");
assert_text("\\\\$", "\\$");
assert_text("{", "{");
assert_text("\\}", "}");
assert_text("\\abc", "\\abc");
assert_text("foo${f:\\}}bar", "foo}bar");
assert_text("\\{", "\\{");
assert_text("I need \\\\\\$", "I need \\$");
assert_text("\\", "\\");
assert_text("\\{{", "\\{{");
assert_text("{{", "{{");
assert_text("{{dd", "{{dd");
assert_text("}}", "}}");
assert_text("ff}}", "ff}}");
assert_text("farboo", "farboo");
assert_text("far{{}}boo", "far{{}}boo");
assert_text("far{{123}}boo", "far{{123}}boo");
assert_text("far\\{{123}}boo", "far\\{{123}}boo");
assert_text("far{{id:bern}}boo", "far{{id:bern}}boo");
assert_text("far{{id:bern {{basel}}}}boo", "far{{id:bern {{basel}}}}boo");
assert_text(
"far{{id:bern {{id:basel}}}}boo",
"far{{id:bern {{id:basel}}}}boo",
);
assert_text(
"far{{id:bern {{id2:basel}}}}boo",
"far{{id:bern {{id2:basel}}}}boo",
);
assert_text("${}$\\a\\$\\}\\\\", "${}$\\a$}\\");
assert_text("farboo", "farboo");
assert_text("far{{}}boo", "far{{}}boo");
assert_text("far{{123}}boo", "far{{123}}boo");
assert_text("far\\{{123}}boo", "far\\{{123}}boo");
assert_text("far`123`boo", "far`123`boo");
assert_text("far\\`123\\`boo", "far\\`123\\`boo");
assert_text("\\$far-boo", "$far-boo");
}
fn assert_snippet(snippet: &str, expect: &[SnippetElement]) {
let parsed_snippet = parse(snippet).unwrap();
assert_eq!(parsed_snippet.elements, expect.to_owned())
}
#[test]
fn parse_variable() {
use SnippetElement::*;
assert_snippet(
"$far-boo",
&[
Variable {
name: "far",
default: None,
regex: None,
},
Text("-boo".into()),
],
);
assert_snippet(
"far$farboo",
&[
Text("far".into()),
Variable {
name: "farboo",
regex: None,
default: None,
},
],
);
assert_snippet(
"far${farboo}",
&[
Text("far".into()),
Variable {
name: "farboo",
regex: None,
default: None,
},
],
);
assert_snippet("$123", &[Tabstop { tabstop: 123 }]);
assert_snippet(
"$farboo",
&[Variable {
name: "farboo",
regex: None,
default: None,
}],
);
assert_snippet(
"$far12boo",
&[Variable {
name: "far12boo",
regex: None,
default: None,
}],
);
assert_snippet(
"000_${far}_000",
&[
Text("000_".into()),
Variable {
name: "far",
regex: None,
default: None,
},
Text("_000".into()),
],
);
}
#[test]
fn parse_variable_transform() {
assert_snippet(
"${foo///}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: Tendril::new(),
replacement: Vec::new(),
options: Tendril::new(),
}),
default: None,
}],
);
assert_snippet(
"${foo/regex/format/gmi}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: "regex".into(),
replacement: vec![FormatItem::Text("format".into())],
options: "gmi".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/([A-Z][a-z])/format/}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: "([A-Z][a-z])".into(),
replacement: vec![FormatItem::Text("format".into())],
options: Tendril::new(),
}),
default: None,
}],
);
// invalid regex TODO: reneable tests once we actually parse this regex flavour
// assert_text(
// "${foo/([A-Z][a-z])/format/GMI}",
// "${foo/([A-Z][a-z])/format/GMI}",
// );
// assert_text(
// "${foo/([A-Z][a-z])/format/funky}",
// "${foo/([A-Z][a-z])/format/funky}",
// );
// assert_text("${foo/([A-Z][a-z]/format/}", "${foo/([A-Z][a-z]/format/}");
assert_text(
"${foo/regex\\/format/options}",
"${foo/regex\\/format/options}",
);
// tricky regex
assert_snippet(
"${foo/m\\/atch/$1/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: "m/atch".into(),
replacement: vec![FormatItem::Capture(1)],
options: "i".into(),
}),
default: None,
}],
);
// incomplete
assert_text("${foo///", "${foo///");
assert_text("${foo/regex/format/options", "${foo/regex/format/options");
// format string
assert_snippet(
"${foo/.*/${0:fooo}/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![FormatItem::Conditional(0, None, Some("fooo".into()))],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/.*/${1}/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![FormatItem::Capture(1)],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/.*/$1/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![FormatItem::Capture(1)],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/.*/This-$1-encloses/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![
FormatItem::Text("This-".into()),
FormatItem::Capture(1),
FormatItem::Text("-encloses".into()),
],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/.*/complex${1:else}/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![
FormatItem::Text("complex".into()),
FormatItem::Conditional(1, None, Some("else".into())),
],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/.*/complex${1:-else}/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![
FormatItem::Text("complex".into()),
FormatItem::Conditional(1, None, Some("else".into())),
],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/.*/complex${1:+if}/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![
FormatItem::Text("complex".into()),
FormatItem::Conditional(1, Some("if".into()), None),
],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/.*/complex${1:?if:else}/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![
FormatItem::Text("complex".into()),
FormatItem::Conditional(1, Some("if".into()), Some("else".into())),
],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${foo/.*/complex${1:/upcase}/i}",
&[Variable {
name: "foo",
regex: Some(Regex {
value: ".*".into(),
replacement: vec![
FormatItem::Text("complex".into()),
FormatItem::CaseChange(1, CaseChange::Upcase),
],
options: "i".into(),
}),
default: None,
}],
);
assert_snippet(
"${TM_DIRECTORY/src\\//$1/}",
&[Variable {
name: "TM_DIRECTORY",
regex: Some(Regex {
value: "src/".into(),
replacement: vec![FormatItem::Capture(1)],
options: Tendril::new(),
}),
default: None,
}],
);
assert_snippet(
"${TM_SELECTED_TEXT/a/\\/$1/g}",
&[Variable {
name: "TM_SELECTED_TEXT",
regex: Some(Regex {
value: "a".into(),
replacement: vec![FormatItem::Text("/".into()), FormatItem::Capture(1)],
options: "g".into(),
}),
default: None,
}],
);
assert_snippet(
"${TM_SELECTED_TEXT/a/in\\/$1ner/g}",
&[Variable {
name: "TM_SELECTED_TEXT",
regex: Some(Regex {
value: "a".into(),
replacement: vec![
FormatItem::Text("in/".into()),
FormatItem::Capture(1),
FormatItem::Text("ner".into()),
],
options: "g".into(),
}),
default: None,
}],
);
assert_snippet(
"${TM_SELECTED_TEXT/a/end\\//g}",
&[Variable {
name: "TM_SELECTED_TEXT",
regex: Some(Regex {
value: "a".into(),
replacement: vec![FormatItem::Text("end/".into())],
options: "g".into(),
}),
default: None,
}],
);
}
// TODO port more tests from https://github.com/microsoft/vscode/blob/dce493cb6e36346ef2714e82c42ce14fc461b15c/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts
}
}

@ -459,6 +459,7 @@ pub fn zero_or_more<'a, P, T>(parser: P) -> impl Parser<'a, Output = Vec<T>>
where
P: Parser<'a, Output = T>,
{
let parser = non_empty(parser);
move |mut input| {
let mut values = Vec::new();
@ -491,6 +492,7 @@ pub fn one_or_more<'a, P, T>(parser: P) -> impl Parser<'a, Output = Vec<T>>
where
P: Parser<'a, Output = T>,
{
let parser = non_empty(parser);
move |mut input| {
let mut values = Vec::new();
@ -559,3 +561,14 @@ where
Ok((input, values))
}
}
pub fn non_empty<'a, T>(p: impl Parser<'a, Output = T>) -> impl Parser<'a, Output = T> {
move |input| {
let (new_input, res) = p.parse(input)?;
if new_input.len() == input.len() {
Err(input)
} else {
Ok((new_input, res))
}
}
}

@ -709,7 +709,16 @@ impl Application {
return;
}
};
let doc = self.editor.document_by_path_mut(&path);
let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
if let Some(version) = params.version {
if version != doc.version() {
log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
return false;
}
}
true
});
if let Some(doc) = doc {
let lang_conf = doc.language_config();
@ -990,16 +999,19 @@ impl Application {
Ok(serde_json::Value::Null)
}
Ok(MethodCall::ApplyWorkspaceEdit(params)) => {
apply_workspace_edit(
let res = apply_workspace_edit(
&mut self.editor,
helix_lsp::OffsetEncoding::Utf8,
&params.edit,
);
Ok(json!(lsp::ApplyWorkspaceEditResponse {
applied: true,
failure_reason: None,
failed_change: None,
applied: res.is_ok(),
failure_reason: res.as_ref().err().map(|err| err.kind.to_string()),
failed_change: res
.as_ref()
.err()
.map(|err| err.failed_change_idx as u32),
}))
}
Ok(MethodCall::WorkspaceFolders) => {

@ -651,7 +651,7 @@ pub fn code_action(cx: &mut Context) {
log::debug!("code action: {:?}", code_action);
if let Some(ref workspace_edit) = code_action.edit {
log::debug!("edit: {:?}", workspace_edit);
apply_workspace_edit(editor, offset_encoding, workspace_edit);
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
// if code action provides both edit and command first the edit
@ -757,19 +757,50 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
}
}
#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,
pub failed_change_idx: usize,
}
#[derive(Debug)]
pub enum ApplyEditErrorKind {
DocumentChanged,
FileNotFound,
UnknownURISchema,
IoError(std::io::Error),
// TODO: check edits before applying and propagate failure
// InvalidEdit,
}
impl ToString for ApplyEditErrorKind {
fn to_string(&self) -> String {
match self {
ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(),
ApplyEditErrorKind::FileNotFound => "file not found".to_string(),
ApplyEditErrorKind::UnknownURISchema => "URI schema not supported".to_string(),
ApplyEditErrorKind::IoError(err) => err.to_string(),
}
}
}
///TODO make this transactional (and set failureMode to transactional)
pub fn apply_workspace_edit(
editor: &mut Editor,
offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit,
) {
let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec<lsp::TextEdit>| {
) -> Result<(), ApplyEditError> {
let mut apply_edits = |uri: &helix_lsp::Url,
version: Option<i32>,
text_edits: Vec<lsp::TextEdit>|
-> Result<(), ApplyEditErrorKind> {
let path = match uri.to_file_path() {
Ok(path) => path,
Err(_) => {
let err = format!("unable to convert URI to filepath: {}", uri);
log::error!("{}", err);
editor.set_error(err);
return;
return Err(ApplyEditErrorKind::UnknownURISchema);
}
};
@ -780,11 +811,19 @@ pub fn apply_workspace_edit(
let err = format!("failed to open document: {}: {}", uri, err);
log::error!("{}", err);
editor.set_error(err);
return;
return Err(ApplyEditErrorKind::FileNotFound);
}
};
let doc = doc_mut!(editor, &doc_id);
if let Some(version) = version {
if version != doc.version() {
let err = format!("outdated workspace edit for {path:?}");
log::error!("{err}, expected {} but got {version}", doc.version());
editor.set_error(err);
return Err(ApplyEditErrorKind::DocumentChanged);
}
}
// Need to determine a view for apply/append_changes_to_history
let selections = doc.selections();
@ -808,31 +847,13 @@ pub fn apply_workspace_edit(
let view = view_mut!(editor, view_id);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
Ok(())
};
if let Some(ref changes) = workspace_edit.changes {
log::debug!("workspace changes: {:?}", changes);
for (uri, text_edits) in changes {
let text_edits = text_edits.to_vec();
apply_edits(uri, text_edits)
}
return;
// Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
// TODO: find some example that uses workspace changes, and test it
// for (url, edits) in changes.iter() {
// let file_path = url.origin().ascii_serialization();
// let file_path = std::path::PathBuf::from(file_path);
// let file = std::fs::File::open(file_path).unwrap();
// let mut text = Rope::from_reader(file).unwrap();
// let transaction = edits_to_changes(&text, edits);
// transaction.apply(&mut text);
// }
}
if let Some(ref document_changes) = workspace_edit.document_changes {
match document_changes {
lsp::DocumentChanges::Edits(document_edits) => {
for document_edit in document_edits {
for (i, document_edit) in document_edits.iter().enumerate() {
let edits = document_edit
.edits
.iter()
@ -844,15 +865,26 @@ pub fn apply_workspace_edit(
})
.cloned()
.collect();
apply_edits(&document_edit.text_document.uri, edits);
apply_edits(
&document_edit.text_document.uri,
document_edit.text_document.version,
edits,
)
.map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
lsp::DocumentChanges::Operations(operations) => {
log::debug!("document changes - operations: {:?}", operations);
for operation in operations {
for (i, operation) in operations.iter().enumerate() {
match operation {
lsp::DocumentChangeOperation::Op(op) => {
apply_document_resource_op(op).unwrap();
apply_document_resource_op(op).map_err(|io| ApplyEditError {
kind: ApplyEditErrorKind::IoError(io),
failed_change_idx: i,
})?;
}
lsp::DocumentChangeOperation::Edit(document_edit) => {
@ -867,13 +899,36 @@ pub fn apply_workspace_edit(
})
.cloned()
.collect();
apply_edits(&document_edit.text_document.uri, edits);
apply_edits(
&document_edit.text_document.uri,
document_edit.text_document.version,
edits,
)
.map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
}
}
}
return Ok(());
}
if let Some(ref changes) = workspace_edit.changes {
log::debug!("workspace changes: {:?}", changes);
for (i, (uri, text_edits)) in changes.iter().enumerate() {
let text_edits = text_edits.to_vec();
apply_edits(uri, None, text_edits).map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
Ok(())
}
fn goto_impl(
@ -1297,7 +1352,9 @@ pub fn rename_symbol(cx: &mut Context) {
}
};
match block_on(future) {
Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
Ok(edits) => {
let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
}
Err(err) => cx.editor.set_error(err.to_string()),
}
},

@ -2060,18 +2060,14 @@ fn run_shell_command(
return Ok(());
}
let shell = &cx.editor.config().shell;
let (output, success) = shell_impl(shell, &args.join(" "), None)?;
if success {
cx.editor.set_status("Command succeeded");
} else {
cx.editor.set_error("Command failed");
}
let shell = cx.editor.config().shell.clone();
let args = args.join(" ");
if !output.is_empty() {
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let callback = async move {
let (output, success) = shell_impl_async(&shell, &args, None).await?;
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
if !output.is_empty() {
let contents = ui::Markdown::new(
format!("```sh\n{}\n```", output),
editor.syn_loader.clone(),
@ -2080,13 +2076,17 @@ fn run_shell_command(
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
));
compositor.replace_or_push("shell", popup);
},
));
Ok(call)
};
cx.jobs.callback(callback);
}
}
if success {
editor.set_status("Command succeeded");
} else {
editor.set_error("Command failed");
}
},
));
Ok(call)
};
cx.jobs.callback(callback);
Ok(())
}

@ -287,9 +287,11 @@ pub fn render_text<'t>(
style_span.0
};
let virt = grapheme.is_virtual();
renderer.draw_grapheme(
grapheme.grapheme,
grapheme_style,
virt,
&mut last_line_indent_level,
&mut is_in_indent_area,
pos,
@ -313,6 +315,7 @@ pub struct TextRenderer<'a> {
pub nbsp: String,
pub space: String,
pub tab: String,
pub virtual_tab: String,
pub indent_width: u16,
pub starting_indent: usize,
pub draw_indent_guides: bool,
@ -342,6 +345,7 @@ impl<'a> TextRenderer<'a> {
} else {
" ".repeat(tab_width)
};
let virtual_tab = " ".repeat(tab_width);
let newline = if ws_render.newline() == WhitespaceRenderValue::All {
ws_chars.newline.into()
} else {
@ -370,6 +374,7 @@ impl<'a> TextRenderer<'a> {
nbsp,
space,
tab,
virtual_tab,
whitespace_style: theme.get("ui.virtual.whitespace"),
indent_width,
starting_indent: col_offset / indent_width as usize
@ -392,6 +397,7 @@ impl<'a> TextRenderer<'a> {
&mut self,
grapheme: Grapheme,
mut style: Style,
is_virtual: bool,
last_indent_level: &mut usize,
is_in_indent_area: &mut bool,
position: Position,
@ -405,14 +411,21 @@ impl<'a> TextRenderer<'a> {
}
let width = grapheme.width();
let space = if is_virtual { " " } else { &self.space };
let nbsp = if is_virtual { " " } else { &self.nbsp };
let tab = if is_virtual {
&self.virtual_tab
} else {
&self.tab
};
let grapheme = match grapheme {
Grapheme::Tab { width } => {
let grapheme_tab_width = char_to_byte_idx(&self.tab, width);
&self.tab[..grapheme_tab_width]
let grapheme_tab_width = char_to_byte_idx(tab, width);
&tab[..grapheme_tab_width]
}
// TODO special rendering for other whitespaces?
Grapheme::Other { ref g } if g == " " => &self.space,
Grapheme::Other { ref g } if g == "\u{00A0}" => &self.nbsp,
Grapheme::Other { ref g } if g == " " => space,
Grapheme::Other { ref g } if g == "\u{00A0}" => nbsp,
Grapheme::Other { ref g } => g,
Grapheme::Newline => &self.newline,
};

@ -11,6 +11,7 @@ use crate::{
};
use helix_core::{
diagnostic::NumberOrString,
graphemes::{
ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
},
@ -30,7 +31,7 @@ use helix_view::{
};
use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::buffer::Buffer as Surface;
use tui::{buffer::Buffer as Surface, text::Span};
use super::statusline;
use super::{document::LineDecoration, lsp::SignatureHelp};
@ -686,6 +687,14 @@ impl EditorView {
});
let text = Text::styled(&diagnostic.message, style);
lines.extend(text.lines);
let code = diagnostic.code.as_ref().map(|x| match x {
NumberOrString::Number(n) => format!("({n})"),
NumberOrString::String(s) => format!("({s})"),
});
if let Some(code) = code {
let span = Span::styled(code, style);
lines.push(span.into());
}
}
let paragraph = Paragraph::new(lines)

@ -812,7 +812,10 @@ impl<T: Item + 'static> Component for Picker<T> {
for cell in row.cells.iter_mut() {
let spans = match cell.content.lines.get(0) {
Some(s) => s,
None => continue,
None => {
cell_start_byte_offset += TEMP_CELL_SEP.len();
continue;
}
};
let mut cell_len = 0;

@ -4,8 +4,8 @@ mod test {
use std::path::PathBuf;
use helix_core::{syntax::AutoPairConfig, Position, Selection};
use helix_term::{args::Args, config::Config};
use helix_core::{syntax::AutoPairConfig, Selection};
use helix_term::config::Config;
use indoc::indoc;
@ -23,5 +23,4 @@ mod test {
mod movement;
mod prompt;
mod splits;
mod write;
}

@ -3,22 +3,16 @@ use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn auto_indent_c() -> anyhow::Result<()> {
test_with_config(
Args {
files: vec![(PathBuf::from("foo.c"), Position::default())],
..Default::default()
},
helpers::test_config(),
helpers::test_syntax_conf(None),
AppBuilder::new().with_file("foo.c", None),
// switches to append mode?
(
helpers::platform_line("void foo() {#[|}]#").as_ref(),
helpers::platform_line("void foo() {#[|}]#"),
"i<ret><esc>",
helpers::platform_line(indoc! {"\
void foo() {
#[|\n]#\
}
"})
.as_ref(),
"}),
),
)
.await?;

@ -41,9 +41,7 @@ async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> {
for (open, close) in pairs.iter() {
test_with_config(
Args::default(),
config.clone(),
helpers::test_syntax_conf(None),
AppBuilder::new().with_config(config.clone()),
(
format!("#[{}|]#", LINE_END),
format!("i{}", open),
@ -53,9 +51,7 @@ async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> {
.await?;
test_with_config(
Args::default(),
config.clone(),
helpers::test_syntax_conf(None),
AppBuilder::new().with_config(config.clone()),
(
format!("{}#[{}|]#{}", open, close, LINE_END),
format!("i{}", close),
@ -170,15 +166,13 @@ async fn insert_before_eol() -> anyhow::Result<()> {
async fn insert_auto_pairs_disabled() -> anyhow::Result<()> {
for pair in DEFAULT_PAIRS {
test_with_config(
Args::default(),
Config {
AppBuilder::new().with_config(Config {
editor: helix_view::editor::Config {
auto_pairs: AutoPairConfig::Enable(false),
..Default::default()
},
..Default::default()
},
helpers::test_syntax_conf(None),
}),
(
format!("#[{}|]#", LINE_END),
format!("i{}", pair.0),

@ -1,99 +1,8 @@
use std::ops::RangeInclusive;
use helix_core::diagnostic::Severity;
use helix_term::application::Application;
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_write_quit_fail() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence(
&mut app,
Some("ihello<esc>:wq<ret>"),
Some(&|app| {
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path));
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
}),
false,
)
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
test_key_sequences(
&mut helpers::AppBuilder::new().build()?,
vec![
(
None,
Some(&|app| {
assert_eq!(1, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
(
Some("ihello<esc>:new<ret>"),
Some(&|app| {
assert_eq!(2, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
(
Some(":buffer<minus>close<ret>"),
Some(&|app| {
assert_eq!(1, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
],
false,
)
.await?;
// verify if writes are queued up, it finishes them before closing the buffer
let mut file = tempfile::NamedTempFile::new()?;
let mut command = String::new();
const RANGE: RangeInclusive<i32> = 1..=1000;
for i in RANGE {
let cmd = format!("%c{}<esc>:w!<ret>", i);
command.push_str(&cmd);
}
command.push_str(":buffer<minus>close<ret>");
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence(
&mut app,
Some(&command),
Some(&|app| {
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
let doc = app.editor.document_by_path(file.path());
assert!(doc.is_none(), "found doc: {:?}", doc);
}),
false,
)
.await?;
helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?;
Ok(())
}
mod write;
#[tokio::test(flavor = "multi_thread")]
async fn test_selection_duplication() -> anyhow::Result<()> {
@ -292,12 +201,12 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
.as_str(),
"|echo foo<ret>",
platform_line(indoc! {"\
#[|foo
]#
#(|foo
)#
#(|foo
)#
#[|foo\n]#
#(|foo\n)#
#(|foo\n)#
"})
.as_str(),
))
@ -313,12 +222,12 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
.as_str(),
"!echo foo<ret>",
platform_line(indoc! {"\
#[|foo
]#lorem
#(|foo
)#ipsum
#(|foo
)#dolor
#[|foo\n]#
lorem
#(|foo\n)#
ipsum
#(|foo\n)#
dolor
"})
.as_str(),
))
@ -334,12 +243,12 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
.as_str(),
"<A-!>echo foo<ret>",
platform_line(indoc! {"\
lorem#[|foo
]#
ipsum#(|foo
)#
dolor#(|foo
)#
lorem#[|foo\n]#
ipsum#(|foo\n)#
dolor#(|foo\n)#
"})
.as_str(),
))
@ -391,8 +300,8 @@ async fn test_extend_line() -> anyhow::Result<()> {
platform_line(indoc! {"\
#[lorem
ipsum
dolor
|]#
dolor\n|]#
"})
.as_str(),
))
@ -409,8 +318,8 @@ async fn test_extend_line() -> anyhow::Result<()> {
"2x",
platform_line(indoc! {"\
#[lorem
ipsum
|]#
ipsum\n|]#
"})
.as_str(),
))

@ -1,5 +1,5 @@
use std::{
io::{Read, Seek, SeekFrom, Write},
io::{Read, Seek, Write},
ops::RangeInclusive,
};
@ -8,6 +8,96 @@ use helix_view::doc;
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_write_quit_fail() -> anyhow::Result<()> {
let file = helpers::new_readonly_tempfile()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence(
&mut app,
Some("ihello<esc>:wq<ret>"),
Some(&|app| {
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path));
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
}),
false,
)
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
test_key_sequences(
&mut helpers::AppBuilder::new().build()?,
vec![
(
None,
Some(&|app| {
assert_eq!(1, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
(
Some("ihello<esc>:new<ret>"),
Some(&|app| {
assert_eq!(2, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
(
Some(":buffer<minus>close<ret>"),
Some(&|app| {
assert_eq!(1, app.editor.documents().count());
assert!(!app.editor.is_err());
}),
),
],
false,
)
.await?;
// verify if writes are queued up, it finishes them before closing the buffer
let mut file = tempfile::NamedTempFile::new()?;
let mut command = String::new();
const RANGE: RangeInclusive<i32> = 1..=1000;
for i in RANGE {
let cmd = format!("%c{}<esc>:w!<ret>", i);
command.push_str(&cmd);
}
command.push_str(":buffer<minus>close<ret>");
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
test_key_sequence(
&mut app,
Some(&command),
Some(&|app| {
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
let doc = app.editor.document_by_path(file.path());
assert!(doc.is_none(), "found doc: {:?}", doc);
}),
false,
)
.await?;
helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_write() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
@ -57,7 +147,7 @@ async fn test_overwrite_protection() -> anyhow::Result<()> {
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
file.seek(SeekFrom::Start(0))?;
file.rewind()?;
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;

@ -9,7 +9,7 @@ use anyhow::bail;
use crossterm::event::{Event, KeyEvent};
use helix_core::{diagnostic::Severity, test, Selection, Transaction};
use helix_term::{application::Application, args::Args, config::Config, keymap::merge_keys};
use helix_view::{doc, editor::LspConfig, input::parse_macro, Editor};
use helix_view::{current_ref, doc, editor::LspConfig, input::parse_macro, Editor};
use tempfile::NamedTempFile;
use tokio_stream::wrappers::UnboundedReceiverStream;
@ -22,8 +22,13 @@ pub struct TestCase {
pub out_selection: Selection,
}
impl<S: Into<String>> From<(S, S, S)> for TestCase {
fn from((input, keys, output): (S, S, S)) -> Self {
impl<S, R, V> From<(S, R, V)> for TestCase
where
S: Into<String>,
R: Into<String>,
V: Into<String>,
{
fn from((input, keys, output): (S, R, V)) -> Self {
let (in_text, in_selection) = test::print(&input.into());
let (out_text, out_selection) = test::print(&output.into());
@ -59,6 +64,11 @@ pub async fn test_key_sequences(
let num_inputs = inputs.len();
for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
let (view, doc) = current_ref!(app.editor);
let state = test::plain(doc.text().slice(..), doc.selection(view.id));
log::debug!("executing test with document state:\n\n-----\n\n{}", state);
if let Some(in_keys) = in_keys {
for key_event in parse_macro(in_keys)?.into_iter() {
let key = Event::Key(KeyEvent::from(key_event));
@ -69,6 +79,16 @@ pub async fn test_key_sequences(
let app_exited = !app.event_loop_until_idle(&mut rx_stream).await;
if !app_exited {
let (view, doc) = current_ref!(app.editor);
let state = test::plain(doc.text().slice(..), doc.selection(view.id));
log::debug!(
"finished running test with document state:\n\n-----\n\n{}",
state
);
}
// the app should not exit from any test until the last one
if i < num_inputs - 1 && app_exited {
bail!("application exited before test function could run");
@ -158,14 +178,11 @@ pub fn test_syntax_conf(overrides: Option<String>) -> helix_core::syntax::Config
/// document, selection, and sequence of key presses, and you just
/// want to verify the resulting document and selection.
pub async fn test_with_config<T: Into<TestCase>>(
args: Args,
mut config: Config,
syn_conf: helix_core::syntax::Configuration,
app_builder: AppBuilder,
test_case: T,
) -> anyhow::Result<()> {
let test_case = test_case.into();
config = helix_term::keymap::merge_keys(config);
let app = Application::new(args, config, syn_conf)?;
let app = app_builder.build()?;
test_key_sequence_with_input_text(
Some(app),
@ -186,13 +203,7 @@ pub async fn test_with_config<T: Into<TestCase>>(
}
pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
test_with_config(
Args::default(),
test_config(),
test_syntax_conf(None),
test_case,
)
.await
test_with_config(AppBuilder::default(), test_case).await
}
pub fn temp_file_with_contents<S: AsRef<str>>(
@ -212,15 +223,19 @@ pub fn temp_file_with_contents<S: AsRef<str>>(
/// Generates a config with defaults more suitable for integration tests
pub fn test_config() -> Config {
merge_keys(Config {
editor: helix_view::editor::Config {
lsp: LspConfig {
enable: false,
..Default::default()
},
editor: test_editor_config(),
..Default::default()
})
}
pub fn test_editor_config() -> helix_view::editor::Config {
helix_view::editor::Config {
lsp: LspConfig {
enable: false,
..Default::default()
},
..Default::default()
})
}
}
/// Replaces all LF chars with the system's appropriate line feed
@ -262,7 +277,7 @@ impl Default for AppBuilder {
fn default() -> Self {
Self {
args: Args::default(),
config: Config::default(),
config: test_config(),
syn_conf: test_syntax_conf(None),
input: None,
}
@ -286,7 +301,7 @@ impl AppBuilder {
// Remove this attribute once `with_config` is used in a test:
#[allow(dead_code)]
pub fn with_config(mut self, config: Config) -> Self {
self.config = config;
self.config = helix_term::keymap::merge_keys(config);
self
}

@ -395,7 +395,7 @@ async fn cursor_position_append_eof() -> anyhow::Result<()> {
test((
"#[foo|]#",
"abar<esc>",
helpers::platform_line("#[foobar|]#\n").as_ref(),
helpers::platform_line("#[foobar|]#\n"),
))
.await?;
@ -403,7 +403,7 @@ async fn cursor_position_append_eof() -> anyhow::Result<()> {
test((
"#[|foo]#",
"abar<esc>",
helpers::platform_line("#[foobar|]#\n").as_ref(),
helpers::platform_line("#[foobar|]#\n"),
))
.await?;
@ -413,28 +413,21 @@ async fn cursor_position_append_eof() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")]
async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::Result<()> {
test_with_config(
Args {
files: vec![(PathBuf::from("foo.rs"), Position::default())],
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(indoc! {"\
#[/|]#// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
"})
.as_ref(),
"}),
"]fv]f",
helpers::platform_line(indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }|]#
"})
.as_ref(),
"}),
),
)
.await?;
@ -445,28 +438,21 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::
#[tokio::test(flavor = "multi_thread")]
async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Result<()> {
test_with_config(
Args {
files: vec![(PathBuf::from("foo.rs"), Position::default())],
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }|]#
"})
.as_ref(),
"}),
"v[f",
helpers::platform_line(indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }|]#
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
"})
.as_ref(),
"}),
),
)
.await?;
@ -478,12 +464,7 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res
async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> anyhow::Result<()> {
// Note: the anchor stays put and the head moves back.
test_with_config(
Args {
files: vec![(PathBuf::from("foo.rs"), Position::default())],
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(indoc! {"\
/// Increments
@ -492,8 +473,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
fn dec(x: usize) -> usize { x - 1 }
/// Identity
#[fn ident(x: usize) -> usize { x }|]#
"})
.as_ref(),
"}),
"v[f",
helpers::platform_line(indoc! {"\
/// Increments
@ -502,19 +482,13 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
#[|fn dec(x: usize) -> usize { x - 1 }
/// Identity
]#fn ident(x: usize) -> usize { x }
"})
.as_ref(),
"}),
),
)
.await?;
test_with_config(
Args {
files: vec![(PathBuf::from("foo.rs"), Position::default())],
..Default::default()
},
Config::default(),
helpers::test_syntax_conf(None),
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(indoc! {"\
/// Increments
@ -523,8 +497,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
fn dec(x: usize) -> usize { x - 1 }
/// Identity
#[fn ident(x: usize) -> usize { x }|]#
"})
.as_ref(),
"}),
"v[f[f",
helpers::platform_line(indoc! {"\
/// Increments
@ -533,8 +506,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
fn dec(x: usize) -> usize { x - 1 }
/// Identity
]#fn ident(x: usize) -> usize { x }
"})
.as_ref(),
"}),
),
)
.await?;

@ -16,7 +16,7 @@ include = ["src/**/*", "README.md"]
default = ["crossterm"]
[dependencies]
bitflags = "1.3"
bitflags = "2.0"
cassowary = "0.3"
unicode-segmentation = "1.10"
crossterm = { version = "0.26", optional = true }

@ -453,10 +453,12 @@ impl<'a> From<&Text<'a>> for String {
let mut output = String::with_capacity(size);
for spans in &text.lines {
if !output.is_empty() {
output.push('\n');
}
for span in &spans.0 {
output.push_str(&span.content);
}
output.push('\n');
}
output
}

@ -27,7 +27,7 @@ use helix_view::graphics::Rect;
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
#[derive(Default)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub struct Borders: u8 {
/// Show the top border
const TOP = 0b0000_0001;
@ -38,7 +38,7 @@ bitflags! {
/// Show the left border
const LEFT = 0b0000_1000;
/// Show all borders
const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
}
}

@ -14,7 +14,7 @@ default = []
term = ["crossterm"]
[dependencies]
bitflags = "1.3"
bitflags = "2.0"
anyhow = "1"
helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" }

@ -776,7 +776,7 @@ impl Document {
let path = self
.path()
.filter(|path| path.exists())
.ok_or_else(|| anyhow!("can't find file to reload from"))?
.ok_or_else(|| anyhow!("can't find file to reload from {:?}", self.display_name()))?
.to_owned();
let mut file = std::fs::File::open(&path)?;

@ -380,6 +380,7 @@ bitflags! {
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;

@ -2,6 +2,7 @@ use bitflags::bitflags;
bitflags! {
/// Represents key modifiers (shift, control, alt).
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
pub struct KeyModifiers: u8 {
const SHIFT = 0b0000_0001;
const CONTROL = 0b0000_0010;

@ -453,7 +453,7 @@ includeInlayVariableTypeHints = true
name = "typescript"
scope = "source.ts"
injection-regex = "(ts|typescript)"
file-types = ["ts"]
file-types = ["ts", "mts", "cts"]
shebangs = []
roots = []
# TODO: highlights-params
@ -775,6 +775,13 @@ comment-token = "(**)"
language-server = { command = "ocamllsp" }
indent = { tab-width = 2, unit = " " }
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
[[grammar]]
name = "ocaml"
source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a", subpath = "ocaml" }
@ -789,6 +796,13 @@ comment-token = "(**)"
language-server = { command = "ocamllsp" }
indent = { tab-width = 2, unit = " " }
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
[[grammar]]
name = "ocaml-interface"
source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a", subpath = "interface" }
@ -1687,7 +1701,7 @@ source = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "c0741320bf
[[language]]
name = "v"
scope = "source.v"
file-types = ["v", "vv"]
file-types = ["v", "vv", "vsh"]
shebangs = ["v run"]
roots = ["v.mod"]
language-server = { command = "v", args = ["ls"] }
@ -1697,7 +1711,7 @@ indent = { tab-width = 4, unit = "\t" }
[[grammar]]
name = "v"
source = { git = "https://github.com/vlang/vls", subpath = "tree_sitter_v", rev = "3e8124ea4ab80aa08ec77f03df53f577902a0cdd" }
source = { git = "https://github.com/vlang/vls", subpath = "tree_sitter_v", rev = "66cf9d3086fb5ecc827cb32c64c5d812ab17d2c6" }
[[language]]
name = "verilog"
@ -2054,7 +2068,7 @@ source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d1
name = "xml"
scope = "source.xml"
injection-regex = "xml"
file-types = ["xml", "mobileconfig", "plist"]
file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard"]
indent = { tab-width = 2, unit = " " }
roots = []
@ -2328,3 +2342,30 @@ roots = []
[[grammar]]
name = "rst"
source = { git = "https://github.com/stsewd/tree-sitter-rst", rev = "25e6328872ac3a764ba8b926aea12719741103f1" }
[[language]]
name = "capnp"
scope = "source.capnp"
injection-regex = "capnp"
file-types = ["capnp"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "capnp"
source = { git = "https://github.com/amaanq/tree-sitter-capnp", rev = "fc6e2addf103861b9b3dffb82c543eb6b71061aa" }
[[language]]
name = "smithy"
scope = "source.smithy"
injection-regex = "smithy"
file-types = ["smithy"]
roots = ["smithy-build.json"]
comment-token = "//"
indent = { tab-width = 4, unit = " " }
language-server = { command = "cs", args = ["launch", "com.disneystreaming.smithy:smithy-language-server:latest.release", "--", "0"] }
[[grammar]]
name = "smithy"
source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "cf8c7eb9faf7c7049839585eac19c94af231e6a0" }

@ -0,0 +1,14 @@
[
(annotation_targets)
(const_list)
(enum)
(interface)
(implicit_generics)
(generics)
(group)
(method_parameters)
(named_return_types)
(struct)
(struct_shorthand)
(union)
] @fold

@ -0,0 +1,141 @@
; Preproc
(unique_id) @keyword.directive
(top_level_annotation_body) @keyword.directive
; Includes
[
"import"
"$import"
"embed"
] @keyword.control.import
(import_path) @string
; Builtins
[
(primitive_type)
"List"
] @type.builtin
; Typedefs
(type_definition) @type
; Labels (@number, @number!)
(field_version) @label
; Methods
(annotation_definition_identifier) @function.method
(method_identifier) @function.method
; Fields
(field_identifier) @variable.other.member
; Properties
(property) @label
; Parameters
(param_identifier) @variable.parameter
(return_identifier) @variable.parameter
; Constants
(const_identifier) @variable
(local_const) @constant
(enum_member) @type.enum.variant
(void) @constant.builtin
; Types
(enum_identifier) @type.enum
(extend_type) @type
(type_identifier) @type
; Attributes
(annotation_identifier) @attribute
(attribute) @attribute
; Operators
[
; @ ! -
"="
] @operator
; Keywords
[
"annotation"
"enum"
"group"
"interface"
"struct"
"union"
] @keyword.storage.type
[
"extends"
"namespace"
"using"
(annotation_target)
] @special
; Literals
[
(string)
(concatenated_string)
(block_text)
(namespace)
] @string
(escape_sequence) @constant.character.escape
(data_string) @string.special
(number) @constant.numeric.integer
(float) @constant.numeric.float
(boolean) @constant.builtin.boolean
; Misc
[
"const"
] @keyword.storage.modifier
[
"*"
"$"
":"
] @string.special.symbol
["{" "}"] @punctuation.bracket
["(" ")"] @punctuation.bracket
["[" "]"] @punctuation.bracket
[
","
";"
"->"
] @punctuation.delimiter
(data_hex) @constant
; Comments
(comment) @comment.line

@ -0,0 +1,19 @@
[
(annotation_targets)
(const_list)
(enum)
(interface)
(implicit_generics)
(generics)
(group)
(method_parameters)
(named_return_types)
(struct)
(struct_shorthand)
(union)
] @indent
[
"}"
")"
] @outdent

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

@ -0,0 +1,96 @@
; Scopes
[
(message)
(annotation_targets)
(const_list)
(enum)
(interface)
(implicit_generics)
(generics)
(group)
(method_parameters)
(named_return_types)
(struct)
(struct_shorthand)
(union)
] @local.scope
; References
[
(extend_type)
(field_type)
] @local.reference
(custom_type (type_identifier) @local.reference)
(custom_type
(generics
(generic_parameters
(generic_identifier) @local.reference)))
; Definitions
(annotation_definition_identifier) @local.definition
(const_identifier) @local.definition
(enum (enum_identifier) @local.definition)
[
(enum_member)
(field_identifier)
] @local.definition
(method_identifier) @local.definition
(namespace) @local.definition
[
(param_identifier)
(return_identifier)
] @local.definition
(group (type_identifier) @local.definition)
(struct (type_identifier) @local.definition)
(union (type_identifier) @local.definition)
(interface (type_identifier) @local.definition)
; Generics Related (don't know how to combine these)
(struct
(generics
(generic_parameters
(generic_identifier) @local.definition)))
(interface
(generics
(generic_parameters
(generic_identifier) @local.definition)))
(method
(implicit_generics
(implicit_generic_parameters
(generic_identifier) @local.definition)))
(method
(generics
(generic_parameters
(generic_identifier) @local.definition)))
(annotation
(generics
(generic_parameters
(generic_identifier) @local.definition)))
(replace_using
(generics
(generic_parameters
(generic_identifier) @local.definition)))
(return_type
(generics
(generic_parameters
(generic_identifier) @local.definition)))

@ -5,6 +5,7 @@
(formal_parameters)
(statement_block)
(switch_statement)
(object_pattern)
(class_body)
(named_imports)
@ -15,6 +16,11 @@
(export_clause)
] @indent
[
(switch_case)
(switch_default)
] @indent @extend
[
"}"
"]"

@ -17,8 +17,11 @@
; Function definitions
(function_definition (name) @function)
(function_definition
name: (name) @function
parameters: (parameters) @variable.parameter )
(constructor_definition "_init" @function)
(lambda (parameters) @variable.parameter)
;; Literals
@ -28,9 +31,28 @@
(type) @type
(expression_statement (array (identifier) @type))
(binary_operator (identifier) @type)
(enum_definition (name) @type.enum)
(enumerator (identifier) @type.enum.variant)
[
(null)
(underscore)
] @type.builtin
(variable_statement (identifier) @variable)
(get_node) @label
(attribute
(identifier)
(identifier) @variable.other.member)
(attribute
(identifier) @type.builtin
(#match? @type.builtin "^(AABB|Array|Basis|bool|Callable|Color|Dictionary|float|int|NodePath|Object|Packed(Byte|Color|String)Array|PackedFloat(32|64)Array|PackedInt(32|64)Array|PackedVector(2|3)Array|Plane|Projection|Quaternion|Rect2([i]{0,1})|RID|Signal|String|StringName|Transform(2|3)D|Variant|Vector(2|3|4)([i]{0,1}))$"))
[
(string_name)
(node_path)
(get_node)
] @label
(signal_statement (name) @label)
(const_statement (name) @constant)
(integer) @constant.numeric.integer
@ -40,7 +62,6 @@
(true)
(false)
] @constant.builtin.boolean
(null) @constant.builtin
[
"+"
@ -66,6 +87,7 @@
"~"
"<<"
">>"
":="
] @operator
(annotation (identifier) @keyword.storage.modifier)
@ -74,6 +96,7 @@
"if"
"else"
"elif"
"match"
] @keyword.control.conditional
[
@ -100,7 +123,6 @@
"in"
"is"
"as"
"match"
"and"
"or"
"not"
@ -128,5 +150,6 @@
"extends"
"set"
"get"
"await"
] @keyword

@ -45,7 +45,9 @@
(raw_text)
] @string
(variable_assignment (word) @string)
(variable_assignment (word) @variable)
(shell_text
[(variable_reference (word) @variable.parameter)])
[
"ifeq"
@ -139,6 +141,7 @@
function: "info"
(arguments (text) @info))
;; Install Command Categories
;; Others special variables
;; Variables Used by Implicit Rules
@ -168,3 +171,5 @@
(targets
(word) @constant.macro
(#match? @constant.macro "^\.(PHONY|SUFFIXES|DEFAULT|PRECIOUS|INTERMEDIATE|SECONDARY|SECONDEXPANSION|DELETE_ON_ERROR|IGNORE|LOW_RESOLUTION_TIME|SILENT|EXPORT_ALL_VARIABLES|NOTPARALLEL|ONESHELL|POSIX)$"))
(targets (word) @constant)

@ -1,2 +1,5 @@
((comment) @injection.content
(#set! injection.language "comment"))
((shell_text) @injection.content
(#set! injection.language "bash"))

@ -0,0 +1,102 @@
; Queries are taken from: https://github.com/indoorvivants/tree-sitter-smithy/blob/main/queries/highlights.scm
; Preproc
(control_key) @keyword.directive
; Namespace
(namespace) @namespace
; Includes
[
"use"
] @keyword.control.import
; Builtins
(primitive) @type.builtin
[
"enum"
"intEnum"
"list"
"map"
"set"
] @type.builtin
; Fields (Members)
; (field) @variable.other.member
(key_identifier) @variable.other.member
(shape_member
(field) @variable.other.member)
(operation_field) @variable.other.member
(operation_error_field) @variable.other.member
; Constants
(enum_member
(enum_field) @type.enum)
; Types
(identifier) @type
(structure_resource
(shape_id) @type)
; Attributes
(mixins
(shape_id) @attribute)
(trait_statement
(shape_id) @attribute)
; Operators
[
"@"
"-"
"="
":="
] @operator
; Keywords
[
"namespace"
"service"
"structure"
"operation"
"union"
"resource"
"metadata"
"apply"
"for"
"with"
] @keyword
; Literals
(string) @string
(escape_sequence) @constant.character.escape
(number) @constant.numeric
(float) @constant.numeric.float
(boolean) @constant.builtin.boolean
(null) @constant.builtin
; Misc
[
"$"
"#"
] @punctuation.special
["{" "}"] @punctuation.bracket
["(" ")"] @punctuation.bracket
["[" "]"] @punctuation.bracket
[
":"
"."
] @punctuation.delimiter
; Comments
[
(comment)
(documentation_comment)
] @comment

@ -14,71 +14,130 @@
(field_identifier) @variable.other.member
(selector_expression
operand: (identifier) @variable
field: (identifier) @variable.other.member)
(int_literal) @constant.numeric.integer
(interpreted_string_literal) @string
(rune_literal) @string
(attribute_declaration) @attribute
(comment) @comment
[
(c_string_literal)
(raw_string_literal)
(interpreted_string_literal)
(string_interpolation)
(rune_literal)
] @string
(escape_sequence) @constant.character.escape
[
(type_identifier)
(builtin_type)
(pointer_type)
(array_type)
] @type
(const_spec name: (identifier) @constant)
(global_var_type_initializer name: (identifier) @constant)
(global_var_spec name: (identifier) @constant)
((identifier) @constant (#match? @constant "^[A-Z][A-Z\\d_]*$"))
[
(generic_type)
(type_identifier)
] @constructor
(builtin_type) @type.builtin
[
(true)
(false)
] @constant.builtin.boolean
[
(identifier)
(module_identifier)
(import_path)
] @namespace
[
(pseudo_comptime_identifier)
(label_name)
] @label
[
(identifier)
] @variable
[
"as"
"asm"
"assert"
;"atomic"
;"break"
"const"
;"continue"
"defer"
"else"
"enum"
"fn"
"for"
"$for"
"go"
"goto"
"if"
"$if"
"import"
"in"
"!in"
"interface"
"is"
"!is"
"lock"
"match"
"module"
"mut"
"or"
"pub"
"return"
"rlock"
"select"
;"shared"
;"static"
"struct"
"type"
;"union"
"unsafe"
"pub"
"assert"
"go"
"asm"
"defer"
"unsafe"
"sql"
(none)
] @keyword
[
(true)
(false)
] @boolean
"interface"
"enum"
"type"
"union"
"struct"
"module"
] @keyword.storage.type
[
"static"
"const"
"__global"
] @keyword.storage.modifier
[
"mut"
] @keyword.storage.modifier.mut
[
"shared"
"lock"
"rlock"
"spawn"
] @keyword.control
[
"if"
"select"
"else"
"match"
] @keyword.control.conditional
[
"for"
] @keyword.control.repeat
[
"goto"
"return"
] @keyword.control.return
[
"fn"
] @keyword.control.function
[
"import"
] @keyword.control.import
[
"as"
"in"
"is"
"or"
] @keyword.operator
[
"."
@ -146,5 +205,3 @@
".."
"..."
] @operator
(comment) @comment

@ -0,0 +1,17 @@
[
(struct_declaration)
(function_declaration)
(if_expression)
(match_expression)
(expression_case)
(default_case)
(for_statement)
(unsafe_expression)
(short_var_declaration)
] @indent
[
"]"
")"
"}"
] @outdent

@ -0,0 +1,6 @@
((comment) @injection.content
(#set! injection.language "comment"))
((sql_expression) @injection.content
(#set! injection.language "sql"))

@ -0,0 +1,27 @@
(function_declaration
body: (block)? @function.inside) @function.around
((function_declaration
name: (identifier) @_name
body: (block)? @test.inside) @test.around
(#match? @_name "^test"))
(fn_literal
body: (block)? @function.inside) @function.around
(parameter_list
((_) @parameter.inside . ","? @parameter.around) @parameter.around)
(call_expression
(argument_list
((_) @parameter.inside) @parameter.around))
(struct_declaration
(struct_field_declaration_list) @class.inside) @class.around
(struct_field_declaration_list
((_) @parameter.inside) @parameter.around)
(comment) @comment.inside
(comment)+ @comment.around

@ -99,6 +99,9 @@
"diagnostic.error" = { fg = "red_4", modifiers = ["underlined"] }
"diagnostic.warning" = { fg = "yellow_2", modifiers = ["underlined"] }
"ui.bufferline" = { fg = "dark_2", bg = "libadwaita_dark" }
"ui.bufferline.active" = { fg = "light_4", bg = "libadwaita_dark_alt" }
[palette]
blue_1 = "#99C1F1"
blue_2 = "#62A0EA"

@ -9,20 +9,20 @@
"ui.background" = { bg = "my_gray0" }
"ui.menu" = { fg = "my_white", bg = "my_gray2" }
"ui.menu.selected" = { fg = "my_gray2", bg = "my_gray5" }
"ui.menu.selected" = { fg = "my_gray2", bg = "my_gray6" }
"ui.linenr" = { fg = "my_gray3", bg = "my_gray0" }
"ui.popup" = { bg = "my_gray2" }
"ui.window" = { fg = "my_gray3", bg = "my_gray2" }
"ui.linenr.selected" = { fg = "my_gray6", bg = "my_gray0"}
"ui.linenr.selected" = { fg = "my_gray7", bg = "my_gray0"}
"ui.selection" = { bg = "my_gray3" }
"comment" = { fg = "my_gray4", modifiers = ["italic"] }
"comment" = { fg = "my_gray5", modifiers = ["italic"] }
"ui.cursorline" = { bg = "my_gray3" }
"ui.statusline" = { fg = "my_gray6", bg = "my_gray2" }
"ui.statusline.inactive" = { fg = 'my_gray4', bg = 'my_gray2' }
"ui.statusline.insert" = {fg = "my_black", bg = "my_gray5", modifiers = ["bold"]}
"ui.statusline.normal" = {fg = "my_gray6", bg = "my_gray2"}
"ui.statusline.select" = {fg = "my_gray6", bg = "my_black", modifiers = ["bold"]}
"ui.cursor" = { fg = "my_gray5", modifiers = ["reversed"] }
"ui.statusline" = { fg = "my_gray7", bg = "my_gray2" }
"ui.statusline.inactive" = { fg = 'my_gray5', bg = 'my_gray2' }
"ui.statusline.insert" = {fg = "my_black", bg = "my_gray6", modifiers = ["bold"]}
"ui.statusline.normal" = {fg = "my_gray7", bg = "my_gray2"}
"ui.statusline.select" = {fg = "my_gray7", bg = "my_black", modifiers = ["bold"]}
"ui.cursor" = { fg = "my_gray6", modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "my_white", modifiers = ["reversed"] }
"ui.cursorline.primary" = { bg = "my_black" }
"ui.cursorline.secondary" = { bg = "my_black" }
@ -44,9 +44,10 @@
"keyword" = "my_red"
"label" = "my_red"
"namespace" = "my_white3"
"ui.help" = { fg = "my_gray6", bg = "my_gray2" }
"ui.virtual.whitespace" = { fg = "my_gray5" }
"ui.help" = { fg = "my_gray7", bg = "my_gray2" }
"ui.virtual.whitespace" = { fg = "my_gray6" }
"ui.virtual.ruler" = { bg = "my_gray1" }
"ui.virtual.inlay-hint" = { fg = "my_gray4", modifiers = ["normal"] }
"ui.virtual.inlay-hint.parameter" = { fg = "my_gray4", modifiers = ["normal"] }
"ui.virtual.inlay-hint.type" = { fg = "my_gray4", modifiers = ["italic"] }
@ -61,13 +62,13 @@
"markup.raw" = "my_green"
"diff.plus" = "my_green"
"diff.delta" = "my_gray4"
"diff.delta" = "my_gray5"
"diff.minus" = "my_red"
"diagnostic" = { modifiers = ["underlined"] }
"diagnostic.error" = { underline = { style = "curl", color = "my_red" } }
"ui.gutter" = { bg = "my_gray0" }
"hint" = "my_gray5"
"hint" = "my_gray6"
"debug" = "my_yellow2"
"info" = "my_yellow2"
"warning" = "my_yellow2"
@ -78,11 +79,12 @@ my_black = "#212121" # Cursorline
my_gray0 = "#262626" # Default Background
my_gray1 = "#2b2b2b" # Ruler
my_gray2 = "#323232" # Lighter Background (Used for status bars, line number and folding marks)
my_gray3 = "#3d3d3d" # Selection Background
my_gray4 = "#7c7c7c" # Comments, Invisibles, Line Highlighting
my_gray5 = "#a8a8a8" # Dark Foreground (Used for status bars)
my_gray6 = "#c8c8c8" # Light Foreground (Not often used)
my_gray7 = "#e8e8e8" # Light Background (Not often used)
my_gray3 = "#404040" # Selection Background
my_gray4 = "#6a6a6a" # Inlay-hint
my_gray5 = "#848484" # Comments, Invisibles, Line Highlighting
my_gray6 = "#a8a8a8" # Dark Foreground (Used for status bars)
my_gray7 = "#c8c8c8" # Light Foreground
my_gray8 = "#e8e8e8" # Light Background
my_white = "#F3F2CC" # Default Foreground, Caret, Delimiters, Operators
my_white2 = "#F3F2CC" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
my_white3 = "#F3F2CC" # Classes, Markup Bold, Search Text Background

@ -14,11 +14,12 @@ my_black = "#111111" # Cursorline
my_gray0 = "#090909" # Default Background
my_gray1 = "#0e0e0e" # Ruler
my_gray2 = "#1a1a1a" # Lighter Background (Used for status bars, line number and folding marks)
my_gray3 = "#323232" # Selection Background
my_gray4 = "#7c7c7c" # Comments, Invisibles, Line Highlighting
my_gray5 = "#aaaaaa" # Dark Foreground (Used for status bars)
my_gray6 = "#c4c4c4" # Light Foreground (Not often used)
my_gray7 = "#e8e8e8" # Light Background (Not often used)
my_gray3 = "#404040" # Selection Background
my_gray4 = "#626262" # Inlay-hint
my_gray5 = "#808080" # Comments, Invisibles, Line Highlighting
my_gray6 = "#aaaaaa" # Dark Foreground (Used for status bars)
my_gray7 = "#c4c4c4" # Light Foreground
my_gray8 = "#e8e8e8" # Light Background
my_white = "#F3F2CC" # Default Foreground, Caret, Delimiters, Operators
my_white2 = "#F3F2CC" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
my_white3 = "#F3F2CC" # Classes, Markup Bold, Search Text Background

@ -47,6 +47,7 @@
"ui.text.info" = "foreground"
"ui.virtual.whitespace" = "dark_gray"
"ui.virtual.ruler" = { bg = "black" }
"ui.virtual.inlay-hint" = { fg = "#e6b450", bg = "#302a20" } # original bg #e6b45033
"ui.menu" = { fg = "foreground", bg = "black" }
"ui.menu.selected" = { bg = "gray", fg = "background" }
"ui.selection" = { bg = "dark_gray" }

@ -47,6 +47,7 @@
"ui.text.info" = "foreground"
"ui.virtual.whitespace" = "dark_gray"
"ui.virtual.ruler" = { bg = "black" }
"ui.virtual.inlay-hint" = { fg = "#f4a028", bg = "#fcf2e3" } # bg original #ffaa3333
"ui.menu" = { fg = "foreground", bg = "black" }
"ui.menu.selected" = { bg = "gray", fg = "background" }
"ui.selection" = { bg = "dark_gray" }

@ -47,6 +47,7 @@
"ui.text.info" = "foreground"
"ui.virtual.whitespace" = "dark_gray"
"ui.virtual.ruler" = { bg = "black" }
"ui.virtual.inlay-hint" = { fg = "#ffcc66", bg = "#47433d" } # original bg #ffcc6633
"ui.menu" = { fg = "foreground", bg = "black" }
"ui.menu.selected" = { bg = "gray", fg = "background" }
"ui.selection" = { bg = "dark_gray" }

@ -41,6 +41,7 @@
"ui.text.focus" = { fg = "cyan" }
"ui.window" = { fg = "foreground" }
"ui.virtual.whitespace" = { fg = "subtle" }
"ui.virtual.wrap" = { fg = "subtle" }
"ui.virtual.ruler" = { bg = "background_dark"}
"error" = { fg = "red" }

@ -59,6 +59,9 @@
"ui.virtual" = { fg = "base5", bg = "base6" }
"ui.virtual.whitespace" = { fg = "base5" }
"ui.virtual.ruler" = { bg = "base6" }
"ui.virtual.inlay-hint" = { fg = "base4", modifiers = ["normal"] }
"ui.virtual.inlay-hint.parameter" = { fg = "base3", modifiers = ["normal"] }
"ui.virtual.inlay-hint.type" = { fg = "base3", modifiers = ["italic"] }
"ui.linenr" = { bg = "base6" }
"ui.linenr.selected" = { bg = "base6", modifiers = ["reversed"] }

@ -66,10 +66,11 @@
# ui specific
"ui.background" = { bg = "Gray 10" } # .separator
"ui.statusline" = { fg = "Gray 120", bg = "Gray 10" } # .inactive / .normal / .insert / .select
# "ui.statusline.normal" = { fg = "lightest", bg = "darker"}
# "ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" }
# "ui.statusline.select" = { fg = "lightest", bg = "orange_accent" }
"ui.statusline" = { fg = "Gray 120", bg = "Gray 20" } # .inactive / .normal / .insert / .select
"ui.statusline.normal" = { fg = "Gray 120", bg = "Gray 20" }
"ui.statusline.inactive" = { fg = "Gray 90"}
"ui.statusline.insert" = { fg = "Gray 20", bg = "Blue 90" }
"ui.statusline.select" = { fg = "Gray 20", bg = "Yellow 60" }
"ui.cursor" = { modifiers = ["reversed"] } # .insert / .select / .match / .primary
"ui.cursor.match" = { bg = "Blue 30" } # .insert / .select / .match / .primary
@ -91,9 +92,9 @@
"ui.text" = "Gray 120" # .focus / .info
"ui.text.focus" = { fg = "White", bg = "Blue 40" }
"ui.virtual" = "Gray 80" # .whitespace
"ui.virtual.inlay-hint" = "Purple 20"
# "ui.virtual.ruler" = { bg = "darker"}
"ui.virtual" = "Gray 90" # .whitespace
"ui.virtual.inlay-hint" = { fg = "Gray 70" }
"ui.virtual.ruler" = { bg = "Gray 20" }
"hint" = "Gray 80"
"info" = "#A366C4"

@ -19,6 +19,7 @@
"ui.statusline.insert" = { fg = "nord0", bg = "nord13" }
"ui.statusline.select" = { fg = "nord0", bg = "nord15" }
# nord1 - status bars, panels, modals, autocompletion
"ui.statusline" = { fg = "nord4", bg = "nord1" }
"ui.popup" = { bg = "nord1" }
@ -33,6 +34,8 @@
"comment" = { fg = "nord3_bright", modifiers = ["italic"] }
"ui.linenr" = "nord3_bright"
"ui.virtual.whitespace" = "nord3_bright"
"ui.virtual.inlay-hint" = { fg = "nord3_bright" }
# Snow Storm
# nord4 - cursor, variables, constants, attributes, fields

@ -38,6 +38,7 @@
"ui.virtual.ruler" = { bg = "overlay" }
"ui.virtual.whitespace" = { fg = "highlight_low" }
"ui.virtual.indent-guide" = { fg = "muted" }
"ui.virtual.inlay-hint" = { fg = "subtle" }
"ui.menu" = { fg = "subtle", bg = "surface" }
"ui.menu.selected" = { fg = "text" }
@ -185,4 +186,4 @@ iris = "#c4a7e7"
iris_10 = "#2b2539"
highlight_low = "#21202e"
highlight_med = "#403d52"
highlight_high = "#524f67"
highlight_high = "#524f67"

@ -27,4 +27,4 @@ iris = "#907aa9"
iris_10 = "#f1e8e6"
highlight_low = "#f4ede8"
highlight_med = "#dfdad9"
highlight_high = "#cecacd"
highlight_high = "#cecacd"

@ -27,4 +27,4 @@ iris = "#c4a7e7"
iris_10 = "#342e4a"
highlight_low = "#2a283e"
highlight_med = "#44415a"
highlight_high = "#56526e"
highlight_high = "#56526e"

@ -18,7 +18,7 @@
"variable.property" = "yellow"
"label" = "aqua"
"punctuation" = "grey0"
"punctuation.delimiter" = "grey2"
"punctuation.delimiter" = "bg4"
"punctuation.bracket" = "fg"
"keyword" = "red"
"operator" = "grey0"
@ -52,6 +52,7 @@
"ui.selection" = { bg = "bg3" }
"ui.virtual.whitespace" = "bg2"
"ui.virtual.ruler" = { bg = "grey2" }
"ui.virtual.inlay-hint" = { fg = "grey2", modifiers = ["italic"] }
"hint" = "blue"
"info" = "aqua"

@ -52,6 +52,7 @@
"ui.selection" = { fg = "bg0", bg = "bg3" }
"ui.virtual.whitespace" = { fg = "bg2" }
"ui.virtual.ruler" = { bg = "bg01" }
"ui.virtual.inlay-hint" = { fg = "grey2", modifiers = ["italic"] }
"hint" = "blue"
"info" = "aqua"

@ -41,6 +41,7 @@
"ui.background" = { bg = "base03" }
"ui.virtual.whitespace" = { fg = "base01" }
"ui.virtual.inlay-hint" = { fg = "base01", modifiers = ["italic"] }
# 行号栏
"ui.linenr" = { fg = "base0", bg = "base02" }

@ -42,6 +42,7 @@
"ui.background" = { bg = "base03" }
"ui.virtual.whitespace" = { fg = "base01" }
"ui.virtual.inlay-hint" = { fg = "base01", modifiers = ["italic"] }
# 行号栏
# line number column

@ -48,6 +48,7 @@
"ui.text.focus" = { fg = "cyan" }
"ui.virtual.ruler" = { bg = "foreground_gutter" }
"ui.virtual.whitespace" = { fg = "foreground_gutter" }
"ui.virtual.inlay-hint" = { fg = "comment" }
"ui.window" = { fg = "black" }
"error" = { fg = "red" }

@ -36,6 +36,8 @@ fn get_rules() -> Vec<Require> {
Require::Difference("ui.statusline.normal", "ui.statusline.select"),
// Check for editor.cursorline
Require::Existence(Rule::has_bg("ui.cursorline.primary")),
// Check for general ui.virtual (such as inlay-hint)
Require::Existence(Rule::has_fg("ui.virtual")),
// Check for editor.whitespace
Require::Existence(Rule::has_fg("ui.virtual.whitespace")),
// Check fir rulers

Loading…
Cancel
Save