Use textDocument/codeLens
data in the actions menu when applicable (#26811)
Similar to how tasks are fetched via LSP, also queries for document's code lens and filters the ones with the commands, supported in server capabilities. Whatever's left and applicable to the range given, is added to the actions menu:  This way, Zed can get more actions to run, albeit neither r-a nor vtsls seem to provide anything by default. Currently, there are no plans to render code lens the way as in VSCode, it's just the extra actions that are show in the menu. ------------------ As part of the attempts to use rust-analyzer LSP data about the runnables, I've explored a way to get this data via standard LSP. When particular experimental client capabilities are enabled (similar to how clangd does this now), r-a starts to send back code lens with the data needed to run a cargo command: ``` {"jsonrpc":"2.0","id":48,"result":{"range":{"start":{"line":0,"character":0},"end":{"line":98,"character":0}},"command":{"title":"▶︎ Run Tests","command":"rust-analyzer.runSingle","arguments":[{"label":"test-mod tests::ecparser","location":{"targetUri":"file:///Users/someonetoignore/work/ec4rs/src/tests/ecparser.rs","targetRange":{"start":{"line":0,"character":0},"end":{"line":98,"character":0}},"targetSelectionRange":{"start":{"line":0,"character":0},"end":{"line":98,"character":0}}},"kind":"cargo","args":{"environment":{"RUSTC_TOOLCHAIN":"/Users/someonetoignore/.rustup/toolchains/1.85-aarch64-apple-darwin"},"cwd":"/Users/someonetoignore/work/ec4rs","overrideCargo":null,"workspaceRoot":"/Users/someonetoignore/work/ec4rs","cargoArgs":["test","--package","ec4rs","--lib"],"executableArgs":["tests::ecparser","--show-output"]}}]}}} ``` This data is passed as is to VSCode task processor, registered in60cd01864a/editors/code/src/main.ts (L195)
where it gets eventually executed as a VSCode's task, all handled by the r-a's extension code. rust-analyzer does not declare server capabilities for such tasks, and has no `workspace/executeCommand` handle, and Zed needs an interactive terminal output during the test runs, so we cannot ask rust-analyzer more than these descriptions. Given that Zed needs experimental capabilities set to get these lens:60cd01864a/editors/code/src/client.ts (L318-L327)
and that the lens may contain other odd tasks (e.g. docs opening or references lookup), a protocol extension to get runnables looks more preferred than lens: https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables This PR does not include any work on this direction, limiting to the general code lens support. As a proof of concept, it's possible to get the lens and even attempt to run it, to no avail:  Release Notes: - Used `textDocument/codeLens` data in the actions menu when applicable
This commit is contained in:
parent
0b492c11de
commit
b61171f152
13 changed files with 618 additions and 19 deletions
|
@ -69,7 +69,7 @@ pub use element::{
|
|||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
};
|
||||
use futures::{
|
||||
future::{self, Shared},
|
||||
future::{self, join, Shared},
|
||||
FutureExt,
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
|
@ -82,10 +82,10 @@ use code_context_menus::{
|
|||
use git::blame::GitBlame;
|
||||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
|
||||
AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds,
|
||||
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
|
||||
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
|
||||
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||
AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, AvailableSpace, Background,
|
||||
Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity,
|
||||
EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight,
|
||||
Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
|
||||
Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
|
||||
WeakEntity, WeakFocusHandle, Window,
|
||||
|
@ -1233,11 +1233,15 @@ impl Editor {
|
|||
project_subscriptions.push(cx.subscribe_in(
|
||||
project,
|
||||
window,
|
||||
|editor, _, event, window, cx| {
|
||||
if let project::Event::RefreshInlayHints = event {
|
||||
|editor, _, event, window, cx| match event {
|
||||
project::Event::RefreshCodeLens => {
|
||||
// we always query lens with actions, without storing them, always refreshing them
|
||||
}
|
||||
project::Event::RefreshInlayHints => {
|
||||
editor
|
||||
.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
|
||||
} else if let project::Event::SnippetEdit(id, snippet_edits) = event {
|
||||
}
|
||||
project::Event::SnippetEdit(id, snippet_edits) => {
|
||||
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
if focus_handle.is_focused(window) {
|
||||
|
@ -1257,6 +1261,7 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
if let Some(task_inventory) = project
|
||||
|
@ -17027,7 +17032,16 @@ impl CodeActionProvider for Entity<Project> {
|
|||
cx: &mut App,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.code_actions(buffer, range, None, cx)
|
||||
let code_lens = project.code_lens(buffer, range.clone(), cx);
|
||||
let code_actions = project.code_actions(buffer, range, None, cx);
|
||||
cx.background_spawn(async move {
|
||||
let (code_lens, code_actions) = join(code_lens, code_actions).await;
|
||||
Ok(code_lens
|
||||
.context("code lens fetch")?
|
||||
.into_iter()
|
||||
.chain(code_actions.context("code action fetch")?)
|
||||
.collect())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -17233,6 +17233,187 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
|
|||
"});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"a.ts": "a",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "TypeScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
|
||||
)));
|
||||
let mut fake_language_servers = language_registry.register_fake_lsp(
|
||||
"TypeScript",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
code_lens_provider: Some(lsp::CodeLensOptions {
|
||||
resolve_provider: Some(true),
|
||||
}),
|
||||
execute_command_provider: Some(lsp::ExecuteCommandOptions {
|
||||
commands: vec!["_the/command".to_string()],
|
||||
..lsp::ExecuteCommandOptions::default()
|
||||
}),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |p, cx| {
|
||||
p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let fake_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
|
||||
drop(buffer_snapshot);
|
||||
let actions = cx
|
||||
.update_window(*workspace, |_, window, cx| {
|
||||
project.code_actions(&buffer, anchor..anchor, window, cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
fake_server
|
||||
.handle_request::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
|
||||
Ok(Some(vec![
|
||||
lsp::CodeLens {
|
||||
range: lsp::Range::default(),
|
||||
command: Some(lsp::Command {
|
||||
title: "Code lens command".to_owned(),
|
||||
command: "_the/command".to_owned(),
|
||||
arguments: None,
|
||||
}),
|
||||
data: None,
|
||||
},
|
||||
lsp::CodeLens {
|
||||
range: lsp::Range::default(),
|
||||
command: Some(lsp::Command {
|
||||
title: "Command not in capabilities".to_owned(),
|
||||
command: "not in capabilities".to_owned(),
|
||||
arguments: None,
|
||||
}),
|
||||
data: None,
|
||||
},
|
||||
lsp::CodeLens {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 1,
|
||||
character: 1,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 1,
|
||||
character: 1,
|
||||
},
|
||||
},
|
||||
command: Some(lsp::Command {
|
||||
title: "Command not in range".to_owned(),
|
||||
command: "_the/command".to_owned(),
|
||||
arguments: None,
|
||||
}),
|
||||
data: None,
|
||||
},
|
||||
]))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
let actions = actions.await.unwrap();
|
||||
assert_eq!(
|
||||
actions.len(),
|
||||
1,
|
||||
"Should have only one valid action for the 0..0 range"
|
||||
);
|
||||
let action = actions[0].clone();
|
||||
let apply = project.update(cx, |project, cx| {
|
||||
project.apply_code_action(buffer.clone(), action, true, cx)
|
||||
});
|
||||
|
||||
// Resolving the code action does not populate its edits. In absence of
|
||||
// edits, we must execute the given command.
|
||||
fake_server.handle_request::<lsp::request::CodeLensResolve, _, _>(|mut lens, _| async move {
|
||||
let lens_command = lens.command.as_mut().expect("should have a command");
|
||||
assert_eq!(lens_command.title, "Code lens command");
|
||||
lens_command.arguments = Some(vec![json!("the-argument")]);
|
||||
Ok(lens)
|
||||
});
|
||||
|
||||
// While executing the command, the language server sends the editor
|
||||
// a `workspaceEdit` request.
|
||||
fake_server
|
||||
.handle_request::<lsp::request::ExecuteCommand, _, _>({
|
||||
let fake = fake_server.clone();
|
||||
move |params, _| {
|
||||
assert_eq!(params.command, "_the/command");
|
||||
let fake = fake.clone();
|
||||
async move {
|
||||
fake.server
|
||||
.request::<lsp::request::ApplyWorkspaceEdit>(
|
||||
lsp::ApplyWorkspaceEditParams {
|
||||
label: None,
|
||||
edit: lsp::WorkspaceEdit {
|
||||
changes: Some(
|
||||
[(
|
||||
lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
|
||||
vec![lsp::TextEdit {
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 0),
|
||||
lsp::Position::new(0, 0),
|
||||
),
|
||||
new_text: "X".into(),
|
||||
}],
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(Some(json!(null)))
|
||||
}
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
// Applying the code lens command returns a project transaction containing the edits
|
||||
// sent by the language server in its `workspaceEdit` request.
|
||||
let transaction = apply.await.unwrap();
|
||||
assert!(transaction.0.contains_key(&buffer));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
assert_eq!(buffer.text(), "Xa");
|
||||
buffer.undo(cx);
|
||||
assert_eq!(buffer.text(), "a");
|
||||
});
|
||||
}
|
||||
|
||||
mod autoclose_tags {
|
||||
use super::*;
|
||||
use language::language_settings::JsxTagAutoCloseSettings;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue