editor: Add automatic markdown list continuation on newline and indent on tab (#42800)
Closes #5089 Release notes: - Markdown lists now continue automatically when you press Enter (unordered, ordered, and task lists). This can be configured with `extend_list_on_newline` (default: true). - You can now indent list markers with Tab to quickly create nested lists. This can be configured with `indent_list_on_tab` (default: true). --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
@@ -1178,6 +1178,10 @@
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
"extend_comment_on_newline": true,
|
||||
// Whether to continue markdown lists when pressing enter.
|
||||
"extend_list_on_newline": true,
|
||||
// Whether to indent list items when pressing tab after a list marker.
|
||||
"indent_list_on_tab": true,
|
||||
// Removes any lines containing only whitespace at the end of the file and
|
||||
// ensures just one newline at the end.
|
||||
"ensure_final_newline_on_save": true,
|
||||
|
||||
@@ -163,6 +163,7 @@ use project::{
|
||||
project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings},
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use regex::Regex;
|
||||
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
|
||||
use selections_collection::{MutableSelectionsCollection, SelectionsCollection};
|
||||
@@ -4787,82 +4788,146 @@ impl Editor {
|
||||
let end = selection.end;
|
||||
let selection_is_empty = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (comment_delimiter, doc_delimiter, newline_formatting) =
|
||||
if let Some(language) = &language_scope {
|
||||
let mut newline_formatting =
|
||||
NewlineFormatting::new(&buffer, start..end, language);
|
||||
let (delimiter, newline_config) = if let Some(language) = &language_scope {
|
||||
let needs_extra_newline = NewlineConfig::insert_extra_newline_brackets(
|
||||
&buffer,
|
||||
start..end,
|
||||
language,
|
||||
)
|
||||
|| NewlineConfig::insert_extra_newline_tree_sitter(
|
||||
&buffer,
|
||||
start..end,
|
||||
);
|
||||
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return comment_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
);
|
||||
});
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return documentation_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_formatting,
|
||||
);
|
||||
});
|
||||
|
||||
(comment_delimiter, doc_delimiter, newline_formatting)
|
||||
} else {
|
||||
(None, None, NewlineFormatting::default())
|
||||
let mut newline_config = NewlineConfig::Newline {
|
||||
additional_indent: IndentSize::spaces(0),
|
||||
extra_line_additional_indent: if needs_extra_newline {
|
||||
Some(IndentSize::spaces(0))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
prevent_auto_indent: false,
|
||||
};
|
||||
|
||||
let prevent_auto_indent = doc_delimiter.is_some();
|
||||
let delimiter = comment_delimiter.or(doc_delimiter);
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
let capacity_for_delimiter =
|
||||
delimiter.as_deref().map(str::len).unwrap_or_default();
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ newline_formatting.indent_on_newline.len as usize
|
||||
+ newline_formatting.indent_on_extra_newline.len as usize,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_newline.chars());
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
return comment_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
);
|
||||
});
|
||||
|
||||
if newline_formatting.insert_extra_newline {
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_extra_newline.chars());
|
||||
}
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
return documentation_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_config,
|
||||
);
|
||||
});
|
||||
|
||||
let list_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_list_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
return list_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_config,
|
||||
);
|
||||
});
|
||||
|
||||
(
|
||||
comment_delimiter.or(doc_delimiter).or(list_delimiter),
|
||||
newline_config,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
None,
|
||||
NewlineConfig::Newline {
|
||||
additional_indent: IndentSize::spaces(0),
|
||||
extra_line_additional_indent: None,
|
||||
prevent_auto_indent: false,
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let (edit_start, new_text, prevent_auto_indent) = match &newline_config {
|
||||
NewlineConfig::ClearCurrentLine => {
|
||||
let row_start =
|
||||
buffer.point_to_offset(Point::new(start_point.row, 0));
|
||||
(row_start, String::new(), false)
|
||||
}
|
||||
NewlineConfig::UnindentCurrentLine { continuation } => {
|
||||
let row_start =
|
||||
buffer.point_to_offset(Point::new(start_point.row, 0));
|
||||
let tab_size = buffer.language_settings_at(start, cx).tab_size;
|
||||
let tab_size_indent = IndentSize::spaces(tab_size.get());
|
||||
let reduced_indent =
|
||||
existing_indent.with_delta(Ordering::Less, tab_size_indent);
|
||||
let mut new_text = String::new();
|
||||
new_text.extend(reduced_indent.chars());
|
||||
new_text.push_str(continuation);
|
||||
(row_start, new_text, true)
|
||||
}
|
||||
NewlineConfig::Newline {
|
||||
additional_indent,
|
||||
extra_line_additional_indent,
|
||||
prevent_auto_indent,
|
||||
} => {
|
||||
let capacity_for_delimiter =
|
||||
delimiter.as_deref().map(str::len).unwrap_or_default();
|
||||
let extra_line_len = extra_line_additional_indent
|
||||
.map(|i| 1 + existing_indent.len as usize + i.len as usize)
|
||||
.unwrap_or(0);
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ additional_indent.len as usize
|
||||
+ extra_line_len,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(additional_indent.chars());
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
if let Some(extra_indent) = extra_line_additional_indent {
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(extra_indent.chars());
|
||||
}
|
||||
(start, new_text, *prevent_auto_indent)
|
||||
}
|
||||
};
|
||||
|
||||
let anchor = buffer.anchor_after(end);
|
||||
let new_selection = selection.map(|_| anchor);
|
||||
(
|
||||
((start..end, new_text), prevent_auto_indent),
|
||||
(newline_formatting.insert_extra_newline, new_selection),
|
||||
((edit_start..end, new_text), prevent_auto_indent),
|
||||
(newline_config.has_extra_line(), new_selection),
|
||||
)
|
||||
})
|
||||
.unzip()
|
||||
@@ -10387,6 +10452,22 @@ impl Editor {
|
||||
}
|
||||
prev_edited_row = selection.end.row;
|
||||
|
||||
// If cursor is after a list prefix, make selection non-empty to trigger line indent
|
||||
if selection.is_empty() {
|
||||
let cursor = selection.head();
|
||||
let settings = buffer.language_settings_at(cursor, cx);
|
||||
if settings.indent_list_on_tab {
|
||||
if let Some(language) = snapshot.language_scope_at(Point::new(cursor.row, 0)) {
|
||||
if is_list_prefix_row(MultiBufferRow(cursor.row), &snapshot, &language) {
|
||||
row_delta = Self::indent_selection(
|
||||
buffer, &snapshot, selection, &mut edits, row_delta, cx,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the selection is non-empty, then increase the indentation of the selected lines.
|
||||
if !selection.is_empty() {
|
||||
row_delta =
|
||||
@@ -23355,7 +23436,7 @@ fn documentation_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
newline_formatting: &mut NewlineFormatting,
|
||||
newline_config: &mut NewlineConfig,
|
||||
) -> Option<Arc<str>> {
|
||||
let BlockCommentConfig {
|
||||
start: start_tag,
|
||||
@@ -23407,6 +23488,9 @@ fn documentation_delimiter_for_newline(
|
||||
}
|
||||
};
|
||||
|
||||
let mut needs_extra_line = false;
|
||||
let mut extra_line_additional_indent = IndentSize::spaces(0);
|
||||
|
||||
let cursor_is_before_end_tag_if_exists = {
|
||||
let mut char_position = 0u32;
|
||||
let mut end_tag_offset = None;
|
||||
@@ -23424,11 +23508,11 @@ fn documentation_delimiter_for_newline(
|
||||
let cursor_is_before_end_tag = column <= end_tag_offset;
|
||||
if cursor_is_after_start_tag {
|
||||
if cursor_is_before_end_tag {
|
||||
newline_formatting.insert_extra_newline = true;
|
||||
needs_extra_line = true;
|
||||
}
|
||||
let cursor_is_at_start_of_end_tag = column == end_tag_offset;
|
||||
if cursor_is_at_start_of_end_tag {
|
||||
newline_formatting.indent_on_extra_newline.len = *len;
|
||||
extra_line_additional_indent.len = *len;
|
||||
}
|
||||
}
|
||||
cursor_is_before_end_tag
|
||||
@@ -23440,39 +23524,240 @@ fn documentation_delimiter_for_newline(
|
||||
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
|
||||
&& cursor_is_before_end_tag_if_exists
|
||||
{
|
||||
if cursor_is_after_start_tag {
|
||||
newline_formatting.indent_on_newline.len = *len;
|
||||
}
|
||||
let additional_indent = if cursor_is_after_start_tag {
|
||||
IndentSize::spaces(*len)
|
||||
} else {
|
||||
IndentSize::spaces(0)
|
||||
};
|
||||
|
||||
*newline_config = NewlineConfig::Newline {
|
||||
additional_indent,
|
||||
extra_line_additional_indent: if needs_extra_line {
|
||||
Some(extra_line_additional_indent)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
prevent_auto_indent: true,
|
||||
};
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct NewlineFormatting {
|
||||
insert_extra_newline: bool,
|
||||
indent_on_newline: IndentSize,
|
||||
indent_on_extra_newline: IndentSize,
|
||||
const ORDERED_LIST_MAX_MARKER_LEN: usize = 16;
|
||||
|
||||
fn list_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
newline_config: &mut NewlineConfig,
|
||||
) -> Option<Arc<str>> {
|
||||
let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
let task_list_entries: Vec<_> = language
|
||||
.task_list()
|
||||
.into_iter()
|
||||
.flat_map(|config| {
|
||||
config
|
||||
.prefixes
|
||||
.iter()
|
||||
.map(|prefix| (prefix.as_ref(), config.continuation.as_ref()))
|
||||
})
|
||||
.collect();
|
||||
let unordered_list_entries: Vec<_> = language
|
||||
.unordered_list()
|
||||
.iter()
|
||||
.map(|marker| (marker.as_ref(), marker.as_ref()))
|
||||
.collect();
|
||||
|
||||
let all_entries: Vec<_> = task_list_entries
|
||||
.into_iter()
|
||||
.chain(unordered_list_entries)
|
||||
.collect();
|
||||
|
||||
if let Some(max_prefix_len) = all_entries.iter().map(|(p, _)| p.len()).max() {
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_prefix_len)
|
||||
.collect();
|
||||
|
||||
if let Some((prefix, continuation)) = all_entries
|
||||
.iter()
|
||||
.filter(|(prefix, _)| candidate.starts_with(*prefix))
|
||||
.max_by_key(|(prefix, _)| prefix.len())
|
||||
{
|
||||
let end_of_prefix = num_of_whitespaces + prefix.len();
|
||||
let cursor_is_after_prefix = end_of_prefix <= start_point.column as usize;
|
||||
let has_content_after_marker = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(end_of_prefix)
|
||||
.any(|c| !c.is_whitespace());
|
||||
|
||||
if has_content_after_marker && cursor_is_after_prefix {
|
||||
return Some((*continuation).into());
|
||||
}
|
||||
|
||||
if start_point.column as usize == end_of_prefix {
|
||||
if num_of_whitespaces == 0 {
|
||||
*newline_config = NewlineConfig::ClearCurrentLine;
|
||||
} else {
|
||||
*newline_config = NewlineConfig::UnindentCurrentLine {
|
||||
continuation: (*continuation).into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(ORDERED_LIST_MAX_MARKER_LEN)
|
||||
.collect();
|
||||
|
||||
for ordered_config in language.ordered_list() {
|
||||
let regex = match Regex::new(&ordered_config.pattern) {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if let Some(captures) = regex.captures(&candidate) {
|
||||
let full_match = captures.get(0)?;
|
||||
let marker_len = full_match.len();
|
||||
let end_of_prefix = num_of_whitespaces + marker_len;
|
||||
let cursor_is_after_prefix = end_of_prefix <= start_point.column as usize;
|
||||
|
||||
let has_content_after_marker = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(end_of_prefix)
|
||||
.any(|c| !c.is_whitespace());
|
||||
|
||||
if has_content_after_marker && cursor_is_after_prefix {
|
||||
let number: u32 = captures.get(1)?.as_str().parse().ok()?;
|
||||
let continuation = ordered_config
|
||||
.format
|
||||
.replace("{1}", &(number + 1).to_string());
|
||||
return Some(continuation.into());
|
||||
}
|
||||
|
||||
if start_point.column as usize == end_of_prefix {
|
||||
let continuation = ordered_config.format.replace("{1}", "1");
|
||||
if num_of_whitespaces == 0 {
|
||||
*newline_config = NewlineConfig::ClearCurrentLine;
|
||||
} else {
|
||||
*newline_config = NewlineConfig::UnindentCurrentLine {
|
||||
continuation: continuation.into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl NewlineFormatting {
|
||||
fn new(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &LanguageScope,
|
||||
) -> Self {
|
||||
Self {
|
||||
insert_extra_newline: Self::insert_extra_newline_brackets(
|
||||
buffer,
|
||||
range.clone(),
|
||||
language,
|
||||
) || Self::insert_extra_newline_tree_sitter(buffer, range),
|
||||
indent_on_newline: IndentSize::spaces(0),
|
||||
indent_on_extra_newline: IndentSize::spaces(0),
|
||||
fn is_list_prefix_row(
|
||||
row: MultiBufferRow,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
) -> bool {
|
||||
let Some((snapshot, range)) = buffer.buffer_line_for_row(row) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
let task_list_prefixes: Vec<_> = language
|
||||
.task_list()
|
||||
.into_iter()
|
||||
.flat_map(|config| {
|
||||
config
|
||||
.prefixes
|
||||
.iter()
|
||||
.map(|p| p.as_ref())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
let unordered_list_markers: Vec<_> = language
|
||||
.unordered_list()
|
||||
.iter()
|
||||
.map(|marker| marker.as_ref())
|
||||
.collect();
|
||||
let all_prefixes: Vec<_> = task_list_prefixes
|
||||
.into_iter()
|
||||
.chain(unordered_list_markers)
|
||||
.collect();
|
||||
if let Some(max_prefix_len) = all_prefixes.iter().map(|p| p.len()).max() {
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_prefix_len)
|
||||
.collect();
|
||||
if all_prefixes
|
||||
.iter()
|
||||
.any(|prefix| candidate.starts_with(*prefix))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let ordered_list_candidate: String = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(num_of_whitespaces)
|
||||
.take(ORDERED_LIST_MAX_MARKER_LEN)
|
||||
.collect();
|
||||
for ordered_config in language.ordered_list() {
|
||||
let regex = match Regex::new(&ordered_config.pattern) {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Some(captures) = regex.captures(&ordered_list_candidate) {
|
||||
return captures.get(0).is_some();
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum NewlineConfig {
|
||||
/// Insert newline with optional additional indent and optional extra blank line
|
||||
Newline {
|
||||
additional_indent: IndentSize,
|
||||
extra_line_additional_indent: Option<IndentSize>,
|
||||
prevent_auto_indent: bool,
|
||||
},
|
||||
/// Clear the current line
|
||||
ClearCurrentLine,
|
||||
/// Unindent the current line and add continuation
|
||||
UnindentCurrentLine { continuation: Arc<str> },
|
||||
}
|
||||
|
||||
impl NewlineConfig {
|
||||
fn has_extra_line(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Newline {
|
||||
extra_line_additional_indent: Some(_),
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn insert_extra_newline_brackets(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
|
||||
@@ -28021,7 +28021,7 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
"
|
||||
});
|
||||
|
||||
// Case 2: Test adding new line after nested list preserves indent of previous line
|
||||
// Case 2: Test adding new line after nested list continues the list with unchecked task
|
||||
cx.set_state(&indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
@@ -28038,20 +28038,12 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
ˇ"
|
||||
- [ ] ˇ"
|
||||
});
|
||||
|
||||
// Case 3: Test adding a new nested list item preserves indent
|
||||
cx.set_state(&indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
ˇ"
|
||||
});
|
||||
// Case 3: Test adding content to continued list item
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("-", window, cx);
|
||||
editor.handle_input("Item 2.c", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -28060,22 +28052,10 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
-ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(" [x] Item 2.c", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
- [x] Item 2.cˇ"
|
||||
- [ ] Item 2.cˇ"
|
||||
});
|
||||
|
||||
// Case 4: Test adding new line after nested ordered list preserves indent of previous line
|
||||
// Case 4: Test adding new line after nested ordered list continues with next number
|
||||
cx.set_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
@@ -28092,44 +28072,12 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
ˇ"
|
||||
3. ˇ"
|
||||
});
|
||||
|
||||
// Case 5: Adding new ordered list item preserves indent
|
||||
cx.set_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
ˇ"
|
||||
});
|
||||
// Case 5: Adding content to continued ordered list item
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("3", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
3ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
3.ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(" Item 2.c", window, cx);
|
||||
editor.handle_input("Item 2.c", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -29497,6 +29445,524 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state(after);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] taskˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with checked task items too
|
||||
cx.set_state(indoc! {"
|
||||
- [x] completed taskˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [x] completed task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] taˇsk
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] ta
|
||||
- [ ] ˇsk
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
- [ ]$$
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] indented
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after prefix, unindents
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after prefix, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ- [ ] task
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ- [ ] task
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
- [ˇ ] task
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [
|
||||
ˇ
|
||||
] task
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
|
||||
cx.set_state(indoc! {"
|
||||
- itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with different markers
|
||||
cx.set_state(indoc! {"
|
||||
* starred itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
* starred item
|
||||
* ˇ
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
+ plus itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
+ plus item
|
||||
+ ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
- itˇem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- it
|
||||
- ˇem
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
- $
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
- indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- indented
|
||||
- ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after marker, unindents
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after marker, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ- item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ- item
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
-ˇ item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
-
|
||||
ˇitem
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
|
||||
cx.set_state(indoc! {"
|
||||
1. first itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. first item
|
||||
2. ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with larger numbers
|
||||
cx.set_state(indoc! {"
|
||||
10. tenth itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
10. tenth item
|
||||
11. ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
1. itˇem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. it
|
||||
2. ˇem
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
1. $
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
1. item
|
||||
2. indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. indented
|
||||
3. ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after marker, unindents
|
||||
cx.set_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
3. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after marker, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ1. item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ1. item
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
1ˇ. item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1
|
||||
ˇ. item
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
|
||||
cx.set_state(indoc! {"
|
||||
1. first item
|
||||
1. sub first item
|
||||
2. sub second item
|
||||
3. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. first item
|
||||
1. sub first item
|
||||
2. sub second item
|
||||
1. ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_list_indent(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Unordered list - cursor after prefix, adds indent before prefix
|
||||
cx.set_state(indoc! {"
|
||||
- ˇitem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- ˇitem
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 2: Task list - cursor after prefix
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] ˇtask
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- [ ] ˇtask
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 3: Ordered list - cursor after prefix
|
||||
cx.set_state(indoc! {"
|
||||
1. ˇfirst
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$1. ˇfirst
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 4: With existing indentation - adds more indent
|
||||
let initial = indoc! {"
|
||||
$$- ˇitem
|
||||
"};
|
||||
cx.set_state(initial.replace("$", " ").as_str());
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$$$- ˇitem
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 5: Empty list item
|
||||
cx.set_state(indoc! {"
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- ˇ
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 6: Cursor at end of line with content
|
||||
cx.set_state(indoc! {"
|
||||
- itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- itemˇ
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 7: Cursor at start of list item, indents it
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"};
|
||||
cx.assert_editor_state(expected);
|
||||
|
||||
// Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
|
||||
cx.update_editor(|_, _, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
- item
|
||||
ˇ- sub item
|
||||
"};
|
||||
cx.assert_editor_state(expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_local_worktree_trust(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -827,6 +827,15 @@ pub struct LanguageConfig {
|
||||
/// Delimiters and configuration for recognizing and formatting documentation comments.
|
||||
#[serde(default, alias = "documentation")]
|
||||
pub documentation_comment: Option<BlockCommentConfig>,
|
||||
/// List markers that are inserted unchanged on newline (e.g., `- `, `* `, `+ `).
|
||||
#[serde(default)]
|
||||
pub unordered_list: Vec<Arc<str>>,
|
||||
/// Configuration for ordered lists with auto-incrementing numbers on newline (e.g., `1. ` becomes `2. `).
|
||||
#[serde(default)]
|
||||
pub ordered_list: Vec<OrderedListConfig>,
|
||||
/// Configuration for task lists where multiple markers map to a single continuation prefix (e.g., `- [x] ` continues as `- [ ] `).
|
||||
#[serde(default)]
|
||||
pub task_list: Option<TaskListConfig>,
|
||||
/// A list of additional regex patterns that should be treated as prefixes
|
||||
/// for creating boundaries during rewrapping, ensuring content from one
|
||||
/// prefixed section doesn't merge with another (e.g., markdown list items).
|
||||
@@ -898,6 +907,24 @@ pub struct DecreaseIndentConfig {
|
||||
pub valid_after: Vec<String>,
|
||||
}
|
||||
|
||||
/// Configuration for continuing ordered lists with auto-incrementing numbers.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
pub struct OrderedListConfig {
|
||||
/// A regex pattern with a capture group for the number portion (e.g., `(\\d+)\\. `).
|
||||
pub pattern: String,
|
||||
/// A format string where `{1}` is replaced with the incremented number (e.g., `{1}. `).
|
||||
pub format: String,
|
||||
}
|
||||
|
||||
/// Configuration for continuing task lists on newline.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
pub struct TaskListConfig {
|
||||
/// The list markers to match (e.g., `- [ ] `, `- [x] `).
|
||||
pub prefixes: Vec<Arc<str>>,
|
||||
/// The marker to insert when continuing the list on a new line (e.g., `- [ ] `).
|
||||
pub continuation: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
|
||||
pub struct LanguageMatcher {
|
||||
/// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`.
|
||||
@@ -1068,6 +1095,9 @@ impl Default for LanguageConfig {
|
||||
line_comments: Default::default(),
|
||||
block_comment: Default::default(),
|
||||
documentation_comment: Default::default(),
|
||||
unordered_list: Default::default(),
|
||||
ordered_list: Default::default(),
|
||||
task_list: Default::default(),
|
||||
rewrap_prefixes: Default::default(),
|
||||
scope_opt_in_language_servers: Default::default(),
|
||||
overrides: Default::default(),
|
||||
@@ -2153,6 +2183,21 @@ impl LanguageScope {
|
||||
self.language.config.documentation_comment.as_ref()
|
||||
}
|
||||
|
||||
/// Returns list markers that are inserted unchanged on newline (e.g., `- `, `* `, `+ `).
|
||||
pub fn unordered_list(&self) -> &[Arc<str>] {
|
||||
&self.language.config.unordered_list
|
||||
}
|
||||
|
||||
/// Returns configuration for ordered lists with auto-incrementing numbers (e.g., `1. ` becomes `2. `).
|
||||
pub fn ordered_list(&self) -> &[OrderedListConfig] {
|
||||
&self.language.config.ordered_list
|
||||
}
|
||||
|
||||
/// Returns configuration for task list continuation, if any (e.g., `- [x] ` continues as `- [ ] `).
|
||||
pub fn task_list(&self) -> Option<&TaskListConfig> {
|
||||
self.language.config.task_list.as_ref()
|
||||
}
|
||||
|
||||
/// Returns additional regex patterns that act as prefix markers for creating
|
||||
/// boundaries during rewrapping.
|
||||
///
|
||||
|
||||
@@ -122,6 +122,10 @@ pub struct LanguageSettings {
|
||||
pub whitespace_map: WhitespaceMap,
|
||||
/// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
pub extend_comment_on_newline: bool,
|
||||
/// Whether to continue markdown lists when pressing enter.
|
||||
pub extend_list_on_newline: bool,
|
||||
/// Whether to indent list items when pressing tab after a list marker.
|
||||
pub indent_list_on_tab: bool,
|
||||
/// Inlay hint related settings.
|
||||
pub inlay_hints: InlayHintSettings,
|
||||
/// Whether to automatically close brackets.
|
||||
@@ -567,6 +571,8 @@ impl settings::Settings for AllLanguageSettings {
|
||||
tab: SharedString::new(whitespace_map.tab.unwrap().to_string()),
|
||||
},
|
||||
extend_comment_on_newline: settings.extend_comment_on_newline.unwrap(),
|
||||
extend_list_on_newline: settings.extend_list_on_newline.unwrap(),
|
||||
indent_list_on_tab: settings.indent_list_on_tab.unwrap(),
|
||||
inlay_hints: InlayHintSettings {
|
||||
enabled: inlay_hints.enabled.unwrap(),
|
||||
show_value_hints: inlay_hints.show_value_hints.unwrap(),
|
||||
|
||||
@@ -20,6 +20,9 @@ rewrap_prefixes = [
|
||||
">\\s*",
|
||||
"[-*+]\\s+\\[[\\sx]\\]\\s+"
|
||||
]
|
||||
unordered_list = ["- ", "* ", "+ "]
|
||||
ordered_list = [{ pattern = "(\\d+)\\. ", format = "{1}. " }]
|
||||
task_list = { prefixes = ["- [ ] ", "- [x] "], continuation = "- [ ] " }
|
||||
|
||||
auto_indent_on_paste = false
|
||||
auto_indent_using_last_non_empty_line = false
|
||||
|
||||
@@ -363,6 +363,14 @@ pub struct LanguageSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub extend_comment_on_newline: Option<bool>,
|
||||
/// Whether to continue markdown lists when pressing enter.
|
||||
///
|
||||
/// Default: true
|
||||
pub extend_list_on_newline: Option<bool>,
|
||||
/// Whether to indent list items when pressing tab after a list marker.
|
||||
///
|
||||
/// Default: true
|
||||
pub indent_list_on_tab: Option<bool>,
|
||||
/// Inlay hint related settings.
|
||||
pub inlay_hints: Option<InlayHintSettingsContent>,
|
||||
/// Whether to automatically type closing characters for you. For example,
|
||||
|
||||
@@ -430,6 +430,8 @@ impl VsCodeSettings {
|
||||
enable_language_server: None,
|
||||
ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"),
|
||||
extend_comment_on_newline: None,
|
||||
extend_list_on_newline: None,
|
||||
indent_list_on_tab: None,
|
||||
format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| {
|
||||
if b {
|
||||
FormatOnSave::On
|
||||
|
||||
@@ -1585,6 +1585,26 @@ Positive `integer` value between 1 and 32. Values outside of this range will be
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Extend List On Newline
|
||||
|
||||
- Description: Whether to continue lists when pressing Enter at the end of a list item. Supports unordered, ordered, and task lists. Pressing Enter on an empty list item removes the marker and exits the list.
|
||||
- Setting: `extend_list_on_newline`
|
||||
- Default: `true`
|
||||
|
||||
**Options**
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Indent List On Tab
|
||||
|
||||
- Description: Whether to indent list items when pressing Tab on a line containing only a list marker. This enables quick creation of nested lists.
|
||||
- Setting: `indent_list_on_tab`
|
||||
- Default: `true`
|
||||
|
||||
**Options**
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Status Bar
|
||||
|
||||
- Description: Control various elements in the status bar. Note that some items in the status bar have their own settings set elsewhere.
|
||||
|
||||
@@ -33,6 +33,40 @@ Zed supports using Prettier to automatically re-format Markdown documents. You c
|
||||
},
|
||||
```
|
||||
|
||||
### List Continuation
|
||||
|
||||
Zed automatically continues lists when you press Enter at the end of a list item. Supported list types:
|
||||
|
||||
- Unordered lists (`-`, `*`, or `+` markers)
|
||||
- Ordered lists (numbers are auto-incremented)
|
||||
- Task lists (`- [ ]` and `- [x]`)
|
||||
|
||||
Pressing Enter on an empty list item removes the marker and exits the list.
|
||||
|
||||
To disable this behavior:
|
||||
|
||||
```json [settings]
|
||||
"languages": {
|
||||
"Markdown": {
|
||||
"extend_list_on_newline": false
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
### List Indentation
|
||||
|
||||
Zed indents list items when you press Tab while the cursor is on a line containing only a list marker. This allows you to quickly create nested lists.
|
||||
|
||||
To disable this behavior:
|
||||
|
||||
```json [settings]
|
||||
"languages": {
|
||||
"Markdown": {
|
||||
"indent_list_on_tab": false
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
### Trailing Whitespace
|
||||
|
||||
By default Zed will remove trailing whitespace on save. If you rely on invisible trailing whitespace being converted to `<br />` in Markdown files you can disable this behavior with:
|
||||
|
||||
Reference in New Issue
Block a user