languages: Fix Bash indentation issues with multi-cursors, newlines, and keyword outdenting (#35116)
Closes #34390 This PR fixes several Bash indentation issues: - Adding indentation or comment using multi cursors no longer breaks relative indentation - Adding newline now places the cursor at the correct indent - Typing a valid keyword triggers context-aware auto outdent It also adds tests for all of them. Release Notes: - Fixed various issues with handling indentation in Bash.
This commit is contained in:
parent
07252c3309
commit
43d0aae617
5 changed files with 459 additions and 25 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4979,6 +4979,7 @@ dependencies = [
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"time",
|
"time",
|
||||||
|
"tree-sitter-bash",
|
||||||
"tree-sitter-html",
|
"tree-sitter-html",
|
||||||
"tree-sitter-python",
|
"tree-sitter-python",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
|
|
|
@ -110,6 +110,7 @@ tree-sitter-html.workspace = true
|
||||||
tree-sitter-rust.workspace = true
|
tree-sitter-rust.workspace = true
|
||||||
tree-sitter-typescript.workspace = true
|
tree-sitter-typescript.workspace = true
|
||||||
tree-sitter-yaml.workspace = true
|
tree-sitter-yaml.workspace = true
|
||||||
|
tree-sitter-bash.workspace = true
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
util = { workspace = true, features = ["test-support"] }
|
util = { workspace = true, features = ["test-support"] }
|
||||||
workspace = { workspace = true, features = ["test-support"] }
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -22585,6 +22585,435 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
|
||||||
"});
|
"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
|
||||||
|
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||||
|
|
||||||
|
// test cursor move to start of each line on tab
|
||||||
|
// for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
function main() {
|
||||||
|
ˇ for item in $items; do
|
||||||
|
ˇ while [ -n \"$item\" ]; do
|
||||||
|
ˇ if [ \"$value\" -gt 10 ]; then
|
||||||
|
ˇ continue
|
||||||
|
ˇ elif [ \"$value\" -lt 0 ]; then
|
||||||
|
ˇ break
|
||||||
|
ˇ else
|
||||||
|
ˇ echo \"$item\"
|
||||||
|
ˇ fi
|
||||||
|
ˇ done
|
||||||
|
ˇ done
|
||||||
|
ˇ}
|
||||||
|
"});
|
||||||
|
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
function main() {
|
||||||
|
ˇfor item in $items; do
|
||||||
|
ˇwhile [ -n \"$item\" ]; do
|
||||||
|
ˇif [ \"$value\" -gt 10 ]; then
|
||||||
|
ˇcontinue
|
||||||
|
ˇelif [ \"$value\" -lt 0 ]; then
|
||||||
|
ˇbreak
|
||||||
|
ˇelse
|
||||||
|
ˇecho \"$item\"
|
||||||
|
ˇfi
|
||||||
|
ˇdone
|
||||||
|
ˇdone
|
||||||
|
ˇ}
|
||||||
|
"});
|
||||||
|
// test relative indent is preserved when tab
|
||||||
|
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
function main() {
|
||||||
|
ˇfor item in $items; do
|
||||||
|
ˇwhile [ -n \"$item\" ]; do
|
||||||
|
ˇif [ \"$value\" -gt 10 ]; then
|
||||||
|
ˇcontinue
|
||||||
|
ˇelif [ \"$value\" -lt 0 ]; then
|
||||||
|
ˇbreak
|
||||||
|
ˇelse
|
||||||
|
ˇecho \"$item\"
|
||||||
|
ˇfi
|
||||||
|
ˇdone
|
||||||
|
ˇdone
|
||||||
|
ˇ}
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test cursor move to start of each line on tab
|
||||||
|
// for `case` statement with patterns
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
function handle() {
|
||||||
|
ˇ case \"$1\" in
|
||||||
|
ˇ start)
|
||||||
|
ˇ echo \"a\"
|
||||||
|
ˇ ;;
|
||||||
|
ˇ stop)
|
||||||
|
ˇ echo \"b\"
|
||||||
|
ˇ ;;
|
||||||
|
ˇ *)
|
||||||
|
ˇ echo \"c\"
|
||||||
|
ˇ ;;
|
||||||
|
ˇ esac
|
||||||
|
ˇ}
|
||||||
|
"});
|
||||||
|
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
function handle() {
|
||||||
|
ˇcase \"$1\" in
|
||||||
|
ˇstart)
|
||||||
|
ˇecho \"a\"
|
||||||
|
ˇ;;
|
||||||
|
ˇstop)
|
||||||
|
ˇecho \"b\"
|
||||||
|
ˇ;;
|
||||||
|
ˇ*)
|
||||||
|
ˇecho \"c\"
|
||||||
|
ˇ;;
|
||||||
|
ˇesac
|
||||||
|
ˇ}
|
||||||
|
"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
|
||||||
|
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||||
|
|
||||||
|
// test indents on comment insert
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
function main() {
|
||||||
|
ˇ for item in $items; do
|
||||||
|
ˇ while [ -n \"$item\" ]; do
|
||||||
|
ˇ if [ \"$value\" -gt 10 ]; then
|
||||||
|
ˇ continue
|
||||||
|
ˇ elif [ \"$value\" -lt 0 ]; then
|
||||||
|
ˇ break
|
||||||
|
ˇ else
|
||||||
|
ˇ echo \"$item\"
|
||||||
|
ˇ fi
|
||||||
|
ˇ done
|
||||||
|
ˇ done
|
||||||
|
ˇ}
|
||||||
|
"});
|
||||||
|
cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
function main() {
|
||||||
|
#ˇ for item in $items; do
|
||||||
|
#ˇ while [ -n \"$item\" ]; do
|
||||||
|
#ˇ if [ \"$value\" -gt 10 ]; then
|
||||||
|
#ˇ continue
|
||||||
|
#ˇ elif [ \"$value\" -lt 0 ]; then
|
||||||
|
#ˇ break
|
||||||
|
#ˇ else
|
||||||
|
#ˇ echo \"$item\"
|
||||||
|
#ˇ fi
|
||||||
|
#ˇ done
|
||||||
|
#ˇ done
|
||||||
|
#ˇ}
|
||||||
|
"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
|
||||||
|
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||||
|
|
||||||
|
// test `else` auto outdents when typed inside `if` block
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
echo \"foo bar\"
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("else", window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
echo \"foo bar\"
|
||||||
|
elseˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test `elif` auto outdents when typed inside `if` block
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
echo \"foo bar\"
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("elif", window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
echo \"foo bar\"
|
||||||
|
elifˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test `fi` auto outdents when typed inside `else` block
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
echo \"foo bar\"
|
||||||
|
else
|
||||||
|
echo \"bar baz\"
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("fi", window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
echo \"foo bar\"
|
||||||
|
else
|
||||||
|
echo \"bar baz\"
|
||||||
|
fiˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test `done` auto outdents when typed inside `while` block
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
while read line; do
|
||||||
|
echo \"$line\"
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("done", window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
while read line; do
|
||||||
|
echo \"$line\"
|
||||||
|
doneˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test `done` auto outdents when typed inside `for` block
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
for file in *.txt; do
|
||||||
|
cat \"$file\"
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("done", window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
for file in *.txt; do
|
||||||
|
cat \"$file\"
|
||||||
|
doneˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test `esac` auto outdents when typed inside `case` block
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
case \"$1\" in
|
||||||
|
start)
|
||||||
|
echo \"foo bar\"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
echo \"bar baz\"
|
||||||
|
;;
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("esac", window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
case \"$1\" in
|
||||||
|
start)
|
||||||
|
echo \"foo bar\"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
echo \"bar baz\"
|
||||||
|
;;
|
||||||
|
esacˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test `*)` auto outdents when typed inside `case` block
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
case \"$1\" in
|
||||||
|
start)
|
||||||
|
echo \"foo bar\"
|
||||||
|
;;
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("*)", window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
case \"$1\" in
|
||||||
|
start)
|
||||||
|
echo \"foo bar\"
|
||||||
|
;;
|
||||||
|
*)ˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test `fi` outdents to correct level with nested if blocks
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
echo \"outer if\"
|
||||||
|
if [ \"$2\" = \"debug\" ]; then
|
||||||
|
echo \"inner if\"
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("fi", window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
echo \"outer if\"
|
||||||
|
if [ \"$2\" = \"debug\" ]; then
|
||||||
|
echo \"inner if\"
|
||||||
|
fiˇ
|
||||||
|
"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
update_test_language_settings(cx, |settings| {
|
||||||
|
settings.defaults.extend_comment_on_newline = Some(false);
|
||||||
|
});
|
||||||
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
|
||||||
|
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||||
|
|
||||||
|
// test correct indent after newline on comment
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
# COMMENT:ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
# COMMENT:
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test correct indent after newline after `then`
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
|
||||||
|
if [ \"$1\" = \"test\" ]; thenˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test correct indent after newline after `else`
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
elseˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
else
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test correct indent after newline after `elif`
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
elifˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
if [ \"$1\" = \"test\" ]; then
|
||||||
|
elif
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test correct indent after newline after `do`
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
for file in *.txt; doˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
for file in *.txt; do
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test correct indent after newline after case pattern
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
case \"$1\" in
|
||||||
|
start)ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
case \"$1\" in
|
||||||
|
start)
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test correct indent after newline after case pattern
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
case \"$1\" in
|
||||||
|
start)
|
||||||
|
;;
|
||||||
|
*)ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
case \"$1\" in
|
||||||
|
start)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test correct indent after newline after function opening brace
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
function test() {ˇ}
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
function test() {
|
||||||
|
ˇ
|
||||||
|
}
|
||||||
|
"});
|
||||||
|
|
||||||
|
// test no extra indent after semicolon on same line
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
echo \"test\";ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.newline(&Newline, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
echo \"test\";
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||||
point..point
|
point..point
|
||||||
|
|
|
@ -18,17 +18,20 @@ brackets = [
|
||||||
{ start = "in", end = "esac", close = false, newline = true, not_in = ["comment", "string"] },
|
{ start = "in", end = "esac", close = false, newline = true, not_in = ["comment", "string"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
### WARN: the following is not working when you insert an `elif` just before an else
|
auto_indent_using_last_non_empty_line = false
|
||||||
### example: (^ is cursor after hitting enter)
|
increase_indent_pattern = "^\\s*(\\b(else|elif)\\b|([^#]+\\b(do|then|in)\\b)|([\\w\\*]+\\)))\\s*$"
|
||||||
### ```
|
decrease_indent_patterns = [
|
||||||
### if true; then
|
{ pattern = "^\\s*elif\\b.*", valid_after = ["if", "elif"] },
|
||||||
### foo
|
{ pattern = "^\\s*else\\b.*", valid_after = ["if", "elif", "for", "while"] },
|
||||||
### elif
|
{ pattern = "^\\s*fi\\b.*", valid_after = ["if", "elif", "else"] },
|
||||||
### ^
|
{ pattern = "^\\s*done\\b.*", valid_after = ["for", "while"] },
|
||||||
### else
|
{ pattern = "^\\s*esac\\b.*", valid_after = ["case"] },
|
||||||
### bar
|
{ pattern = "^\\s*[\\w\\*]+\\)\\s*$", valid_after = ["case_item"] },
|
||||||
### fi
|
]
|
||||||
### ```
|
|
||||||
increase_indent_pattern = "(^|\\s+|;)(do|then|in|else|elif)\\b.*$"
|
# We can't use decrease_indent_patterns simply for elif, because
|
||||||
decrease_indent_pattern = "(^|\\s+|;)(fi|done|esac|else|elif)\\b.*$"
|
# there is bug in tree sitter which throws ERROR on if match.
|
||||||
# make sure to test each line mode & block mode
|
#
|
||||||
|
# This is workaround. That means, elif will outdents with despite
|
||||||
|
# of wrong context. Like using elif after else.
|
||||||
|
decrease_indent_pattern = "(^|\\s+|;)(elif)\\b.*$"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
(function_definition
|
(_ "[" "]" @end) @indent
|
||||||
"function"?
|
(_ "{" "}" @end) @indent
|
||||||
body: (
|
(_ "(" ")" @end) @indent
|
||||||
_
|
|
||||||
"{" @start
|
|
||||||
"}" @end
|
|
||||||
)) @indent
|
|
||||||
|
|
||||||
(array
|
(function_definition) @start.function
|
||||||
"(" @start
|
(if_statement) @start.if
|
||||||
")" @end
|
(elif_clause) @start.elif
|
||||||
) @indent
|
(else_clause) @start.else
|
||||||
|
(for_statement) @start.for
|
||||||
|
(while_statement) @start.while
|
||||||
|
(case_statement) @start.case
|
||||||
|
(case_item) @start.case_item
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue