feat: add native LSP hover action and popup
This commit is contained in:
+171
-5
@@ -70,7 +70,7 @@ held = []
|
||||
definition_count = 0
|
||||
source_uri = uri(source)
|
||||
loop do
|
||||
if initialize_id && !initialized_sent && (mode != 'gated' || File.exist?(release))
|
||||
if initialize_id && !initialized_sent && (!['gated', 'hover-gated'].include?(mode) || File.exist?(release))
|
||||
sync = case mode
|
||||
when 'numeric' then 1
|
||||
when 'none' then {'openClose' => false, 'change' => 0, 'save' => false}
|
||||
@@ -78,11 +78,14 @@ loop do
|
||||
else {'openClose' => true, 'change' => 1, 'save' => {'includeText' => true}}
|
||||
end
|
||||
result(initialize_id, {'capabilities' => {'positionEncoding' => 'utf-16',
|
||||
'textDocumentSync' => sync, 'definitionProvider' => mode == 'unsupported' ? false : {'workDoneProgress' => false}}})
|
||||
'textDocumentSync' => sync, 'definitionProvider' => mode == 'unsupported' ? false : {'workDoneProgress' => false},
|
||||
'hoverProvider' => mode == 'hover-unsupported' ? false : {'workDoneProgress' => false}}})
|
||||
initialized_sent = true
|
||||
end
|
||||
if !held.empty? && File.exist?(release) && mode == 'stale'
|
||||
held.each { |request| result(request['id'], location(target)) }
|
||||
if !held.empty? && File.exist?(release) && ['stale', 'hover-stale'].include?(mode)
|
||||
held.each do |request|
|
||||
result(request['id'], request['method'] == 'textDocument/hover' ? {'contents' => 'stale hover'} : location(target))
|
||||
end
|
||||
held.clear
|
||||
emit({'method' => 'window/showMessage', 'params' => {'type' => 3, 'message' => 'stale response complete'}})
|
||||
end
|
||||
@@ -156,6 +159,28 @@ loop do
|
||||
result(frame['id'], nil)
|
||||
end
|
||||
end
|
||||
when 'textDocument/hover'
|
||||
if mode == 'hover-stale'
|
||||
held << frame
|
||||
elsif mode == 'hover-superseded'
|
||||
held << frame
|
||||
if held.length == 2
|
||||
result(held[0]['id'], {'contents' => 'old hover'})
|
||||
result(held[1]['id'], {'contents' => 'new hover'})
|
||||
held.clear
|
||||
end
|
||||
else
|
||||
contents = case mode
|
||||
when 'hover-markdown' then {'kind' => 'markdown', 'value' => "**Header**\n```rust\nfn main() {}\n```"}
|
||||
when 'hover-marked' then ['explanation', {'language' => 'rust', 'value' => "fn main() {\n}"}, 'tail']
|
||||
when 'hover-string' then "one\nsecond"
|
||||
when 'hover-empty' then nil
|
||||
when 'hover-malformed' then {'kind' => 'html', 'value' => 'bad'}
|
||||
else {'kind' => 'plaintext', 'value' => "hover first\r\n😀e\u{301}\tsecond\e\u{85}\nlast"}
|
||||
end
|
||||
hover = contents.nil? ? nil : {'contents' => contents, 'range' => range(mode == 'hover-invalid-range' ? 999 : 0, 2, 4)}
|
||||
result(frame['id'], hover)
|
||||
end
|
||||
when 'shutdown'
|
||||
result(frame['id'], nil)
|
||||
when 'exit'
|
||||
@@ -220,7 +245,7 @@ impl Fixture {
|
||||
"settings": {"rust-analyzer": {"cargo": {"allFeatures": true}}},
|
||||
});
|
||||
let config = format!(
|
||||
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n config.lsp['fixture-language'] = JSON.parse({})\n config.bind 'leader d', config.core.lsp_definition\n config.bind 'leader D', config.core.lsp_diagnostics\nend\n",
|
||||
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n config.lsp['fixture-language'] = JSON.parse({})\n config.bind 'leader d', config.core.lsp_definition\n config.bind 'leader D', config.core.lsp_diagnostics\n config.bind 'K', config.core.lsp_hover\nend\n",
|
||||
serde_json::to_string(&options.to_string()).unwrap(),
|
||||
);
|
||||
let config_path = self.root.join("config.rb");
|
||||
@@ -793,3 +818,144 @@ fn lsp_session_shutdown_finishes_the_server_protocol_before_returning() {
|
||||
drop(session);
|
||||
assert_eq!(fixture.method_count("shutdown"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_hover_typed_action_preserves_multiline_content_and_primary_utf16_position() {
|
||||
for (mode, expected) in [
|
||||
("hover-plain", "hover first\n😀e\u{301} second \nlast"),
|
||||
("hover-markdown", "**Header**\n```rust\nfn main() {}\n```"),
|
||||
("hover-marked", "explanation\n\nfn main() {\n}\n\ntail"),
|
||||
("hover-string", "one\nsecond"),
|
||||
] {
|
||||
let fixture = Fixture::new();
|
||||
let mut session = fixture.open(mode);
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
for _ in 0..3 {
|
||||
session.handle(Input::Right);
|
||||
}
|
||||
let before = session.editor().cursor();
|
||||
session.handle(Input::Char('K'));
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().hover().is_some()
|
||||
});
|
||||
assert_eq!(session.editor().hover().unwrap().text(), expected);
|
||||
assert_eq!(session.editor().cursor(), before);
|
||||
assert_eq!(session.editor().mode(), Mode::Normal);
|
||||
assert!(!session.editor().dirty());
|
||||
let frames = fixture.frames();
|
||||
let hover = frames
|
||||
.iter()
|
||||
.find(|frame| frame["method"] == "textDocument/hover")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hover["params"]["position"],
|
||||
json!({"line": 0, "character": 5})
|
||||
);
|
||||
let initialize = frames
|
||||
.iter()
|
||||
.find(|frame| frame["method"] == "initialize")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
initialize["params"]["capabilities"]["textDocument"]["hover"]["contentFormat"],
|
||||
json!(["plaintext", "markdown"])
|
||||
);
|
||||
session.handle(Input::Down);
|
||||
assert_eq!(session.editor().cursor(), before);
|
||||
assert_eq!(session.editor().hover().unwrap().top, 1);
|
||||
session.handle(Input::Escape);
|
||||
assert!(session.editor().hover().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_hover_queues_before_initialize_and_ignores_cancelled_and_superseded_requests() {
|
||||
let fixture = Fixture::new();
|
||||
let mut session = fixture.open("hover-gated");
|
||||
session.execute("lsp_diagnostics");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().message().contains("server initializing")
|
||||
});
|
||||
assert_eq!(fixture.method_count("textDocument/hover"), 0);
|
||||
session.execute("lsp_hover");
|
||||
fixture.release();
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().hover().is_some()
|
||||
});
|
||||
drop(session);
|
||||
|
||||
for input in [Input::Escape, Input::Right, Input::Char('i')] {
|
||||
let fixture = Fixture::new();
|
||||
let mut session = fixture.open("hover-stale");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.execute("lsp_hover");
|
||||
until(&mut session, &fixture, |_| {
|
||||
fixture.method_count("textDocument/hover") == 1
|
||||
});
|
||||
session.handle(input);
|
||||
fixture.release();
|
||||
until(&mut session, &fixture, |session| {
|
||||
session
|
||||
.editor()
|
||||
.message()
|
||||
.contains("stale response complete")
|
||||
});
|
||||
assert!(session.editor().hover().is_none());
|
||||
}
|
||||
|
||||
let fixture = Fixture::new();
|
||||
let mut session = fixture.open("hover-superseded");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.execute("lsp_hover");
|
||||
session.execute("lsp_hover");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().hover().is_some()
|
||||
});
|
||||
assert_eq!(session.editor().hover().unwrap().text(), "new hover");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_hover_absent_unsupported_and_invalid_results_preserve_native_editing() {
|
||||
for mode in [
|
||||
"hover-empty",
|
||||
"hover-unsupported",
|
||||
"hover-malformed",
|
||||
"hover-invalid-range",
|
||||
] {
|
||||
let fixture = Fixture::new();
|
||||
let mut session = fixture.open(mode);
|
||||
until(&mut session, &fixture, |session| {
|
||||
session.editor().diagnostics().count() == 2
|
||||
});
|
||||
session.execute("lsp_hover");
|
||||
until(&mut session, &fixture, |session| {
|
||||
session
|
||||
.editor()
|
||||
.message()
|
||||
.contains(if mode == "hover-empty" {
|
||||
"no hover information"
|
||||
} else if mode == "hover-unsupported" {
|
||||
"does not support hover"
|
||||
} else {
|
||||
"hover failed"
|
||||
})
|
||||
});
|
||||
assert!(session.editor().hover().is_none());
|
||||
assert_eq!(
|
||||
fixture.method_count("textDocument/hover"),
|
||||
usize::from(mode != "hover-unsupported")
|
||||
);
|
||||
session.handle(Input::Char('A'));
|
||||
session.handle(Input::Char('!'));
|
||||
session.handle(Input::Escape);
|
||||
assert_eq!(session.editor().lines(), &["A😀e\u{301}Z!", ""]);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().lines(), &["A😀e\u{301}Z", ""]);
|
||||
assert_eq!(fixture.method_count("textDocument/definition"), 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user