feat: expand completion snippets and navigate placeholders
This commit is contained in:
+366
-3
@@ -2301,7 +2301,7 @@ fn lsp_completion_live_cycle_original_and_ordinary_keys_use_one_request() {
|
||||
.iter()
|
||||
.map(|item| item.label.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["alpha", "alps"]
|
||||
["alpha", "alps", "alSnippet"]
|
||||
);
|
||||
assert_eq!(popup.items[0].detail.as_deref(), Some("first candidate"));
|
||||
assert_eq!(popup.selected, 1);
|
||||
@@ -2310,9 +2310,13 @@ fn lsp_completion_live_cycle_original_and_ordinary_keys_use_one_request() {
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(session.editor().context().text, "alps tail\n");
|
||||
session.handle(Input::Ctrl('j'));
|
||||
assert_eq!(session.editor().context().text, "alsnippet tail\n");
|
||||
session.handle(Input::Ctrl('j'));
|
||||
assert_eq!(session.editor().completion().unwrap().selected, 0);
|
||||
assert_eq!(session.editor().context().text, "al tail\n");
|
||||
session.handle(Input::Ctrl('k'));
|
||||
assert_eq!(session.editor().context().text, "alsnippet tail\n");
|
||||
session.handle(Input::Ctrl('k'));
|
||||
assert_eq!(session.editor().context().text, "alps tail\n");
|
||||
session.handle(Input::Ctrl('k'));
|
||||
assert_eq!(session.editor().context().text, "alpha tail\n");
|
||||
@@ -2481,7 +2485,7 @@ fn lsp_completion_utf16_text_edit_insert_replace_and_defaults_are_atomic() {
|
||||
.iter()
|
||||
.find(|frame| frame["method"] == "initialize")
|
||||
.unwrap()["params"]["capabilities"]["textDocument"]["completion"];
|
||||
assert_eq!(capabilities["completionItem"]["snippetSupport"], false);
|
||||
assert_eq!(capabilities["completionItem"]["snippetSupport"], true);
|
||||
assert_eq!(capabilities["completionItem"]["insertReplaceSupport"], true);
|
||||
assert_eq!(
|
||||
capabilities["completionItem"]["resolveSupport"]["properties"],
|
||||
@@ -2556,6 +2560,10 @@ fn lsp_completion_feature_errors_keep_healthy_services() {
|
||||
json!({"result": [{"label": "x", "textEdit": {"range": range(0, 1), "newText": "x"}, "additionalTextEdits": [{"range": range(0, 1), "newText": "bad"}]}]}),
|
||||
json!({"result": [{"label": "x", "textEdit": {"range": range(0, 0), "newText": "x"}, "additionalTextEdits": [{"range": range(0, 0), "newText": "bad"}]}]}),
|
||||
json!({"result": [{"label": "x", "additionalTextEdits": {}}]}),
|
||||
json!({"result": [{"label": "x", "insertTextFormat": 2, "insertText": "${1"}]}),
|
||||
json!({"result": [{"label": "x", "insertTextFormat": 2, "insertText": "${1:$1}"}]}),
|
||||
json!({"result": [{"label": "x", "insertTextFormat": 2, "insertText": "${1:e}\u{301}"}]}),
|
||||
json!({"result": [{"label": "x", "insertTextFormat": 2, "insertText": format!("{}x{}", "${1:".repeat(33), "}".repeat(33))}]}),
|
||||
json!({"error": {"code": -32603, "message": "completion unavailable"}}),
|
||||
json!({"error": {"message": "malformed error"}}),
|
||||
];
|
||||
@@ -2623,7 +2631,6 @@ fn lsp_completion_fallback_filter_text_insert_text_and_empty_shapes() {
|
||||
Value::Null,
|
||||
json!([]),
|
||||
json!({"isIncomplete": false, "items": []}),
|
||||
json!([{"label": "alSnippet", "insertTextFormat": 2, "insertText": "${1:al}"}]),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -2710,3 +2717,359 @@ fn lsp_completion_routes_only_to_first_configured_server_without_fallback() {
|
||||
session.handle(Input::Char('!'));
|
||||
assert_eq!(session.editor().context().text, "!A😀e\u{301}Z\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_snippet_preview_mirrors_navigation_final_stop_undo_and_resolved_dot() {
|
||||
let fixture = Fixture::new();
|
||||
fs::write(&fixture.source, " tail\n").unwrap();
|
||||
let mut session = fixture.open("completion-controlled");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.handle(Input::Char('i'));
|
||||
session.handle(Input::Char('c'));
|
||||
session.handle(Input::Char('a'));
|
||||
let original_carets = session.editor().carets();
|
||||
session.handle(Input::Ctrl('n'));
|
||||
until(&mut session, &fixture, |_| {
|
||||
fixture.method_count("textDocument/completion") == 1
|
||||
});
|
||||
completion_reply(
|
||||
&mut session,
|
||||
&fixture,
|
||||
1,
|
||||
json!({"result": [{
|
||||
"label": "call", "insertTextFormat": 2,
|
||||
"insertText": "call(${1:arg}, $1, ${2|left,right|})$0"
|
||||
}]}),
|
||||
);
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"call(arg, arg, left) tail\n"
|
||||
);
|
||||
assert_eq!(session.editor().snippet_selections().len(), 2);
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(session.editor().context().text, "ca tail\n");
|
||||
assert_eq!(session.editor().carets(), original_carets);
|
||||
assert!(session.editor().snippet_selections().is_empty());
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"call(arg, arg, left) tail\n"
|
||||
);
|
||||
session.handle(Input::Char('x'));
|
||||
session.handle(Input::Char('y'));
|
||||
assert_eq!(session.editor().context().text, "call(xy, xy, left) tail\n");
|
||||
session.handle(Input::Char('\t'));
|
||||
assert_eq!(session.editor().snippet_selections().len(), 1);
|
||||
session.handle(Input::BackTab);
|
||||
assert_eq!(session.editor().snippet_selections().len(), 2);
|
||||
session.handle(Input::Char('v'));
|
||||
assert_eq!(session.editor().context().text, "call(v, v, left) tail\n");
|
||||
session.handle(Input::Char('\t'));
|
||||
session.handle(Input::Char('r'));
|
||||
assert_eq!(session.editor().context().text, "call(v, v, r) tail\n");
|
||||
session.handle(Input::Char('\t'));
|
||||
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 13 });
|
||||
assert!(session.editor().snippet_selections().is_empty());
|
||||
session.handle(Input::Char('\t'));
|
||||
session.handle(Input::Enter);
|
||||
let resolved = "call(v, v, r)\t\n tail\n";
|
||||
assert_eq!(session.editor().context().text, resolved);
|
||||
session.handle(Input::Escape);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().context().text, " tail\n");
|
||||
session.handle(Input::Char('.'));
|
||||
assert_eq!(session.editor().context().text, resolved);
|
||||
assert_eq!(fixture.method_count("textDocument/completion"), 1);
|
||||
assert_eq!(fixture.method_count("completionItem/resolve"), 0);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().context().text, " tail\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_snippet_primary_edit_preserves_secondary_carets_and_escape_keeps_defaults() {
|
||||
let fixture = Fixture::new();
|
||||
fs::write(&fixture.source, "ca tail\nca tail\n").unwrap();
|
||||
let mut session = fixture.open("completion-controlled");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.handle(Input::Ctrl('e'));
|
||||
session.handle(Input::Char('i'));
|
||||
session.handle(Input::Right);
|
||||
session.handle(Input::Right);
|
||||
let original = session.editor().carets();
|
||||
session.handle(Input::Ctrl('n'));
|
||||
until(&mut session, &fixture, |_| {
|
||||
fixture.method_count("textDocument/completion") == 1
|
||||
});
|
||||
completion_reply(
|
||||
&mut session,
|
||||
&fixture,
|
||||
1,
|
||||
json!({"result": [{
|
||||
"label": "call", "insertTextFormat": 2, "insertText": "call(${1:arg}, $1)$0",
|
||||
"additionalTextEdits": [{
|
||||
"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
|
||||
"newText": "use X;\n"
|
||||
}]
|
||||
}]}),
|
||||
);
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"use X;\nca tail\ncall(arg, arg) tail\n"
|
||||
);
|
||||
assert_eq!(session.editor().carets()[0], Cursor { row: 1, column: 2 });
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(session.editor().context().text, "ca tail\nca tail\n");
|
||||
assert_eq!(session.editor().carets(), original);
|
||||
session.handle(Input::Ctrl('n'));
|
||||
session.handle(Input::Char('x'));
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"use X;\nca tail\ncall(x, x) tail\n"
|
||||
);
|
||||
assert_eq!(session.editor().carets()[0], Cursor { row: 1, column: 2 });
|
||||
session.handle(Input::Escape);
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"use X;\nca tail\ncall(x, x) tail\n"
|
||||
);
|
||||
assert!(session.editor().snippet_selections().is_empty());
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().context().text, "ca tail\nca tail\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_snippet_defaults_all_edit_shapes_context_and_unsupported_siblings() {
|
||||
let fixture = Fixture::new();
|
||||
fs::write(&fixture.source, "ca suffix\n// tail\n").unwrap();
|
||||
let mut session = fixture.open("completion-controlled");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.handle(Input::Char('i'));
|
||||
session.handle(Input::Right);
|
||||
session.handle(Input::Right);
|
||||
session.handle(Input::Ctrl('n'));
|
||||
until(&mut session, &fixture, |_| {
|
||||
fixture.method_count("textDocument/completion") == 1
|
||||
});
|
||||
let range = json!({"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 2}});
|
||||
completion_reply(
|
||||
&mut session,
|
||||
&fixture,
|
||||
1,
|
||||
json!({"result": {
|
||||
"isIncomplete": false,
|
||||
"itemDefaults": {"editRange": range, "insertTextFormat": 2},
|
||||
"items": [
|
||||
{"label": "unsupported transform", "sortText": "0", "textEditText": "${1/a/b/}"},
|
||||
{"label": "unknown context", "sortText": "0", "textEditText": "$CLIPBOARD"},
|
||||
{"label": "replacement", "sortText": "1", "textEdit": {"range": range, "newText": "call(${1:λ})\r\n$0"},
|
||||
"additionalTextEdits": [{"range": {"start": {"line": 1, "character": 0}, "end": {"line": 1, "character": 0}}, "newText": "// $1\n"}]},
|
||||
{"label": "insert replace", "sortText": "2", "textEdit": {"insert": range, "replace": range, "newText": "call(${1:${2:arg}})$0"}},
|
||||
{"label": "default text edit", "sortText": "3", "textEditText": "call(${1|one,two|})$0"},
|
||||
{"label": "default insert text", "sortText": "4", "insertText": "call(${1:${UNKNOWN:${TM_LINE_NUMBER}}})$0"},
|
||||
{"label": "default label ${1:arg}$0", "sortText": "5"}
|
||||
]
|
||||
}}),
|
||||
);
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"call(λ)\n suffix\n// $1\n// tail\n"
|
||||
);
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"call(arg) suffix\n// tail\n"
|
||||
);
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"call(one) suffix\n// tail\n"
|
||||
);
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(session.editor().context().text, "call(1) suffix\n// tail\n");
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"default label arg suffix\n// tail\n"
|
||||
);
|
||||
session.handle(Input::Ctrl('n'));
|
||||
assert_eq!(session.editor().context().text, "ca suffix\n// tail\n");
|
||||
session.handle(Input::Escape);
|
||||
assert!(!session.editor().dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_snippet_escape_preserves_preview_and_ordinary_keys_remain_literal() {
|
||||
let fixture = Fixture::new();
|
||||
fs::write(&fixture.source, "ca\n").unwrap();
|
||||
let mut session = fixture.open("completion-controlled");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.handle(Input::Char('A'));
|
||||
session.handle(Input::Ctrl('n'));
|
||||
until(&mut session, &fixture, |_| {
|
||||
fixture.method_count("textDocument/completion") == 1
|
||||
});
|
||||
completion_reply(
|
||||
&mut session,
|
||||
&fixture,
|
||||
1,
|
||||
json!({"result": [{
|
||||
"label": "call", "insertTextFormat": 2, "insertText": "call(${1:arg})$0"
|
||||
}]}),
|
||||
);
|
||||
session.handle(Input::Escape);
|
||||
assert_eq!(session.editor().mode(), Mode::Normal);
|
||||
assert_eq!(session.editor().context().text, "call(arg)\n");
|
||||
assert!(session.editor().snippet_selections().is_empty());
|
||||
session.handle(Input::Char('A'));
|
||||
session.handle(Input::Char('\t'));
|
||||
session.handle(Input::Enter);
|
||||
assert_eq!(session.editor().context().text, "call(arg)\t\n\n");
|
||||
assert_eq!(fixture.method_count("textDocument/completion"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_snippet_nested_defaults_update_enclosing_mirrors_then_replace_outer() {
|
||||
let fixture = Fixture::new();
|
||||
fs::write(&fixture.source, "ca\n").unwrap();
|
||||
let mut session = fixture.open("completion-controlled");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.handle(Input::Char('A'));
|
||||
session.handle(Input::Ctrl('n'));
|
||||
until(&mut session, &fixture, |_| {
|
||||
fixture.method_count("textDocument/completion") == 1
|
||||
});
|
||||
completion_reply(
|
||||
&mut session,
|
||||
&fixture,
|
||||
1,
|
||||
json!({"result": [{
|
||||
"label": "call", "insertTextFormat": 2,
|
||||
"insertText": "call(${2:outer ${1:inner}}, $2)$0"
|
||||
}]}),
|
||||
);
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
"call(outer inner, outer inner)\n"
|
||||
);
|
||||
assert_eq!(session.editor().snippet_selections().len(), 2);
|
||||
session.handle(Input::Char('x'));
|
||||
assert_eq!(session.editor().context().text, "call(outer x, outer x)\n");
|
||||
session.handle(Input::Char('\t'));
|
||||
assert_eq!(session.editor().snippet_selections().len(), 2);
|
||||
session.handle(Input::Char('y'));
|
||||
assert_eq!(session.editor().context().text, "call(y, y)\n");
|
||||
session.handle(Input::Char('\t'));
|
||||
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 10 });
|
||||
assert!(session.editor().snippet_selections().is_empty());
|
||||
session.handle(Input::Escape);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().context().text, "ca\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_snippet_ancestry_distinguishes_empty_child_from_adjacent_root() {
|
||||
for (template, nested) in [("${1:foo${2}}-$3$0", true), ("${1:foo}$2-$3$0", false)] {
|
||||
let fixture = Fixture::new();
|
||||
fs::write(&fixture.source, "ca\n").unwrap();
|
||||
let mut session = fixture.open("completion-controlled");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.handle(Input::Char('A'));
|
||||
session.handle(Input::Ctrl('n'));
|
||||
until(&mut session, &fixture, |_| {
|
||||
fixture.method_count("textDocument/completion") == 1
|
||||
});
|
||||
completion_reply(
|
||||
&mut session,
|
||||
&fixture,
|
||||
1,
|
||||
json!({"result": [{
|
||||
"label": "call", "insertTextFormat": 2, "insertText": template
|
||||
}]}),
|
||||
);
|
||||
assert_eq!(session.editor().context().text, "foo-\n");
|
||||
let snippet = session.editor().completion().unwrap().items[0]
|
||||
.snippet
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
snippet.tabstops[1].ancestors,
|
||||
[if nested { vec![1] } else { vec![] }]
|
||||
);
|
||||
session.handle(Input::Char('X'));
|
||||
assert_eq!(session.editor().context().text, "X-\n");
|
||||
session.handle(Input::Char('\t'));
|
||||
if nested {
|
||||
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 2 });
|
||||
} else {
|
||||
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 1 });
|
||||
session.handle(Input::Char('y'));
|
||||
assert_eq!(session.editor().context().text, "Xy-\n");
|
||||
session.handle(Input::Char('\t'));
|
||||
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 3 });
|
||||
}
|
||||
session.handle(Input::Char('z'));
|
||||
assert_eq!(
|
||||
session.editor().context().text,
|
||||
if nested { "X-z\n" } else { "Xy-z\n" }
|
||||
);
|
||||
session.handle(Input::Char('\t'));
|
||||
assert!(session.editor().snippet_selections().is_empty());
|
||||
session.handle(Input::Escape);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().context().text, "ca\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_snippet_coincident_empty_mirrors_insert_and_delete_each_occurrence() {
|
||||
let fixture = Fixture::new();
|
||||
fs::write(&fixture.source, "ca\n").unwrap();
|
||||
let mut session = fixture.open("completion-controlled");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.handle(Input::Char('A'));
|
||||
session.handle(Input::Ctrl('n'));
|
||||
until(&mut session, &fixture, |_| {
|
||||
fixture.method_count("textDocument/completion") == 1
|
||||
});
|
||||
completion_reply(
|
||||
&mut session,
|
||||
&fixture,
|
||||
1,
|
||||
json!({"result": [{
|
||||
"label": "call", "insertTextFormat": 2, "insertText": "call(${1}$1)$0"
|
||||
}]}),
|
||||
);
|
||||
assert_eq!(session.editor().context().text, "call()\n");
|
||||
assert_eq!(session.editor().snippet_selections().len(), 2);
|
||||
session.handle(Input::Char('x'));
|
||||
session.handle(Input::Char('y'));
|
||||
assert_eq!(session.editor().context().text, "call(xyxy)\n");
|
||||
session.handle(Input::Backspace);
|
||||
assert_eq!(session.editor().context().text, "call(xx)\n");
|
||||
session.handle(Input::Backspace);
|
||||
assert_eq!(session.editor().context().text, "call()\n");
|
||||
session.handle(Input::Char('z'));
|
||||
assert_eq!(session.editor().context().text, "call(zz)\n");
|
||||
session.handle(Input::Char('\t'));
|
||||
assert_eq!(session.editor().cursor(), Cursor { row: 0, column: 8 });
|
||||
assert!(session.editor().snippet_selections().is_empty());
|
||||
session.handle(Input::Char('\t'));
|
||||
assert_eq!(session.editor().context().text, "call(zz)\t\n");
|
||||
session.handle(Input::Escape);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().context().text, "ca\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user