Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: continue single-line comments #8664

Closed
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3844eb7
Add `continue_comment` function
seanchen1991 Oct 20, 2023
3d31e66
Add test for `get_comment_token` fn
seanchen1991 Oct 20, 2023
129ecae
Fix incorrect assertion
seanchen1991 Oct 20, 2023
2449622
Wire up continue comment functionality
seanchen1991 Oct 20, 2023
036e2bf
Add `comment-tokens` field to languages.toml
seanchen1991 Oct 20, 2023
adda266
Add additional comment tokens for Rust
seanchen1991 Oct 20, 2023
be9224e
Match all comment-tokens fields with comment-token fields
seanchen1991 Oct 20, 2023
e77d15d
Refactor continue comment function to also return position of comment…
seanchen1991 Oct 20, 2023
751d15d
Update calls to find_first_non_whitespace_char
seanchen1991 Oct 22, 2023
4315906
Continue single comments with indentation
seanchen1991 Oct 22, 2023
0d224e5
Implement `count_whitespace_after` fn
seanchen1991 Oct 24, 2023
ee9c015
Get tests for continue comment logic passing
seanchen1991 Oct 24, 2023
e44d55a
Don't count newlines when counting whitespace
seanchen1991 Oct 24, 2023
d816982
Rename a variable
seanchen1991 Oct 24, 2023
e6a492b
Add `continue-comment` config parameter
seanchen1991 Oct 25, 2023
c3a6190
Contiue comments in insert mode based on config
seanchen1991 Oct 25, 2023
d25c959
Merge branch 'helix-editor:master' into feature/continue-comment
seanchen1991 Oct 26, 2023
cbd05dc
Use pre-existing indentation functions to indent comment lines
seanchen1991 Oct 26, 2023
d73cd87
Merge branch 'feature/continue-comment' of https://github.com/seanche…
seanchen1991 Oct 26, 2023
e170d40
Clean up for PR submission
seanchen1991 Oct 30, 2023
d52dae9
Fix merge conflict in languages.toml
seanchen1991 Oct 30, 2023
b03f870
Remove changes in indent.rs
seanchen1991 Oct 30, 2023
57261e5
Remove incorrect unit test
seanchen1991 Nov 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions helix-core/src/chars.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Utility functions to categorize a `char`.

use ropey::RopeSlice;

use crate::LineEnding;

#[derive(Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -85,6 +87,10 @@ pub fn char_is_word(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}

