Merge branch 'helix-editor:master' into better-bufferline

pull/8362/head
Cedric Meukens 2 weeks ago committed by GitHub
commit 993da83217
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

47
Cargo.lock generated

@ -68,9 +68,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.89" version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.23" version = "1.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bbb537bb4a30b90362caddba8f360c0a56bc13d3a5570028e7197204cb54a17" checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -355,9 +355,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]] [[package]]
name = "fern" name = "fern"
version = "0.6.2" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc"
dependencies = [ dependencies = [
"log", "log",
] ]
@ -412,15 +412,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -429,15 +429,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -1609,9 +1609,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.159" version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]] [[package]]
name = "libloading" name = "libloading"
@ -1753,12 +1753,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.1" version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "open" name = "open"
@ -1841,9 +1838,9 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
[[package]] [[package]]
name = "pulldown-cmark" name = "pulldown-cmark"
version = "0.12.1" version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666f0f59e259aea2d72e6012290c09877a780935cc3c18b1ceded41f3890d59c" checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"memchr", "memchr",
@ -2029,9 +2026,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.128" version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",

@ -47,7 +47,7 @@ Note: Only certain languages have indentation definitions at the moment. Check
[Installation documentation](https://docs.helix-editor.com/install.html). [Installation documentation](https://docs.helix-editor.com/install.html).
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg?exclude_unsupported=1)](https://repology.org/project/helix/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/helix-editor.svg?exclude_unsupported=1)](https://repology.org/project/helix-editor/versions)
# Contributing # Contributing

@ -29,6 +29,7 @@
| crystal | ✓ | ✓ | | `crystalline` | | crystal | ✓ | ✓ | | `crystalline` |
| css | ✓ | | ✓ | `vscode-css-language-server` | | css | ✓ | | ✓ | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` | | cue | ✓ | | | `cuelsp` |
| cylc | ✓ | ✓ | ✓ | |
| d | ✓ | ✓ | ✓ | `serve-d` | | d | ✓ | ✓ | ✓ | `serve-d` |
| dart | ✓ | ✓ | ✓ | `dart` | | dart | ✓ | ✓ | ✓ | `dart` |
| dbml | ✓ | | | | | dbml | ✓ | | | |
@ -69,7 +70,7 @@
| gjs | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` | | gjs | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| gleam | ✓ | ✓ | | `gleam` | | gleam | ✓ | ✓ | | `gleam` |
| glimmer | ✓ | | | `ember-language-server` | | glimmer | ✓ | | | `ember-language-server` |
| glsl | ✓ | ✓ | ✓ | | | glsl | ✓ | ✓ | ✓ | `glsl_analyzer` |
| gn | ✓ | | | | | gn | ✓ | | | |
| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` | | go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
| godot-resource | ✓ | ✓ | | | | godot-resource | ✓ | ✓ | | |
@ -186,6 +187,7 @@
| smali | ✓ | | ✓ | | | smali | ✓ | | ✓ | |
| smithy | ✓ | | | `cs` | | smithy | ✓ | | | `cs` |
| sml | ✓ | | | | | sml | ✓ | | | |
| snakemake | ✓ | | ✓ | `pylsp` |
| solidity | ✓ | ✓ | | `solc` | | solidity | ✓ | ✓ | | `solc` |
| spicedb | ✓ | | | | | spicedb | ✓ | | | |
| sql | ✓ | ✓ | | | | sql | ✓ | ✓ | | |

@ -17,7 +17,7 @@
- [Chocolatey](#chocolatey) - [Chocolatey](#chocolatey)
- [MSYS2](#msys2) - [MSYS2](#msys2)
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/helix-editor.svg)](https://repology.org/project/helix-editor/versions)
## Linux ## Linux

@ -283,7 +283,6 @@ These scopes are used for theming the editor interface:
| `ui.debug.active` | Indicator for the line at which debugging execution is paused at, found in the gutter | | `ui.debug.active` | Indicator for the line at which debugging execution is paused at, found in the gutter |
| `ui.gutter` | Gutter | | `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on | | `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.highlight.frameline` | Line at which debugging execution is paused at |
| `ui.linenr` | Line numbers | | `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on | | `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline | | `ui.statusline` | Statusline |
@ -298,7 +297,7 @@ These scopes are used for theming the editor interface:
| `ui.bufferline.marker` | Style for bufferline underflow and overflow markers | | `ui.bufferline.marker` | Style for bufferline underflow and overflow markers |
| `ui.popup` | Documentation popups (e.g. Space + k) | | `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options | | `ui.popup.info` | Prompt for multiple key options |
| `ui.picker.header` | Header row area in pickers with multiple columns | | `ui.picker.header` | Header row area in pickers with multiple columns |
| `ui.picker.header.column` | Column names in pickers with multiple columns | | `ui.picker.header.column` | Column names in pickers with multiple columns |
| `ui.picker.header.column.active` | The column name in pickers with multiple columns where the cursor is entering into. | | `ui.picker.header.column.active` | The column name in pickers with multiple columns where the cursor is entering into. |
| `ui.window` | Borderlines separating splits | | `ui.window` | Borderlines separating splits |
@ -321,6 +320,7 @@ These scopes are used for theming the editor interface:
| `ui.selection` | For selections in the editing area | | `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | | | `ui.selection.primary` | |
| `ui.highlight` | Highlighted lines in the picker preview | | `ui.highlight` | Highlighted lines in the picker preview |
| `ui.highlight.frameline` | Line at which debugging execution is paused at |
| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) | | `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.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) | | `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |

