mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 10:29:35 +03:00
Add basic support for ruby
Co-authored-by: Kay Simmons <kay@zed.dev>
This commit is contained in:
parent
9f3ea0c87f
commit
d222904471
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -3009,6 +3009,7 @@ dependencies = [
|
|||||||
"tree-sitter-javascript",
|
"tree-sitter-javascript",
|
||||||
"tree-sitter-json 0.19.0",
|
"tree-sitter-json 0.19.0",
|
||||||
"tree-sitter-python",
|
"tree-sitter-python",
|
||||||
|
"tree-sitter-ruby",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"tree-sitter-typescript",
|
"tree-sitter-typescript",
|
||||||
"unindent",
|
"unindent",
|
||||||
@ -6491,6 +6492,16 @@ dependencies = [
|
|||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter-ruby"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ac30cbb1560363ae76e1ccde543d6d99087421e228cc47afcec004b86bb711a"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"tree-sitter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter-rust"
|
name = "tree-sitter-rust"
|
||||||
version = "0.20.3"
|
version = "0.20.3"
|
||||||
@ -7712,6 +7723,7 @@ dependencies = [
|
|||||||
"tree-sitter-json 0.20.0",
|
"tree-sitter-json 0.20.0",
|
||||||
"tree-sitter-markdown",
|
"tree-sitter-markdown",
|
||||||
"tree-sitter-python",
|
"tree-sitter-python",
|
||||||
|
"tree-sitter-ruby",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"tree-sitter-toml",
|
"tree-sitter-toml",
|
||||||
"tree-sitter-typescript",
|
"tree-sitter-typescript",
|
||||||
|
@ -71,4 +71,5 @@ tree-sitter-json = "*"
|
|||||||
tree-sitter-rust = "*"
|
tree-sitter-rust = "*"
|
||||||
tree-sitter-python = "*"
|
tree-sitter-python = "*"
|
||||||
tree-sitter-typescript = "*"
|
tree-sitter-typescript = "*"
|
||||||
|
tree-sitter-ruby = "*"
|
||||||
unindent = "0.1.7"
|
unindent = "0.1.7"
|
||||||
|
@ -1764,6 +1764,7 @@ impl BufferSnapshot {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut indent_ranges = Vec::<Range<Point>>::new();
|
let mut indent_ranges = Vec::<Range<Point>>::new();
|
||||||
|
let mut outdent_positions = Vec::<Point>::new();
|
||||||
while let Some(mat) = matches.peek() {
|
while let Some(mat) = matches.peek() {
|
||||||
let mut start: Option<Point> = None;
|
let mut start: Option<Point> = None;
|
||||||
let mut end: Option<Point> = None;
|
let mut end: Option<Point> = None;
|
||||||
@ -1777,6 +1778,8 @@ impl BufferSnapshot {
|
|||||||
start = Some(Point::from_ts_point(capture.node.end_position()));
|
start = Some(Point::from_ts_point(capture.node.end_position()));
|
||||||
} else if Some(capture.index) == config.end_capture_ix {
|
} else if Some(capture.index) == config.end_capture_ix {
|
||||||
end = Some(Point::from_ts_point(capture.node.start_position()));
|
end = Some(Point::from_ts_point(capture.node.start_position()));
|
||||||
|
} else if Some(capture.index) == config.outdent_capture_ix {
|
||||||
|
outdent_positions.push(Point::from_ts_point(capture.node.start_position()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1797,6 +1800,19 @@ impl BufferSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outdent_positions.sort();
|
||||||
|
for outdent_position in outdent_positions {
|
||||||
|
// find the innermost indent range containing this outdent_position
|
||||||
|
// set its end to the outdent position
|
||||||
|
if let Some(range_to_truncate) = indent_ranges
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|indent_range| indent_range.contains(&outdent_position))
|
||||||
|
.last()
|
||||||
|
{
|
||||||
|
range_to_truncate.end = outdent_position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find the suggested indentation increases and decreased based on regexes.
|
// Find the suggested indentation increases and decreased based on regexes.
|
||||||
let mut indent_change_rows = Vec::<(u32, Ordering)>::new();
|
let mut indent_change_rows = Vec::<(u32, Ordering)>::new();
|
||||||
self.for_each_line(
|
self.for_each_line(
|
||||||
|
@ -1150,6 +1150,49 @@ fn test_autoindent_with_injected_languages(cx: &mut MutableAppContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_autoindent_query_with_outdent_captures(cx: &mut MutableAppContext) {
|
||||||
|
let mut settings = Settings::test(cx);
|
||||||
|
settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
|
||||||
|
cx.set_global(settings);
|
||||||
|
cx.add_model(|cx| {
|
||||||
|
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx);
|
||||||
|
|
||||||
|
let text = r#"
|
||||||
|
class C
|
||||||
|
def a(b, c)
|
||||||
|
puts b
|
||||||
|
puts c
|
||||||
|
rescue
|
||||||
|
puts "errored"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
buffer.edit([(0..0, text)], Some(AutoindentMode::EachLine), cx);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
buffer.text(),
|
||||||
|
r#"
|
||||||
|
class C
|
||||||
|
def a(b, c)
|
||||||
|
puts b
|
||||||
|
puts c
|
||||||
|
rescue
|
||||||
|
puts "errored"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
"#
|
||||||
|
.unindent()
|
||||||
|
);
|
||||||
|
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_serialization(cx: &mut gpui::MutableAppContext) {
|
fn test_serialization(cx: &mut gpui::MutableAppContext) {
|
||||||
let mut now = Instant::now();
|
let mut now = Instant::now();
|
||||||
@ -1497,6 +1540,26 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ruby_lang() -> Language {
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Ruby".into(),
|
||||||
|
path_suffixes: vec!["rb".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_ruby::language()),
|
||||||
|
)
|
||||||
|
.with_indents_query(
|
||||||
|
r#"
|
||||||
|
(class "end" @end) @indent
|
||||||
|
(method "end" @end) @indent
|
||||||
|
(rescue) @outdent
|
||||||
|
(then) @indent
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn rust_lang() -> Language {
|
fn rust_lang() -> Language {
|
||||||
Language::new(
|
Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
|
@ -312,6 +312,7 @@ struct IndentConfig {
|
|||||||
indent_capture_ix: u32,
|
indent_capture_ix: u32,
|
||||||
start_capture_ix: Option<u32>,
|
start_capture_ix: Option<u32>,
|
||||||
end_capture_ix: Option<u32>,
|
end_capture_ix: Option<u32>,
|
||||||
|
outdent_capture_ix: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OutlineConfig {
|
struct OutlineConfig {
|
||||||
@ -670,12 +671,14 @@ impl Language {
|
|||||||
let mut indent_capture_ix = None;
|
let mut indent_capture_ix = None;
|
||||||
let mut start_capture_ix = None;
|
let mut start_capture_ix = None;
|
||||||
let mut end_capture_ix = None;
|
let mut end_capture_ix = None;
|
||||||
|
let mut outdent_capture_ix = None;
|
||||||
get_capture_indices(
|
get_capture_indices(
|
||||||
&query,
|
&query,
|
||||||
&mut [
|
&mut [
|
||||||
("indent", &mut indent_capture_ix),
|
("indent", &mut indent_capture_ix),
|
||||||
("start", &mut start_capture_ix),
|
("start", &mut start_capture_ix),
|
||||||
("end", &mut end_capture_ix),
|
("end", &mut end_capture_ix),
|
||||||
|
("outdent", &mut outdent_capture_ix),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
if let Some(indent_capture_ix) = indent_capture_ix {
|
if let Some(indent_capture_ix) = indent_capture_ix {
|
||||||
@ -684,6 +687,7 @@ impl Language {
|
|||||||
indent_capture_ix,
|
indent_capture_ix,
|
||||||
start_capture_ix,
|
start_capture_ix,
|
||||||
end_capture_ix,
|
end_capture_ix,
|
||||||
|
outdent_capture_ix,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
|
@ -102,6 +102,7 @@ tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown",
|
|||||||
tree-sitter-python = "0.20.2"
|
tree-sitter-python = "0.20.2"
|
||||||
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
||||||
tree-sitter-typescript = "0.20.1"
|
tree-sitter-typescript = "0.20.1"
|
||||||
|
tree-sitter-ruby = "0.20.0"
|
||||||
tree-sitter-html = "0.19.0"
|
tree-sitter-html = "0.19.0"
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
|
||||||
|
@ -15,6 +15,15 @@ mod python;
|
|||||||
mod rust;
|
mod rust;
|
||||||
mod typescript;
|
mod typescript;
|
||||||
|
|
||||||
|
// 1. Add tree-sitter-{language} parser to zed crate
|
||||||
|
// 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below
|
||||||
|
// 3. Add config.toml to the newly created language directory using existing languages as a template
|
||||||
|
// 4. Copy highlights from tree sitter repo for the language into a highlights.scm file.
|
||||||
|
// Note: github highlights take the last match while zed takes the first
|
||||||
|
// 5. Add indents.scm, outline.scm, and brackets.scm to implement indent on newline, outline/breadcrumbs,
|
||||||
|
// and autoclosing brackets respectively
|
||||||
|
// 6. If the language has injections add an injections.scm query file
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "src/languages"]
|
#[folder = "src/languages"]
|
||||||
#[exclude = "*.rs"]
|
#[exclude = "*.rs"]
|
||||||
@ -107,6 +116,7 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
|
|||||||
tree_sitter_html::language(),
|
tree_sitter_html::language(),
|
||||||
Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
|
Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
|
||||||
),
|
),
|
||||||
|
("ruby", tree_sitter_ruby::language(), None),
|
||||||
] {
|
] {
|
||||||
languages.add(language(name, grammar, lsp_adapter));
|
languages.add(language(name, grammar, lsp_adapter));
|
||||||
}
|
}
|
||||||
|
14
crates/zed/src/languages/ruby/brackets.scm
Normal file
14
crates/zed/src/languages/ruby/brackets.scm
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
("[" @open "]" @close)
|
||||||
|
("{" @open "}" @close)
|
||||||
|
("\"" @open "\"" @close)
|
||||||
|
("do" @open "end" @close)
|
||||||
|
|
||||||
|
(block_parameters "|" @open "|" @close)
|
||||||
|
(interpolation "#{" @open "}" @close)
|
||||||
|
|
||||||
|
(if "if" @open "end" @close)
|
||||||
|
(unless "unless" @open "end" @close)
|
||||||
|
(begin "begin" @open "end" @close)
|
||||||
|
(module "module" @open "end" @close)
|
||||||
|
(_ . "def" @open "end" @close)
|
||||||
|
(_ . "class" @open "end" @close)
|
11
crates/zed/src/languages/ruby/config.toml
Normal file
11
crates/zed/src/languages/ruby/config.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
name = "Ruby"
|
||||||
|
path_suffixes = ["rb", "Gemfile"]
|
||||||
|
line_comment = "# "
|
||||||
|
autoclose_before = ";:.,=}])>"
|
||||||
|
brackets = [
|
||||||
|
{ start = "{", end = "}", close = true, newline = true },
|
||||||
|
{ start = "[", end = "]", close = true, newline = true },
|
||||||
|
{ start = "(", end = ")", close = true, newline = true },
|
||||||
|
{ start = "\"", end = "\"", close = true, newline = false },
|
||||||
|
{ start = "'", end = "'", close = false, newline = false },
|
||||||
|
]
|
181
crates/zed/src/languages/ruby/highlights.scm
Normal file
181
crates/zed/src/languages/ruby/highlights.scm
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
; Keywords
|
||||||
|
|
||||||
|
[
|
||||||
|
"alias"
|
||||||
|
"and"
|
||||||
|
"begin"
|
||||||
|
"break"
|
||||||
|
"case"
|
||||||
|
"class"
|
||||||
|
"def"
|
||||||
|
"do"
|
||||||
|
"else"
|
||||||
|
"elsif"
|
||||||
|
"end"
|
||||||
|
"ensure"
|
||||||
|
"for"
|
||||||
|
"if"
|
||||||
|
"in"
|
||||||
|
"module"
|
||||||
|
"next"
|
||||||
|
"or"
|
||||||
|
"rescue"
|
||||||
|
"retry"
|
||||||
|
"return"
|
||||||
|
"then"
|
||||||
|
"unless"
|
||||||
|
"until"
|
||||||
|
"when"
|
||||||
|
"while"
|
||||||
|
"yield"
|
||||||
|
] @keyword
|
||||||
|
|
||||||
|
(identifier) @variable
|
||||||
|
|
||||||
|
((identifier) @keyword
|
||||||
|
(#match? @keyword "^(private|protected|public)$"))
|
||||||
|
|
||||||
|
; Function calls
|
||||||
|
|
||||||
|
((identifier) @function.method.builtin
|
||||||
|
(#eq? @function.method.builtin "require"))
|
||||||
|
|
||||||
|
"defined?" @function.method.builtin
|
||||||
|
|
||||||
|
(call
|
||||||
|
method: [(identifier) (constant)] @function.method)
|
||||||
|
|
||||||
|
; Function definitions
|
||||||
|
|
||||||
|
(alias (identifier) @function.method)
|
||||||
|
(setter (identifier) @function.method)
|
||||||
|
(method name: [(identifier) (constant)] @function.method)
|
||||||
|
(singleton_method name: [(identifier) (constant)] @function.method)
|
||||||
|
|
||||||
|
; Identifiers
|
||||||
|
|
||||||
|
[
|
||||||
|
(class_variable)
|
||||||
|
(instance_variable)
|
||||||
|
] @property
|
||||||
|
|
||||||
|
((identifier) @constant.builtin
|
||||||
|
(#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
|
||||||
|
|
||||||
|
(file) @constant.builtin
|
||||||
|
(line) @constant.builtin
|
||||||
|
(encoding) @constant.builtin
|
||||||
|
|
||||||
|
(hash_splat_nil
|
||||||
|
"**" @operator
|
||||||
|
) @constant.builtin
|
||||||
|
|
||||||
|
((constant) @constant
|
||||||
|
(#match? @constant "^[A-Z\\d_]+$"))
|
||||||
|
|
||||||
|
(constant) @type
|
||||||
|
|
||||||
|
(self) @variable.special
|
||||||
|
(super) @variable.special
|
||||||
|
|
||||||
|
; Literals
|
||||||
|
|
||||||
|
[
|
||||||
|
(string)
|
||||||
|
(bare_string)
|
||||||
|
(subshell)
|
||||||
|
(heredoc_body)
|
||||||
|
(heredoc_beginning)
|
||||||
|
] @string
|
||||||
|
|
||||||
|
[
|
||||||
|
(simple_symbol)
|
||||||
|
(delimited_symbol)
|
||||||
|
(hash_key_symbol)
|
||||||
|
(bare_symbol)
|
||||||
|
] @string.special.symbol
|
||||||
|
|
||||||
|
(regex) @string.special.regex
|
||||||
|
(escape_sequence) @escape
|
||||||
|
|
||||||
|
[
|
||||||
|
(integer)
|
||||||
|
(float)
|
||||||
|
] @number
|
||||||
|
|
||||||
|
[
|
||||||
|
(nil)
|
||||||
|
(true)
|
||||||
|
(false)
|
||||||
|
] @constant.builtin
|
||||||
|
|
||||||
|
(interpolation
|
||||||
|
"#{" @punctuation.special
|
||||||
|
"}" @punctuation.special) @embedded
|
||||||
|
|
||||||
|
(comment) @comment
|
||||||
|
|
||||||
|
; Operators
|
||||||
|
|
||||||
|
[
|
||||||
|
"!"
|
||||||
|
"~"
|
||||||
|
"+"
|
||||||
|
"-"
|
||||||
|
"**"
|
||||||
|
"*"
|
||||||
|
"/"
|
||||||
|
"%"
|
||||||
|
"<<"
|
||||||
|
">>"
|
||||||
|
"&"
|
||||||
|
"|"
|
||||||
|
"^"
|
||||||
|
">"
|
||||||
|
"<"
|
||||||
|
"<="
|
||||||
|
">="
|
||||||
|
"=="
|
||||||
|
"!="
|
||||||
|
"=~"
|
||||||
|
"!~"
|
||||||
|
"<=>"
|
||||||
|
"||"
|
||||||
|
"&&"
|
||||||
|
".."
|
||||||
|
"..."
|
||||||
|
"="
|
||||||
|
"**="
|
||||||
|
"*="
|
||||||
|
"/="
|
||||||
|
"%="
|
||||||
|
"+="
|
||||||
|
"-="
|
||||||
|
"<<="
|
||||||
|
">>="
|
||||||
|
"&&="
|
||||||
|
"&="
|
||||||
|
"||="
|
||||||
|
"|="
|
||||||
|
"^="
|
||||||
|
"=>"
|
||||||
|
"->"
|
||||||
|
(operator)
|
||||||
|
] @operator
|
||||||
|
|
||||||
|
[
|
||||||
|
","
|
||||||
|
";"
|
||||||
|
"."
|
||||||
|
] @punctuation.delimiter
|
||||||
|
|
||||||
|
[
|
||||||
|
"("
|
||||||
|
")"
|
||||||
|
"["
|
||||||
|
"]"
|
||||||
|
"{"
|
||||||
|
"}"
|
||||||
|
"%w("
|
||||||
|
"%i("
|
||||||
|
] @punctuation.bracket
|
17
crates/zed/src/languages/ruby/indents.scm
Normal file
17
crates/zed/src/languages/ruby/indents.scm
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
(method "end" @end) @indent
|
||||||
|
(class "end" @end) @indent
|
||||||
|
(module "end" @end) @indent
|
||||||
|
(begin "end" @end) @indent
|
||||||
|
(do_block "end" @end) @indent
|
||||||
|
|
||||||
|
(then) @indent
|
||||||
|
(call) @indent
|
||||||
|
|
||||||
|
(ensure) @outdent
|
||||||
|
(rescue) @outdent
|
||||||
|
(else) @outdent
|
||||||
|
|
||||||
|
|
||||||
|
(_ "[" "]" @end) @indent
|
||||||
|
(_ "{" "}" @end) @indent
|
||||||
|
(_ "(" ")" @end) @indent
|
11
crates/zed/src/languages/ruby/outline.scm
Normal file
11
crates/zed/src/languages/ruby/outline.scm
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
(class
|
||||||
|
"class" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(method
|
||||||
|
"def" @context
|
||||||
|
name: (_) @name) @item
|
||||||
|
|
||||||
|
(module
|
||||||
|
"module" @context
|
||||||
|
name: (_) @name) @item
|
@ -1,10 +1,6 @@
|
|||||||
import { fontWeights } from "../common";
|
import { fontWeights } from "../common";
|
||||||
import { withOpacity } from "../utils/color";
|
import { withOpacity } from "../utils/color";
|
||||||
import {
|
import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme";
|
||||||
ColorScheme,
|
|
||||||
Layer,
|
|
||||||
StyleSets,
|
|
||||||
} from "../themes/common/colorScheme";
|
|
||||||
import {
|
import {
|
||||||
background,
|
background,
|
||||||
border,
|
border,
|
||||||
@ -50,6 +46,11 @@ export default function editor(colorScheme: ColorScheme) {
|
|||||||
color: colorScheme.ramps.neutral(1).hex(),
|
color: colorScheme.ramps.neutral(1).hex(),
|
||||||
weight: fontWeights.normal,
|
weight: fontWeights.normal,
|
||||||
},
|
},
|
||||||
|
"variable.special": {
|
||||||
|
// Highlights for self, this, etc
|
||||||
|
color: colorScheme.ramps.blue(0.7).hex(),
|
||||||
|
weight: fontWeights.normal,
|
||||||
|
},
|
||||||
comment: {
|
comment: {
|
||||||
color: colorScheme.ramps.neutral(0.71).hex(),
|
color: colorScheme.ramps.neutral(0.71).hex(),
|
||||||
weight: fontWeights.normal,
|
weight: fontWeights.normal,
|
||||||
@ -270,9 +271,9 @@ export default function editor(colorScheme: ColorScheme) {
|
|||||||
background: withOpacity(background(layer, "inverted"), 0.4),
|
background: withOpacity(background(layer, "inverted"), 0.4),
|
||||||
border: {
|
border: {
|
||||||
width: 1,
|
width: 1,
|
||||||
color: borderColor(layer, 'variant'),
|
color: borderColor(layer, "variant"),
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
compositionMark: {
|
compositionMark: {
|
||||||
underline: {
|
underline: {
|
||||||
|
Loading…
Reference in New Issue
Block a user