mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 10:29:35 +03:00
Merge remote-tracking branch 'origin/main' into chat-font-size
This commit is contained in:
commit
29ac1fd081
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1548,6 +1548,7 @@ dependencies = [
|
||||
"log",
|
||||
"menu",
|
||||
"notifications",
|
||||
"parking_lot 0.11.2",
|
||||
"picker",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
@ -9080,6 +9081,7 @@ dependencies = [
|
||||
"nvim-rs",
|
||||
"parking_lot 0.11.2",
|
||||
"project",
|
||||
"regex",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
|
@ -104,8 +104,6 @@
|
||||
"shift-v": "vim::ToggleVisualLine",
|
||||
"ctrl-v": "vim::ToggleVisualBlock",
|
||||
"ctrl-q": "vim::ToggleVisualBlock",
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"ctrl-f": "vim::PageDown",
|
||||
"pagedown": "vim::PageDown",
|
||||
@ -329,6 +327,8 @@
|
||||
"backwards": true
|
||||
}
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
";": "vim::RepeatFind",
|
||||
",": [
|
||||
"vim::RepeatFind",
|
||||
@ -421,6 +421,18 @@
|
||||
"shift-r": "vim::SubstituteLine",
|
||||
"c": "vim::Substitute",
|
||||
"~": "vim::ChangeCase",
|
||||
"*": [
|
||||
"vim::MoveToNext",
|
||||
{
|
||||
"partialWord": true
|
||||
}
|
||||
],
|
||||
"#": [
|
||||
"vim::MoveToPrev",
|
||||
{
|
||||
"partialWord": true
|
||||
}
|
||||
],
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"g ctrl-a": [
|
||||
|
@ -60,6 +60,7 @@ anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
schemars.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
|
@ -1,17 +1,22 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
|
||||
use client::UserId;
|
||||
use collections::HashMap;
|
||||
use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
|
||||
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, Completion,
|
||||
LanguageRegistry, LanguageServerId, ToOffset,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use project::search::SearchQuery;
|
||||
use settings::Settings;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, UiTextSize};
|
||||
|
||||
@ -31,6 +36,43 @@ pub struct MessageEditor {
|
||||
channel_id: Option<ChannelId>,
|
||||
}
|
||||
|
||||
struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
|
||||
|
||||
impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<Vec<language::Completion>>> {
|
||||
let Some(handle) = self.0.upgrade() else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
handle.update(cx, |message_editor, cx| {
|
||||
message_editor.completions(buffer, buffer_position, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Arc<RwLock<Box<[language::Completion]>>>,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_buffer: Model<Buffer>,
|
||||
_completion: Completion,
|
||||
_push_to_history: bool,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@ -38,9 +80,11 @@ impl MessageEditor {
|
||||
editor: View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let this = cx.view().downgrade();
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_use_autoclose(false);
|
||||
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
|
||||
});
|
||||
|
||||
let buffer = editor
|
||||
@ -150,6 +194,71 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn completions(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
end_anchor: Anchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
let end_offset = end_anchor.to_offset(buffer.read(cx));
|
||||
|
||||
let Some(query) = buffer.update(cx, |buffer, _| {
|
||||
let mut query = String::new();
|
||||
for ch in buffer.reversed_chars_at(end_offset).take(100) {
|
||||
if ch == '@' {
|
||||
return Some(query.chars().rev().collect::<String>());
|
||||
}
|
||||
if ch.is_whitespace() || !ch.is_ascii() {
|
||||
break;
|
||||
}
|
||||
query.push(ch);
|
||||
}
|
||||
return None;
|
||||
}) else {
|
||||
return Task::ready(Ok(vec![]));
|
||||
};
|
||||
|
||||
let start_offset = end_offset - query.len();
|
||||
let start_anchor = buffer.read(cx).anchor_before(start_offset);
|
||||
|
||||
let candidates = self
|
||||
.users
|
||||
.keys()
|
||||
.map(|user| StringMatchCandidate {
|
||||
id: 0,
|
||||
string: user.clone(),
|
||||
char_bag: user.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
cx.spawn(|_, cx| async move {
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
10,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|mat| Completion {
|
||||
old_range: start_anchor..end_anchor,
|
||||
new_text: mat.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 1..mat.string.len() + 1,
|
||||
text: format!("@{}", mat.string),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
async fn find_mentions(
|
||||
this: WeakView<MessageEditor>,
|
||||
buffer: BufferSnapshot,
|
||||
|
@ -85,7 +85,14 @@ impl Render for CollabTitlebarItem {
|
||||
.gap_1()
|
||||
.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
.child(div().pr_1().children(self.render_project_branch(cx)))
|
||||
.children(self.render_project_branch(cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id("collaborator-list")
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.overflow_x_scroll()
|
||||
.when_some(
|
||||
current_user.clone().zip(client.peer_id()).zip(room.clone()),
|
||||
|this, ((current_user, peer_id), room)| {
|
||||
|
@ -40,7 +40,7 @@ pub(crate) use actions::*;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
use client::{Client, Collaborator, ParticipantIndex};
|
||||
use client::{Collaborator, ParticipantIndex};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
@ -71,8 +71,7 @@ use language::{
|
||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
|
||||
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
|
||||
Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection,
|
||||
SelectionGoal, TransactionId,
|
||||
Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
|
||||
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
|
||||
@ -88,7 +87,7 @@ use ordered_float::OrderedFloat;
|
||||
use parking_lot::RwLock;
|
||||
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::{self, *};
|
||||
use rpc::proto::*;
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -365,6 +364,7 @@ pub struct Editor {
|
||||
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
||||
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
|
||||
project: Option<Model<Project>>,
|
||||
completion_provider: Option<Box<dyn CompletionProvider>>,
|
||||
collaboration_hub: Option<Box<dyn CollaborationHub>>,
|
||||
blink_manager: Model<BlinkManager>,
|
||||
show_cursor_names: bool,
|
||||
@ -732,85 +732,21 @@ impl CompletionsMenu {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(project) = editor.project.clone() else {
|
||||
let Some(provider) = editor.completion_provider.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let client = project.read(cx).client();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.matches.iter().map(|m| m.candidate_id).collect(),
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
let is_remote = project.read(cx).is_remote();
|
||||
let project_id = project.read(cx).remote_id();
|
||||
|
||||
let completions = self.completions.clone();
|
||||
let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
|
||||
|
||||
Some(cx.spawn(move |this, mut cx| async move {
|
||||
if is_remote {
|
||||
let Some(project_id) = project_id else {
|
||||
log::error!("Remote project without remote_id");
|
||||
return;
|
||||
};
|
||||
|
||||
for completion_index in completion_indices {
|
||||
let completions_guard = completions.read();
|
||||
let completion = &completions_guard[completion_index];
|
||||
if completion.documentation.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let server_id = completion.server_id;
|
||||
let completion = completion.lsp_completion.clone();
|
||||
drop(completions_guard);
|
||||
|
||||
Self::resolve_completion_documentation_remote(
|
||||
project_id,
|
||||
server_id,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
completion,
|
||||
client.clone(),
|
||||
language_registry.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
_ = this.update(&mut cx, |_, cx| cx.notify());
|
||||
}
|
||||
} else {
|
||||
for completion_index in completion_indices {
|
||||
let completions_guard = completions.read();
|
||||
let completion = &completions_guard[completion_index];
|
||||
if completion.documentation.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let server_id = completion.server_id;
|
||||
let completion = completion.lsp_completion.clone();
|
||||
drop(completions_guard);
|
||||
|
||||
let server = project
|
||||
.read_with(&mut cx, |project, _| {
|
||||
project.language_server_for_id(server_id)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some(server) = server else {
|
||||
return;
|
||||
};
|
||||
|
||||
Self::resolve_completion_documentation_local(
|
||||
server,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
completion,
|
||||
language_registry.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
_ = this.update(&mut cx, |_, cx| cx.notify());
|
||||
}
|
||||
return Some(cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
fn attempt_resolve_selected_completion_documentation(
|
||||
@ -827,146 +763,16 @@ impl CompletionsMenu {
|
||||
let Some(project) = project else {
|
||||
return;
|
||||
};
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let completions = self.completions.clone();
|
||||
let completions_guard = completions.read();
|
||||
let completion = &completions_guard[completion_index];
|
||||
if completion.documentation.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let server_id = completion.server_id;
|
||||
let completion = completion.lsp_completion.clone();
|
||||
drop(completions_guard);
|
||||
|
||||
if project.read(cx).is_remote() {
|
||||
let Some(project_id) = project.read(cx).remote_id() else {
|
||||
log::error!("Remote project without remote_id");
|
||||
return;
|
||||
};
|
||||
|
||||
let client = project.read(cx).client();
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
Self::resolve_completion_documentation_remote(
|
||||
project_id,
|
||||
server_id,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
completion,
|
||||
client,
|
||||
language_registry.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
_ = this.update(&mut cx, |_, cx| cx.notify());
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
let Some(server) = project.read(cx).language_server_for_id(server_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
Self::resolve_completion_documentation_local(
|
||||
server,
|
||||
completions,
|
||||
completion_index,
|
||||
completion,
|
||||
language_registry,
|
||||
)
|
||||
.await;
|
||||
|
||||
_ = this.update(&mut cx, |_, cx| cx.notify());
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_completion_documentation_remote(
|
||||
project_id: u64,
|
||||
server_id: LanguageServerId,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
completion_index: usize,
|
||||
completion: lsp::CompletionItem,
|
||||
client: Arc<Client>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) {
|
||||
let request = proto::ResolveCompletionDocumentation {
|
||||
project_id,
|
||||
language_server_id: server_id.0 as u64,
|
||||
lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
|
||||
};
|
||||
|
||||
let Some(response) = client
|
||||
.request(request)
|
||||
.await
|
||||
.context("completion documentation resolve proto request")
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if response.text.is_empty() {
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(Documentation::Undocumented);
|
||||
}
|
||||
|
||||
let documentation = if response.is_markdown {
|
||||
Documentation::MultiLineMarkdown(
|
||||
markdown::parse_markdown(&response.text, &language_registry, None).await,
|
||||
)
|
||||
} else if response.text.lines().count() <= 1 {
|
||||
Documentation::SingleLine(response.text)
|
||||
} else {
|
||||
Documentation::MultiLinePlainText(response.text)
|
||||
};
|
||||
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(documentation);
|
||||
}
|
||||
|
||||
async fn resolve_completion_documentation_local(
|
||||
server: Arc<lsp::LanguageServer>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
completion_index: usize,
|
||||
completion: lsp::CompletionItem,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) {
|
||||
let can_resolve = server
|
||||
.capabilities()
|
||||
.completion_provider
|
||||
.as_ref()
|
||||
.and_then(|options| options.resolve_provider)
|
||||
.unwrap_or(false);
|
||||
if !can_resolve {
|
||||
return;
|
||||
}
|
||||
|
||||
let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
|
||||
let Some(completion_item) = request.await.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(lsp_documentation) = completion_item.documentation {
|
||||
let documentation = language::prepare_completion_documentation(
|
||||
&lsp_documentation,
|
||||
&language_registry,
|
||||
None, // TODO: Try to reasonably work out which language the completion is for
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(documentation);
|
||||
} else {
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(Documentation::Undocumented);
|
||||
}
|
||||
let resolve_task = project.update(cx, |project, cx| {
|
||||
project.resolve_completions(vec![completion_index], self.completions.clone(), cx)
|
||||
});
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn visible(&self) -> bool {
|
||||
@ -1575,6 +1381,7 @@ impl Editor {
|
||||
ime_transaction: Default::default(),
|
||||
active_diagnostics: None,
|
||||
soft_wrap_mode_override,
|
||||
completion_provider: project.clone().map(|project| Box::new(project) as _),
|
||||
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
|
||||
project,
|
||||
blink_manager: blink_manager.clone(),
|
||||
@ -1808,6 +1615,10 @@ impl Editor {
|
||||
self.collaboration_hub = Some(hub);
|
||||
}
|
||||
|
||||
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
|
||||
self.completion_provider = Some(hub);
|
||||
}
|
||||
|
||||
pub fn placeholder_text(&self) -> Option<&str> {
|
||||
self.placeholder_text.as_deref()
|
||||
}
|
||||
@ -3263,9 +3074,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let project = if let Some(project) = self.project.clone() {
|
||||
project
|
||||
} else {
|
||||
let Some(provider) = self.completion_provider.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@ -3281,9 +3090,7 @@ impl Editor {
|
||||
};
|
||||
|
||||
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone());
|
||||
let completions = project.update(cx, |project, cx| {
|
||||
project.completions(&buffer, buffer_position, cx)
|
||||
});
|
||||
let completions = provider.completions(&buffer, buffer_position, cx);
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
@ -3392,6 +3199,7 @@ impl Editor {
|
||||
let buffer_handle = completions_menu.buffer;
|
||||
let completions = completions_menu.completions.read();
|
||||
let completion = completions.get(mat.candidate_id)?;
|
||||
cx.stop_propagation();
|
||||
|
||||
let snippet;
|
||||
let text;
|
||||
@ -3488,15 +3296,13 @@ impl Editor {
|
||||
this.refresh_copilot_suggestions(true, cx);
|
||||
});
|
||||
|
||||
let project = self.project.clone()?;
|
||||
let apply_edits = project.update(cx, |project, cx| {
|
||||
project.apply_additional_edits_for_completion(
|
||||
buffer_handle,
|
||||
completion.clone(),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let provider = self.completion_provider.as_ref()?;
|
||||
let apply_edits = provider.apply_additional_edits_for_completion(
|
||||
buffer_handle,
|
||||
completion.clone(),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
apply_edits.await?;
|
||||
Ok(())
|
||||
@ -3918,7 +3724,7 @@ impl Editor {
|
||||
self.show_cursor_names = true;
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.show_cursor_names = false;
|
||||
cx.notify()
|
||||
@ -9108,6 +8914,66 @@ impl CollaborationHub for Model<Project> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: text::Anchor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Vec<Completion>>>;
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
completion_indices: Vec<usize>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>>;
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
completion: Completion,
|
||||
push_to_history: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>>;
|
||||
}
|
||||
|
||||
impl CompletionProvider for Model<Project> {
|
||||
fn completions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: text::Anchor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.completions(&buffer, buffer_position, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
completion_indices: Vec<usize>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.resolve_completions(completion_indices, completions, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
completion: Completion,
|
||||
push_to_history: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn inlay_hint_settings(
|
||||
location: Anchor,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
|
@ -1218,9 +1218,11 @@ impl EditorElement {
|
||||
popover_origin.x = popover_origin.x + x_out_of_bounds;
|
||||
}
|
||||
|
||||
cx.break_content_mask(|cx| {
|
||||
hover_popover.draw(popover_origin, available_space, cx)
|
||||
});
|
||||
if cx.was_top_layer(&popover_origin, cx.stacking_order()) {
|
||||
cx.break_content_mask(|cx| {
|
||||
hover_popover.draw(popover_origin, available_space, cx)
|
||||
});
|
||||
}
|
||||
|
||||
current_y = popover_origin.y - HOVER_POPOVER_GAP;
|
||||
}
|
||||
@ -2128,7 +2130,13 @@ impl EditorElement {
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
if editor.context_menu_visible() {
|
||||
let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.);
|
||||
let max_height = cmp::min(
|
||||
12. * line_height,
|
||||
cmp::max(
|
||||
3. * line_height,
|
||||
(bounds.size.height - line_height) / 2.,
|
||||
)
|
||||
);
|
||||
context_menu =
|
||||
editor.render_context_menu(newest_selection_head, &self.style, max_height, cx);
|
||||
}
|
||||
|
@ -3,13 +3,10 @@ use anyhow::Result;
|
||||
use cocoa::{
|
||||
appkit::NSScreen,
|
||||
base::{id, nil},
|
||||
foundation::{NSDictionary, NSString},
|
||||
foundation::{NSDictionary, NSPoint, NSRect, NSSize, NSString},
|
||||
};
|
||||
use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
|
||||
use core_graphics::{
|
||||
display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList},
|
||||
geometry::{CGPoint, CGRect, CGSize},
|
||||
};
|
||||
use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -77,14 +74,14 @@ extern "C" {
|
||||
fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
|
||||
}
|
||||
|
||||
/// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space.
|
||||
/// Convert the given rectangle from Cocoa's coordinate space to GPUI's coordinate space.
|
||||
///
|
||||
/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
|
||||
/// Cocoa's coordinate space has its origin at the bottom left of the primary screen,
|
||||
/// with the Y axis pointing upwards.
|
||||
///
|
||||
/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
|
||||
/// screen, with the Y axis pointing downwards.
|
||||
pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> {
|
||||
/// screen, with the Y axis pointing downwards (matching CoreGraphics)
|
||||
pub(crate) fn global_bounds_from_ns_rect(rect: NSRect) -> Bounds<GlobalPixels> {
|
||||
let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size;
|
||||
|
||||
Bounds {
|
||||
@ -101,22 +98,22 @@ pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the given rectangle from GPUI's coordinate system to CoreGraphics' native coordinate space.
|
||||
/// Convert the given rectangle from GPUI's coordinate system to Cocoa's native coordinate space.
|
||||
///
|
||||
/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
|
||||
/// Cocoa's coordinate space has its origin at the bottom left of the primary screen,
|
||||
/// with the Y axis pointing upwards.
|
||||
///
|
||||
/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
|
||||
/// screen, with the Y axis pointing downwards.
|
||||
pub(crate) fn display_bounds_to_native(bounds: Bounds<GlobalPixels>) -> CGRect {
|
||||
/// screen, with the Y axis pointing downwards (matching CoreGraphics)
|
||||
pub(crate) fn global_bounds_to_ns_rect(bounds: Bounds<GlobalPixels>) -> NSRect {
|
||||
let primary_screen_height = MacDisplay::primary().bounds().size.height;
|
||||
|
||||
CGRect::new(
|
||||
&CGPoint::new(
|
||||
NSRect::new(
|
||||
NSPoint::new(
|
||||
bounds.origin.x.into(),
|
||||
(primary_screen_height - bounds.origin.y - bounds.size.height).into(),
|
||||
),
|
||||
&CGSize::new(bounds.size.width.into(), bounds.size.height.into()),
|
||||
NSSize::new(bounds.size.width.into(), bounds.size.height.into()),
|
||||
)
|
||||
}
|
||||
|
||||
@ -155,8 +152,20 @@ impl PlatformDisplay for MacDisplay {
|
||||
|
||||
fn bounds(&self) -> Bounds<GlobalPixels> {
|
||||
unsafe {
|
||||
let native_bounds = CGDisplayBounds(self.0);
|
||||
display_bounds_from_native(native_bounds)
|
||||
// CGDisplayBounds is in "global display" coordinates, where 0 is
|
||||
// the top left of the primary display.
|
||||
let bounds = CGDisplayBounds(self.0);
|
||||
|
||||
Bounds {
|
||||
origin: point(
|
||||
GlobalPixels(bounds.origin.x as f32),
|
||||
GlobalPixels(bounds.origin.y as f32),
|
||||
),
|
||||
size: size(
|
||||
GlobalPixels(bounds.size.width as f32),
|
||||
GlobalPixels(bounds.size.height as f32),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
|
||||
use super::{global_bounds_from_ns_rect, ns_string, MacDisplay, MetalRenderer, NSRange};
|
||||
use crate::{
|
||||
display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
|
||||
global_bounds_to_ns_rect, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
|
||||
FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||
@ -411,10 +411,8 @@ impl MacWindowState {
|
||||
}
|
||||
|
||||
fn frame(&self) -> Bounds<GlobalPixels> {
|
||||
unsafe {
|
||||
let frame = NSWindow::frame(self.native_window);
|
||||
display_bounds_from_native(mem::transmute::<NSRect, CGRect>(frame))
|
||||
}
|
||||
let frame = unsafe { NSWindow::frame(self.native_window) };
|
||||
global_bounds_from_ns_rect(frame)
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
@ -650,11 +648,11 @@ impl MacWindow {
|
||||
WindowBounds::Fixed(bounds) => {
|
||||
let display_bounds = display.bounds();
|
||||
let frame = if bounds.intersects(&display_bounds) {
|
||||
display_bounds_to_native(bounds)
|
||||
global_bounds_to_ns_rect(bounds)
|
||||
} else {
|
||||
display_bounds_to_native(display_bounds)
|
||||
global_bounds_to_ns_rect(display_bounds)
|
||||
};
|
||||
native_window.setFrame_display_(mem::transmute::<CGRect, NSRect>(frame), YES);
|
||||
native_window.setFrame_display_(frame, YES);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -379,8 +379,11 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CodeLabel {
|
||||
/// The text to display.
|
||||
pub text: String,
|
||||
/// Syntax highlighting runs.
|
||||
pub runs: Vec<(Range<usize>, HighlightId)>,
|
||||
/// The portion of the text that should be used in fuzzy filtering.
|
||||
pub filter_range: Range<usize>,
|
||||
}
|
||||
|
||||
|
@ -34,16 +34,16 @@ use gpui::{
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
||||
point_to_lsp,
|
||||
markdown, point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||
serialize_anchor, serialize_version, split_operations,
|
||||
},
|
||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
|
||||
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
|
||||
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
|
||||
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
|
||||
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName,
|
||||
LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer,
|
||||
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
};
|
||||
use log::error;
|
||||
use lsp::{
|
||||
@ -52,7 +52,7 @@ use lsp::{
|
||||
};
|
||||
use lsp_command::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use postage::watch;
|
||||
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||
use project_settings::{LspSettings, ProjectSettings};
|
||||
@ -4828,6 +4828,170 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_completions(
|
||||
&self,
|
||||
completion_indices: Vec<usize>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
let client = self.client();
|
||||
let language_registry = self.languages().clone();
|
||||
|
||||
let is_remote = self.is_remote();
|
||||
let project_id = self.remote_id();
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let mut did_resolve = false;
|
||||
if is_remote {
|
||||
let project_id =
|
||||
project_id.ok_or_else(|| anyhow!("Remote project without remote_id"))?;
|
||||
|
||||
for completion_index in completion_indices {
|
||||
let completions_guard = completions.read();
|
||||
let completion = &completions_guard[completion_index];
|
||||
if completion.documentation.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
did_resolve = true;
|
||||
let server_id = completion.server_id;
|
||||
let completion = completion.lsp_completion.clone();
|
||||
drop(completions_guard);
|
||||
|
||||
Self::resolve_completion_documentation_remote(
|
||||
project_id,
|
||||
server_id,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
completion,
|
||||
client.clone(),
|
||||
language_registry.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
for completion_index in completion_indices {
|
||||
let completions_guard = completions.read();
|
||||
let completion = &completions_guard[completion_index];
|
||||
if completion.documentation.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let server_id = completion.server_id;
|
||||
let completion = completion.lsp_completion.clone();
|
||||
drop(completions_guard);
|
||||
|
||||
let server = this
|
||||
.read_with(&mut cx, |project, _| {
|
||||
project.language_server_for_id(server_id)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some(server) = server else {
|
||||
continue;
|
||||
};
|
||||
|
||||
did_resolve = true;
|
||||
Self::resolve_completion_documentation_local(
|
||||
server,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
completion,
|
||||
language_registry.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(did_resolve)
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_completion_documentation_local(
|
||||
server: Arc<lsp::LanguageServer>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
completion_index: usize,
|
||||
completion: lsp::CompletionItem,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) {
|
||||
let can_resolve = server
|
||||
.capabilities()
|
||||
.completion_provider
|
||||
.as_ref()
|
||||
.and_then(|options| options.resolve_provider)
|
||||
.unwrap_or(false);
|
||||
if !can_resolve {
|
||||
return;
|
||||
}
|
||||
|
||||
let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
|
||||
let Some(completion_item) = request.await.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(lsp_documentation) = completion_item.documentation {
|
||||
let documentation = language::prepare_completion_documentation(
|
||||
&lsp_documentation,
|
||||
&language_registry,
|
||||
None, // TODO: Try to reasonably work out which language the completion is for
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(documentation);
|
||||
} else {
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(Documentation::Undocumented);
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_completion_documentation_remote(
|
||||
project_id: u64,
|
||||
server_id: LanguageServerId,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
completion_index: usize,
|
||||
completion: lsp::CompletionItem,
|
||||
client: Arc<Client>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) {
|
||||
let request = proto::ResolveCompletionDocumentation {
|
||||
project_id,
|
||||
language_server_id: server_id.0 as u64,
|
||||
lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
|
||||
};
|
||||
|
||||
let Some(response) = client
|
||||
.request(request)
|
||||
.await
|
||||
.context("completion documentation resolve proto request")
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if response.text.is_empty() {
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(Documentation::Undocumented);
|
||||
}
|
||||
|
||||
let documentation = if response.is_markdown {
|
||||
Documentation::MultiLineMarkdown(
|
||||
markdown::parse_markdown(&response.text, &language_registry, None).await,
|
||||
)
|
||||
} else if response.text.lines().count() <= 1 {
|
||||
Documentation::SingleLine(response.text)
|
||||
} else {
|
||||
Documentation::MultiLinePlainText(response.text)
|
||||
};
|
||||
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(documentation);
|
||||
}
|
||||
|
||||
pub fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
buffer_handle: Model<Buffer>,
|
||||
|
@ -599,6 +599,10 @@ impl Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selection_started(&self) -> bool {
|
||||
self.selection_phase == SelectionPhase::Selecting
|
||||
}
|
||||
|
||||
/// Updates the cached process info, returns whether the Zed-relevant info has changed
|
||||
fn update_process_info(&mut self) -> bool {
|
||||
let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
|
||||
|
@ -621,9 +621,17 @@ impl TerminalElement {
|
||||
}
|
||||
|
||||
if e.pressed_button.is_some() && !cx.has_active_drag() {
|
||||
let visibly_contains = interactive_bounds.visibly_contains(&e.position, cx);
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.mouse_drag(e, origin, bounds);
|
||||
cx.notify();
|
||||
if !terminal.selection_started() {
|
||||
if visibly_contains {
|
||||
terminal.mouse_drag(e, origin, bounds);
|
||||
cx.notify();
|
||||
}
|
||||
} else {
|
||||
terminal.mouse_drag(e, origin, bounds);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ async-trait = { workspace = true, "optional" = true }
|
||||
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
|
||||
tokio = { version = "1.15", "optional" = true }
|
||||
serde_json.workspace = true
|
||||
regex.workspace = true
|
||||
|
||||
collections = { path = "../collections" }
|
||||
command_palette = { path = "../command_palette" }
|
||||
|
36
crates/vim/README.md
Normal file
36
crates/vim/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This contains the code for Zed's Vim emulation mode.
|
||||
|
||||
Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. This means Zed will never be 100% vim compatible, but should be 100% vim familiar!
|
||||
|
||||
The backlog is maintained in the `#vim` channel notes.
|
||||
|
||||
## Testing against Neovim
|
||||
|
||||
If you are making a change to make Zed's behaviour more closely match vim/nvim, you can create a test using the `NeovimBackedTestContext`.
|
||||
|
||||
For example, the following test checks that Zed and Neovim have the same behaviour when running `*` in visual mode:
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
|
||||
cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await;
|
||||
cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await;
|
||||
}
|
||||
```
|
||||
|
||||
To keep CI runs fast, by default the neovim tests use a cached JSON file that records what neovim did (see crates/vim/test_data),
|
||||
but while developing this test you'll need to run it with the neovim flag enabled:
|
||||
|
||||
```
|
||||
cargo test -p vim --features neovim test_visual_star_hash
|
||||
```
|
||||
|
||||
This will run your keystrokes against a headless neovim and cache the results in the test_data directory.
|
||||
|
||||
|
||||
## Testing zed-only behaviour
|
||||
|
||||
Zed does more than vim/neovim in their default modes. The `VimTestContext` can be used instead. This lets you test integration with the language server and other parts of zed's UI that don't have a NeoVim equivalent.
|
@ -91,7 +91,6 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
|
||||
|
||||
if query.is_empty() {
|
||||
search_bar.set_replacement(None, cx);
|
||||
search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
|
||||
search_bar.activate_search_mode(SearchMode::Regex, cx);
|
||||
}
|
||||
vim.workspace_state.search = SearchState {
|
||||
@ -149,15 +148,19 @@ pub fn move_to_internal(
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
let search = search_bar.update(cx, |search_bar, cx| {
|
||||
let mut options = SearchOptions::CASE_SENSITIVE;
|
||||
options.set(SearchOptions::WHOLE_WORD, whole_word);
|
||||
if search_bar.show(cx) {
|
||||
search_bar
|
||||
.query_suggestion(cx)
|
||||
.map(|query| search_bar.search(&query, Some(options), cx))
|
||||
} else {
|
||||
None
|
||||
let options = SearchOptions::CASE_SENSITIVE;
|
||||
if !search_bar.show(cx) {
|
||||
return None;
|
||||
}
|
||||
let Some(query) = search_bar.query_suggestion(cx) else {
|
||||
return None;
|
||||
};
|
||||
let mut query = regex::escape(&query);
|
||||
if whole_word {
|
||||
query = format!(r"\b{}\b", query);
|
||||
}
|
||||
search_bar.activate_search_mode(SearchMode::Regex, cx);
|
||||
Some(search_bar.search(&query, Some(options), cx))
|
||||
});
|
||||
|
||||
if let Some(search) = search {
|
||||
@ -350,7 +353,10 @@ mod test {
|
||||
use editor::DisplayPoint;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
|
||||
@ -474,4 +480,13 @@ mod test {
|
||||
cx.simulate_keystrokes(["shift-enter"]);
|
||||
cx.assert_editor_state("«oneˇ» one one one");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
|
||||
cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await;
|
||||
cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await;
|
||||
}
|
||||
}
|
||||
|
6
crates/vim/test_data/test_visual_star_hash.json
Normal file
6
crates/vim/test_data/test_visual_star_hash.json
Normal file
@ -0,0 +1,6 @@
|
||||
{"Put":{"state":"ˇa.c. abcd a.c. abcd"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"3"}
|
||||
{"Key":"l"}
|
||||
{"Key":"*"}
|
||||
{"Get":{"state":"a.c. abcd ˇa.c. abcd","mode":"Normal"}}
|
@ -112,18 +112,22 @@ impl Render for Toolbar {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.when(has_left_items, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.flex_auto()
|
||||
.justify_start()
|
||||
.overflow_x_hidden()
|
||||
.children(self.left_items().map(|item| item.to_any())),
|
||||
)
|
||||
})
|
||||
.when(has_right_items, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
// We're using `flex_none` here to prevent some flickering that can occur when the
|
||||
// size of the left items container changes.
|
||||
.flex_none()
|
||||
.justify_end()
|
||||
.children(self.right_items().map(|item| item.to_any())),
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user