Detailed changes
@@ -107,9 +107,9 @@ pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
- CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText,
- IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
- TransactionId, TreeSitterOptions, WordsQuery,
+ CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
+ EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
+ Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -3912,7 +3912,7 @@ impl Editor {
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
self.transact(window, cx, |this, window, cx| {
- let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
+ let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = {
let selections = this.selections.all::<usize>(cx);
let multi_buffer = this.buffer.read(cx);
let buffer = multi_buffer.snapshot(cx);
@@ -3920,22 +3920,26 @@ impl Editor {
.iter()
.map(|selection| {
let start_point = selection.start.to_point(&buffer);
- let mut indent =
+ let mut existing_indent =
buffer.indent_size_for_line(MultiBufferRow(start_point.row));
- indent.len = cmp::min(indent.len, start_point.column);
+ existing_indent.len = cmp::min(existing_indent.len, start_point.column);
let start = selection.start;
let end = selection.end;
let selection_is_empty = start == end;
let language_scope = buffer.language_scope_at(start);
- let (comment_delimiter, insert_extra_newline) = if let Some(language) =
- &language_scope
- {
+ let (
+ comment_delimiter,
+ doc_delimiter,
+ insert_extra_newline,
+ indent_on_newline,
+ indent_on_extra_newline,
+ ) = if let Some(language) = &language_scope {
let mut insert_extra_newline =
insert_extra_newline_brackets(&buffer, start..end, language)
|| insert_extra_newline_tree_sitter(&buffer, start..end);
// Comment extension on newline is allowed only for cursor selections
- let mut comment_delimiter = maybe!({
+ let comment_delimiter = maybe!({
if !selection_is_empty {
return None;
}
@@ -3975,128 +3979,181 @@ impl Editor {
}
});
- if comment_delimiter.is_none() {
- comment_delimiter = maybe!({
- if !selection_is_empty {
- return None;
- }
+ let mut indent_on_newline = IndentSize::spaces(0);
+ let mut indent_on_extra_newline = IndentSize::spaces(0);
- if !multi_buffer.language_settings(cx).extend_comment_on_newline
- {
- return None;
+ let doc_delimiter = maybe!({
+ if !selection_is_empty {
+ return None;
+ }
+
+ if !multi_buffer.language_settings(cx).extend_comment_on_newline {
+ return None;
+ }
+
+ let DocumentationConfig {
+ start: start_tag,
+ end: end_tag,
+ prefix: delimiter,
+ tab_size: len,
+ } = language.documentation()?;
+
+ 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 cursor_is_after_start_tag = {
+ let start_tag_len = start_tag.len();
+ let start_tag_line = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(start_tag_len)
+ .collect::<String>();
+ if start_tag_line.starts_with(start_tag.as_ref()) {
+ num_of_whitespaces + start_tag_len
+ <= start_point.column as usize
+ } else {
+ false
}
+ };
- let doc_block = language.documentation_block();
- let doc_block_prefix = doc_block.first()?;
- let doc_block_suffix = doc_block.last()?;
-
- let doc_comment_prefix =
- language.documentation_comment_prefix()?;
-
- let (snapshot, range) = buffer
- .buffer_line_for_row(MultiBufferRow(start_point.row))?;
-
- let cursor_is_after_prefix = {
- let doc_block_prefix_len = doc_block_prefix.len();
- let max_len_of_delimiter = std::cmp::max(
- doc_comment_prefix.len(),
- doc_block_prefix_len,
- );
- let index_of_first_non_whitespace = snapshot
- .chars_for_range(range.clone())
- .take_while(|c| c.is_whitespace())
- .count();
- let doc_line_candidate = snapshot
- .chars_for_range(range.clone())
- .skip(index_of_first_non_whitespace)
- .take(max_len_of_delimiter)
- .collect::<String>();
- if doc_line_candidate.starts_with(doc_block_prefix.as_ref())
- {
- index_of_first_non_whitespace + doc_block_prefix_len
- <= start_point.column as usize
- } else if doc_line_candidate
- .starts_with(doc_comment_prefix.as_ref())
- {
- index_of_first_non_whitespace + doc_comment_prefix.len()
- <= start_point.column as usize
- } else {
- false
- }
- };
+ let cursor_is_after_delimiter = {
+ let delimiter_trim = delimiter.trim_end();
+ let delimiter_line = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(delimiter_trim.len())
+ .collect::<String>();
+ if delimiter_line.starts_with(delimiter_trim) {
+ num_of_whitespaces + delimiter_trim.len()
+ <= start_point.column as usize
+ } else {
+ false
+ }
+ };
- let cursor_is_before_suffix_if_exits = {
- let whitespace_char_from_last = snapshot
- .reversed_chars_for_range(range.clone())
- .take_while(|c| c.is_whitespace())
- .count();
- let mut line_rev_iter = snapshot
- .reversed_chars_for_range(range)
- .skip(whitespace_char_from_last);
- let suffix_exists = doc_block_suffix
- .chars()
- .rev()
- .all(|char| line_rev_iter.next() == Some(char));
- if suffix_exists {
- let max_point =
- snapshot.line_len(start_point.row) as usize;
- let cursor_is_before_suffix = whitespace_char_from_last
- + doc_block_suffix.len()
- + start_point.column as usize
- <= max_point;
- if cursor_is_before_suffix {
+ let cursor_is_before_end_tag_if_exists = {
+ let num_of_whitespaces_rev = snapshot
+ .reversed_chars_for_range(range.clone())
+ .take_while(|c| c.is_whitespace())
+ .count();
+ let mut line_iter = snapshot
+ .reversed_chars_for_range(range)
+ .skip(num_of_whitespaces_rev);
+ let end_tag_exists = end_tag
+ .chars()
+ .rev()
+ .all(|char| line_iter.next() == Some(char));
+ if end_tag_exists {
+ let max_point = snapshot.line_len(start_point.row) as usize;
+ let ordering = (num_of_whitespaces_rev
+ + end_tag.len()
+ + start_point.column as usize)
+ .cmp(&max_point);
+ let cursor_is_before_end_tag =
+ ordering != Ordering::Greater;
+ if cursor_is_after_start_tag {
+ if cursor_is_before_end_tag {
insert_extra_newline = true;
}
- cursor_is_before_suffix
- } else {
- true
+ let cursor_is_at_start_of_end_tag =
+ ordering == Ordering::Equal;
+ if cursor_is_at_start_of_end_tag {
+ indent_on_extra_newline.len = (*len).into();
+ }
}
- };
-
- if cursor_is_after_prefix && cursor_is_before_suffix_if_exits {
- Some(doc_comment_prefix.clone())
+ cursor_is_before_end_tag
} else {
- None
+ true
}
- });
- }
+ };
- (comment_delimiter, insert_extra_newline)
+ if (cursor_is_after_start_tag || cursor_is_after_delimiter)
+ && cursor_is_before_end_tag_if_exists
+ {
+ if cursor_is_after_start_tag {
+ indent_on_newline.len = (*len).into();
+ }
+ Some(delimiter.clone())
+ } else {
+ None
+ }
+ });
+
+ (
+ comment_delimiter,
+ doc_delimiter,
+ insert_extra_newline,
+ indent_on_newline,
+ indent_on_extra_newline,
+ )
} else {
- (None, false)
+ (
+ None,
+ None,
+ false,
+ IndentSize::default(),
+ IndentSize::default(),
+ )
};
- let capacity_for_delimiter = comment_delimiter
- .as_deref()
- .map(str::len)
- .unwrap_or_default();
- let mut new_text =
- String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
+ let prevent_auto_indent = doc_delimiter.is_some();
+ let delimiter = comment_delimiter.or(doc_delimiter);
+
+ 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
+ + indent_on_newline.len as usize
+ + indent_on_extra_newline.len as usize,
+ );
new_text.push('\n');
- new_text.extend(indent.chars());
+ new_text.extend(existing_indent.chars());
+ new_text.extend(indent_on_newline.chars());
- if let Some(delimiter) = &comment_delimiter {
+ if let Some(delimiter) = &delimiter {
new_text.push_str(delimiter);
}
if insert_extra_newline {
new_text.push('\n');
- new_text.extend(indent.chars());
+ new_text.extend(existing_indent.chars());
+ new_text.extend(indent_on_extra_newline.chars());
}
let anchor = buffer.anchor_after(end);
let new_selection = selection.map(|_| anchor);
(
- (start..end, new_text),
+ ((start..end, new_text), prevent_auto_indent),
(insert_extra_newline, new_selection),
)
})
.unzip()
};
- this.edit_with_autoindent(edits, cx);
+ let mut auto_indent_edits = Vec::new();
+ let mut edits = Vec::new();
+ for (edit, prevent_auto_indent) in edits_with_flags {
+ if prevent_auto_indent {
+ edits.push(edit);
+ } else {
+ auto_indent_edits.push(edit);
+ }
+ }
+ if !edits.is_empty() {
+ this.edit(edits, cx);
+ }
+ if !auto_indent_edits.is_empty() {
+ this.edit_with_autoindent(auto_indent_edits, cx);
+ }
+
let buffer = this.buffer.read(cx).snapshot(cx);
- let new_selections = selection_fixup_info
+ let new_selections = selection_info
.into_iter()
.map(|(extra_newline_inserted, new_selection)| {
let mut cursor = new_selection.end.to_point(&buffer);
@@ -2805,8 +2805,12 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
let language = Arc::new(Language::new(
LanguageConfig {
- documentation_block: Some(vec!["/**".into(), "*/".into()]),
- documentation_comment_prefix: Some("*".into()),
+ documentation: Some(language::DocumentationConfig {
+ start: "/**".into(),
+ end: "*/".into(),
+ prefix: "* ".into(),
+ tab_size: NonZeroU32::new(1).unwrap(),
+ }),
..LanguageConfig::default()
},
None,
@@ -2821,9 +2825,10 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
- *ˇ
+ * ˇ
"});
- // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
+ // Ensure that if cursor is before the comment start,
+ // we do not actually insert a comment prefix.
cx.set_state(indoc! {"
ˇ/**
"});
@@ -2848,8 +2853,71 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
- *ˇ
- */
+ * ˇ
+ */
+ "});
+ // Ensure that if suffix exists on same line after cursor with space it adds new line.
+ cx.set_state(indoc! {"
+ /**ˇ */
+ "});
+ cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
+ cx.assert_editor_state(indoc! {"
+ /**
+ * ˇ
+ */
+ "});
+ // Ensure that if suffix exists on same line after cursor with space it adds new line.
+ cx.set_state(indoc! {"
+ /** ˇ*/
+ "});
+ cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
+ cx.assert_editor_state(
+ indoc! {"
+ /**s
+ * ˇ
+ */
+ "}
+ .replace("s", " ") // s is used as space placeholder to prevent format on save
+ .as_str(),
+ );
+ // Ensure that delimiter space is preserved when newline on already
+ // spaced delimiter.
+ cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
+ cx.assert_editor_state(
+ indoc! {"
+ /**s
+ *s
+ * ˇ
+ */
+ "}
+ .replace("s", " ") // s is used as space placeholder to prevent format on save
+ .as_str(),
+ );
+ // Ensure that delimiter space is preserved when space is not
+ // on existing delimiter.
+ cx.set_state(indoc! {"
+ /**
+ *ˇ
+ */
+ "});
+ cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
+ cx.assert_editor_state(indoc! {"
+ /**
+ *
+ * ˇ
+ */
+ "});
+ // Ensure that if suffix exists on same line after cursor it
+ // doesn't add extra new line if prefix is not on same line.
+ cx.set_state(indoc! {"
+ /**
+ ˇ*/
+ "});
+ cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
+ cx.assert_editor_state(indoc! {"
+ /**
+
+ ˇ*/
"});
// Ensure that it detects suffix after existing prefix.
cx.set_state(indoc! {"
@@ -2860,7 +2928,8 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
/**
ˇ/
"});
- // Ensure that if suffix exists on same line before cursor it does not add comment prefix.
+ // Ensure that if suffix exists on same line before
+ // cursor it does not add comment prefix.
cx.set_state(indoc! {"
/** */ˇ
"});
@@ -2869,18 +2938,19 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
/** */
ˇ
"});
- // Ensure that if suffix exists on same line before cursor it does not add comment prefix.
+ // Ensure that if suffix exists on same line before
+ // cursor it does not add comment prefix.
cx.set_state(indoc! {"
/**
- *
- */ˇ
+ *
+ */ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
- *
- */
- ˇ
+ *
+ */
+ ˇ
"});
}
// Ensure that comment continuations can be disabled.
@@ -755,12 +755,10 @@ pub struct LanguageConfig {
/// A list of preferred debuggers for this language.
#[serde(default)]
pub debuggers: IndexSet<SharedString>,
- /// A character to add as a prefix when a new line is added to a documentation block.
- #[serde(default)]
- pub documentation_comment_prefix: Option<Arc<str>>,
- /// Returns string documentation block of this language should start with.
+ /// Whether to treat documentation comment of this language differently by
+ /// auto adding prefix on new line, adjusting the indenting , etc.
#[serde(default)]
- pub documentation_block: Option<Vec<Arc<str>>>,
+ pub documentation: Option<DocumentationConfig>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@@ -811,6 +809,19 @@ pub struct JsxTagAutoCloseConfig {
pub erroneous_close_tag_name_node_name: Option<String>,
}
+/// The configuration for documentation block for this language.
+#[derive(Clone, Deserialize, JsonSchema)]
+pub struct DocumentationConfig {
+ /// A start tag of documentation block.
+ pub start: Arc<str>,
+ /// A end tag of documentation block.
+ pub end: Arc<str>,
+ /// A character to add as a prefix when a new line is added to a documentation block.
+ pub prefix: Arc<str>,
+ /// A indent to add for prefix and end line upon new line.
+ pub tab_size: NonZeroU32,
+}
+
/// Represents a language for the given range. Some languages (e.g. HTML)
/// interleave several languages together, thus a single buffer might actually contain
/// several nested scopes.
@@ -889,8 +900,7 @@ impl Default for LanguageConfig {
completion_query_characters: Default::default(),
debuggers: Default::default(),
significant_indentation: Default::default(),
- documentation_comment_prefix: None,
- documentation_block: None,
+ documentation: None,
}
}
}
@@ -1810,21 +1820,12 @@ impl LanguageScope {
.unwrap_or(false)
}
- /// A character to add as a prefix when a new line is added to a documentation block.
+ /// Returns config to documentation block for this language.
///
/// Used for documentation styles that require a leading character on each line,
/// such as the asterisk in JSDoc, Javadoc, etc.
- pub fn documentation_comment_prefix(&self) -> Option<&Arc<str>> {
- self.language.config.documentation_comment_prefix.as_ref()
- }
-
- /// Returns prefix and suffix for documentation block of this language.
- pub fn documentation_block(&self) -> &[Arc<str>] {
- self.language
- .config
- .documentation_block
- .as_ref()
- .map_or([].as_slice(), |e| e.as_slice())
+ pub fn documentation(&self) -> Option<&DocumentationConfig> {
+ self.language.config.documentation.as_ref()
}
/// Returns a list of bracket pairs for a given language with an additional
@@ -20,8 +20,7 @@ tab_size = 2
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
prettier_parser_name = "babel"
debuggers = ["JavaScript"]
-documentation_comment_prefix = "*"
-documentation_block = ["/**", "*/"]
+documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
[jsx_tag_auto_close]
open_tag_node_name = "jsx_opening_element"
@@ -18,8 +18,7 @@ scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-
prettier_parser_name = "typescript"
tab_size = 2
debuggers = ["JavaScript"]
-documentation_comment_prefix = "*"
-documentation_block = ["/**", "*/"]
+documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
[jsx_tag_auto_close]
open_tag_node_name = "jsx_opening_element"
@@ -18,8 +18,7 @@ word_characters = ["#", "$"]
prettier_parser_name = "typescript"
tab_size = 2
debuggers = ["JavaScript"]
-documentation_comment_prefix = "*"
-documentation_block = ["/**", "*/"]
+documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
[overrides.string]
completion_query_characters = ["."]