Support Emmet's wrap with abbreviation (#63383)
Closes https://github.com/zed-industries/zed/issues/15588 https://github.com/user-attachments/assets/2813d235-6dac-4fe6-9ae0-2783e16270ac Release Notes: - Added Emmet's wrap with abbreviation support
This commit is contained in:
1 parent
7e7087a589
commit
74f3fa1454
26 files changed
+2581
-40
No files matched your search
@@ -834,6 +834,12 @@
|
||||
"enter": "editor::ConfirmRename",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_input",
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"bindings": {
|
||||
|
||||
@@ -887,6 +887,13 @@
|
||||
"enter": "editor::ConfirmRename",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_input",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -824,6 +824,13 @@
|
||||
"enter": "editor::ConfirmRename",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_input",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -376,6 +376,9 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUnstagedDiff>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtExpandMacro>)
|
||||
.add_request_handler(
|
||||
forward_read_only_project_request::<proto::LspExtExpandAbbreviation>,
|
||||
)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtOpenDocs>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::LspExtRunnables>)
|
||||
.add_request_handler(
|
||||
|
||||
@@ -8,12 +8,12 @@ use editor::{
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, CopyFileLocation,
|
||||
CopyFileName, CopyFileNameWithoutExtension, ExpandMacroRecursively, MoveToEnd, Redo,
|
||||
Rename, SelectAll, ToggleCodeActions, Undo,
|
||||
Rename, SelectAll, ToggleCodeActions, Undo, WrapWithAbbreviation,
|
||||
},
|
||||
code_context_menus::CodeContextMenu,
|
||||
test::{
|
||||
editor_test_context::{AssertionContextManager, EditorTestContext},
|
||||
expand_macro_recursively,
|
||||
expand_macro_recursively, wrap_with_abbreviation,
|
||||
},
|
||||
};
|
||||
use fs::Fs;
|
||||
@@ -24,12 +24,16 @@ use gpui::{
|
||||
VisualTestContext,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{FakeLspAdapter, language_settings::LanguageSettings, rust_lang};
|
||||
use language::{
|
||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, language_settings::LanguageSettings,
|
||||
rust_lang,
|
||||
};
|
||||
use lsp::DEFAULT_LSP_REQUEST_TIMEOUT;
|
||||
use multi_buffer::{AnchorRangeExt as _, MultiBufferRow};
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{
|
||||
ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
lsp_store::emmet_ext::LspExpandAbbreviation,
|
||||
lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
@@ -5144,6 +5148,154 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_client_can_wrap_with_emmet_abbreviation(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let html_lang = || {
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "HTML".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["html".into()],
|
||||
..LanguageMatcher::default()
|
||||
}
|
||||
.into(),
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
))
|
||||
};
|
||||
client_a.language_registry().add(html_lang());
|
||||
client_b.language_registry().add(html_lang());
|
||||
let mut fake_html_servers = client_a.language_registry().register_fake_lsp(
|
||||
"HTML",
|
||||
FakeLspAdapter {
|
||||
name: "vscode-html-language-server",
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
let mut fake_emmet_servers = client_a.language_registry().register_fake_lsp(
|
||||
"HTML",
|
||||
FakeLspAdapter {
|
||||
name: "emmet-language-server",
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"HTML",
|
||||
FakeLspAdapter {
|
||||
name: "emmet-language-server",
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
path!("/a"),
|
||||
json!({
|
||||
"index.html": "<p>hello</p>",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_a = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_buffer((worktree_id, rel_path("index.html")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
let editor_b = workspace_b
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path(
|
||||
(worktree_id, rel_path("index.html")),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let _fake_html_server = fake_html_servers.next().await.unwrap();
|
||||
let fake_emmet_server = fake_emmet_servers.next().await.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let mut wrap_request = fake_emmet_server.set_request_handler::<LspExpandAbbreviation, _, _>(
|
||||
|params, _| async move {
|
||||
assert_eq!(params.abbreviation, "div.wrap");
|
||||
assert_eq!(params.language, "html");
|
||||
assert_eq!(params.options.text, Some(vec!["hello".to_string()]));
|
||||
Ok(Some("<div class=\"wrap\">hello</div>".to_string()))
|
||||
},
|
||||
);
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 3)..Point::new(0, 8)])
|
||||
});
|
||||
wrap_with_abbreviation(editor, &WrapWithAbbreviation, window, cx);
|
||||
});
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
let input = editor
|
||||
.pending_inline_input()
|
||||
.expect("emmet wrap input should be pending on the client")
|
||||
.editor
|
||||
.clone();
|
||||
input.update(cx, |input, cx| input.set_text("div.wrap", window, cx));
|
||||
editor.confirm_inline_input(window, cx);
|
||||
});
|
||||
wrap_request.next().await.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "<p><div class=\"wrap\">hello</div></p>");
|
||||
});
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"<p><div class=\"wrap\">hello</div></p>",
|
||||
"the wrap edit should sync back to the host"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copy_file_name_without_extension(
|
||||
cx_a: &mut TestAppContext,
|
||||
|
||||
@@ -938,6 +938,8 @@ actions!(
|
||||
UnwrapSyntaxNode,
|
||||
/// Wraps selections in tag specified by language.
|
||||
WrapSelectionsInTag,
|
||||
/// Wraps selections in an expanded Emmet abbreviation.
|
||||
WrapWithAbbreviation,
|
||||
/// Aligns selections from different rows into the same column
|
||||
AlignSelections,
|
||||
/// Saves the current location to navigation history.
|
||||
|
||||
@@ -51,6 +51,7 @@ pub fn switch_source_header(
|
||||
let request = proto::LspExtSwitchSourceHeader {
|
||||
project_id,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
server_id: server_to_query.to_proto(),
|
||||
};
|
||||
let response = client
|
||||
.request(request)
|
||||
@@ -63,7 +64,9 @@ pub fn switch_source_header(
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_store::lsp_ext_command::SwitchSourceHeader,
|
||||
project::lsp_store::lsp_ext_command::SwitchSourceHeader {
|
||||
server_id: server_to_query,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ mod document_links;
|
||||
mod document_symbols;
|
||||
mod editor_settings;
|
||||
mod element;
|
||||
mod emmet_ext;
|
||||
mod fold;
|
||||
mod folding_ranges;
|
||||
mod git;
|
||||
@@ -31,6 +32,7 @@ pub mod hover_links;
|
||||
pub mod hover_popover;
|
||||
mod indent_guides;
|
||||
mod inlays;
|
||||
mod inline_input;
|
||||
pub mod items;
|
||||
mod jsx_tag_auto_close;
|
||||
mod linked_editing_ranges;
|
||||
@@ -116,6 +118,8 @@ use git::{DiffReviewDragState, DiffReviewOverlay, InlineBlamePopover};
|
||||
pub(crate) use git::{DisplayDiffHunk, PhantomDiffReviewIndicator};
|
||||
pub use hover_popover::hover_markdown_style;
|
||||
pub use inlays::Inlay;
|
||||
pub use inline_input::InlineInputState;
|
||||
pub(crate) use inline_input::{InlineInputHistoryDirection, InlineInputPreview};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
pub use linked_editing_ranges::LinkedEdits;
|
||||
pub use lsp::CompletionContext;
|
||||
@@ -1045,6 +1049,7 @@ pub struct Editor {
|
||||
linked_editing_range_task: Option<Task<Option<()>>>,
|
||||
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
|
||||
pending_rename: Option<RenameState>,
|
||||
pending_inline_input: Option<InlineInputState>,
|
||||
searchable: bool,
|
||||
cursor_shape: CursorShape,
|
||||
/// Whether the cursor is offset one character to the left when something is
|
||||
@@ -2385,6 +2390,7 @@ impl Editor {
|
||||
document_highlights_task: None,
|
||||
linked_editing_range_task: None,
|
||||
pending_rename: None,
|
||||
pending_inline_input: None,
|
||||
searchable: !is_minimap,
|
||||
cursor_shape: EditorSettings::get_global(cx)
|
||||
.cursor_shape
|
||||
@@ -2734,6 +2740,9 @@ impl Editor {
|
||||
if self.pending_rename.is_some() {
|
||||
key_context.add("renaming");
|
||||
}
|
||||
if self.pending_inline_input.is_some() {
|
||||
key_context.add("inline_input");
|
||||
}
|
||||
|
||||
if let Some(snippet_stack) = self.snippet_stack.last() {
|
||||
key_context.add("in_snippet");
|
||||
@@ -3496,6 +3505,7 @@ impl Editor {
|
||||
let mut dismissed = false;
|
||||
|
||||
dismissed |= self.take_rename(false, window, cx).is_some();
|
||||
dismissed |= self.take_inline_input(window, cx).is_some();
|
||||
dismissed |= self.hide_blame_popover(true, cx);
|
||||
dismissed |= hide_hover(self, cx);
|
||||
dismissed |= self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
|
||||
@@ -4900,6 +4910,25 @@ impl Editor {
|
||||
snippet: Snippet,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
self.insert_snippet_with_autoindent(
|
||||
insertion_ranges,
|
||||
snippet,
|
||||
Some(AutoindentMode::Block {
|
||||
original_indent_columns: Vec::new(),
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn insert_snippet_with_autoindent(
|
||||
&mut self,
|
||||
insertion_ranges: &[Range<MultiBufferOffset>],
|
||||
snippet: Snippet,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
struct Tabstop<T> {
|
||||
is_end_tabstop: bool,
|
||||
@@ -4913,10 +4942,7 @@ impl Editor {
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|range| (range, snippet_text.clone()));
|
||||
let autoindent_mode = AutoindentMode::Block {
|
||||
original_indent_columns: Vec::new(),
|
||||
};
|
||||
buffer.edit(edits, Some(autoindent_mode), cx);
|
||||
buffer.edit(edits, autoindent_mode, cx);
|
||||
|
||||
let snapshot = &*buffer.read(cx);
|
||||
let snippet = &snippet;
|
||||
|
||||
@@ -278,6 +278,13 @@ impl EditorElement {
|
||||
|
||||
crate::rust_analyzer_ext::apply_related_actions(editor, window, cx);
|
||||
crate::clangd_ext::apply_related_actions(editor, window, cx);
|
||||
crate::emmet_ext::apply_related_actions(editor, window, cx);
|
||||
|
||||
if editor.read(cx).pending_inline_input().is_some() {
|
||||
register_action(editor, window, |editor, _: &menu::Confirm, window, cx| {
|
||||
editor.confirm_inline_input(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
register_action(editor, window, Editor::open_context_menu);
|
||||
register_action(editor, window, Editor::move_left);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,370 @@
|
||||
use gpui::StyledText;
|
||||
use language::HighlightId;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct InlineInputState {
|
||||
pub editor: Entity<Editor>,
|
||||
block_id: CustomBlockId,
|
||||
position: Anchor,
|
||||
confirm_task: Option<Task<()>>,
|
||||
preview_task: Option<Task<()>>,
|
||||
pub(crate) preview: Option<InlineInputPreview>,
|
||||
preview_language: Option<Arc<Language>>,
|
||||
block_height: u32,
|
||||
history: Vec<SharedString>,
|
||||
history_ix: Option<usize>,
|
||||
draft: String,
|
||||
_subscription: Subscription,
|
||||
on_confirm:
|
||||
Rc<dyn Fn(&mut Editor, String, &mut Window, &mut Context<Editor>) -> Option<Task<()>>>,
|
||||
}
|
||||
|
||||
impl InlineInputState {
|
||||
pub(crate) fn take_confirm_task(&mut self) -> Option<Task<()>> {
|
||||
self.confirm_task.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum InlineInputPreview {
|
||||
Text(String),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum InlineInputHistoryDirection {
|
||||
Older,
|
||||
Newer,
|
||||
}
|
||||
|
||||
const INLINE_INPUT_PREVIEW_MIN_LINES: usize = 8;
|
||||
|
||||
impl InlineInputPreview {
|
||||
fn display_text(&self, max_lines: usize) -> (String, u32) {
|
||||
let text = match self {
|
||||
InlineInputPreview::Text(text) => text,
|
||||
InlineInputPreview::Error(message) => message,
|
||||
};
|
||||
let line_count = text.lines().count().max(1);
|
||||
if line_count <= max_lines {
|
||||
return (text.to_string(), line_count as u32);
|
||||
}
|
||||
let hidden_lines = line_count - max_lines;
|
||||
let noun = if hidden_lines == 1 { "line" } else { "lines" };
|
||||
let shown = text.lines().take(max_lines).collect::<Vec<_>>().join("\n");
|
||||
(
|
||||
format!("{shown}\n… +{hidden_lines} more {noun}"),
|
||||
max_lines as u32 + 1,
|
||||
)
|
||||
}
|
||||
|
||||
fn is_error(&self) -> bool {
|
||||
matches!(self, InlineInputPreview::Error(_))
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderedPreview {
|
||||
text: SharedString,
|
||||
highlights: Vec<(Range<usize>, HighlightId)>,
|
||||
height_in_lines: u32,
|
||||
is_error: bool,
|
||||
}
|
||||
|
||||
impl RenderedPreview {
|
||||
fn new(
|
||||
preview: &InlineInputPreview,
|
||||
language: Option<&Arc<Language>>,
|
||||
max_lines: usize,
|
||||
) -> Self {
|
||||
let (text, height_in_lines) = preview.display_text(max_lines);
|
||||
let highlights = match (preview, language) {
|
||||
(InlineInputPreview::Text(_), Some(language)) => {
|
||||
language.highlight_text(&Rope::from(text.as_str()), 0..text.len())
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
Self {
|
||||
text: SharedString::from(text),
|
||||
highlights,
|
||||
height_in_lines,
|
||||
is_error: preview.is_error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn pending_inline_input(&self) -> Option<&InlineInputState> {
|
||||
self.pending_inline_input.as_ref()
|
||||
}
|
||||
|
||||
pub fn confirm_inline_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(state) = self.pending_inline_input.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let text = state.editor.read(cx).text(cx);
|
||||
let on_confirm = state.on_confirm.clone();
|
||||
if let Some(task) = on_confirm(self, text, window, cx)
|
||||
&& let Some(state) = self.pending_inline_input.as_mut()
|
||||
{
|
||||
state.confirm_task = Some(task);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn show_inline_input(
|
||||
&mut self,
|
||||
placeholder: &str,
|
||||
position: Anchor,
|
||||
preview_language: Option<Arc<Language>>,
|
||||
history: Vec<SharedString>,
|
||||
on_confirm: impl Fn(&mut Editor, String, &mut Window, &mut Context<Editor>) -> Option<Task<()>>
|
||||
+ 'static,
|
||||
on_change: impl Fn(&mut Editor, String, &mut Window, &mut Context<Editor>) -> Option<Task<()>>
|
||||
+ 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.take_inline_input(window, cx);
|
||||
|
||||
let input = cx.new(|cx| {
|
||||
let mut input = Editor::single_line(window, cx);
|
||||
input.set_placeholder_text(placeholder, window, cx);
|
||||
if let Some(newest) = history.first() {
|
||||
input.set_text(newest.clone(), window, cx);
|
||||
input.select_all(&SelectAll, window, cx);
|
||||
}
|
||||
input
|
||||
});
|
||||
let subscription = cx.subscribe_in(
|
||||
&input,
|
||||
window,
|
||||
move |editor, input, event: &EditorEvent, window, cx| match event {
|
||||
EditorEvent::Focused => cx.emit(EditorEvent::FocusedIn),
|
||||
EditorEvent::BufferEdited => {
|
||||
let Some(state) = editor.pending_inline_input.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let text = input.read(cx).text(cx);
|
||||
if state.history_ix.is_some_and(|ix| {
|
||||
state.history.get(ix).map(SharedString::as_ref) != Some(text.as_str())
|
||||
}) {
|
||||
state.history_ix = None;
|
||||
}
|
||||
let task = on_change(editor, text, window, cx);
|
||||
if let Some(state) = editor.pending_inline_input.as_mut() {
|
||||
state.preview_task = task;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
);
|
||||
let block_ids = self.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
placement: BlockPlacement::Below(position),
|
||||
height: Some(1),
|
||||
render: render_inline_input_block(input.clone(), None),
|
||||
priority: 0,
|
||||
}],
|
||||
Some(reveal_block_below(
|
||||
position,
|
||||
&self.buffer().read(cx).snapshot(cx),
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
let Some(&block_id) = block_ids.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let input_focus_handle = input.focus_handle(cx);
|
||||
window.focus(&input_focus_handle, cx);
|
||||
self.pending_inline_input = Some(InlineInputState {
|
||||
editor: input,
|
||||
block_id,
|
||||
position,
|
||||
confirm_task: None,
|
||||
preview_task: None,
|
||||
preview: None,
|
||||
preview_language,
|
||||
block_height: 1,
|
||||
history_ix: (!history.is_empty()).then_some(0),
|
||||
history,
|
||||
draft: String::new(),
|
||||
_subscription: subscription,
|
||||
on_confirm: Rc::new(on_confirm),
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_inline_input_preview(
|
||||
&mut self,
|
||||
preview: Option<InlineInputPreview>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let max_lines = self.inline_input_preview_max_lines();
|
||||
let Some(state) = self.pending_inline_input.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let block_id = state.block_id;
|
||||
let position = state.position;
|
||||
let input = state.editor.clone();
|
||||
let rendered = preview.as_ref().map(|preview| {
|
||||
RenderedPreview::new(preview, state.preview_language.as_ref(), max_lines)
|
||||
});
|
||||
let height = 1 + rendered
|
||||
.as_ref()
|
||||
.map_or(0, |rendered| rendered.height_in_lines);
|
||||
state.preview = preview;
|
||||
let resized = height != state.block_height;
|
||||
state.block_height = height;
|
||||
let autoscroll =
|
||||
resized.then(|| reveal_block_below(position, &self.buffer().read(cx).snapshot(cx)));
|
||||
self.replace_blocks(
|
||||
HashMap::from_iter([(block_id, render_inline_input_block(input, rendered))]),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
self.resize_blocks(HashMap::from_iter([(block_id, height)]), autoscroll, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn cycle_inline_input_history(
|
||||
&mut self,
|
||||
direction: InlineInputHistoryDirection,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(state) = self.pending_inline_input.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
let next_ix = match (direction, state.history_ix) {
|
||||
(InlineInputHistoryDirection::Older, None) => {
|
||||
if state.history.is_empty() {
|
||||
return true;
|
||||
}
|
||||
state.draft = state.editor.read(cx).text(cx);
|
||||
Some(0)
|
||||
}
|
||||
(InlineInputHistoryDirection::Older, Some(ix)) => {
|
||||
if ix + 1 >= state.history.len() {
|
||||
return true;
|
||||
}
|
||||
Some(ix + 1)
|
||||
}
|
||||
(InlineInputHistoryDirection::Newer, None) => return true,
|
||||
(InlineInputHistoryDirection::Newer, Some(0)) => None,
|
||||
(InlineInputHistoryDirection::Newer, Some(ix)) => Some(ix - 1),
|
||||
};
|
||||
state.history_ix = next_ix;
|
||||
let text = match next_ix {
|
||||
Some(ix) => state.history.get(ix).cloned().unwrap_or_default(),
|
||||
None => SharedString::from(mem::take(&mut state.draft)),
|
||||
};
|
||||
let input = state.editor.clone();
|
||||
input.update(cx, |input, cx| {
|
||||
input.set_text(text, window, cx);
|
||||
input.select_all(&SelectAll, window, cx);
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn take_inline_input(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<InlineInputState> {
|
||||
let state = self.pending_inline_input.take()?;
|
||||
if state.editor.focus_handle(cx).is_focused(window) {
|
||||
window.focus(&self.focus_handle, cx);
|
||||
}
|
||||
self.remove_blocks(
|
||||
HashSet::from_iter([state.block_id]),
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
Some(state)
|
||||
}
|
||||
|
||||
fn inline_input_preview_max_lines(&self) -> usize {
|
||||
let half_viewport = (self.visible_line_count().unwrap_or(0.) / 2.).floor() as usize;
|
||||
half_viewport.max(INLINE_INPUT_PREVIEW_MIN_LINES)
|
||||
}
|
||||
}
|
||||
|
||||
fn reveal_block_below(position: Anchor, snapshot: &MultiBufferSnapshot) -> Autoscroll {
|
||||
let row_below = position.to_point(snapshot).row + 1;
|
||||
if row_below > snapshot.max_point().row {
|
||||
return Autoscroll::center().for_anchor(position);
|
||||
}
|
||||
Autoscroll::fit().for_anchor(snapshot.anchor_before(Point::new(row_below, 0)))
|
||||
}
|
||||
|
||||
fn render_inline_input_block(
|
||||
input: Entity<Editor>,
|
||||
preview: Option<RenderedPreview>,
|
||||
) -> RenderBlock {
|
||||
Arc::new(move |cx: &mut BlockContext| {
|
||||
v_flex()
|
||||
.block_mouse_except_scroll()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.pl(cx.anchor_x)
|
||||
.child(EditorElement::new(
|
||||
&input,
|
||||
EditorStyle {
|
||||
background: cx.theme().system().transparent,
|
||||
local_player: cx.editor_style.local_player,
|
||||
text: cx.editor_style.text.clone(),
|
||||
scrollbar_width: cx.editor_style.scrollbar_width,
|
||||
syntax: cx.editor_style.syntax.clone(),
|
||||
status: cx.editor_style.status.clone(),
|
||||
..EditorStyle::default()
|
||||
},
|
||||
))
|
||||
.when_some(preview.as_ref(), |this, preview| {
|
||||
let mut text_style = cx.editor_style.text.clone();
|
||||
text_style.color = if preview.is_error {
|
||||
cx.theme().status().error
|
||||
} else {
|
||||
cx.theme().colors().text_muted
|
||||
};
|
||||
let syntax = cx.theme().syntax();
|
||||
let highlights = preview
|
||||
.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight_id)| {
|
||||
Some((range.clone(), *syntax.get(*highlight_id)?))
|
||||
});
|
||||
this.child(
|
||||
StyledText::new(preview.text.clone())
|
||||
.with_default_highlights(&text_style, highlights),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_preview_truncation_reports_hidden_lines() {
|
||||
let text = (1..=12)
|
||||
.map(|ix| format!("line{ix}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let preview = InlineInputPreview::Text(text.clone());
|
||||
let (shown, height) = preview.display_text(8);
|
||||
assert_eq!(height, 9);
|
||||
assert_eq!(
|
||||
shown,
|
||||
format!(
|
||||
"{}\n… +4 more lines",
|
||||
text.lines().take(8).collect::<Vec<_>>().join("\n")
|
||||
)
|
||||
);
|
||||
let (shown, height) = preview.display_text(11);
|
||||
assert_eq!(height, 12);
|
||||
assert_eq!(shown.lines().last(), Some("… +1 more line"));
|
||||
assert_eq!(preview.display_text(12), (text, 12));
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,7 @@ pub fn lsp_tasks(
|
||||
GetLspRunnables {
|
||||
buffer_id,
|
||||
position: for_position,
|
||||
server_id,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -45,6 +45,9 @@ impl Editor {
|
||||
if self.take_rename(true, window, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
if self.cycle_inline_input_history(InlineInputHistoryDirection::Older, window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
@@ -262,6 +265,9 @@ impl Editor {
|
||||
if self.take_rename(true, window, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
if self.cycle_inline_input_history(InlineInputHistoryDirection::Newer, window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.mode.is_single_line() {
|
||||
cx.propagate();
|
||||
|
||||
@@ -89,6 +89,7 @@ pub fn go_to_parent_module(
|
||||
project_id,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
position: Some(serialize_anchor(&trigger_anchor)),
|
||||
server_id: server_to_query.to_proto(),
|
||||
};
|
||||
let response = client
|
||||
.request(request)
|
||||
@@ -112,7 +113,10 @@ pub fn go_to_parent_module(
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_store::lsp_ext_command::GoToParentModule { position },
|
||||
project::lsp_store::lsp_ext_command::GoToParentModule {
|
||||
position,
|
||||
server_id: server_to_query,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -169,6 +173,7 @@ pub fn expand_macro_recursively(
|
||||
project_id,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
position: Some(serialize_anchor(&trigger_anchor)),
|
||||
server_id: server_to_query.to_proto(),
|
||||
};
|
||||
let response = client
|
||||
.request(request)
|
||||
@@ -186,7 +191,10 @@ pub fn expand_macro_recursively(
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
ExpandMacro { position },
|
||||
ExpandMacro {
|
||||
position,
|
||||
server_id: server_to_query,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -258,6 +266,7 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
|
||||
project_id,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
position: Some(serialize_anchor(&trigger_anchor)),
|
||||
server_id: server_to_query.to_proto(),
|
||||
};
|
||||
let response = client
|
||||
.request(request)
|
||||
@@ -275,7 +284,10 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_store::lsp_ext_command::OpenDocs { position },
|
||||
project::lsp_store::lsp_ext_command::OpenDocs {
|
||||
position,
|
||||
server_id: server_to_query,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1600,6 +1600,7 @@ impl Editor {
|
||||
self.invalidate_autoclose_regions(&selection_anchors, buffer);
|
||||
self.snippet_stack.invalidate(&selection_anchors, buffer);
|
||||
self.take_rename(false, window, cx);
|
||||
self.take_inline_input(window, cx);
|
||||
|
||||
let newest_selection = self.selections.newest_anchor();
|
||||
let new_cursor_position = newest_selection.head();
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod editor_test_context;
|
||||
|
||||
use std::{rc::Rc, sync::LazyLock};
|
||||
|
||||
pub use crate::emmet_ext::wrap_with_abbreviation;
|
||||
pub use crate::rust_analyzer_ext::expand_macro_recursively;
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects, Size,
|
||||
|
||||
@@ -894,6 +894,20 @@ impl LanguageRegistry {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn lsp_adapter(
|
||||
&self,
|
||||
language_name: &LanguageName,
|
||||
server_name: &LanguageServerName,
|
||||
) -> Option<Arc<CachedLspAdapter>> {
|
||||
self.state
|
||||
.read()
|
||||
.lsp_adapters
|
||||
.get(language_name)?
|
||||
.iter()
|
||||
.find(|adapter| adapter.name() == *server_name)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn all_lsp_adapters(&self) -> Vec<Arc<CachedLspAdapter>> {
|
||||
self.state
|
||||
.read()
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
|
||||
LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectPath,
|
||||
ProjectTransaction, PulledDiagnostics, ResolveState,
|
||||
lsp_store::{LocalLspStore, LspDocumentLink, LspFoldingRange, LspStore},
|
||||
lsp_store::{LanguageServerToQuery, LocalLspStore, LspDocumentLink, LspFoldingRange, LspStore},
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
@@ -99,6 +99,10 @@ pub trait LspCommand: 'static + Sized + Send + std::fmt::Debug {
|
||||
None
|
||||
}
|
||||
|
||||
fn language_server_to_query(&self) -> LanguageServerToQuery {
|
||||
LanguageServerToQuery::FirstCapable
|
||||
}
|
||||
|
||||
fn to_lsp_params_or_response(
|
||||
&self,
|
||||
path: &Path,
|
||||
|
||||
@@ -15,6 +15,7 @@ mod document_colors;
|
||||
mod document_links;
|
||||
mod document_symbols;
|
||||
mod dynamic_registration;
|
||||
pub mod emmet_ext;
|
||||
mod folding_ranges;
|
||||
mod inlay_hints;
|
||||
pub mod json_language_server_ext;
|
||||
@@ -4605,6 +4606,8 @@ impl LspStore {
|
||||
client.add_entity_request_handler(Self::handle_lsp_ext_run_flycheck);
|
||||
client.add_entity_request_handler(Self::handle_lsp_ext_clear_flycheck);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
|
||||
client
|
||||
.add_entity_request_handler(Self::handle_lsp_command::<emmet_ext::ExpandAbbreviation>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<lsp_ext_command::OpenDocs>);
|
||||
client.add_entity_request_handler(
|
||||
Self::handle_lsp_command::<lsp_ext_command::GoToParentModule>,
|
||||
@@ -5714,16 +5717,16 @@ impl LspStore {
|
||||
Ok(LspParamsOrResponse::Params(lsp_params)) => lsp_params,
|
||||
Ok(LspParamsOrResponse::Response(response)) => return Task::ready(Ok(response)),
|
||||
Err(err) => {
|
||||
let message = format!(
|
||||
"{} via {} failed: {}",
|
||||
let err = err.context(format!(
|
||||
"{} via {} failed",
|
||||
request.display_name(),
|
||||
language_server.name(),
|
||||
err
|
||||
);
|
||||
));
|
||||
let message = format!("{err:#}");
|
||||
if should_log_lsp_request_failure(&message) {
|
||||
log::warn!("{message}");
|
||||
}
|
||||
return Task::ready(Err(anyhow!(message)));
|
||||
return Task::ready(Err(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5775,16 +5778,16 @@ impl LspStore {
|
||||
let result = lsp_request.await.into_response();
|
||||
|
||||
let response = result.map_err(|err| {
|
||||
let message = format!(
|
||||
"{} via {} failed: {}",
|
||||
let err = err.context(format!(
|
||||
"{} via {} failed",
|
||||
request.display_name(),
|
||||
language_server.name(),
|
||||
err
|
||||
);
|
||||
));
|
||||
let message = format!("{err:#}");
|
||||
if should_log_lsp_request_failure(&message) {
|
||||
log::warn!("{message}");
|
||||
}
|
||||
anyhow::anyhow!(message)
|
||||
err
|
||||
})?;
|
||||
|
||||
request
|
||||
@@ -9168,6 +9171,13 @@ impl LspStore {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn language_server_ids_for_opened_buffer(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
) -> Option<&HashSet<LanguageServerId>> {
|
||||
self.as_local()?.buffers_opened_in_servers.get(&buffer_id)
|
||||
}
|
||||
|
||||
pub fn language_server_for_local_buffer<'a>(
|
||||
&'a self,
|
||||
buffer: &'a Buffer,
|
||||
@@ -9969,14 +9979,10 @@ impl LspStore {
|
||||
cx.clone(),
|
||||
)
|
||||
.await?;
|
||||
let server = request.language_server_to_query();
|
||||
let response = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.request_lsp(
|
||||
buffer_handle.clone(),
|
||||
LanguageServerToQuery::FirstCapable,
|
||||
request,
|
||||
cx,
|
||||
)
|
||||
this.request_lsp(buffer_handle.clone(), server, request, cx)
|
||||
})
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::{App, AsyncApp, Entity};
|
||||
use language::Buffer;
|
||||
use lsp::{AdapterServerCapabilities, LanguageServer, LanguageServerId};
|
||||
use rpc::proto::{self, PeerId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::BufferId;
|
||||
|
||||
use crate::{
|
||||
lsp_command::LspCommand,
|
||||
lsp_store::{LanguageServerToQuery, LspStore},
|
||||
};
|
||||
|
||||
pub const EMMET_SERVER_NAME: lsp::LanguageServerName =
|
||||
lsp::LanguageServerName::new_static("emmet-language-server");
|
||||
|
||||
pub enum LspExpandAbbreviation {}
|
||||
|
||||
impl lsp::request::Request for LspExpandAbbreviation {
|
||||
type Params = ExpandAbbreviationParams;
|
||||
type Result = Option<String>;
|
||||
const METHOD: &'static str = "emmet/expandAbbreviation";
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExpandAbbreviationParams {
|
||||
pub abbreviation: String,
|
||||
pub language: String,
|
||||
pub options: ExpandAbbreviationOptions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExpandAbbreviationOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<Vec<String>>,
|
||||
pub options: EmmetOutputOptions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
|
||||
pub struct EmmetOutputOptions {
|
||||
#[serde(rename = "output.indent")]
|
||||
pub indent: String,
|
||||
#[serde(rename = "output.baseIndent")]
|
||||
pub base_indent: String,
|
||||
#[serde(rename = "comment.enabled", skip_serializing_if = "Option::is_none")]
|
||||
pub comment_enabled: Option<bool>,
|
||||
#[serde(rename = "bem.enabled", skip_serializing_if = "Option::is_none")]
|
||||
pub bem_enabled: Option<bool>,
|
||||
#[serde(rename = "output.inlineBreak", skip_serializing_if = "Option::is_none")]
|
||||
pub inline_break: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExpandAbbreviation {
|
||||
pub abbreviation: String,
|
||||
pub text: Option<Vec<String>>,
|
||||
pub language: String,
|
||||
pub server_id: LanguageServerId,
|
||||
pub indent: String,
|
||||
pub base_indent: String,
|
||||
pub comment_filter: bool,
|
||||
pub bem_filter: bool,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for ExpandAbbreviation {
|
||||
type Response = Option<String>;
|
||||
type LspRequest = LspExpandAbbreviation;
|
||||
type ProtoRequest = proto::LspExtExpandAbbreviation;
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"Expand Emmet abbreviation"
|
||||
}
|
||||
|
||||
fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn language_server_to_query(&self) -> LanguageServerToQuery {
|
||||
LanguageServerToQuery::Other(self.server_id)
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
_: &Path,
|
||||
_: &Buffer,
|
||||
server: &Arc<LanguageServer>,
|
||||
_: &App,
|
||||
) -> Result<ExpandAbbreviationParams> {
|
||||
anyhow::ensure!(
|
||||
server.name() == EMMET_SERVER_NAME,
|
||||
"cannot expand Emmet abbreviations with the {} server",
|
||||
server.name()
|
||||
);
|
||||
let multiline = self.text.as_ref().is_some_and(|text| text.len() > 1);
|
||||
Ok(ExpandAbbreviationParams {
|
||||
abbreviation: self.abbreviation.clone(),
|
||||
language: self.language.clone(),
|
||||
options: ExpandAbbreviationOptions {
|
||||
text: self.text.clone(),
|
||||
options: EmmetOutputOptions {
|
||||
indent: self.indent.clone(),
|
||||
base_indent: self.base_indent.clone(),
|
||||
comment_enabled: self.comment_filter.then_some(true),
|
||||
bem_enabled: self.bem_filter.then_some(true),
|
||||
inline_break: multiline.then_some(1),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: Option<String>,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: LanguageServerId,
|
||||
_: AsyncApp,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
Ok(message.filter(|expansion| !expansion.is_empty()))
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandAbbreviation {
|
||||
proto::LspExtExpandAbbreviation {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().into(),
|
||||
abbreviation: self.abbreviation.clone(),
|
||||
text: self.text.clone().unwrap_or_default(),
|
||||
language: self.language.clone(),
|
||||
server_id: self.server_id.to_proto(),
|
||||
indent: self.indent.clone(),
|
||||
base_indent: self.base_indent.clone(),
|
||||
comment_filter: self.comment_filter,
|
||||
bem_filter: self.bem_filter,
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: Self::ProtoRequest,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: AsyncApp,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
abbreviation: message.abbreviation,
|
||||
text: (!message.text.is_empty()).then_some(message.text),
|
||||
language: message.language,
|
||||
server_id: LanguageServerId::from_proto(message.server_id),
|
||||
indent: message.indent,
|
||||
base_indent: message.base_indent,
|
||||
comment_filter: message.comment_filter,
|
||||
bem_filter: message.bem_filter,
|
||||
})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Option<String>,
|
||||
_: &mut LspStore,
|
||||
_: PeerId,
|
||||
_: &clock::Global,
|
||||
_: &mut App,
|
||||
) -> proto::LspExtExpandAbbreviationResponse {
|
||||
proto::LspExtExpandAbbreviationResponse {
|
||||
expansion: response,
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
message: proto::LspExtExpandAbbreviationResponse,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: AsyncApp,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
Ok(message.expansion.filter(|expansion| !expansion.is_empty()))
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::LspExtExpandAbbreviation) -> Result<BufferId> {
|
||||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
location_link_to_proto, location_links_from_lsp, location_links_from_proto,
|
||||
location_links_to_proto,
|
||||
},
|
||||
lsp_store::LspStore,
|
||||
lsp_store::{LanguageServerToQuery, LspStore},
|
||||
make_lsp_text_document_position, make_text_document_identifier,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
@@ -56,6 +56,7 @@ impl ExpandedMacro {
|
||||
#[derive(Debug)]
|
||||
pub struct ExpandMacro {
|
||||
pub position: PointUtf16,
|
||||
pub server_id: LanguageServerId,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
@@ -72,6 +73,10 @@ impl LspCommand for ExpandMacro {
|
||||
true
|
||||
}
|
||||
|
||||
fn language_server_to_query(&self) -> LanguageServerToQuery {
|
||||
LanguageServerToQuery::Other(self.server_id)
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -108,6 +113,7 @@ impl LspCommand for ExpandMacro {
|
||||
position: Some(language::proto::serialize_anchor(
|
||||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
server_id: self.server_id.to_proto(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +129,7 @@ impl LspCommand for ExpandMacro {
|
||||
.context("invalid position")?;
|
||||
Ok(Self {
|
||||
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
|
||||
server_id: LanguageServerId::from_proto(message.server_id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -188,6 +195,7 @@ impl DocsUrls {
|
||||
#[derive(Debug)]
|
||||
pub struct OpenDocs {
|
||||
pub position: PointUtf16,
|
||||
pub server_id: LanguageServerId,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
@@ -204,6 +212,10 @@ impl LspCommand for OpenDocs {
|
||||
true
|
||||
}
|
||||
|
||||
fn language_server_to_query(&self) -> LanguageServerToQuery {
|
||||
LanguageServerToQuery::Other(self.server_id)
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -242,6 +254,7 @@ impl LspCommand for OpenDocs {
|
||||
position: Some(language::proto::serialize_anchor(
|
||||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
server_id: self.server_id.to_proto(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +270,7 @@ impl LspCommand for OpenDocs {
|
||||
.context("invalid position")?;
|
||||
Ok(Self {
|
||||
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
|
||||
server_id: LanguageServerId::from_proto(message.server_id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -307,13 +321,15 @@ pub struct SwitchSourceHeaderParams(lsp::TextDocumentIdentifier);
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwitchSourceHeaderResult(pub String);
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwitchSourceHeader;
|
||||
#[derive(Debug)]
|
||||
pub struct SwitchSourceHeader {
|
||||
pub server_id: LanguageServerId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GoToParentModule {
|
||||
pub position: PointUtf16,
|
||||
pub server_id: LanguageServerId,
|
||||
}
|
||||
|
||||
pub struct LspGoToParentModule {}
|
||||
@@ -338,6 +354,10 @@ impl LspCommand for SwitchSourceHeader {
|
||||
true
|
||||
}
|
||||
|
||||
fn language_server_to_query(&self) -> LanguageServerToQuery {
|
||||
LanguageServerToQuery::Other(self.server_id)
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -367,16 +387,19 @@ impl LspCommand for SwitchSourceHeader {
|
||||
proto::LspExtSwitchSourceHeader {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().into(),
|
||||
server_id: self.server_id.to_proto(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
_: Self::ProtoRequest,
|
||||
message: Self::ProtoRequest,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: AsyncApp,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Self {})
|
||||
Ok(Self {
|
||||
server_id: LanguageServerId::from_proto(message.server_id),
|
||||
})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
@@ -420,6 +443,10 @@ impl LspCommand for GoToParentModule {
|
||||
true
|
||||
}
|
||||
|
||||
fn language_server_to_query(&self) -> LanguageServerToQuery {
|
||||
LanguageServerToQuery::Other(self.server_id)
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -455,6 +482,7 @@ impl LspCommand for GoToParentModule {
|
||||
position: Some(language::proto::serialize_anchor(
|
||||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
server_id: self.server_id.to_proto(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,6 +498,7 @@ impl LspCommand for GoToParentModule {
|
||||
.context("bad request with bad position")?;
|
||||
Ok(Self {
|
||||
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
|
||||
server_id: LanguageServerId::from_proto(request.server_id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -606,6 +635,7 @@ pub struct ShellRunnableArgs {
|
||||
pub struct GetLspRunnables {
|
||||
pub buffer_id: BufferId,
|
||||
pub position: Option<text::Anchor>,
|
||||
pub server_id: LanguageServerId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -677,6 +707,10 @@ impl LspCommand for GetLspRunnables {
|
||||
true
|
||||
}
|
||||
|
||||
fn language_server_to_query(&self) -> LanguageServerToQuery {
|
||||
LanguageServerToQuery::Other(self.server_id)
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -723,6 +757,7 @@ impl LspCommand for GetLspRunnables {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().to_proto(),
|
||||
position: self.position.as_ref().map(serialize_anchor),
|
||||
server_id: self.server_id.to_proto(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,6 +772,7 @@ impl LspCommand for GetLspRunnables {
|
||||
Ok(Self {
|
||||
buffer_id,
|
||||
position,
|
||||
server_id: LanguageServerId::from_proto(message.server_id),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6332,14 +6332,16 @@ impl Project {
|
||||
if !relevant_language_servers.contains(name) {
|
||||
return None;
|
||||
}
|
||||
let opened_in_servers = self
|
||||
.lsp_store
|
||||
.read(cx)
|
||||
.language_server_ids_for_opened_buffer(buffer.remote_id());
|
||||
self.language_server_statuses(cx)
|
||||
.filter(|(_, server_status)| relevant_language_servers.contains(&server_status.name))
|
||||
.find_map(|(server_id, server_status)| {
|
||||
if &server_status.name == name {
|
||||
Some(server_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
(&server_status.name == name
|
||||
&& opened_in_servers.is_none_or(|server_ids| server_ids.contains(&server_id)))
|
||||
.then_some(server_id)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -299,6 +299,7 @@ message LspExtExpandMacro {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
uint64 server_id = 4;
|
||||
}
|
||||
|
||||
message LspExtExpandMacroResponse {
|
||||
@@ -306,10 +307,28 @@ message LspExtExpandMacroResponse {
|
||||
string expansion = 2;
|
||||
}
|
||||
|
||||
message LspExtExpandAbbreviation {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
string abbreviation = 3;
|
||||
repeated string text = 4;
|
||||
string language = 5;
|
||||
uint64 server_id = 6;
|
||||
bool comment_filter = 7;
|
||||
bool bem_filter = 8;
|
||||
string indent = 9;
|
||||
string base_indent = 10;
|
||||
}
|
||||
|
||||
message LspExtExpandAbbreviationResponse {
|
||||
optional string expansion = 1;
|
||||
}
|
||||
|
||||
message LspExtOpenDocs {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
uint64 server_id = 4;
|
||||
}
|
||||
|
||||
message LspExtOpenDocsResponse {
|
||||
@@ -320,6 +339,7 @@ message LspExtOpenDocsResponse {
|
||||
message LspExtSwitchSourceHeader {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
uint64 server_id = 3;
|
||||
}
|
||||
|
||||
message LspExtSwitchSourceHeaderResponse {
|
||||
@@ -330,6 +350,7 @@ message LspExtGoToParentModule {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
uint64 server_id = 4;
|
||||
}
|
||||
|
||||
message LspExtGoToParentModuleResponse {
|
||||
@@ -1068,6 +1089,7 @@ message LspExtRunnables {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
optional Anchor position = 3;
|
||||
uint64 server_id = 4;
|
||||
}
|
||||
|
||||
message LspExtRunnablesResponse {
|
||||
|
||||
@@ -516,7 +516,9 @@ message Envelope {
|
||||
GetIncomingCalls get_incoming_calls = 484;
|
||||
GetIncomingCallsResponse get_incoming_calls_response = 485;
|
||||
GetOutgoingCalls get_outgoing_calls = 486;
|
||||
GetOutgoingCallsResponse get_outgoing_calls_response = 487; // current max
|
||||
GetOutgoingCallsResponse get_outgoing_calls_response = 487;
|
||||
LspExtExpandAbbreviation lsp_ext_expand_abbreviation = 488;
|
||||
LspExtExpandAbbreviationResponse lsp_ext_expand_abbreviation_response = 489; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
||||
@@ -176,6 +176,8 @@ messages!(
|
||||
(LoadCommitDiffResponse, Foreground),
|
||||
(LspExtExpandMacro, Background),
|
||||
(LspExtExpandMacroResponse, Background),
|
||||
(LspExtExpandAbbreviation, Background),
|
||||
(LspExtExpandAbbreviationResponse, Background),
|
||||
(LspExtOpenDocs, Background),
|
||||
(LspExtOpenDocsResponse, Background),
|
||||
(LspExtRunnables, Background),
|
||||
@@ -547,6 +549,7 @@ request_messages!(
|
||||
(UpdateRepository, Ack),
|
||||
(RemoveRepository, Ack),
|
||||
(LspExtExpandMacro, LspExtExpandMacroResponse),
|
||||
(LspExtExpandAbbreviation, LspExtExpandAbbreviationResponse),
|
||||
(LspExtOpenDocs, LspExtOpenDocsResponse),
|
||||
(LspExtRunnables, LspExtRunnablesResponse),
|
||||
(SetRoomParticipantRole, Ack),
|
||||
@@ -784,6 +787,7 @@ entity_messages!(
|
||||
UpdateWorktreeSettings,
|
||||
UpdateUserSettings,
|
||||
LspExtExpandMacro,
|
||||
LspExtExpandAbbreviation,
|
||||
LspExtOpenDocs,
|
||||
LspExtRunnables,
|
||||
LspExtSwitchSourceHeader,
|
||||
|
||||
@@ -11,6 +11,39 @@ Emmet support is available through the [Emmet extension](https://github.com/zed-
|
||||
|
||||
- Language Server: [olrtg/emmet-language-server](https://github.com/olrtg/emmet-language-server)
|
||||
|
||||
## Wrap with abbreviation
|
||||
|
||||
With the Emmet extension installed, you can wrap the current selections in an expanded [Emmet abbreviation](https://docs.emmet.io/abbreviations/):
|
||||
|
||||
1. Select the text to wrap. With an empty selection, the enclosing HTML element is wrapped; if no enclosing element can be determined, the current line is wrapped instead.
|
||||
2. Run `editor: wrap with abbreviation` from the command palette. The last used abbreviation is prefilled and selected, so typing replaces it; `up` and `down` cycle through previously used abbreviations.
|
||||
3. Type an abbreviation, for example `div.wrapper>ul>li*3`, into the input that appears below the selection. A preview of the expansion is shown under the input as you type.
|
||||
4. Press `enter` to apply, or `escape` to cancel. If the abbreviation produces no expansion, the error is shown next to the input and the input stays open so you can correct it.
|
||||
|
||||
For a single selection, the expansion is inserted as a snippet and the cursor is placed at the end of the expansion. With multiple selections, each selection is wrapped separately; overlapping selections are merged and wrapped once.
|
||||
|
||||
The [documented wrap behaviors](https://docs.emmet.io/actions/wrap-with-abbreviation/) are supported:
|
||||
|
||||
- Wrapping individual lines: mark the repeating element with `*` (without a number), for example `ul>li*`, and each selected line is wrapped in its own element. `$` numbering works too: `ul>li.item$*>a`.
|
||||
- Removing list markers: append the trim filter `|t`, for example `ul>li*|t`, to strip list markers such as `*`, `-`, or `1.` from the wrapped lines.
|
||||
- Controlling output position: use the `$#` placeholder inside attribute values or text nodes, for example `ul>li[title=$#]*>{$#}`.
|
||||
- The [comment filter](https://docs.emmet.io/filters/comment/) `|c` and the [BEM filter](https://docs.emmet.io/filters/bem/) `|bem` are also supported and can be combined, for example `div#page>p.intro|c|bem`.
|
||||
|
||||
Selections that partially cover an opening or closing tag are expanded to the whole element before wrapping. Wrapping is a markup action: it is not offered in stylesheets, where Emmet provides completions instead.
|
||||
|
||||
The action has no default keybinding. To add one, bind it in your keymap:
|
||||
|
||||
```json [keymap]
|
||||
[
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-alt-w": "editor::WrapWithAbbreviation"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
<!--
|
||||
TBD: Document Emmet usage in zed with: HTML, PHP, ERB, Javascript, TSX, CSS
|
||||
-->
|
||||
Reference in new issue
Block a user