@ -1,17 +1,12 @@
{ {
"nodes": { "nodes": {
"crane": { "crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": { "locked": {
"lastModified": 1709610799, "lastModified": 1727974419,
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=", "narHash": "sha256-WD0//20h+2/yPGkO88d2nYbb23WMWYvnRyDQ9Dx4UHg=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "81c393c776d5379c030607866afef6406ca1be57", "rev": "37e4f9f0976cb9281cd3f0c70081e5e0ecaee93f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -25,11 +20,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1709126324, "lastModified": 1726560853,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605", "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -40,11 +35,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1709479366, "lastModified": 1728018373,
"narHash": "sha256-n6F0n8UV6lnTZbYPl1A9q1BS0p4hduAv1mGAP17CVd0=", "narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b8697e57f10292a6165a20f03d2f42920dfaf973", "rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -64,19 +59,16 @@
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1709604635, "lastModified": 1728268235,
"narHash": "sha256-le4fwmWmjGRYWwkho0Gr7mnnZndOOe4XGbLw68OvF40=", "narHash": "sha256-lJMFnMO4maJuNO6PQ5fZesrTmglze3UFTTBuKGwR1Nw=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "e86c0fb5d3a22a5f30d7f64ecad88643fe26449d", "rev": "25685cc2c7054efc31351c172ae77b21814f2d42",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -6,15 +6,9 @@
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
crane.url = "github:ipetkov/crane";
}; };
outputs = { outputs = {
@ -114,7 +108,7 @@
if pkgs.stdenv.isLinux if pkgs.stdenv.isLinux
then pkgs.stdenv then pkgs.stdenv
else pkgs.clangStdenv; else pkgs.clangStdenv;
rustFlagsEnv = pkgs.lib.optionalString stdenv.isLinux "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"; rustFlagsEnv = pkgs.lib.optionalString stdenv.isLinux "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment --cfg tokio_unstable";
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain; craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain;
craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default; craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default;

@ -9,6 +9,24 @@ use crate::{
use helix_stdx::rope::RopeSliceExt; use helix_stdx::rope::RopeSliceExt;
use std::borrow::Cow; use std::borrow::Cow;
pub const DEFAULT_COMMENT_TOKEN: &str = "//";
/// Returns the longest matching comment token of the given line (if it exists).
pub fn get_comment_token<'a, S: AsRef<str>>(
text: RopeSlice,
tokens: &'a [S],
line_num: usize,
) -> Option<&'a str> {
let line = text.line(line_num);
let start = line.first_non_whitespace_char()?;
tokens
.iter()
.map(AsRef::as_ref)
.filter(|token| line.slice(start..).starts_with(token))
.max_by_key(|token| token.len())
}
/// Given text, a comment token, and a set of line indices, returns the following: /// Given text, a comment token, and a set of line indices, returns the following:
/// - Whether the given lines should be considered commented /// - Whether the given lines should be considered commented
/// - If any of the lines are uncommented, all lines are considered as such. /// - If any of the lines are uncommented, all lines are considered as such.
@ -28,21 +46,20 @@ fn find_line_comment(
let mut min = usize::MAX; // minimum col for first_non_whitespace_char let mut min = usize::MAX; // minimum col for first_non_whitespace_char
let mut margin = 1; let mut margin = 1;
let token_len = token.chars().count(); let token_len = token.chars().count();
for line in lines { for line in lines {
let line_slice = text.line(line); let line_slice = text.line(line);
if let Some(pos) = line_slice.first_non_whitespace_char() { if let Some(pos) = line_slice.first_non_whitespace_char() {
let len = line_slice.len_chars(); let len = line_slice.len_chars();
if pos < min { min = std::cmp::min(min, pos);
min = pos;
}
// line can be shorter than pos + token len // line can be shorter than pos + token len
let fragment = Cow::from(line_slice.slice(pos..std::cmp::min(pos + token.len(), len))); let fragment = Cow::from(line_slice.slice(pos..std::cmp::min(pos + token.len(), len)));
// as soon as one of the non-blank lines doesn't have a comment, the whole block is
// considered uncommented.
if fragment != token { if fragment != token {
// as soon as one of the non-blank lines doesn't have a comment, the whole block is
// considered uncommented.
commented = false; commented = false;
} }
@ -56,6 +73,7 @@ fn find_line_comment(
to_change.push(line); to_change.push(line);
} }
} }
(commented, to_change, min, margin) (commented, to_change, min, margin)
} }
@ -63,7 +81,7 @@ fn find_line_comment(
pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction { pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction {
let text = doc.slice(..); let text = doc.slice(..);
let token = token.unwrap_or("//"); let token = token.unwrap_or(DEFAULT_COMMENT_TOKEN);
let comment = Tendril::from(format!("{} ", token)); let comment = Tendril::from(format!("{} ", token));
let mut lines: Vec<usize> = Vec::with_capacity(selection.len()); let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
@ -317,56 +335,87 @@ pub fn split_lines_of_selection(text: RopeSlice, selection: &Selection) -> Selec
mod test { mod test {
use super::*; use super::*;
#[test] mod find_line_comment {
fn test_find_line_comment() { use super::*;
// four lines, two space indented, except for line 1 which is blank.
let mut doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
let mut selection = Selection::single(0, doc.len_chars() - 1);
let text = doc.slice(..); #[test]
fn not_commented() {
// four lines, two space indented, except for line 1 which is blank.
let doc = Rope::from(" 1\n\n 2\n 3");
let res = find_line_comment("//", text, 0..3); let text = doc.slice(..);
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 0)
assert_eq!(res, (false, vec![0, 2], 2, 0));
// comment let res = find_line_comment("//", text, 0..3);
let transaction = toggle_line_comments(&doc, &selection, None); // (commented = false, to_change = [line 0, line 2], min = col 2, margin = 0)
transaction.apply(&mut doc); assert_eq!(res, (false, vec![0, 2], 2, 0));
selection = selection.map(transaction.changes()); }
assert_eq!(doc, " // 1\n\n // 2\n // 3"); #[test]
fn is_commented() {
// three lines where the second line is empty.
let doc = Rope::from("// hello\n\n// there");
// uncomment let res = find_line_comment("//", doc.slice(..), 0..3);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
// 0 margin comments // (commented = true, to_change = [line 0, line 2], min = col 0, margin = 1)
doc = Rope::from(" //1\n\n //2\n //3"); assert_eq!(res, (true, vec![0, 2], 0, 1));
// reset the selection. }
selection = Selection::single(0, doc.len_chars() - 1); }
let transaction = toggle_line_comments(&doc, &selection, None); // TODO: account for uncommenting with uneven comment indentation
transaction.apply(&mut doc); mod toggle_line_comment {
selection = selection.map(transaction.changes()); use super::*;
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
// 0 margin comments, with no space #[test]
doc = Rope::from("//"); fn comment() {
// reset the selection. // four lines, two space indented, except for line 1 which is blank.
selection = Selection::single(0, doc.len_chars() - 1); let mut doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
let selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc); transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, ""); assert_eq!(doc, " // 1\n\n // 2\n // 3");
assert!(selection.len() == 1); // to ignore the selection unused warning }
// TODO: account for uncommenting with uneven comment indentation #[test]
fn uncomment() {
let mut doc = Rope::from(" // 1\n\n // 2\n // 3");
let mut selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
}
#[test]
fn uncomment_0_margin_comments() {
let mut doc = Rope::from(" //1\n\n //2\n //3");
let mut selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning
}
#[test]
fn uncomment_0_margin_comments_with_no_space() {
let mut doc = Rope::from("//");
let mut selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, "");
assert!(selection.len() == 1); // to ignore the selection unused warning
}
} }
#[test] #[test]
@ -413,4 +462,32 @@ mod test {
transaction.apply(&mut doc); transaction.apply(&mut doc);
assert_eq!(doc, ""); assert_eq!(doc, "");
} }
/// Test, if `get_comment_tokens` works, even if the content of the file includes chars, whose
/// byte size unequal the amount of chars
#[test]
fn test_get_comment_with_char_boundaries() {
let rope = Rope::from("··");
let tokens = ["//", "///"];
assert_eq!(
super::get_comment_token(rope.slice(..), tokens.as_slice(), 0),
None
);
}
/// Test for `get_comment_token`.
///
/// Assuming the comment tokens are stored as `["///", "//"]`, `get_comment_token` should still
/// return `///` instead of `//` if the user is in a doc-comment section.
#[test]
fn test_use_longest_comment() {
let text = Rope::from(" /// amogus");
let tokens = ["///", "//"];
assert_eq!(
super::get_comment_token(text.slice(..), tokens.as_slice(), 0),
Some("///")
);
}
} }

@ -24,4 +24,4 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std
thiserror.workspace = true thiserror.workspace = true
[dev-dependencies] [dev-dependencies]
fern = "0.6" fern = "0.7"

@ -23,7 +23,7 @@ once_cell = "1.20"
anyhow = "1" anyhow = "1"
log = "0.4" log = "0.4"
futures-executor = "0.3.28" futures-executor = "0.3.31"
[features] [features]
integration_test = [] integration_test = []

@ -23,7 +23,7 @@ license = "MIT"
[dependencies] [dependencies]
bitflags = "2.6.0" bitflags = "2.6.0"
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127" serde_json = "1.0.132"
serde_repr = "0.1" serde_repr = "0.1"
url = {version = "2.0.0", features = ["serde"]} url = {version = "2.0.0", features = ["serde"]}

@ -45,7 +45,7 @@ arc-swap = { version = "1.7.1" }
termini = "1" termini = "1"
# Logging # Logging
fern = "0.6" fern = "0.7"
chrono = { version = "0.4", default-features = false, features = ["clock"] } chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4" log = "0.4"
@ -74,7 +74,7 @@ grep-searcher = "0.1.14"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.159" libc = "0.2.161"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] } crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }

@ -22,8 +22,8 @@ use helix_core::{
encoding, find_workspace, encoding, find_workspace,
graphemes::{self, next_grapheme_boundary, RevRopeGraphemes}, graphemes::{self, next_grapheme_boundary, RevRopeGraphemes},
history::UndoKind, history::UndoKind,
increment, indent, increment,
indent::IndentStyle, indent::{self, IndentStyle},
line_ending::{get_line_ending_of_str, line_end_char_index}, line_ending::{get_line_ending_of_str, line_end_char_index},
match_brackets, match_brackets,
movement::{self, move_vertically_visual, Direction}, movement::{self, move_vertically_visual, Direction},
@ -3467,31 +3467,51 @@ fn open(cx: &mut Context, open: Open) {
) )
}; };
let indent = indent::indent_for_newline( let continue_comment_token = doc
doc.language_config(), .language_config()
doc.syntax(), .and_then(|config| config.comment_tokens.as_ref())
&doc.config.load().indent_heuristic, .and_then(|tokens| comment::get_comment_token(text, tokens, cursor_line));
&doc.indent_style,
doc.tab_width(), let line = text.line(cursor_line);
text, let indent = match line.first_non_whitespace_char() {
line_num, Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
line_end_index, _ => indent::indent_for_newline(
cursor_line, doc.language_config(),
); doc.syntax(),
&doc.config.load().indent_heuristic,
&doc.indent_style,
doc.tab_width(),
text,
line_num,
line_end_index,
cursor_line,
),
};
let indent_len = indent.len(); let indent_len = indent.len();
let mut text = String::with_capacity(1 + indent_len); let mut text = String::with_capacity(1 + indent_len);
text.push_str(doc.line_ending.as_str()); text.push_str(doc.line_ending.as_str());
text.push_str(&indent); text.push_str(&indent);
if let Some(token) = continue_comment_token {
text.push_str(token);
text.push(' ');
}
let text = text.repeat(count); let text = text.repeat(count);
// calculate new selection ranges // calculate new selection ranges
let pos = offs + line_end_index + line_end_offset_width; let pos = offs + line_end_index + line_end_offset_width;
let comment_len = continue_comment_token
.map(|token| token.len() + 1) // `+ 1` for the extra space added
.unwrap_or_default();
for i in 0..count { for i in 0..count {
// pos -> beginning of reference line, // pos -> beginning of reference line,
// + (i * (1+indent_len)) -> beginning of i'th line from pos // + (i * (1+indent_len + comment_len)) -> beginning of i'th line from pos (possibly including comment token)
// + indent_len -> -> indent for i'th line // + indent_len + comment_len -> -> indent for i'th line
ranges.push(Range::point(pos + (i * (1 + indent_len)) + indent_len)); ranges.push(Range::point(
pos + (i * (1 + indent_len + comment_len)) + indent_len + comment_len,
));
} }
offs += text.chars().count(); offs += text.chars().count();
@ -3929,6 +3949,11 @@ pub mod insert {
let mut new_text = String::new(); let mut new_text = String::new();
let continue_comment_token = doc
.language_config()
.and_then(|config| config.comment_tokens.as_ref())
.and_then(|tokens| comment::get_comment_token(text, tokens, current_line));
// If the current line is all whitespace, insert a line ending at the beginning of // If the current line is all whitespace, insert a line ending at the beginning of
// the current line. This makes the current line empty and the new line contain the // the current line. This makes the current line empty and the new line contain the
// indentation of the old line. // indentation of the old line.
@ -3938,17 +3963,22 @@ pub mod insert {
(line_start, line_start, new_text.chars().count()) (line_start, line_start, new_text.chars().count())
} else { } else {
let indent = indent::indent_for_newline( let line = text.line(current_line);
doc.language_config(),
doc.syntax(), let indent = match line.first_non_whitespace_char() {
&doc.config.load().indent_heuristic, Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
&doc.indent_style, _ => indent::indent_for_newline(
doc.tab_width(), doc.language_config(),
text, doc.syntax(),
current_line, &doc.config.load().indent_heuristic,
pos, &doc.indent_style,
current_line, doc.tab_width(),
); text,
current_line,
pos,
current_line,
),
};
// If we are between pairs (such as brackets), we want to // If we are between pairs (such as brackets), we want to
// insert an additional line which is indented one level // insert an additional line which is indented one level
@ -3958,19 +3988,30 @@ pub mod insert {
.and_then(|pairs| pairs.get(prev)) .and_then(|pairs| pairs.get(prev))
.map_or(false, |pair| pair.open == prev && pair.close == curr); .map_or(false, |pair| pair.open == prev && pair.close == curr);
let local_offs = if on_auto_pair { let local_offs = if let Some(token) = continue_comment_token {
new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent);
new_text.push_str(token);
new_text.push(' ');
new_text.chars().count()
} else if on_auto_pair {
// line where the cursor will be
let inner_indent = indent.clone() + doc.indent_style.as_str(); let inner_indent = indent.clone() + doc.indent_style.as_str();
new_text.reserve_exact(2 + indent.len() + inner_indent.len()); new_text.reserve_exact(2 + indent.len() + inner_indent.len());
new_text.push_str(doc.line_ending.as_str()); new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&inner_indent); new_text.push_str(&inner_indent);
// line where the matching pair will be
let local_offs = new_text.chars().count(); let local_offs = new_text.chars().count();
new_text.push_str(doc.line_ending.as_str()); new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent); new_text.push_str(&indent);
local_offs local_offs
} else { } else {
new_text.reserve_exact(1 + indent.len()); new_text.reserve_exact(1 + indent.len());
new_text.push_str(doc.line_ending.as_str()); new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent); new_text.push_str(&indent);
new_text.chars().count() new_text.chars().count()
}; };

@ -41,6 +41,7 @@ forth-lsp = { command = "forth-lsp" }
fortls = { command = "fortls", args = ["--lowercase_intrinsics"] } fortls = { command = "fortls", args = ["--lowercase_intrinsics"] }
fsharp-ls = { command = "fsautocomplete", config = { AutomaticWorkspaceInit = true } } fsharp-ls = { command = "fsautocomplete", config = { AutomaticWorkspaceInit = true } }
gleam = { command = "gleam", args = ["lsp"] } gleam = { command = "gleam", args = ["lsp"] }
glsl_analyzer = { command = "glsl_analyzer" }
graphql-language-service = { command = "graphql-lsp", args = ["server", "-m", "stream"] } graphql-language-service = { command = "graphql-lsp", args = ["server", "-m", "stream"] }
haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] } haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
idris2-lsp = { command = "idris2-lsp" } idris2-lsp = { command = "idris2-lsp" }
@ -448,7 +449,6 @@ file-types = [
{ glob = "composer.lock" }, { glob = "composer.lock" },
{ glob = ".watchmanconfig" }, { glob = ".watchmanconfig" },
"avsc", "avsc",
{ glob = ".prettierrc" },
"ldtk", "ldtk",
"ldtkl", "ldtkl",
] ]
@ -1305,7 +1305,7 @@ source = { git = "https://github.com/ikatyang/tree-sitter-vue", rev = "91fe27547
[[language]] [[language]]
name = "yaml" name = "yaml"
scope = "source.yaml" scope = "source.yaml"
file-types = ["yml", "yaml"] file-types = ["yml", "yaml", { glob = ".prettierrc" }]
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-servers = [ "yaml-language-server", "ansible-language-server" ] language-servers = [ "yaml-language-server", "ansible-language-server" ]
@ -1452,6 +1452,7 @@ file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
comment-token = "//" comment-token = "//"
block-comment-tokens = { start = "/*", end = "*/" } block-comment-tokens = { start = "/*", end = "*/" }
indent = { tab-width = 4, unit = " " } indent = { tab-width = 4, unit = " " }
language-servers = [ "glsl_analyzer" ]
injection-regex = "glsl" injection-regex = "glsl"
[[grammar]] [[grammar]]
@ -1965,7 +1966,7 @@ file-types = ["swift", "swiftinterface"]
roots = [ "Package.swift" ] roots = [ "Package.swift" ]
comment-token = "//" comment-token = "//"
block-comment-tokens = { start = "/*", end = "*/" } block-comment-tokens = { start = "/*", end = "*/" }
formatter = { command = "swift-format", args = [ "--configuration", ".swift-format"] } formatter = { command = "swift-format" }
auto-format = true auto-format = true
language-servers = [ "sourcekit-lsp" ] language-servers = [ "sourcekit-lsp" ]
@ -3138,7 +3139,7 @@ indent = { tab-width = 4, unit = " " }
[[grammar]] [[grammar]]
name = "just" name = "just"
source = { git = "https://github.com/poliorcetics/tree-sitter-just", rev = "f58a8fd869035ac4653081401e6c2030251240ab" } source = { git = "https://github.com/poliorcetics/tree-sitter-just", rev = "6e28fa6cba511c694247cd802d1c3b14f8d34dbb" }
[[language]] [[language]]
name = "gn" name = "gn"
@ -3270,7 +3271,7 @@ text-width = 72
[[grammar]] [[grammar]]
name = "jjdescription" name = "jjdescription"
source = { git = "https://github.com/kareigu/tree-sitter-jjdescription", rev = "2ddec6cad07b366aee276a608e1daa2c29d3caf2" } source = { git = "https://github.com/kareigu/tree-sitter-jjdescription", rev = "23dd3dd18ee29bdd761642511aa314215801afd8" }
[[language]] [[language]]
name = "jq" name = "jq"
@ -3833,3 +3834,28 @@ language-servers = ["circom-lsp"]
[[grammar]] [[grammar]]
name = "circom" name = "circom"
source = { git = "https://github.com/Decurity/tree-sitter-circom", rev = "02150524228b1e6afef96949f2d6b7cc0aaf999e" } source = { git = "https://github.com/Decurity/tree-sitter-circom", rev = "02150524228b1e6afef96949f2d6b7cc0aaf999e" }
[[language]]
name = "snakemake"
scope = "source.snakemake"
roots = ["Snakefile", "config.yaml", "environment.yaml", "workflow/"]
file-types = ["smk", "Snakefile"]
comment-tokens = ["#", "##"]
indent = { tab-width = 2, unit = " " }
language-servers = ["pylsp" ]
[[grammar]]
name = "snakemake"
source = { git = "https://github.com/osthomas/tree-sitter-snakemake", rev = "e909815acdbe37e69440261ebb1091ed52e1dec6" }
[[language]]
name = "cylc"
scope = "source.cylc"
injection-regex = "cylc"
file-types = ["cylc", { glob = "suite.rc" }]
comment-tokens = "#"
indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "cylc"
source = { git = "https://github.com/elliotfontaine/tree-sitter-cylc", rev = "30dd40d9bf23912e4aefa93eeb4c7090bda3d0f6" }

@ -0,0 +1,100 @@
(ERROR) @markup.bold
[
(jinja2_expression)
(jinja2_statement)
(jinja2_comment)
(jinja2_shebang)
] @special
(include_statement
directive: _ @keyword.directive
path: _ @string.special.path)
(comment) @comment.line
(graph_section
name: _? @label)
(task_section
name: (_
(task_name) @namespace))
(top_section
brackets_open: _ @punctuation.bracket
name: _? @label
brackets_close: _ @punctuation.bracket)
(sub_section_1
brackets_open: _ @punctuation.bracket
name: _? @label
brackets_close: _ @punctuation.bracket)
(sub_section_2
brackets_open: _ @punctuation.bracket
name: _? @label
brackets_close: _ @punctuation.bracket)
(runtime_section
brackets_open: _ @punctuation.bracket
name: _? @label
brackets_close: _ @punctuation.bracket)
(graph_setting
key: (_) @constant.numeric.integer
operator: (_)? @operator)
(quoted_graph_string
quotes_open: _ @string
quotes_close: _ @string)
(multiline_graph_string
quotes_open: _ @string
quotes_close: _ @string)
[
(graph_logical)
(graph_arrow)
(graph_parenthesis)
] @operator
(intercycle_annotation
(recurrence) @constant.numeric.integer)
(graph_task
xtrigger: _? @operator
suicide: _? @operator
name: _ @namespace)
(task_parameter
"<" @tag
name: (_)? @special
","? @tag
"="? @tag
selection: (_)? @special
">" @tag)
(intercycle_annotation
"[" @tag
(recurrence)? @constant.numeric.integer
"]" @tag)
(task_output
":" @tag
(nametag) @variable.other)
(task_output
"?"? @tag)
(setting
key: (key) @variable
operator: (_)? @operator
value: [
(unquoted_string) @string
(quoted_string) @string
(multiline_string) @string
(boolean) @constant.builtin.boolean
(integer) @constant.numeric.integer
]?)
(datetime) @constant.numeric.float

@ -0,0 +1,19 @@
[
(top_section)
(sub_section_1)
(sub_section_2)
(graph_section)
(runtime_section)
(task_section)
] @indent
[
(top_section)
(sub_section_1)
(sub_section_2)
(graph_section)
(runtime_section)
(task_section)
] @extend
(line_continuation) @indent.always

@ -0,0 +1,20 @@
((setting
key: (key) @key
(#match? @key "^script$|-script$|^script-")
value: (_
(string_content) @injection.content))
(#set! "injection.language" "bash"))
; Requires no spacing around "=" in environment settings for proper highlighting.
; Could be improved if Tree-sitter allowed to specify the target node of the injected
; language, instead of always using the root node.
; See this proposal:
; https://github.com/tree-sitter/tree-sitter/issues/3625
((task_section
(sub_section_2
name: (_) @section_name
(#eq? @section_name "environment")
(setting) @injection.content))
(#set! "injection.language" "bash")
(#set! injection.combined)
(#set! injection.include-children))

@ -0,0 +1,23 @@
(comment) @comment.inside
(comment)+ @comment.around
(_
brackets_open: _
name: _?
brackets_close: _
_* @class.inside) @class.around
(setting
value: _? @function.inside) @function.around
(graph_setting
value: _? @function.inside) @function.around
(graph_string_content
(graph_task) @entry.inside)
(task_parameter
((_) @parameter.inside
.
","? @parameter.around) @parameter.around)

@ -0,0 +1,20 @@
Copyright (c) 2016 Max Brunsfeld
Copyright (c) 2023 Oliver Thomas
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,8 @@
; inherits: python
[
(rule_definition)
(rule_inheritance)
(module_definition)
(checkpoint_definition)
] @fold

@ -0,0 +1,76 @@
; inherits: python
; Compound directives
[
"rule"
"checkpoint"
"module"
] @keyword
; Top level directives (eg. configfile, include)
(module
(directive
name: _ @keyword))
; Subordinate directives (eg. input, output)
((_)
body: (_
(directive
name: _ @label)))
; rule/module/checkpoint names
(rule_definition
name: (identifier) @type)
(module_definition
name: (identifier) @type)
(checkpoint_definition
name: (identifier) @type)
; Rule imports
(rule_import
"use" @keyword.import
"rule" @keyword.import
"from" @keyword.import
"exclude"? @keyword.import
"as"? @keyword.import
"with"? @keyword.import)
; Rule inheritance
(rule_inheritance
"use" @keyword
"rule" @keyword
"with" @keyword)
; Wildcard names
(wildcard (identifier) @variable)
(wildcard (flag) @variable.parameter.builtin)
; builtin variables
((identifier) @variable.builtin
(#any-of? @variable.builtin "checkpoints" "config" "gather" "rules" "scatter" "workflow"))
; References to directive labels in wildcard interpolations
; the #any-of? queries are moved above the #has-ancestor? queries to
; short-circuit the potentially expensive tree traversal, if possible
; see:
; https://github.com/nvim-treesitter/nvim-treesitter/pull/4302#issuecomment-1685789790
; directive labels in wildcard context
((wildcard
(identifier) @label)
(#any-of? @label "input" "log" "output" "params" "resources" "threads" "wildcards"))
((wildcard
(attribute
object: (identifier) @label))
(#any-of? @label "input" "log" "output" "params" "resources" "threads" "wildcards"))
((wildcard
(subscript
value: (identifier) @label))
(#any-of? @label "input" "log" "output" "params" "resources" "threads" "wildcards"))
; directive labels in block context (eg. within 'run:')
((identifier) @label
(#any-of? @label "input" "log" "output" "params" "resources" "threads" "wildcards"))

@ -0,0 +1,27 @@
; inherits: python
[
(rule_definition)
(checkpoint_definition)
(rule_inheritance)
(module_definition)
] @indent
[
(rule_definition)
(checkpoint_definition)
(rule_inheritance)
(module_definition)
] @extend
(directive) @indent
(directive) @extend
(rule_import
"with"
":") @indent
(rule_import
"with"
":") @extend

@ -0,0 +1,5 @@
; inherits: python
(wildcard
(constraint) @injection.content
(#set! injection.language "regex"))
Loading…
Cancel
Save