Merge branch 'helix-editor:master' into scroll_preview_fixed

pull/11441/head
Shaun_Sheep 1 month ago committed by GitHub
commit f23721dbb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install nix - name: Install nix
uses: cachix/install-nix-action@v29 uses: cachix/install-nix-action@v30
- name: Authenticate with Cachix - name: Authenticate with Cachix
uses: cachix/cachix-action@v15 uses: cachix/cachix-action@v15

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

@ -39,6 +39,7 @@
| dockerfile | ✓ | ✓ | | `docker-langserver` | | dockerfile | ✓ | ✓ | | `docker-langserver` |
| dot | ✓ | | | `dot-language-server` | | dot | ✓ | | | `dot-language-server` |
| dtd | ✓ | | | | | dtd | ✓ | | | |
| dune | ✓ | | | |
| earthfile | ✓ | ✓ | ✓ | `earthlyls` | | earthfile | ✓ | ✓ | ✓ | `earthlyls` |
| edoc | ✓ | | | | | edoc | ✓ | | | |
| eex | ✓ | | | | | eex | ✓ | | | |
@ -68,7 +69,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 | ✓ | ✓ | | |
@ -185,6 +186,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 |
@ -320,6 +319,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)));
if fragment != token {
// as soon as one of the non-blank lines doesn't have a comment, the whole block is // as soon as one of the non-blank lines doesn't have a comment, the whole block is
// considered uncommented. // considered uncommented.
if fragment != token {
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::*;
mod find_line_comment {
use super::*;
#[test] #[test]
fn test_find_line_comment() { fn not_commented() {
// four lines, two space indented, except for line 1 which is blank. // four lines, two space indented, except for line 1 which is blank.
let mut doc = Rope::from(" 1\n\n 2\n 3"); let 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(..); let text = doc.slice(..);
let res = find_line_comment("//", text, 0..3); let res = find_line_comment("//", text, 0..3);
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 0) // (commented = false, to_change = [line 0, line 2], min = col 2, margin = 0)
assert_eq!(res, (false, vec![0, 2], 2, 0)); assert_eq!(res, (false, vec![0, 2], 2, 0));
}
#[test]
fn is_commented() {
// three lines where the second line is empty.
let doc = Rope::from("// hello\n\n// there");
let res = find_line_comment("//", doc.slice(..), 0..3);
// (commented = true, to_change = [line 0, line 2], min = col 0, margin = 1)
assert_eq!(res, (true, vec![0, 2], 0, 1));
}
}
// TODO: account for uncommenting with uneven comment indentation
mod toggle_line_comment {
use super::*;
#[test]
fn comment() {
// four lines, two space indented, except for line 1 which is blank.
let mut doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
let selection = Selection::single(0, doc.len_chars() - 1);
// comment
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, " // 1\n\n // 2\n // 3"); assert_eq!(doc, " // 1\n\n // 2\n // 3");
}
#[test]
fn uncomment() {
let mut doc = Rope::from(" // 1\n\n // 2\n // 3");
let mut selection = Selection::single(0, doc.len_chars() - 1);
// uncomment
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()); selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3"); assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning assert!(selection.len() == 1); // to ignore the selection unused warning
}
// 0 margin comments #[test]
doc = Rope::from(" //1\n\n //2\n //3"); fn uncomment_0_margin_comments() {
// reset the selection. let mut doc = Rope::from(" //1\n\n //2\n //3");
selection = Selection::single(0, doc.len_chars() - 1); let mut 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()); selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3"); assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning assert!(selection.len() == 1); // to ignore the selection unused warning
}
// 0 margin comments, with no space #[test]
doc = Rope::from("//"); fn uncomment_0_margin_comments_with_no_space() {
// reset the selection. let mut doc = Rope::from("//");
selection = Selection::single(0, doc.len_chars() - 1); let mut 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()); selection = selection.map(transaction.changes());
assert_eq!(doc, ""); assert_eq!(doc, "");
assert!(selection.len() == 1); // to ignore the selection unused warning assert!(selection.len() == 1); // to ignore the selection unused warning
}
// TODO: account for uncommenting with uneven comment indentation
} }
#[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,7 +3467,15 @@ fn open(cx: &mut Context, open: Open) {
) )
}; };
let indent = indent::indent_for_newline( let continue_comment_token = doc
.language_config()
.and_then(|config| config.comment_tokens.as_ref())
.and_then(|tokens| comment::get_comment_token(text, tokens, cursor_line));
let line = text.line(cursor_line);
let indent = match line.first_non_whitespace_char() {
Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
_ => indent::indent_for_newline(
doc.language_config(), doc.language_config(),
doc.syntax(), doc.syntax(),
&doc.config.load().indent_heuristic, &doc.config.load().indent_heuristic,
@ -3477,21 +3485,33 @@ fn open(cx: &mut Context, open: Open) {
line_num, line_num,
line_end_index, line_end_index,
cursor_line, 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,7 +3963,11 @@ 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);
let indent = match line.first_non_whitespace_char() {
Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
_ => indent::indent_for_newline(
doc.language_config(), doc.language_config(),
doc.syntax(), doc.syntax(),
&doc.config.load().indent_heuristic, &doc.config.load().indent_heuristic,
@ -3948,7 +3977,8 @@ pub mod insert {
current_line, current_line,
pos, pos,
current_line, 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()
}; };

@ -96,7 +96,10 @@ impl Component for SignatureHelp {
fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
let margin = Margin::horizontal(1); let margin = Margin::horizontal(1);
let signature = &self.signatures[self.active_signature]; let signature = self
.signatures
.get(self.active_signature)
.unwrap_or_else(|| &self.signatures[0]);
let active_param_span = signature.active_param_range.map(|(start, end)| { let active_param_span = signature.active_param_range.map(|(start, end)| {
vec![( vec![(
@ -108,9 +111,13 @@ impl Component for SignatureHelp {
)] )]
}); });
let sig = &self.signatures[self.active_signature]; let signature = self
.signatures
.get(self.active_signature)
.unwrap_or_else(|| &self.signatures[0]);
let sig_text = crate::ui::markdown::highlighted_code_block( let sig_text = crate::ui::markdown::highlighted_code_block(
sig.signature.as_str(), signature.signature.as_str(),
&self.language, &self.language,
Some(&cx.editor.theme), Some(&cx.editor.theme),
Arc::clone(&self.config_loader), Arc::clone(&self.config_loader),
@ -130,7 +137,7 @@ impl Component for SignatureHelp {
let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false }); let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false });
sig_text_para.render(sig_text_area, surface); sig_text_para.render(sig_text_area, surface);
if sig.signature_doc.is_none() { if signature.signature_doc.is_none() {
return; return;
} }
@ -142,7 +149,7 @@ impl Component for SignatureHelp {
} }
} }
let sig_doc = match &sig.signature_doc { let sig_doc = match &signature.signature_doc {
None => return, None => return,
Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)), Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
}; };
@ -160,12 +167,15 @@ impl Component for SignatureHelp {
const PADDING: u16 = 2; const PADDING: u16 = 2;
const SEPARATOR_HEIGHT: u16 = 1; const SEPARATOR_HEIGHT: u16 = 1;
let sig = &self.signatures[self.active_signature]; let signature = self
.signatures
.get(self.active_signature)
.unwrap_or_else(|| &self.signatures[0]);
let max_text_width = viewport.0.saturating_sub(PADDING).clamp(10, 120); let max_text_width = viewport.0.saturating_sub(PADDING).clamp(10, 120);
let signature_text = crate::ui::markdown::highlighted_code_block( let signature_text = crate::ui::markdown::highlighted_code_block(
sig.signature.as_str(), signature.signature.as_str(),
&self.language, &self.language,
None, None,
Arc::clone(&self.config_loader), Arc::clone(&self.config_loader),
@ -174,7 +184,7 @@ impl Component for SignatureHelp {
let (sig_width, sig_height) = let (sig_width, sig_height) =
crate::ui::text::required_size(&signature_text, max_text_width); crate::ui::text::required_size(&signature_text, max_text_width);
let (width, height) = match sig.signature_doc { let (width, height) = match signature.signature_doc {
Some(ref doc) => { Some(ref doc) => {
let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader)); let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader));
let doc_text = doc_md.parse(None); let doc_text = doc_md.parse(None);

@ -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" }
@ -1243,6 +1244,23 @@ indent = { tab-width = 2, unit = " " }
name = "ocaml-interface" name = "ocaml-interface"
source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "9965d208337d88bbf1a38ad0b0fe49e5f5ec9677", subpath = "interface" } source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "9965d208337d88bbf1a38ad0b0fe49e5f5ec9677", subpath = "interface" }
[[language]]
name = "dune"
scope = "source.dune"
roots = ["dune-project"]
file-types = [{ glob = "dune-project" }, { glob = "dune" }]
comment-token = ";"
indent = { tab-width = 1, unit = " " }
grammar = "scheme"
auto-format = true
formatter = { command = "dune", args = ["format-dune-file"] }
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
[[language]] [[language]]
name = "lua" name = "lua"
injection-regex = "lua" injection-regex = "lua"
@ -1435,6 +1453,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]]
@ -2465,6 +2484,12 @@ injection-regex = "sml"
file-types = ["sml"] file-types = ["sml"]
block-comment-tokens = { start = "(*", end = "*)" } block-comment-tokens = { start = "(*", end = "*)" }
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
[[grammar]] [[grammar]]
name = "sml" name = "sml"
source = { git = "https://github.com/Giorbo/tree-sitter-sml", rev = "bd4055d5554614520d4a0706b34dc0c317c6b608" } source = { git = "https://github.com/Giorbo/tree-sitter-sml", rev = "bd4055d5554614520d4a0706b34dc0c317c6b608" }
@ -3247,7 +3272,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"
@ -3810,3 +3835,17 @@ 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" }

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