pub fn find_first_non_whitespace_char(line: &RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
151 changes: 144 additions & 7 deletions helix-core/src/comment.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! This module contains the functionality toggle comments on lines over the selection
//! using the comment character defined in the user's `languages.toml`
//! This module contains the functionality for the following comment-related features
//! using the comment character defined in the user's `languages.toml`:
//! * toggle comments on lines over the selection
//! * continue comment when opening a new line

use crate::{
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
};
use crate::{chars, Change, Rope, RopeSlice, Selection, Tendril, Transaction};
use std::borrow::Cow;

/// Given text, a comment token, and a set of line indices, returns the following:
Expand All @@ -27,7 +27,7 @@ fn find_line_comment(
let token_len = token.chars().count();
for line in lines {
let line_slice = text.line(line);
if let Some(pos) = find_first_non_whitespace_char(line_slice) {
if let Some(pos) = chars::find_first_non_whitespace_char(&line_slice) {
let len = line_slice.len_chars();

if pos < min {
Expand Down Expand Up @@ -94,12 +94,56 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
Transaction::change(doc, changes.into_iter())
}

/// Return the comment token of the current line if it is commented.
///
/// Return None otherwise.
pub fn get_comment_token<'a>(line: &RopeSlice, tokens: &'a [String]) -> Option<&'a str> {
// TODO: don't continue shebangs
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO was carried over from #1937, but I'm not sure what it means.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About shebangs in general: https://en.wikipedia.org/wiki/Shebang_(Unix)

We shouldn't continue shebangs as if they were regular comments since they only take up one line

if tokens.is_empty() {
return None;
}

let mut result = None;

if let Some(pos) = chars::find_first_non_whitespace_char(line) {
let len = line.len_chars();

for token in tokens {
// line can be shorter than pos + token length
let fragment = Cow::from(line.slice(pos..std::cmp::min(pos + token.len(), len)));

if fragment == *token {
// We don't necessarily want to break upon finding the first matching comment token
// Instead, we check against all of the comment tokens and end up returning the longest
// comment token that matches
result = Some(token.as_str());
}
}
}

result
}

/// Determines whether the new line following the given line should be
/// prepended with a comment token. If it does, the `text` string is
/// appended with the appropriate comment token.
pub fn handle_comment_continue<'a>(
line: &'a RopeSlice,
text: &'a mut String,
comment_tokens: &'a [String],
) {
if let Some(token) = get_comment_token(line, comment_tokens) {
let new_line = format!("{} ", token);
text.push_str(new_line.as_str());
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_find_line_comment() {
fn test_toggle_line_comments() {
// 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
Expand Down Expand Up @@ -149,4 +193,97 @@ mod test {

// TODO: account for uncommenting with uneven comment indentation
}

#[test]
fn test_get_comment_token() {
let tokens = vec![
String::from("//"),
String::from("///"),
String::from("//!"),
String::from(";"),
];

assert_eq!(get_comment_token(&RopeSlice::from("# 1\n"), &tokens), None);
assert_eq!(
get_comment_token(&RopeSlice::from(" // 2 \n"), &tokens),
Some("//")
);
assert_eq!(
get_comment_token(&RopeSlice::from("///3\n"), &tokens),
Some("///")
);
assert_eq!(
get_comment_token(&RopeSlice::from("/// 4\n"), &tokens),
Some("///")
);
assert_eq!(
get_comment_token(&RopeSlice::from("//! 5\n"), &tokens),
Some("//!")
);
assert_eq!(
get_comment_token(&RopeSlice::from("//! /// 6\n"), &tokens),
Some("//!")
);
assert_eq!(
get_comment_token(&RopeSlice::from("7 ///\n"), &tokens),
None
);
assert_eq!(
get_comment_token(&RopeSlice::from(";8\n"), &tokens),
Some(";")
);
assert_eq!(
get_comment_token(&RopeSlice::from("//////////// 9"), &tokens),
Some("///")
);
}

#[test]
fn test_handle_comment_continue() {
let comment_tokens = vec![String::from("//"), String::from("///")];
let mut line = RopeSlice::from("// 1\n");
let mut text = String::from(line);

handle_comment_continue(&line, &mut text, &comment_tokens);

assert_eq!(text, String::from("// 1\n// "));

line = RopeSlice::from("///2\n");
text = String::from(line);

handle_comment_continue(&line, &mut text, &comment_tokens);

assert_eq!(text, String::from("///2\n/// "));

line = RopeSlice::from(" // 3\n");
text = String::from(line);

handle_comment_continue(&line, &mut text, &comment_tokens);

assert_eq!(text, String::from(" // 3\n // "));

line = RopeSlice::from("/// 4\n");
text = String::from(line);

handle_comment_continue(&line, &mut text, &comment_tokens);

assert_eq!(text, String::from("/// 4\n/// "));

line = RopeSlice::from("// \n");
text = String::from(line);

handle_comment_continue(&line, &mut text, &comment_tokens);

assert_eq!(text, String::from("// \n// "));

line = RopeSlice::from(" // Lorem Ipsum\n // dolor sit amet\n");
text = String::from(line);

handle_comment_continue(&line, &mut text, &comment_tokens);

assert_eq!(
text,
String::from(" // Lorem Ipsum\n // dolor sit amet\n // ")
);
}
}
26 changes: 24 additions & 2 deletions helix-core/src/indent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,16 +184,38 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
/// To determine indentation of a newly inserted line, figure out the indentation at the last col
/// of the previous line.
pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: usize) -> usize {
if let Some(indent_level) = indent_level_at(&line, tab_width, indent_width, 0) {
indent_level
} else {
0
}
}

/// Determine the indentation level starting from the specified position index in the line.
///
/// Returns None if the specified index is out of bounds of the line.
pub fn indent_level_at(
line: &RopeSlice,
tab_width: usize,
indent_width: usize,
starting_pos: usize,
) -> Option<usize> {
let line_len = line.len_chars();

if starting_pos >= line_len {
return None;
}

let mut len = 0;
for ch in line.chars() {
for ch in line.chars_at(starting_pos) {
match ch {
'\t' => len += tab_width_at(len, tab_width as u16),
' ' => len += 1,
_ => break,
}
}

len / indent_width
Some(len / indent_width)
}

/// Computes for node and all ancestors whether they are the first node on their line.
Expand Down
3 changes: 0 additions & 3 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ pub mod unicode {

pub use helix_loader::find_workspace;

pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}
mod rope_reader;

pub use rope_reader::RopeReader;
Expand Down
4 changes: 3 additions & 1 deletion helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ pub struct LanguageConfiguration {
#[serde(default)]
pub shebangs: Vec<String>, // interpreter(s) associated with language
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>,
pub comment_token: Option<String>, // TODO: Replace all usages of `comment_token` with `comment_tokens`
#[serde(default)]
pub comment_tokens: Vec<String>,
pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,

Expand Down
2 changes: 1 addition & 1 deletion helix-core/tests/indent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ fn test_treesitter_indent(
if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
continue;
}
if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
if let Some(pos) = helix_core::chars::find_first_non_whitespace_char(&line) {
let tab_width: usize = 4;
let suggested_indent = treesitter_indent_for_pos(
indent_query,
Expand Down
40 changes: 33 additions & 7 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use tui::widgets::Row;
pub use typed::*;

use helix_core::{
char_idx_at_visual_offset, comment,
char_idx_at_visual_offset, chars, comment,
doc_formatter::TextFormat,
encoding, find_first_non_whitespace_char, find_workspace, graphemes,
encoding, find_workspace, graphemes,
history::UndoKind,
increment, indent,
indent::IndentStyle,
Expand Down Expand Up @@ -814,7 +814,7 @@ fn kill_to_line_start(cx: &mut Context) {
let head = if anchor == first_char && line != 0 {
// select until previous line
line_end_char_index(&text, line - 1)
} else if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
} else if let Some(pos) = chars::find_first_non_whitespace_char(&text.line(line)) {
if first_char + pos < anchor {
// select until first non-blank in line if cursor is after it
first_char + pos
Expand Down Expand Up @@ -876,7 +876,7 @@ fn goto_first_nonwhitespace_impl(view: &mut View, doc: &mut Document, movement:
let selection = doc.selection(view.id).clone().transform(|range| {
let line = range.cursor_line(text);

if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
if let Some(pos) = chars::find_first_non_whitespace_char(&text.line(line)) {
let pos = pos + text.line_to_char(line);
range.put_cursor(text, pos, movement == Movement::Extend)
} else {
Expand Down Expand Up @@ -3004,7 +3004,7 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
// move cursor to the fallback position
let pos = match cursor_fallback {
IndentFallbackPos::LineStart => {
find_first_non_whitespace_char(text.line(cursor_line))
chars::find_first_non_whitespace_char(&text.line(cursor_line))
.map(|ws_offset| ws_offset + cursor_line_start)
.unwrap_or(cursor_line_start)
}
Expand Down Expand Up @@ -3077,6 +3077,7 @@ fn open(cx: &mut Context, open: Open) {
enter_insert_mode(cx);
let (view, doc) = current!(cx.editor);

let config = doc.config.load();
let text = doc.text().slice(..);
let contents = doc.text();
let selection = doc.selection(view.id);
Expand Down Expand Up @@ -3125,6 +3126,11 @@ fn open(cx: &mut Context, open: Open) {
let mut text = String::with_capacity(1 + indent_len);
text.push_str(doc.line_ending.as_str());
text.push_str(&indent);

if config.continue_comments {
handle_comment_continue(doc, &mut text, cursor_line);
}

let text = text.repeat(count);

// calculate new selection ranges
Expand All @@ -3144,6 +3150,21 @@ fn open(cx: &mut Context, open: Open) {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));

doc.apply(&transaction, view.id);

// Since we might have added a comment token, move to the end of the line.
goto_line_end_newline(cx);
}

// Currently only continues single-line comments
// TODO: Handle block comments as well
fn handle_comment_continue(doc: &Document, text: &mut String, cursor_line: usize) {
let line = doc.text().line(cursor_line);

if let Some(lang_config) = doc.language_config() {
let comment_tokens = &lang_config.comment_tokens;

comment::handle_comment_continue(&line, text, comment_tokens);
}
}

// o inserts a new line after each line with a selection
Expand Down Expand Up @@ -3613,6 +3634,7 @@ pub mod insert {

pub fn insert_newline(cx: &mut Context) {
let (view, doc) = current_ref!(cx.editor);
let config = doc.config.load();
let text = doc.text().slice(..);

let contents = doc.text();
Expand Down Expand Up @@ -3681,6 +3703,11 @@ pub mod insert {
new_text.reserve_exact(1 + indent.len());
new_text.push_str(doc.line_ending.as_str());
new_text.push_str(&indent);

if config.continue_comments {
handle_comment_continue(doc, &mut new_text, current_line);
}

new_text.chars().count()
};

Expand Down Expand Up @@ -4514,7 +4541,6 @@ pub fn completion(cx: &mut Context) {
// TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
// completion filtering. For example logger.te| should filter the initial suggestion list with "te".

use helix_core::chars;
let mut iter = text.chars_at(cursor);
iter.reverse();
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
Expand Down Expand Up @@ -4583,7 +4609,7 @@ fn toggle_comments(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let token = doc
.language_config()
.and_then(|lc| lc.comment_token.as_ref())
.and_then(|lc| lc.comment_tokens.get(0))
.map(|tc| tc.as_ref());
let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);

Expand Down
Loading