Merge branch 'main' into breadcrumbs

This commit is contained in:
Antonio Scandurra 2022-03-29 10:05:05 +02:00
commit d7026c2228
79 changed files with 7650 additions and 3174 deletions

35
Cargo.lock generated
View file

@ -1617,6 +1617,7 @@ dependencies = [
"collections",
"ctor",
"env_logger",
"futures",
"fuzzy",
"gpui",
"itertools",
@ -1629,6 +1630,7 @@ dependencies = [
"postage",
"project",
"rand 0.8.3",
"rpc",
"serde",
"smallvec",
"smol",
@ -1782,7 +1784,9 @@ dependencies = [
name = "file_finder"
version = "0.1.0"
dependencies = [
"ctor",
"editor",
"env_logger",
"fuzzy",
"gpui",
"postage",
@ -2524,6 +2528,15 @@ dependencies = [
"hashbrown 0.9.1",
]
[[package]]
name = "indoc"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e"
dependencies = [
"unindent",
]
[[package]]
name = "infer"
version = "0.2.3"
@ -5550,9 +5563,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "unindent"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8"
[[package]]
name = "universal-hash"
@ -5670,6 +5683,21 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "vim"
version = "0.1.0"
dependencies = [
"collections",
"editor",
"gpui",
"indoc",
"language",
"log",
"project",
"util",
"workspace",
]
[[package]]
name = "waker-fn"
version = "1.1.0"
@ -5929,7 +5957,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "zed"
version = "0.21.0"
version = "0.23.0"
dependencies = [
"anyhow",
"async-compression",
@ -6000,6 +6028,7 @@ dependencies = [
"unindent",
"url",
"util",
"vim",
"workspace",
]

View file

@ -64,13 +64,13 @@ impl ChatPanel {
ix,
item_type,
is_hovered,
&cx.app_state::<Settings>().theme.chat_panel.channel_select,
&cx.global::<Settings>().theme.chat_panel.channel_select,
cx,
)
}
})
.with_style(move |cx| {
let theme = &cx.app_state::<Settings>().theme.chat_panel.channel_select;
let theme = &cx.global::<Settings>().theme.chat_panel.channel_select;
SelectStyle {
header: theme.header.container.clone(),
menu: theme.menu.clone(),
@ -200,7 +200,7 @@ impl ChatPanel {
}
fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.app_state::<Settings>().theme;
let theme = &cx.global::<Settings>().theme;
Flex::column()
.with_child(
Container::new(ChildView::new(&self.channel_select).boxed())
@ -224,7 +224,7 @@ impl ChatPanel {
fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
let now = OffsetDateTime::now_utc();
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
let theme = if message.is_pending() {
&settings.theme.chat_panel.pending_message
} else {
@ -267,7 +267,7 @@ impl ChatPanel {
}
fn render_input_box(&self, cx: &AppContext) -> ElementBox {
let theme = &cx.app_state::<Settings>().theme;
let theme = &cx.global::<Settings>().theme;
Container::new(ChildView::new(&self.input_editor).boxed())
.with_style(theme.chat_panel.input_editor.container)
.boxed()
@ -304,7 +304,7 @@ impl ChatPanel {
}
fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.app_state::<Settings>().theme.clone();
let theme = cx.global::<Settings>().theme.clone();
let rpc = self.rpc.clone();
let this = cx.handle();
@ -327,7 +327,12 @@ impl ChatPanel {
let rpc = rpc.clone();
let this = this.clone();
cx.spawn(|mut cx| async move {
if rpc.authenticate_and_connect(&cx).log_err().await.is_some() {
if rpc
.authenticate_and_connect(true, &cx)
.log_err()
.await
.is_some()
{
cx.update(|cx| {
if let Some(this) = this.upgrade(cx) {
if this.is_focused(cx) {
@ -385,7 +390,7 @@ impl View for ChatPanel {
} else {
self.render_sign_in_prompt(cx)
};
let theme = &cx.app_state::<Settings>().theme;
let theme = &cx.global::<Settings>().theme;
ConstrainedBox::new(
Container::new(element)
.with_style(theme.chat_panel.container)

View file

@ -181,7 +181,7 @@ impl Entity for Channel {
impl Channel {
pub fn init(rpc: &Arc<Client>) {
rpc.add_entity_message_handler(Self::handle_message_sent);
rpc.add_model_message_handler(Self::handle_message_sent);
}
pub fn new(

View file

@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{
};
use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
use gpui::{
action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task,
action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
};
use http::HttpClient;
use lazy_static::lazy_static;
@ -45,7 +45,7 @@ pub use user::*;
lazy_static! {
static ref ZED_SERVER_URL: String =
std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string());
static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
}
@ -55,7 +55,7 @@ action!(Authenticate);
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action(move |_: &Authenticate, cx| {
let rpc = rpc.clone();
cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await })
cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await })
.detach();
});
}
@ -136,26 +136,37 @@ impl Status {
struct ClientState {
credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
_reconnect_task: Option<Task<()>>,
reconnect_interval: Duration,
models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>,
entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
model_types_by_message_type: HashMap<TypeId, TypeId>,
entity_types_by_message_type: HashMap<TypeId, TypeId>,
message_handlers: HashMap<
TypeId,
Arc<
dyn Send
+ Sync
+ Fn(
AnyModelHandle,
AnyEntityHandle,
Box<dyn AnyTypedEnvelope>,
&Arc<Client>,
AsyncAppContext,
) -> LocalBoxFuture<'static, Result<()>>,
>,
>,
}
enum AnyWeakEntityHandle {
Model(AnyWeakModelHandle),
View(AnyWeakViewHandle),
}
enum AnyEntityHandle {
Model(AnyModelHandle),
View(AnyViewHandle),
}
#[derive(Clone, Debug)]
pub struct Credentials {
pub user_id: u64,
@ -171,8 +182,8 @@ impl Default for ClientState {
_reconnect_task: None,
reconnect_interval: Duration::from_secs(5),
models_by_message_type: Default::default(),
models_by_entity_type_and_remote_id: Default::default(),
model_types_by_message_type: Default::default(),
entities_by_type_and_remote_id: Default::default(),
entity_types_by_message_type: Default::default(),
message_handlers: Default::default(),
}
}
@ -195,13 +206,13 @@ impl Drop for Subscription {
Subscription::Entity { client, id } => {
if let Some(client) = client.upgrade() {
let mut state = client.state.write();
let _ = state.models_by_entity_type_and_remote_id.remove(id);
let _ = state.entities_by_type_and_remote_id.remove(id);
}
}
Subscription::Message { client, id } => {
if let Some(client) = client.upgrade() {
let mut state = client.state.write();
let _ = state.model_types_by_message_type.remove(id);
let _ = state.entity_types_by_message_type.remove(id);
let _ = state.message_handlers.remove(id);
}
}
@ -239,7 +250,7 @@ impl Client {
state._reconnect_task.take();
state.message_handlers.clear();
state.models_by_message_type.clear();
state.models_by_entity_type_and_remote_id.clear();
state.entities_by_type_and_remote_id.clear();
state.entity_id_extractors.clear();
self.peer.reset();
}
@ -291,7 +302,7 @@ impl Client {
state._reconnect_task = Some(cx.spawn(|cx| async move {
let mut rng = StdRng::from_entropy();
let mut delay = Duration::from_millis(100);
while let Err(error) = this.authenticate_and_connect(&cx).await {
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
log::error!("failed to connect {}", error);
this.set_status(
Status::ReconnectionError {
@ -313,17 +324,32 @@ impl Client {
}
}
pub fn add_view_for_remote_entity<T: View>(
self: &Arc<Self>,
remote_id: u64,
cx: &mut ViewContext<T>,
) -> Subscription {
let id = (TypeId::of::<T>(), remote_id);
self.state
.write()
.entities_by_type_and_remote_id
.insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into()));
Subscription::Entity {
client: Arc::downgrade(self),
id,
}
}
pub fn add_model_for_remote_entity<T: Entity>(
self: &Arc<Self>,
remote_id: u64,
cx: &mut ModelContext<T>,
) -> Subscription {
let handle = AnyModelHandle::from(cx.handle());
let mut state = self.state.write();
let id = (TypeId::of::<T>(), remote_id);
state
.models_by_entity_type_and_remote_id
.insert(id, handle.downgrade());
self.state
.write()
.entities_by_type_and_remote_id
.insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into()));
Subscription::Entity {
client: Arc::downgrade(self),
id,
@ -346,7 +372,6 @@ impl Client {
{
let message_type_id = TypeId::of::<M>();
let client = Arc::downgrade(self);
let mut state = self.state.write();
state
.models_by_message_type
@ -354,14 +379,15 @@ impl Client {
let prev_handler = state.message_handlers.insert(
message_type_id,
Arc::new(move |handle, envelope, cx| {
Arc::new(move |handle, envelope, client, cx| {
let handle = if let AnyEntityHandle::Model(handle) = handle {
handle
} else {
unreachable!();
};
let model = handle.downcast::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
if let Some(client) = client.upgrade() {
handler(model, *envelope, client.clone(), cx).boxed_local()
} else {
async move { Ok(()) }.boxed_local()
}
}),
);
if prev_handler.is_some() {
@ -374,7 +400,26 @@ impl Client {
}
}
pub fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
E: View,
H: 'static
+ Send
+ Sync
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
if let AnyEntityHandle::View(handle) = handle {
handler(handle.downcast::<E>().unwrap(), message, client, cx)
} else {
unreachable!();
}
})
}
pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
E: Entity,
@ -383,38 +428,51 @@ impl Client {
+ Sync
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
if let AnyEntityHandle::Model(handle) = handle {
handler(handle.downcast::<E>().unwrap(), message, client, cx)
} else {
unreachable!();
}
})
}
fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
E: Entity,
H: 'static
+ Send
+ Sync
+ Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
{
let model_type_id = TypeId::of::<E>();
let message_type_id = TypeId::of::<M>();
let client = Arc::downgrade(self);
let mut state = self.state.write();
state
.model_types_by_message_type
.entity_types_by_message_type
.insert(message_type_id, model_type_id);
state
.entity_id_extractors
.entry(message_type_id)
.or_insert_with(|| {
Box::new(|envelope| {
let envelope = envelope
|envelope| {
envelope
.as_any()
.downcast_ref::<TypedEnvelope<M>>()
.unwrap();
envelope.payload.remote_entity_id()
})
.unwrap()
.payload
.remote_entity_id()
}
});
let prev_handler = state.message_handlers.insert(
message_type_id,
Arc::new(move |handle, envelope, cx| {
let model = handle.downcast::<E>().unwrap();
Arc::new(move |handle, envelope, client, cx| {
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
if let Some(client) = client.upgrade() {
handler(model, *envelope, client.clone(), cx).boxed_local()
} else {
async move { Ok(()) }.boxed_local()
}
handler(handle, *envelope, client.clone(), cx).boxed_local()
}),
);
if prev_handler.is_some() {
@ -422,7 +480,7 @@ impl Client {
}
}
pub fn add_entity_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage + RequestMessage,
E: Entity,
@ -432,10 +490,39 @@ impl Client {
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_entity_message_handler(move |model, envelope, client, cx| {
let receipt = envelope.receipt();
let response = handler(model, envelope, client.clone(), cx);
async move {
self.add_model_message_handler(move |entity, envelope, client, cx| {
Self::respond_to_request::<M, _>(
envelope.receipt(),
handler(entity, envelope, client.clone(), cx),
client,
)
})
}
pub fn add_view_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage + RequestMessage,
E: View,
H: 'static
+ Send
+ Sync
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_view_message_handler(move |entity, envelope, client, cx| {
Self::respond_to_request::<M, _>(
envelope.receipt(),
handler(entity, envelope, client.clone(), cx),
client,
)
})
}
async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
receipt: Receipt<T>,
response: F,
client: Arc<Self>,
) -> Result<()> {
match response.await {
Ok(response) => {
client.respond(receipt, response)?;
@ -452,8 +539,6 @@ impl Client {
}
}
}
})
}
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
read_credentials_from_keychain(cx).is_some()
@ -462,6 +547,7 @@ impl Client {
#[async_recursion(?Send)]
pub async fn authenticate_and_connect(
self: &Arc<Self>,
try_keychain: bool,
cx: &AsyncAppContext,
) -> anyhow::Result<()> {
let was_disconnected = match *self.status().borrow() {
@ -483,23 +569,22 @@ impl Client {
self.set_status(Status::Reauthenticating, cx)
}
let mut used_keychain = false;
let credentials = self.state.read().credentials.clone();
let credentials = if let Some(credentials) = credentials {
credentials
} else if let Some(credentials) = read_credentials_from_keychain(cx) {
used_keychain = true;
credentials
} else {
let credentials = match self.authenticate(&cx).await {
let mut read_from_keychain = false;
let mut credentials = self.state.read().credentials.clone();
if credentials.is_none() && try_keychain {
credentials = read_credentials_from_keychain(cx);
read_from_keychain = credentials.is_some();
}
if credentials.is_none() {
credentials = Some(match self.authenticate(&cx).await {
Ok(credentials) => credentials,
Err(err) => {
self.set_status(Status::ConnectionError, cx);
return Err(err);
}
};
credentials
};
});
}
let credentials = credentials.unwrap();
if was_disconnected {
self.set_status(Status::Connecting, cx);
@ -510,7 +595,7 @@ impl Client {
match self.establish_connection(&credentials, cx).await {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !used_keychain && IMPERSONATE_LOGIN.is_none() {
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
write_credentials_to_keychain(&credentials, cx).log_err();
}
self.set_connection(conn, cx).await;
@ -518,10 +603,10 @@ impl Client {
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if used_keychain {
if read_from_keychain {
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(cx).await
self.authenticate_and_connect(false, cx).await
} else {
self.set_status(Status::ConnectionError, cx);
Err(EstablishConnectionError::Unauthorized)?
@ -561,24 +646,26 @@ impl Client {
.models_by_message_type
.get(&payload_type_id)
.and_then(|model| model.upgrade(&cx))
.map(AnyEntityHandle::Model)
.or_else(|| {
let model_type_id =
*state.model_types_by_message_type.get(&payload_type_id)?;
let entity_type_id =
*state.entity_types_by_message_type.get(&payload_type_id)?;
let entity_id = state
.entity_id_extractors
.get(&message.payload_type_id())
.map(|extract_entity_id| {
(extract_entity_id)(message.as_ref())
})?;
let model = state
.models_by_entity_type_and_remote_id
.get(&(model_type_id, entity_id))?;
if let Some(model) = model.upgrade(&cx) {
Some(model)
let entity = state
.entities_by_type_and_remote_id
.get(&(entity_type_id, entity_id))?;
if let Some(entity) = entity.upgrade(&cx) {
Some(entity)
} else {
state
.models_by_entity_type_and_remote_id
.remove(&(model_type_id, entity_id));
.entities_by_type_and_remote_id
.remove(&(entity_type_id, entity_id));
None
}
});
@ -593,7 +680,7 @@ impl Client {
if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned()
{
drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
let future = handler(model, message, cx.clone());
let future = handler(model, message, &this, cx.clone());
let client_id = this.id;
log::debug!(
@ -891,6 +978,15 @@ impl Client {
}
}
impl AnyWeakEntityHandle {
fn upgrade(&self, cx: &AsyncAppContext) -> Option<AnyEntityHandle> {
match self {
AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
}
}
}
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
if IMPERSONATE_LOGIN.is_some() {
return None;
@ -994,7 +1090,7 @@ mod tests {
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
client.add_entity_message_handler(
client.add_model_message_handler(
move |model: ModelHandle<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| {
match model.read_with(&cx, |model, _| model.id) {
1 => done_tx1.try_send(()).unwrap(),

View file

@ -91,7 +91,7 @@ impl FakeServer {
});
client
.authenticate_and_connect(&cx.to_async())
.authenticate_and_connect(false, &cx.to_async())
.await
.unwrap();
server

View file

@ -55,7 +55,7 @@ impl ContactsPanel {
app_state: Arc<AppState>,
cx: &mut LayoutContext,
) -> ElementBox {
let theme = cx.app_state::<Settings>().theme.clone();
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.contacts_panel;
let project_count = collaborator.projects.len();
let font_cache = cx.font_cache();
@ -236,7 +236,7 @@ impl View for ContactsPanel {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.app_state::<Settings>().theme.contacts_panel;
let theme = &cx.global::<Settings>().theme.contacts_panel;
Container::new(List::new(self.contacts.clone()).boxed())
.with_style(theme.container)
.boxed()

View file

@ -25,7 +25,7 @@ use std::{
sync::Arc,
};
use util::TryFutureExt;
use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Settings, Workspace};
use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace};
action!(Deploy);
@ -38,12 +38,8 @@ pub fn init(cx: &mut MutableAppContext) {
type Event = editor::Event;
struct ProjectDiagnostics {
project: ModelHandle<Project>,
}
struct ProjectDiagnosticsEditor {
model: ModelHandle<ProjectDiagnostics>,
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
editor: ViewHandle<Editor>,
summary: DiagnosticSummary,
@ -65,16 +61,6 @@ struct DiagnosticGroupState {
block_count: usize,
}
impl ProjectDiagnostics {
fn new(project: ModelHandle<Project>) -> Self {
Self { project }
}
}
impl Entity for ProjectDiagnostics {
type Event = ();
}
impl Entity for ProjectDiagnosticsEditor {
type Event = Event;
}
@ -86,7 +72,7 @@ impl View for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.path_states.is_empty() {
let theme = &cx.app_state::<Settings>().theme.project_diagnostics;
let theme = &cx.global::<Settings>().theme.project_diagnostics;
Label::new(
"No problems in workspace".to_string(),
theme.empty_message.clone(),
@ -109,12 +95,11 @@ impl View for ProjectDiagnosticsEditor {
impl ProjectDiagnosticsEditor {
fn new(
model: ModelHandle<ProjectDiagnostics>,
project_handle: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let project = model.read(cx).project.clone();
cx.subscribe(&project, |this, _, event, cx| match event {
cx.subscribe(&project_handle, |this, _, event, cx| match event {
project::Event::DiskBasedDiagnosticsFinished => {
this.update_excerpts(cx);
this.update_title(cx);
@ -126,20 +111,22 @@ impl ProjectDiagnosticsEditor {
})
.detach();
let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(excerpts.clone(), Some(project.clone()), cx);
let mut editor =
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});
cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
.detach();
let project = project.read(cx);
let project = project_handle.read(cx);
let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
let summary = project.diagnostic_summary(cx);
let mut this = Self {
model,
summary: project.diagnostic_summary(cx),
project: project_handle,
summary,
workspace,
excerpts,
editor,
@ -151,18 +138,20 @@ impl ProjectDiagnosticsEditor {
}
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
workspace.activate_item(&existing, cx);
} else {
let diagnostics =
cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
workspace.open_item(diagnostics, cx);
let workspace_handle = cx.weak_handle();
let diagnostics = cx.add_view(|cx| {
ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
});
workspace.add_item(Box::new(diagnostics), cx);
}
}
fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
let paths = mem::take(&mut self.paths_to_update);
let project = self.model.read(cx).project.clone();
let project = self.project.clone();
cx.spawn(|this, mut cx| {
async move {
for path in paths {
@ -289,7 +278,7 @@ impl ProjectDiagnosticsEditor {
prev_excerpt_id = excerpt_id.clone();
first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
group_state.excerpts.push(excerpt_id.clone());
let header_position = (excerpt_id.clone(), language::Anchor::min());
let header_position = (excerpt_id.clone(), language::Anchor::MIN);
if is_first_excerpt_for_group {
is_first_excerpt_for_group = false;
@ -378,8 +367,7 @@ impl ProjectDiagnosticsEditor {
range_a
.start
.cmp(&range_b.start, &snapshot)
.unwrap()
.then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
.then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
});
if path_state.diagnostic_groups.is_empty() {
@ -443,42 +431,17 @@ impl ProjectDiagnosticsEditor {
}
fn update_title(&mut self, cx: &mut ViewContext<Self>) {
self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx);
self.summary = self.project.read(cx).diagnostic_summary(cx);
cx.emit(Event::TitleChanged);
}
}
impl workspace::Item for ProjectDiagnostics {
type View = ProjectDiagnosticsEditor;
fn build_view(
handle: ModelHandle<Self>,
workspace: &Workspace,
nav_history: ItemNavHistory,
cx: &mut ViewContext<Self::View>,
) -> Self::View {
let diagnostics = ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), cx);
diagnostics
.editor
.update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
diagnostics
}
fn project_path(&self) -> Option<project::ProjectPath> {
None
}
}
impl workspace::ItemView for ProjectDiagnosticsEditor {
fn item(&self, _: &AppContext) -> Box<dyn ItemHandle> {
Box::new(self.model.clone())
}
impl workspace::Item for ProjectDiagnosticsEditor {
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
render_summary(
&self.summary,
&style.label.text,
&cx.app_state::<Settings>().theme.project_diagnostics,
&cx.global::<Settings>().theme.project_diagnostics,
)
}
@ -486,9 +449,13 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
None
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
None
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, cx));
.update(cx, |editor, cx| editor.navigate(data, cx))
}
fn is_dirty(&self, cx: &AppContext) -> bool {
@ -532,20 +499,21 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
}
fn clone_on_split(
&self,
nav_history: ItemNavHistory,
cx: &mut ViewContext<Self>,
) -> Option<Self>
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
let diagnostics =
ProjectDiagnosticsEditor::new(self.model.clone(), self.workspace.clone(), cx);
diagnostics.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
Some(diagnostics)
Some(ProjectDiagnosticsEditor::new(
self.project.clone(),
self.workspace.clone(),
cx,
))
}
fn act_as_type(
@ -571,7 +539,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
Arc::new(move |cx| {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
let theme = &settings.theme.editor;
let style = &theme.diagnostic_header;
let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
@ -829,9 +797,8 @@ mod tests {
});
// Open the project diagnostics view while there are already diagnostics.
let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
let view = cx.add_view(0, |cx| {
ProjectDiagnosticsEditor::new(model, workspace.downgrade(), cx)
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
view.next_notification(&cx).await;

View file

@ -49,7 +49,7 @@ impl View for DiagnosticSummary {
let in_progress = self.in_progress;
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
let theme = &cx.app_state::<Settings>().theme.project_diagnostics;
let theme = &cx.global::<Settings>().theme.project_diagnostics;
if in_progress {
Label::new(
"Checking... ".to_string(),
@ -71,7 +71,7 @@ impl View for DiagnosticSummary {
impl StatusItemView for DiagnosticSummary {
fn set_active_pane_item(
&mut self,
_: Option<&dyn workspace::ItemViewHandle>,
_: Option<&dyn workspace::ItemHandle>,
_: &mut ViewContext<Self>,
) {
}

View file

@ -27,6 +27,7 @@ gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
project = { path = "../project" }
rpc = { path = "../rpc" }
snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" }
theme = { path = "../theme" }
@ -34,6 +35,7 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"
futures = "0.3"
itertools = "0.10"
lazy_static = "1.4"
log = "0.4"

View file

@ -12,7 +12,7 @@ use gpui::{
Entity, ModelContext, ModelHandle,
};
use language::{Point, Subscription as BufferSubscription};
use std::{any::TypeId, ops::Range, sync::Arc};
use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
use wrap_map::WrapMap;
@ -36,6 +36,7 @@ pub struct DisplayMap {
wrap_map: ModelHandle<WrapMap>,
block_map: BlockMap,
text_highlights: TextHighlights,
pub clip_at_line_ends: bool,
}
impl Entity for DisplayMap {
@ -67,6 +68,7 @@ impl DisplayMap {
wrap_map,
block_map,
text_highlights: Default::default(),
clip_at_line_ends: false,
}
}
@ -87,6 +89,7 @@ impl DisplayMap {
wraps_snapshot,
blocks_snapshot,
text_highlights: self.text_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
}
}
@ -114,6 +117,7 @@ impl DisplayMap {
pub fn unfold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
@ -124,7 +128,7 @@ impl DisplayMap {
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
let (snapshot, edits) = self
.wrap_map
@ -204,6 +208,7 @@ pub struct DisplaySnapshot {
wraps_snapshot: wrap_map::WrapSnapshot,
blocks_snapshot: block_map::BlockSnapshot,
text_highlights: TextHighlights,
clip_at_line_ends: bool,
}
impl DisplaySnapshot {
@ -331,7 +336,12 @@ impl DisplaySnapshot {
}
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
DisplayPoint(self.blocks_snapshot.clip_point(point.0, bias))
let mut clipped = self.blocks_snapshot.clip_point(point.0, bias);
if self.clip_at_line_ends && clipped.column == self.line_len(clipped.row) {
clipped.column = clipped.column.saturating_sub(1);
clipped = self.blocks_snapshot.clip_point(clipped, Bias::Left);
}
DisplayPoint(clipped)
}
pub fn folds_in_range<'a, T>(
@ -414,9 +424,19 @@ impl DisplaySnapshot {
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct DisplayPoint(BlockPoint);
impl Debug for DisplayPoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"DisplayPoint({}, {})",
self.row(),
self.column()
))
}
}
impl DisplayPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(BlockPoint(Point::new(row, column)))
@ -426,7 +446,6 @@ impl DisplayPoint {
Self::new(0, 0)
}
#[cfg(test)]
pub fn is_zero(&self) -> bool {
self.0.is_zero()
}
@ -478,16 +497,16 @@ impl ToDisplayPoint for Anchor {
}
#[cfg(test)]
mod tests {
pub mod tests {
use super::*;
use crate::movement;
use crate::{movement, test::marked_display_snapshot};
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
use rand::{prelude::*, Rng};
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use theme::SyntaxTheme;
use util::test::sample_text;
use util::test::{marked_text_ranges, sample_text};
use Bias::*;
#[gpui::test(iterations = 100)]
@ -620,7 +639,7 @@ mod tests {
if rng.gen() && fold_count > 0 {
log::info!("unfolding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.unfold(ranges, cx);
map.unfold(ranges, true, cx);
});
} else {
log::info!("folding ranges: {:?}", ranges);
@ -705,7 +724,7 @@ mod tests {
log::info!("Moving from point {:?}", point);
let moved_right = movement::right(&snapshot, point).unwrap();
let moved_right = movement::right(&snapshot, point);
log::info!("Right {:?}", moved_right);
if point < max_point {
assert!(moved_right > point);
@ -719,7 +738,7 @@ mod tests {
assert_eq!(moved_right, point);
}
let moved_left = movement::left(&snapshot, point).unwrap();
let moved_left = movement::left(&snapshot, point);
log::info!("Left {:?}", moved_left);
if point > min_point {
assert!(moved_left < point);
@ -777,15 +796,15 @@ mod tests {
DisplayPoint::new(1, 0)
);
assert_eq!(
movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
movement::right(&snapshot, DisplayPoint::new(0, 7)),
DisplayPoint::new(1, 0)
);
assert_eq!(
movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(),
movement::left(&snapshot, DisplayPoint::new(1, 0)),
DisplayPoint::new(0, 7)
);
assert_eq!(
movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None).unwrap(),
movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None),
(DisplayPoint::new(0, 7), SelectionGoal::Column(10))
);
assert_eq!(
@ -793,8 +812,7 @@ mod tests {
&snapshot,
DisplayPoint::new(0, 7),
SelectionGoal::Column(10)
)
.unwrap(),
),
(DisplayPoint::new(1, 10), SelectionGoal::Column(10))
);
assert_eq!(
@ -802,8 +820,7 @@ mod tests {
&snapshot,
DisplayPoint::new(1, 10),
SelectionGoal::Column(10)
)
.unwrap(),
),
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
);
@ -922,7 +939,7 @@ mod tests {
let map = cx
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
assert_eq!(
cx.update(|cx| chunks(0..5, &map, &theme, cx)),
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
vec![
("fn ".to_string(), None),
("outer".to_string(), Some(Color::blue())),
@ -933,7 +950,7 @@ mod tests {
]
);
assert_eq!(
cx.update(|cx| chunks(3..5, &map, &theme, cx)),
cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
vec![
(" fn ".to_string(), Some(Color::red())),
("inner".to_string(), Some(Color::blue())),
@ -945,7 +962,7 @@ mod tests {
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
});
assert_eq!(
cx.update(|cx| chunks(0..2, &map, &theme, cx)),
cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
vec![
("fn ".to_string(), None),
("out".to_string(), Some(Color::blue())),
@ -1011,7 +1028,7 @@ mod tests {
DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
});
assert_eq!(
cx.update(|cx| chunks(0..5, &map, &theme, cx)),
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
[
("fn \n".to_string(), None),
("oute\nr".to_string(), Some(Color::blue())),
@ -1019,7 +1036,7 @@ mod tests {
]
);
assert_eq!(
cx.update(|cx| chunks(3..5, &map, &theme, cx)),
cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
[("{}\n\n".to_string(), None)]
);
@ -1027,7 +1044,7 @@ mod tests {
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
});
assert_eq!(
cx.update(|cx| chunks(1..4, &map, &theme, cx)),
cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
[
("out".to_string(), Some(Color::blue())),
("\n".to_string(), None),
@ -1038,50 +1055,151 @@ mod tests {
}
#[gpui::test]
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
use Bias::{Left, Right};
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n";
let buffer = MultiBuffer::build_simple(text, cx);
let theme = SyntaxTheme::new(vec![
("operator".to_string(), Color::red().into()),
("string".to_string(), Color::green().into()),
]);
let language = Arc::new(
Language::new(
LanguageConfig {
name: "Test".into(),
path_suffixes: vec![".test".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_highlights_query(
r#"
":" @operator
(string_literal) @string
"#,
)
.unwrap(),
);
language.set_theme(&theme);
let (text, highlighted_ranges) = marked_text_ranges(r#"const[] [a]: B = "c [d]""#);
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let tab_size = 4;
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
});
let map = map.update(cx, |map, cx| map.snapshot(cx));
let font_size = 16.0;
let map = cx
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
enum MyType {}
let style = HighlightStyle {
color: Some(Color::blue()),
..Default::default()
};
map.update(cx, |map, _cx| {
map.highlight_text(
TypeId::of::<MyType>(),
highlighted_ranges
.into_iter()
.map(|range| {
buffer_snapshot.anchor_before(range.start)
..buffer_snapshot.anchor_before(range.end)
})
.collect(),
style,
);
});
assert_eq!(map.text(), display_text);
for (input_column, bias, output_column) in vec![
("'a', '".len(), Left, "'a', '".len()),
("'a', '".len() + 1, Left, "'a', '".len()),
("'a', '".len() + 1, Right, "'a', 'α".len()),
("'a', 'α', ".len(), Left, "'a', 'α',".len()),
("'a', 'α', ".len(), Right, "'a', 'α', ".len()),
("'a', 'α', '".len() + 1, Left, "'a', 'α', '".len()),
("'a', 'α', '".len() + 1, Right, "'a', 'α', '✋".len()),
("'a', 'α', '✋',".len(), Right, "'a', 'α', '✋',".len()),
("'a', 'α', '✋', ".len(), Left, "'a', 'α', '✋',".len()),
(
"'a', 'α', '✋', ".len(),
Right,
"'a', 'α', '✋', ".len(),
),
] {
assert_eq!(
map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
DisplayPoint::new(1, output_column as u32),
"clip_point(({}, {}))",
1,
input_column,
cx.update(|cx| chunks(0..10, &map, &theme, cx)),
[
("const ".to_string(), None, None),
("a".to_string(), None, Some(Color::blue())),
(":".to_string(), Some(Color::red()), None),
(" B = ".to_string(), None, None),
("\"c ".to_string(), Some(Color::green()), None),
("d".to_string(), Some(Color::green()), Some(Color::blue())),
("\"".to_string(), Some(Color::green()), None),
]
);
}
#[gpui::test]
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
match bias {
Bias::Left => {
if shift_right {
*markers[1].column_mut() += 1;
}
assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
}
Bias::Right => {
if shift_right {
*markers[0].column_mut() += 1;
}
assert_eq!(
unmarked_snapshot.clip_point(dbg!(markers[0]), bias),
markers[1]
)
}
};
}
use Bias::{Left, Right};
assert("||α", false, Left, cx);
assert("||α", true, Left, cx);
assert("||α", false, Right, cx);
assert("|α|", true, Right, cx);
assert("||✋", false, Left, cx);
assert("||✋", true, Left, cx);
assert("||✋", false, Right, cx);
assert("|✋|", true, Right, cx);
assert("||🍐", false, Left, cx);
assert("||🍐", true, Left, cx);
assert("||🍐", false, Right, cx);
assert("|🍐|", true, Right, cx);
assert("||\t", false, Left, cx);
assert("||\t", true, Left, cx);
assert("||\t", false, Right, cx);
assert("|\t|", true, Right, cx);
assert(" ||\t", false, Left, cx);
assert(" ||\t", true, Left, cx);
assert(" ||\t", false, Right, cx);
assert(" |\t|", true, Right, cx);
assert(" ||\t", false, Left, cx);
assert(" ||\t", false, Right, cx);
}
#[gpui::test]
fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
unmarked_snapshot.clip_at_line_ends = true;
assert_eq!(
unmarked_snapshot.clip_point(markers[1], Bias::Left),
markers[0]
);
}
assert("||", cx);
assert("|a|", cx);
assert("a|b|", cx);
assert("a|α|", cx);
}
#[gpui::test]
@ -1163,27 +1281,38 @@ mod tests {
)
}
fn chunks<'a>(
fn syntax_chunks<'a>(
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,
theme: &'a SyntaxTheme,
cx: &mut MutableAppContext,
) -> Vec<(String, Option<Color>)> {
chunks(rows, map, theme, cx)
.into_iter()
.map(|(text, color, _)| (text, color))
.collect()
}
fn chunks<'a>(
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,
theme: &'a SyntaxTheme,
cx: &mut MutableAppContext,
) -> Vec<(String, Option<Color>, Option<Color>)> {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut chunks: Vec<(String, Option<Color>)> = Vec::new();
let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
for chunk in snapshot.chunks(rows, true) {
let color = chunk
let syntax_color = chunk
.syntax_highlight_id
.and_then(|id| id.style(theme)?.color);
if let Some((last_chunk, last_color)) = chunks.last_mut() {
if color == *last_color {
let highlight_color = chunk.highlight_style.and_then(|style| style.color);
if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
last_chunk.push_str(chunk.text);
} else {
chunks.push((chunk.text.to_string(), color));
continue;
}
} else {
chunks.push((chunk.text.to_string(), color));
}
chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
}
chunks
}

View file

@ -499,7 +499,7 @@ impl<'a> BlockMapWriter<'a> {
let block_ix = match self
.0
.blocks
.binary_search_by(|probe| probe.position.cmp(&position, &buffer).unwrap())
.binary_search_by(|probe| probe.position.cmp(&position, &buffer))
{
Ok(ix) | Err(ix) => ix,
};

View file

@ -140,13 +140,14 @@ impl<'a> FoldMapWriter<'a> {
pub fn unfold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut fold_ixs_to_delete = Vec::new();
let buffer = self.0.buffer.lock().clone();
for range in ranges.into_iter() {
// Remove intersecting folds and add their ranges to edits that are passed to sync.
let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, true);
let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive);
while let Some(fold) = folds_cursor.item() {
let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
if offset_range.end > offset_range.start {
@ -256,7 +257,7 @@ impl FoldMap {
let mut folds = self.folds.iter().peekable();
while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() {
let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()).unwrap();
let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock());
assert!(comparison.is_le());
}
}
@ -699,10 +700,7 @@ impl FoldSnapshot {
let ranges = &highlights.1;
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe
.end
.cmp(&transform_start, &self.buffer_snapshot())
.unwrap();
let cmp = probe.end.cmp(&transform_start, &self.buffer_snapshot());
if cmp.is_gt() {
Ordering::Greater
} else {
@ -715,7 +713,6 @@ impl FoldSnapshot {
if range
.start
.cmp(&transform_end, &self.buffer_snapshot)
.unwrap()
.is_ge()
{
break;
@ -820,8 +817,8 @@ where
let start = buffer.anchor_before(range.start.to_offset(buffer));
let end = buffer.anchor_after(range.end.to_offset(buffer));
let mut cursor = folds.filter::<_, usize>(move |summary| {
let start_cmp = start.cmp(&summary.max_end, buffer).unwrap();
let end_cmp = end.cmp(&summary.min_start, buffer).unwrap();
let start_cmp = start.cmp(&summary.max_end, buffer);
let end_cmp = end.cmp(&summary.min_start, buffer);
if inclusive {
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
@ -962,19 +959,19 @@ impl sum_tree::Summary for FoldSummary {
type Context = MultiBufferSnapshot;
fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) {
if other.min_start.cmp(&self.min_start, buffer).unwrap() == Ordering::Less {
if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
self.min_start = other.min_start.clone();
}
if other.max_end.cmp(&self.max_end, buffer).unwrap() == Ordering::Greater {
if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater {
self.max_end = other.max_end.clone();
}
#[cfg(debug_assertions)]
{
let start_comparison = self.start.cmp(&other.start, buffer).unwrap();
let start_comparison = self.start.cmp(&other.start, buffer);
assert!(start_comparison <= Ordering::Equal);
if start_comparison == Ordering::Equal {
assert!(self.end.cmp(&other.end, buffer).unwrap() >= Ordering::Equal);
assert!(self.end.cmp(&other.end, buffer) >= Ordering::Equal);
}
}
@ -993,7 +990,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
self.0.cmp(&other.0, buffer).unwrap()
self.0.cmp(&other.0, buffer)
}
}
@ -1156,7 +1153,7 @@ impl Ord for HighlightEndpoint {
fn cmp(&self, other: &Self) -> Ordering {
self.offset
.cmp(&other.offset)
.then_with(|| self.is_start.cmp(&other.is_start))
.then_with(|| other.is_start.cmp(&self.is_start))
}
}
@ -1282,9 +1279,14 @@ mod tests {
assert_eq!(snapshot4.text(), "123a…c123456eee");
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 5)));
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot5.text(), "123aaaaa\nbbbbbb\nccc123456eee");
assert_eq!(snapshot5.text(), "123a…c123456eee");
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
let (snapshot6, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee");
}
#[gpui::test]
@ -1600,9 +1602,8 @@ mod tests {
.filter(|fold| {
let start = buffer_snapshot.anchor_before(start);
let end = buffer_snapshot.anchor_after(end);
start.cmp(&fold.0.end, &buffer_snapshot).unwrap() == Ordering::Less
&& end.cmp(&fold.0.start, &buffer_snapshot).unwrap()
== Ordering::Greater
start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
&& end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
})
.map(|fold| fold.0)
.collect::<Vec<_>>();
@ -1680,7 +1681,7 @@ mod tests {
let buffer = self.buffer.lock().clone();
let mut folds = self.folds.items(&buffer);
// Ensure sorting doesn't change how folds get merged and displayed.
folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer).unwrap());
folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer));
let mut fold_ranges = folds
.iter()
.map(|fold| fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer))

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,7 @@ use gpui::{
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
};
use json::json;
use language::Bias;
use language::{Bias, DiagnosticSeverity};
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
@ -665,13 +665,15 @@ impl EditorElement {
}
}
let mut diagnostic_highlight = HighlightStyle {
..Default::default()
};
let mut diagnostic_highlight = HighlightStyle::default();
if chunk.is_unnecessary {
diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
} else if let Some(severity) = chunk.diagnostic_severity {
}
if let Some(severity) = chunk.diagnostic_severity {
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
let diagnostic_style = super::diagnostic_style(severity, true, style);
diagnostic_highlight.underline = Some(Underline {
color: Some(diagnostic_style.message.text.color),
@ -679,6 +681,7 @@ impl EditorElement {
squiggly: true,
});
}
}
if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(diagnostic_highlight);
@ -906,7 +909,7 @@ impl Element for EditorElement {
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
};
let mut selections = HashMap::default();
let mut selections = Vec::new();
let mut active_rows = BTreeMap::new();
let mut highlighted_rows = None;
let mut highlighted_ranges = Vec::new();
@ -919,11 +922,32 @@ impl Element for EditorElement {
&display_map,
);
let mut remote_selections = HashMap::default();
for (replica_id, selection) in display_map
.buffer_snapshot
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
{
// The local selections match the leader's selections.
if Some(replica_id) == view.leader_replica_id {
continue;
}
remote_selections
.entry(replica_id)
.or_insert(Vec::new())
.push(crate::Selection {
id: selection.id,
goal: selection.goal,
reversed: selection.reversed,
start: selection.start.to_display_point(&display_map),
end: selection.end.to_display_point(&display_map),
});
}
selections.extend(remote_selections);
if view.show_local_selections {
let local_selections = view.local_selections_in_range(
start_anchor.clone()..end_anchor.clone(),
&display_map,
);
let local_selections =
view.local_selections_in_range(start_anchor..end_anchor, &display_map);
for selection in &local_selections {
let is_empty = selection.start == selection.end;
let selection_start = snapshot.prev_line_boundary(selection.start).1;
@ -936,8 +960,12 @@ impl Element for EditorElement {
*contains_non_empty_selection |= !is_empty;
}
}
selections.insert(
view.replica_id(cx),
// Render the local selections in the leader's color when following.
let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx));
selections.push((
local_replica_id,
local_selections
.into_iter()
.map(|selection| crate::Selection {
@ -948,23 +976,7 @@ impl Element for EditorElement {
end: selection.end.to_display_point(&display_map),
})
.collect(),
);
}
for (replica_id, selection) in display_map
.buffer_snapshot
.remote_selections_in_range(&(start_anchor..end_anchor))
{
selections
.entry(replica_id)
.or_insert(Vec::new())
.push(crate::Selection {
id: selection.id,
goal: selection.goal,
reversed: selection.reversed,
start: selection.start.to_display_point(&display_map),
end: selection.end.to_display_point(&display_map),
});
));
}
});
@ -1210,7 +1222,7 @@ pub struct LayoutState {
em_width: f32,
em_advance: f32,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
}
@ -1280,7 +1292,7 @@ impl PaintState {
}
}
#[derive(Copy, Clone)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum CursorShape {
Bar,
Block,
@ -1487,7 +1499,7 @@ mod tests {
#[gpui::test]
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
cx.add_app_state(Settings::test(cx));
cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
Editor::new(EditorMode::Full, buffer, None, None, cx)
@ -1509,7 +1521,7 @@ mod tests {
#[gpui::test]
fn test_layout_with_placeholder_text_and_blocks(cx: &mut gpui::MutableAppContext) {
cx.add_app_state(Settings::test(cx));
cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("", cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
Editor::new(EditorMode::Full, buffer, None, None, cx)

View file

@ -1,162 +1,251 @@
use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _};
use anyhow::Result;
use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _};
use anyhow::{anyhow, Result};
use futures::FutureExt;
use gpui::{
elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
};
use language::{Bias, Buffer, Diagnostic, File as _};
use project::{File, Project, ProjectPath};
use std::path::PathBuf;
use std::rc::Rc;
use std::{cell::RefCell, fmt::Write};
use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
use project::{File, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view};
use std::{fmt::Write, path::PathBuf, time::Duration};
use text::{Point, Selection};
use util::ResultExt;
use util::TryFutureExt;
use workspace::{
ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings,
StatusItemView, WeakItemHandle, Workspace,
FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
};
pub struct BufferOpener;
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Clone)]
pub struct BufferItemHandle(pub ModelHandle<Buffer>);
#[derive(Clone)]
struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
#[derive(Clone)]
pub struct MultiBufferItemHandle(pub ModelHandle<MultiBuffer>);
#[derive(Clone)]
struct WeakMultiBufferItemHandle(WeakModelHandle<MultiBuffer>);
impl PathOpener for BufferOpener {
fn open(
&self,
project: &mut Project,
project_path: ProjectPath,
cx: &mut ModelContext<Project>,
) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
let buffer = project.open_buffer(project_path, cx);
let task = cx.spawn(|_, _| async move {
let buffer = buffer.await?;
Ok(Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
});
Some(task)
}
}
impl ItemHandle for BufferItemHandle {
fn add_view(
&self,
window_id: usize,
workspace: &Workspace,
nav_history: Rc<RefCell<NavHistory>>,
impl FollowableItem for Editor {
fn from_state_proto(
pane: ViewHandle<workspace::Pane>,
project: ModelHandle<Project>,
state: &mut Option<proto::view::Variant>,
cx: &mut MutableAppContext,
) -> Box<dyn ItemViewHandle> {
let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx));
Box::new(cx.add_view(window_id, |cx| {
let mut editor = Editor::for_buffer(buffer, Some(workspace.project().clone()), cx);
editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
editor
}))
}
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
Box::new(self.clone())
}
fn to_any(&self) -> gpui::AnyModelHandle {
self.0.clone().into()
}
fn downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
Box::new(WeakBufferItemHandle(self.0.downgrade()))
}
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
File::from_dyn(self.0.read(cx).file()).map(|f| ProjectPath {
worktree_id: f.worktree_id(cx),
path: f.path().clone(),
})
}
fn id(&self) -> usize {
self.0.id()
}
}
impl ItemHandle for MultiBufferItemHandle {
fn add_view(
&self,
window_id: usize,
workspace: &Workspace,
nav_history: Rc<RefCell<NavHistory>>,
cx: &mut MutableAppContext,
) -> Box<dyn ItemViewHandle> {
Box::new(cx.add_view(window_id, |cx| {
let mut editor =
Editor::for_buffer(self.0.clone(), Some(workspace.project().clone()), cx);
editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
editor
}))
}
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
Box::new(self.clone())
}
fn to_any(&self) -> gpui::AnyModelHandle {
self.0.clone().into()
}
fn downgrade(&self) -> Box<dyn WeakItemHandle> {
Box::new(WeakMultiBufferItemHandle(self.0.downgrade()))
}
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
None
}
fn id(&self) -> usize {
self.0.id()
}
}
impl WeakItemHandle for WeakBufferItemHandle {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.0
.upgrade(cx)
.map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
}
fn id(&self) -> usize {
self.0.id()
}
}
impl WeakItemHandle for WeakMultiBufferItemHandle {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.0
.upgrade(cx)
.map(|buffer| Box::new(MultiBufferItemHandle(buffer)) as Box<dyn ItemHandle>)
}
fn id(&self) -> usize {
self.0.id()
}
}
impl ItemView for Editor {
fn item(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
Box::new(BufferItemHandle(buffer))
) -> Option<Task<Result<ViewHandle<Self>>>> {
let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
if let Some(proto::view::Variant::Editor(state)) = state.take() {
state
} else {
Box::new(MultiBufferItemHandle(self.buffer.clone()))
unreachable!()
}
} else {
return None;
};
let buffer = project.update(cx, |project, cx| {
project.open_buffer_by_id(state.buffer_id, cx)
});
Some(cx.spawn(|mut cx| async move {
let buffer = buffer.await?;
let editor = pane
.read_with(&cx, |pane, cx| {
pane.items_of_type::<Self>().find(|editor| {
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
})
})
.unwrap_or_else(|| {
cx.add_view(pane.window_id(), |cx| {
Editor::for_buffer(buffer, Some(project), cx)
})
});
editor.update(&mut cx, |editor, cx| {
let excerpt_id;
let buffer_id;
{
let buffer = editor.buffer.read(cx).read(cx);
let singleton = buffer.as_singleton().unwrap();
excerpt_id = singleton.0.clone();
buffer_id = singleton.1;
}
let selections = state
.selections
.into_iter()
.map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection)
.ok_or_else(|| anyhow!("invalid selection"))
})
.collect::<Result<Vec<_>>>()?;
if !selections.is_empty() {
editor.set_selections_from_remote(selections.into(), cx);
}
if let Some(anchor) = state.scroll_top_anchor {
editor.set_scroll_top_anchor(
Anchor {
buffer_id: Some(state.buffer_id as usize),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(anchor)
.ok_or_else(|| anyhow!("invalid scroll top"))?,
},
vec2f(state.scroll_x, state.scroll_y),
cx,
);
}
Ok::<_, anyhow::Error>(())
})?;
Ok(editor)
}))
}
fn set_leader_replica_id(
&mut self,
leader_replica_id: Option<u16>,
cx: &mut ViewContext<Self>,
) {
self.leader_replica_id = leader_replica_id;
if self.leader_replica_id.is_some() {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_active_selections(cx);
});
} else {
self.buffer.update(cx, |buffer, cx| {
if self.focused {
buffer.set_active_selections(&self.selections, cx);
}
});
}
cx.notify();
}
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
Some(proto::view::Variant::Editor(proto::view::Editor {
buffer_id,
scroll_top_anchor: Some(language::proto::serialize_anchor(
&self.scroll_top_anchor.text_anchor,
)),
scroll_x: self.scroll_position.x(),
scroll_y: self.scroll_position.y(),
selections: self.selections.iter().map(serialize_selection).collect(),
}))
}
fn add_event_to_update_proto(
&self,
event: &Self::Event,
update: &mut Option<proto::update_view::Variant>,
_: &AppContext,
) -> bool {
let update =
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
match update {
proto::update_view::Variant::Editor(update) => match event {
Event::ScrollPositionChanged { .. } => {
update.scroll_top_anchor = Some(language::proto::serialize_anchor(
&self.scroll_top_anchor.text_anchor,
));
update.scroll_x = self.scroll_position.x();
update.scroll_y = self.scroll_position.y();
true
}
Event::SelectionsChanged { .. } => {
update.selections = self
.selections
.iter()
.chain(self.pending_selection.as_ref().map(|p| &p.selection))
.map(serialize_selection)
.collect();
true
}
_ => false,
},
}
}
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
fn apply_update_proto(
&mut self,
message: update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Result<()> {
match message {
update_view::Variant::Editor(message) => {
let buffer = self.buffer.read(cx);
let buffer = buffer.read(cx);
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
let excerpt_id = excerpt_id.clone();
drop(buffer);
let selections = message
.selections
.into_iter()
.filter_map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection)
})
.collect::<Vec<_>>();
if !selections.is_empty() {
self.set_selections_from_remote(selections, cx);
self.request_autoscroll_remotely(Autoscroll::Newest, cx);
} else {
if let Some(anchor) = message.scroll_top_anchor {
self.set_scroll_top_anchor(
Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(anchor)
.ok_or_else(|| anyhow!("invalid scroll top"))?,
},
vec2f(message.scroll_x, message.scroll_y),
cx,
);
}
}
}
}
Ok(())
}
fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
match event {
Event::Edited => true,
Event::SelectionsChanged { local } => *local,
Event::ScrollPositionChanged { local } => *local,
_ => false,
}
}
}
fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection {
id: selection.id as u64,
start: Some(language::proto::serialize_anchor(
&selection.start.text_anchor,
)),
end: Some(language::proto::serialize_anchor(
&selection.end.text_anchor,
)),
reversed: selection.reversed,
}
}
fn deserialize_selection(
excerpt_id: &ExcerptId,
buffer_id: usize,
selection: proto::Selection,
) -> Option<Selection<Anchor>> {
Some(Selection {
id: selection.id as usize,
start: Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(selection.start?)?,
},
end: Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(selection.end?)?,
},
reversed: selection.reversed,
goal: SelectionGoal::None,
})
}
impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Some(data) = data.downcast_ref::<NavigationData>() {
let buffer = self.buffer.read(cx).read(cx);
let offset = if buffer.can_resolve(&data.anchor) {
@ -164,11 +253,19 @@ impl ItemView for Editor {
} else {
buffer.clip_offset(data.offset, Bias::Left)
};
let newest_selection = self.newest_selection_with_snapshot::<usize>(&buffer);
drop(buffer);
if newest_selection.head() == offset {
false
} else {
let nav_history = self.nav_history.take();
self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
self.nav_history = nav_history;
true
}
} else {
false
}
}
@ -184,15 +281,19 @@ impl ItemView for Editor {
})
}
fn clone_on_split(
&self,
nav_history: ItemNavHistory,
cx: &mut ViewContext<Self>,
) -> Option<Self>
fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx))
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
Some(self.clone(nav_history, cx))
Some(self.clone(cx))
}
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
self.nav_history = Some(history);
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
@ -219,9 +320,17 @@ impl ItemView for Editor {
) -> Task<Result<()>> {
let buffer = self.buffer().clone();
let buffers = buffer.read(cx).all_buffers();
let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx));
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
cx.spawn(|this, mut cx| async move {
let transaction = transaction.await.log_err();
let transaction = futures::select_biased! {
_ = timeout => {
log::warn!("timed out waiting for formatting");
None
}
transaction = format.log_err().fuse() => transaction,
};
this.update(&mut cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::Fit, cx)
});
@ -275,6 +384,18 @@ impl ItemView for Editor {
}
}
impl ProjectItem for Editor {
type Item = Buffer;
fn for_project_item(
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>,
) -> Self {
Self::for_buffer(buffer, Some(project), cx)
}
}
pub struct CursorPosition {
position: Option<Point>,
selected_count: usize,
@ -322,7 +443,7 @@ impl View for CursorPosition {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(position) = self.position {
let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
let mut text = format!("{},{}", position.row + 1, position.column + 1);
if self.selected_count > 0 {
write!(text, " ({} selected)", self.selected_count).unwrap();
@ -337,7 +458,7 @@ impl View for CursorPosition {
impl StatusItemView for CursorPosition {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemViewHandle>,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
@ -395,7 +516,7 @@ impl View for DiagnosticMessage {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(diagnostic) = &self.diagnostic {
let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(),
theme.diagnostic_message.clone(),
@ -410,7 +531,7 @@ impl View for DiagnosticMessage {
impl StatusItemView for DiagnosticMessage {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemViewHandle>,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {

View file

@ -1,20 +1,19 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, ToPoint};
use anyhow::Result;
use language::Point;
use std::ops::Range;
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 {
*point.column_mut() -= 1;
} else if point.row() > 0 {
*point.row_mut() -= 1;
*point.column_mut() = map.line_len(point.row());
}
Ok(map.clip_point(point, Bias::Left))
map.clip_point(point, Bias::Left)
}
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let max_column = map.line_len(point.row());
if point.column() < max_column {
*point.column_mut() += 1;
@ -22,14 +21,14 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPo
*point.row_mut() += 1;
*point.column_mut() = 0;
}
Ok(map.clip_point(point, Bias::Right))
map.clip_point(point, Bias::Right)
}
pub fn up(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
) -> Result<(DisplayPoint, SelectionGoal)> {
) -> (DisplayPoint, SelectionGoal) {
let mut goal_column = if let SelectionGoal::Column(column) = goal {
column
} else {
@ -54,17 +53,17 @@ pub fn up(
Bias::Right
};
Ok((
(
map.clip_point(point, clip_bias),
SelectionGoal::Column(goal_column),
))
)
}
pub fn down(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
) -> Result<(DisplayPoint, SelectionGoal)> {
) -> (DisplayPoint, SelectionGoal) {
let mut goal_column = if let SelectionGoal::Column(column) = goal {
column
} else {
@ -86,10 +85,10 @@ pub fn down(
Bias::Right
};
Ok((
(
map.clip_point(point, clip_bias),
SelectionGoal::Column(goal_column),
))
)
}
pub fn line_beginning(
@ -132,68 +131,110 @@ pub fn line_end(
}
}
pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let mut line_start = 0;
if point.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
line_start = indent;
}
}
if point.column() == line_start {
if point.row() == 0 {
return DisplayPoint::new(0, 0);
} else {
let row = point.row() - 1;
point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
}
}
let mut boundary = DisplayPoint::new(point.row(), 0);
let mut column = 0;
let mut prev_char_kind = CharKind::Newline;
for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
if column >= point.column() {
break;
}
let char_kind = char_kind(c);
if char_kind != prev_char_kind
&& char_kind != CharKind::Whitespace
&& char_kind != CharKind::Newline
{
*boundary.column_mut() = column;
}
prev_char_kind = char_kind;
column += c.len_utf8() as u32;
}
boundary
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
find_preceding_boundary(map, point, |left, right| {
(char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
})
}
pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let mut prev_char_kind = None;
for c in map.chars_at(point) {
let char_kind = char_kind(c);
if let Some(prev_char_kind) = prev_char_kind {
if c == '\n' {
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
find_preceding_boundary(map, point, |left, right| {
let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start || left == '\n'
})
}
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
find_boundary(map, point, |left, right| {
(char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
})
}
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
find_boundary(map, point, |left, right| {
let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end || right == '\n'
})
}
/// Scans for a boundary from the start of each line preceding the given end point until a boundary
/// is found, indicated by the given predicate returning true. The predicate is called with the
/// character to the left and right of the candidate boundary location, and will be called with `\n`
/// characters indicating the start or end of a line. If the predicate returns true multiple times
/// on a line, the *rightmost* boundary is returned.
pub fn find_preceding_boundary(
map: &DisplaySnapshot,
end: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut point = end;
loop {
*point.column_mut() = 0;
if point.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
*point.column_mut() = indent;
}
}
let mut boundary = None;
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
for ch in map.chars_at(point) {
if point >= end {
break;
}
if prev_char_kind != char_kind
&& prev_char_kind != CharKind::Whitespace
&& prev_char_kind != CharKind::Newline
{
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
boundary = Some(point);
}
}
if ch == '\n' {
break;
}
prev_ch = Some(ch);
*point.column_mut() += ch.len_utf8() as u32;
}
if let Some(boundary) = boundary {
return boundary;
} else if point.row() == 0 {
return DisplayPoint::zero();
} else {
*point.row_mut() -= 1;
}
}
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line.
pub fn find_boundary(
map: &DisplaySnapshot,
mut point: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut prev_ch = None;
for ch in map.chars_at(point) {
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
break;
}
}
if c == '\n' {
if ch == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += c.len_utf8() as u32;
*point.column_mut() += ch.len_utf8() as u32;
}
prev_char_kind = Some(char_kind);
prev_ch = Some(ch);
}
map.clip_point(point, Bias::Right)
}
@ -225,9 +266,205 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
#[cfg(test)]
mod tests {
use super::*;
use crate::{Buffer, DisplayMap, MultiBuffer};
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
use language::Point;
#[gpui::test]
fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
previous_word_start(&snapshot, display_points[1]),
display_points[0]
);
}
assert("\n| |lorem", cx);
assert("|\n| lorem", cx);
assert(" |lorem|", cx);
assert("| |lorem", cx);
assert(" |lor|em", cx);
assert("\nlorem\n| |ipsum", cx);
assert("\n\n|\n|", cx);
assert(" |lorem |ipsum", cx);
assert("lorem|-|ipsum", cx);
assert("lorem|-#$@|ipsum", cx);
assert("|lorem_|ipsum", cx);
assert(" |defγ|", cx);
assert(" |bcΔ|", cx);
assert(" ab|——|cd", cx);
}
#[gpui::test]
fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
previous_subword_start(&snapshot, display_points[1]),
display_points[0]
);
}
// Subword boundaries are respected
assert("lorem_|ip|sum", cx);
assert("lorem_|ipsum|", cx);
assert("|lorem_|ipsum", cx);
assert("lorem_|ipsum_|dolor", cx);
assert("lorem|Ip|sum", cx);
assert("lorem|Ipsum|", cx);
// Word boundaries are still respected
assert("\n| |lorem", cx);
assert(" |lorem|", cx);
assert(" |lor|em", cx);
assert("\nlorem\n| |ipsum", cx);
assert("\n\n|\n|", cx);
assert(" |lorem |ipsum", cx);
assert("lorem|-|ipsum", cx);
assert("lorem|-#$@|ipsum", cx);
assert(" |defγ|", cx);
assert(" bc|Δ|", cx);
assert(" |bcδ|", cx);
assert(" ab|——|cd", cx);
}
#[gpui::test]
fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
fn assert(
marked_text: &str,
cx: &mut gpui::MutableAppContext,
is_boundary: impl FnMut(char, char) -> bool,
) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
find_preceding_boundary(&snapshot, display_points[1], is_boundary),
display_points[0]
);
}
assert("abc|def\ngh\nij|k", cx, |left, right| {
left == 'c' && right == 'd'
});
assert("abcdef\n|gh\nij|k", cx, |left, right| {
left == '\n' && right == 'g'
});
let mut line_count = 0;
assert("abcdef\n|gh\nij|k", cx, |left, _| {
if left == '\n' {
line_count += 1;
line_count == 2
} else {
false
}
});
}
#[gpui::test]
fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
next_word_end(&snapshot, display_points[0]),
display_points[1]
);
}
assert("\n| lorem|", cx);
assert(" |lorem|", cx);
assert(" lor|em|", cx);
assert(" lorem| |\nipsum\n", cx);
assert("\n|\n|\n\n", cx);
assert("lorem| ipsum| ", cx);
assert("lorem|-|ipsum", cx);
assert("lorem|#$@-|ipsum", cx);
assert("lorem|_ipsum|", cx);
assert(" |bcΔ|", cx);
assert(" ab|——|cd", cx);
}
#[gpui::test]
fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
next_subword_end(&snapshot, display_points[0]),
display_points[1]
);
}
// Subword boundaries are respected
assert("lo|rem|_ipsum", cx);
assert("|lorem|_ipsum", cx);
assert("lorem|_ipsum|", cx);
assert("lorem|_ipsum|_dolor", cx);
assert("lo|rem|Ipsum", cx);
assert("lorem|Ipsum|Dolor", cx);
// Word boundaries are still respected
assert("\n| lorem|", cx);
assert(" |lorem|", cx);
assert(" lor|em|", cx);
assert(" lorem| |\nipsum\n", cx);
assert("\n|\n|\n\n", cx);
assert("lorem| ipsum| ", cx);
assert("lorem|-|ipsum", cx);
assert("lorem|#$@-|ipsum", cx);
assert("lorem|_ipsum|", cx);
assert(" |bc|Δ", cx);
assert(" ab|——|cd", cx);
}
#[gpui::test]
fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
fn assert(
marked_text: &str,
cx: &mut gpui::MutableAppContext,
is_boundary: impl FnMut(char, char) -> bool,
) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
find_boundary(&snapshot, display_points[0], is_boundary),
display_points[1]
);
}
assert("abc|def\ngh\nij|k", cx, |left, right| {
left == 'j' && right == 'k'
});
assert("ab|cdef\ngh\n|ijk", cx, |left, right| {
left == '\n' && right == 'i'
});
let mut line_count = 0;
assert("abc|def\ngh\n|ijk", cx, |left, _| {
if left == '\n' {
line_count += 1;
line_count == 2
} else {
false
}
});
}
#[gpui::test]
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
surrounding_word(&snapshot, display_points[1]),
display_points[0]..display_points[2]
);
}
assert("||lorem| ipsum", cx);
assert("|lo|rem| ipsum", cx);
assert("|lorem|| ipsum", cx);
assert("lorem| | |ipsum", cx);
assert("lorem\n|||\nipsum", cx);
assert("lorem\n||ipsum|", cx);
assert("lorem,|| |ipsum", cx);
assert("|lorem||, ipsum", cx);
}
#[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
@ -249,180 +486,50 @@ mod tests {
);
multibuffer
});
let display_map =
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
// Can't move up into the first excerpt's header
assert_eq!(
up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)),
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
);
assert_eq!(
up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None),
(DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
);
// Move up and down within first excerpt
assert_eq!(
up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
(DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
);
assert_eq!(
down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
(DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
);
// Move up and down across second excerpt's header
assert_eq!(
up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(),
up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
(DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
);
assert_eq!(
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
(DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
);
// Can't move down off the end
assert_eq!(
down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(),
down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)),
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
);
assert_eq!(
down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(),
down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
);
}
#[gpui::test]
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
let display_map = cx
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
DisplayPoint::new(0, 7)
);
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
DisplayPoint::new(0, 2)
);
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
DisplayPoint::new(0, 2)
);
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
DisplayPoint::new(0, 0)
);
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
DisplayPoint::new(0, 0)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
DisplayPoint::new(0, 1)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
DisplayPoint::new(0, 6)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
DisplayPoint::new(0, 6)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
DisplayPoint::new(0, 12)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
DisplayPoint::new(0, 12)
);
}
#[gpui::test]
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
let display_map = cx
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
);
assert_eq!(
surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
);
}
}

View file

@ -211,7 +211,7 @@ impl MultiBuffer {
pub fn singleton(buffer: ModelHandle<Buffer>, cx: &mut ModelContext<Self>) -> Self {
let mut this = Self::new(buffer.read(cx).replica_id());
this.singleton = true;
this.push_excerpts(buffer, [text::Anchor::min()..text::Anchor::max()], cx);
this.push_excerpts(buffer, [text::Anchor::MIN..text::Anchor::MAX], cx);
this.snapshot.borrow_mut().singleton = true;
this
}
@ -522,24 +522,14 @@ impl MultiBuffer {
self.buffers.borrow()[&buffer_id]
.buffer
.update(cx, |buffer, cx| {
selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer).unwrap());
selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer));
let mut selections = selections.into_iter().peekable();
let merged_selections = Arc::from_iter(iter::from_fn(|| {
let mut selection = selections.next()?;
while let Some(next_selection) = selections.peek() {
if selection
.end
.cmp(&next_selection.start, buffer)
.unwrap()
.is_ge()
{
if selection.end.cmp(&next_selection.start, buffer).is_ge() {
let next_selection = selections.next().unwrap();
if next_selection
.end
.cmp(&selection.end, buffer)
.unwrap()
.is_ge()
{
if next_selection.end.cmp(&selection.end, buffer).is_ge() {
selection.end = next_selection.end;
}
} else {
@ -814,11 +804,38 @@ impl MultiBuffer {
cx.notify();
}
pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle<Buffer>) -> Vec<ExcerptId> {
pub fn excerpts_for_buffer(
&self,
buffer: &ModelHandle<Buffer>,
cx: &AppContext,
) -> Vec<(ExcerptId, Range<text::Anchor>)> {
let mut excerpts = Vec::new();
let snapshot = self.read(cx);
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
for excerpt_id in buffers
.get(&buffer.id())
.map(|state| &state.excerpts)
.into_iter()
.flatten()
{
cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
if let Some(excerpt) = cursor.item() {
if excerpt.id == *excerpt_id {
excerpts.push((excerpt.id.clone(), excerpt.range.clone()));
}
}
}
excerpts
}
pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
self.buffers
.borrow()
.get(&buffer.id())
.map_or(Vec::new(), |state| state.excerpts.clone())
.values()
.flat_map(|state| state.excerpts.iter().cloned())
.collect()
}
pub fn excerpt_containing(
@ -1407,7 +1424,7 @@ impl MultiBufferSnapshot {
);
for ch in prev_chars {
if Some(char_kind(ch)) == word_kind {
if Some(char_kind(ch)) == word_kind && ch != '\n' {
start -= ch.len_utf8();
} else {
break;
@ -1415,7 +1432,7 @@ impl MultiBufferSnapshot {
}
for ch in next_chars {
if Some(char_kind(ch)) == word_kind {
if Some(char_kind(ch)) == word_kind && ch != '\n' {
end += ch.len_utf8();
} else {
break;
@ -1909,11 +1926,7 @@ impl MultiBufferSnapshot {
.range
.start
.bias(anchor.text_anchor.bias, &excerpt.buffer);
if text_anchor
.cmp(&excerpt.range.end, &excerpt.buffer)
.unwrap()
.is_gt()
{
if text_anchor.cmp(&excerpt.range.end, &excerpt.buffer).is_gt() {
text_anchor = excerpt.range.end.clone();
}
Anchor {
@ -1928,7 +1941,6 @@ impl MultiBufferSnapshot {
.bias(anchor.text_anchor.bias, &excerpt.buffer);
if text_anchor
.cmp(&excerpt.range.start, &excerpt.buffer)
.unwrap()
.is_lt()
{
text_anchor = excerpt.range.start.clone();
@ -1948,7 +1960,7 @@ impl MultiBufferSnapshot {
result.push((anchor_ix, anchor, kept_position));
}
}
result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self).unwrap());
result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self));
result
}
@ -2295,10 +2307,10 @@ impl MultiBufferSnapshot {
excerpt_id: excerpt.id.clone(),
text_anchor: selection.end.clone(),
};
if range.start.cmp(&start, self).unwrap().is_gt() {
if range.start.cmp(&start, self).is_gt() {
start = range.start.clone();
}
if range.end.cmp(&end, self).unwrap().is_lt() {
if range.end.cmp(&end, self).is_lt() {
end = range.end.clone();
}
@ -2522,17 +2534,9 @@ impl Excerpt {
}
fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
if text_anchor
.cmp(&self.range.start, &self.buffer)
.unwrap()
.is_lt()
{
if text_anchor.cmp(&self.range.start, &self.buffer).is_lt() {
self.range.start.clone()
} else if text_anchor
.cmp(&self.range.end, &self.buffer)
.unwrap()
.is_gt()
{
} else if text_anchor.cmp(&self.range.end, &self.buffer).is_gt() {
self.range.end.clone()
} else {
text_anchor
@ -2545,13 +2549,11 @@ impl Excerpt {
.range
.start
.cmp(&anchor.text_anchor, &self.buffer)
.unwrap()
.is_le()
&& self
.range
.end
.cmp(&anchor.text_anchor, &self.buffer)
.unwrap()
.is_ge()
}
}
@ -3062,7 +3064,8 @@ mod tests {
);
let snapshot = multibuffer.update(cx, |multibuffer, cx| {
let buffer_2_excerpt_id = multibuffer.excerpt_ids_for_buffer(&buffer_2)[0].clone();
let (buffer_2_excerpt_id, _) =
multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone();
multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx);
multibuffer.snapshot(cx)
});
@ -3357,7 +3360,7 @@ mod tests {
let bias = if rng.gen() { Bias::Left } else { Bias::Right };
log::info!("Creating anchor at {} with bias {:?}", offset, bias);
anchors.push(multibuffer.anchor_at(offset, bias));
anchors.sort_by(|a, b| a.cmp(&b, &multibuffer).unwrap());
anchors.sort_by(|a, b| a.cmp(&b, &multibuffer));
}
40..=44 if !anchors.is_empty() => {
let multibuffer = multibuffer.read(cx).read(cx);

View file

@ -1,5 +1,4 @@
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
use anyhow::Result;
use std::{
cmp::Ordering,
ops::{Range, Sub},
@ -19,7 +18,7 @@ impl Anchor {
Self {
buffer_id: None,
excerpt_id: ExcerptId::min(),
text_anchor: text::Anchor::min(),
text_anchor: text::Anchor::MIN,
}
}
@ -27,7 +26,7 @@ impl Anchor {
Self {
buffer_id: None,
excerpt_id: ExcerptId::max(),
text_anchor: text::Anchor::max(),
text_anchor: text::Anchor::MAX,
}
}
@ -35,18 +34,18 @@ impl Anchor {
&self.excerpt_id
}
pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Result<Ordering> {
pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
if excerpt_id_cmp.is_eq() {
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
Ok(Ordering::Equal)
Ordering::Equal
} else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
} else {
Ok(Ordering::Equal)
Ordering::Equal
}
} else {
Ok(excerpt_id_cmp)
excerpt_id_cmp
}
}
@ -97,17 +96,17 @@ impl ToPoint for Anchor {
}
pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering>;
fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
}
impl AnchorRangeExt for Range<Anchor> {
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Result<Ordering> {
Ok(match self.start.cmp(&other.start, buffer)? {
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering {
match self.start.cmp(&other.start, buffer) {
Ordering::Equal => other.end.cmp(&self.end, buffer),
ord @ _ => ord,
})
}
}
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {

View file

@ -1,3 +1,10 @@
use util::test::marked_text;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
DisplayPoint, MultiBuffer,
};
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
@ -5,3 +12,30 @@ fn init_logger() {
env_logger::init();
}
}
// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
pub fn marked_display_snapshot(
text: &str,
cx: &mut gpui::MutableAppContext,
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
let (unmarked_text, markers) = marked_text(text);
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
let markers = markers
.into_iter()
.map(|offset| offset.to_display_point(&snapshot))
.collect();
(snapshot, markers)
}

View file

@ -21,3 +21,5 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
gpui = { path = "../gpui", features = ["test-support"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.8"

View file

@ -67,7 +67,7 @@ impl View for FileFinder {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
Align::new(
ConstrainedBox::new(
Container::new(
@ -106,7 +106,7 @@ impl View for FileFinder {
impl FileFinder {
fn render_matches(&self, cx: &AppContext) -> ElementBox {
if self.matches.is_empty() {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
return Container::new(
Label::new(
"No matches".into(),
@ -142,7 +142,7 @@ impl FileFinder {
fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox {
let selected_index = self.selected_index();
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
let style = if index == selected_index {
&settings.theme.selector.active_item
} else {
@ -291,7 +291,7 @@ impl FileFinder {
cx: &mut ViewContext<Self>,
) {
match event {
editor::Event::Edited => {
editor::Event::BufferEdited { .. } => {
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
if query.is_empty() {
self.latest_search_id = post_inc(&mut self.search_count);
@ -407,16 +407,21 @@ mod tests {
use std::path::PathBuf;
use workspace::{Workspace, WorkspaceParams};
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
#[gpui::test]
async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
let mut path_openers = Vec::new();
cx.update(|cx| {
super::init(cx);
editor::init(cx, &mut path_openers);
editor::init(cx);
});
let mut params = cx.update(WorkspaceParams::test);
params.path_openers = Arc::from(path_openers);
let params = cx.update(WorkspaceParams::test);
params
.fs
.as_fake()

View file

@ -59,7 +59,8 @@ impl GoToLine {
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
if let Some(editor) = workspace.active_item(cx)
if let Some(editor) = workspace
.active_item(cx)
.and_then(|active_item| active_item.downcast::<Editor>())
{
workspace.toggle_modal(cx, |cx, _| {
@ -101,7 +102,7 @@ impl GoToLine {
) {
match event {
editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::Edited => {
editor::Event::BufferEdited { .. } => {
let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
let mut components = line_editor.trim().split(&[',', ':'][..]);
let row = components.next().and_then(|row| row.parse::<u32>().ok());
@ -148,7 +149,7 @@ impl View for GoToLine {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.app_state::<Settings>().theme.selector;
let theme = &cx.global::<Settings>().theme.selector;
let label = format!(
"{},{} of {} lines",

File diff suppressed because it is too large Load diff

View file

@ -337,7 +337,7 @@ impl Deterministic {
if let Some((_, wakeup_time, _)) = state.pending_timers.first() {
let wakeup_time = *wakeup_time;
if wakeup_time < new_now {
if wakeup_time <= new_now {
let timer_count = state
.pending_timers
.iter()

View file

@ -224,15 +224,19 @@ impl Keystroke {
key: key.unwrap(),
})
}
pub fn modified(&self) -> bool {
self.ctrl || self.alt || self.shift || self.cmd
}
}
impl Context {
pub fn extend(&mut self, other: Context) {
for v in other.set {
self.set.insert(v);
pub fn extend(&mut self, other: &Context) {
for v in &other.set {
self.set.insert(v.clone());
}
for (k, v) in other.map {
self.map.insert(k, v);
for (k, v) in &other.map {
self.map.insert(k.clone(), v.clone());
}
}
}

View file

@ -561,9 +561,10 @@ impl Renderer {
}
for icon in icons {
let origin = icon.bounds.origin() * scale_factor;
let target_size = icon.bounds.size() * scale_factor;
let source_size = (target_size * 2.).ceil().to_i32();
// Snap sprite to pixel grid.
let origin = (icon.bounds.origin() * scale_factor).floor();
let target_size = (icon.bounds.size() * scale_factor).ceil();
let source_size = (target_size * 2.).to_i32();
let sprite =
self.sprite_cache

View file

@ -20,7 +20,7 @@ use std::{
pub struct Presenter {
window_id: usize,
rendered_views: HashMap<usize, ElementBox>,
pub(crate) rendered_views: HashMap<usize, ElementBox>,
parents: HashMap<usize, usize>,
font_cache: Arc<FontCache>,
text_layout_cache: TextLayoutCache,
@ -63,41 +63,36 @@ impl Presenter {
path
}
pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) {
pub fn invalidate(
&mut self,
invalidation: &mut WindowInvalidation,
cx: &mut MutableAppContext,
) {
cx.start_frame();
for view_id in invalidation.removed {
for view_id in &invalidation.removed {
invalidation.updated.remove(&view_id);
self.rendered_views.remove(&view_id);
self.parents.remove(&view_id);
}
for view_id in invalidation.updated {
for view_id in &invalidation.updated {
self.rendered_views.insert(
view_id,
cx.render_view(self.window_id, view_id, self.titlebar_height, false)
*view_id,
cx.render_view(self.window_id, *view_id, self.titlebar_height, false)
.unwrap(),
);
}
}
pub fn refresh(
&mut self,
invalidation: Option<WindowInvalidation>,
cx: &mut MutableAppContext,
) {
cx.start_frame();
if let Some(invalidation) = invalidation {
for view_id in invalidation.removed {
self.rendered_views.remove(&view_id);
self.parents.remove(&view_id);
}
}
pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) {
self.invalidate(invalidation, cx);
for (view_id, view) in &mut self.rendered_views {
if !invalidation.updated.contains(view_id) {
*view = cx
.render_view(self.window_id, *view_id, self.titlebar_height, true)
.unwrap();
}
}
}
pub fn build_scene(
&mut self,
@ -304,6 +299,10 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> {
fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
self.app.upgrade_view_handle(handle)
}
fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option<AnyViewHandle> {
self.app.upgrade_any_view_handle(handle)
}
}
impl<'a> ElementStateContext for LayoutContext<'a> {

View file

@ -271,7 +271,6 @@ pub(crate) struct DiagnosticEndpoint {
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub enum CharKind {
Newline,
Punctuation,
Whitespace,
Word,
@ -1621,8 +1620,13 @@ impl BufferSnapshot {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = tree.root_node().walk();
// Descend to smallest leaf that touches or exceeds the start of the range.
while cursor.goto_first_child_for_byte(range.start).is_some() {}
// Descend to the first leaf that touches the start of the range,
// and if the range is non-empty, extends beyond the start.
while cursor.goto_first_child_for_byte(range.start).is_some() {
if !range.is_empty() && cursor.node().end_byte() == range.start {
cursor.goto_next_sibling();
}
}
// Ascend to the smallest ancestor that strictly contains the range.
loop {
@ -1656,6 +1660,9 @@ impl BufferSnapshot {
}
}
// If there is a candidate node on both sides of the (empty) range, then
// decide between the two by favoring a named node over an anonymous token.
// If both nodes are the same in that regard, favor the right one.
if let Some(right_node) = right_node {
if right_node.is_named() || !left_node.is_named() {
return Some(right_node.byte_range());
@ -1822,12 +1829,6 @@ impl BufferSnapshot {
.min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
}
/*
impl BufferSnapshot
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
*/
pub fn remote_selections_in_range<'a>(
&'a self,
range: Range<Anchor>,
@ -1840,20 +1841,12 @@ impl BufferSnapshot {
})
.map(move |(replica_id, set)| {
let start_ix = match set.selections.binary_search_by(|probe| {
probe
.end
.cmp(&range.start, self)
.unwrap()
.then(Ordering::Greater)
probe.end.cmp(&range.start, self).then(Ordering::Greater)
}) {
Ok(ix) | Err(ix) => ix,
};
let end_ix = match set.selections.binary_search_by(|probe| {
probe
.start
.cmp(&range.end, self)
.unwrap()
.then(Ordering::Less)
probe.start.cmp(&range.end, self).then(Ordering::Less)
}) {
Ok(ix) | Err(ix) => ix,
};
@ -2280,9 +2273,7 @@ pub fn contiguous_ranges(
}
pub fn char_kind(c: char) -> CharKind {
if c == '\n' {
CharKind::Newline
} else if c.is_whitespace() {
if c.is_whitespace() {
CharKind::Whitespace
} else if c.is_alphanumeric() || c == '_' {
CharKind::Word

View file

@ -81,8 +81,8 @@ impl DiagnosticSet {
let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias);
let mut cursor = self.diagnostics.filter::<_, ()>({
move |summary: &Summary| {
let start_cmp = range.start.cmp(&summary.max_end, buffer).unwrap();
let end_cmp = range.end.cmp(&summary.min_start, buffer).unwrap();
let start_cmp = range.start.cmp(&summary.max_end, buffer);
let end_cmp = range.end.cmp(&summary.min_start, buffer);
if inclusive {
start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
} else {
@ -123,7 +123,7 @@ impl DiagnosticSet {
let start_ix = output.len();
output.extend(groups.into_values().filter_map(|mut entries| {
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap());
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer));
entries
.iter()
.position(|entry| entry.diagnostic.is_primary)
@ -137,7 +137,6 @@ impl DiagnosticSet {
.range
.start
.cmp(&b.entries[b.primary_ix].range.start, buffer)
.unwrap()
});
}
@ -187,10 +186,10 @@ impl DiagnosticEntry<Anchor> {
impl Default for Summary {
fn default() -> Self {
Self {
start: Anchor::min(),
end: Anchor::max(),
min_start: Anchor::max(),
max_end: Anchor::min(),
start: Anchor::MIN,
end: Anchor::MAX,
min_start: Anchor::MAX,
max_end: Anchor::MIN,
count: 0,
}
}
@ -200,15 +199,10 @@ impl sum_tree::Summary for Summary {
type Context = text::BufferSnapshot;
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
if other
.min_start
.cmp(&self.min_start, buffer)
.unwrap()
.is_lt()
{
if other.min_start.cmp(&self.min_start, buffer).is_lt() {
self.min_start = other.min_start.clone();
}
if other.max_end.cmp(&self.max_end, buffer).unwrap().is_gt() {
if other.max_end.cmp(&self.max_end, buffer).is_gt() {
self.max_end = other.max_end.clone();
}
self.start = other.start.clone();

View file

@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry(
}
pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
selections
.iter()
.map(|selection| proto::Selection {
selections.iter().map(serialize_selection).collect()
}
pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection {
id: selection.id as u64,
start: Some(serialize_anchor(&selection.start)),
end: Some(serialize_anchor(&selection.end)),
reversed: selection.reversed,
})
.collect()
}
}
pub fn serialize_diagnostics<'a>(
@ -274,7 +275,12 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
Arc::from(
selections
.into_iter()
.filter_map(|selection| {
.filter_map(deserialize_selection)
.collect::<Vec<_>>(),
)
}
pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
Some(Selection {
id: selection.id as usize,
start: deserialize_anchor(selection.start?)?,
@ -282,9 +288,6 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
reversed: selection.reversed,
goal: SelectionGoal::None,
})
})
.collect::<Vec<_>>(),
)
}
pub fn deserialize_diagnostics(

View file

@ -508,6 +508,44 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
);
}
#[gpui::test]
fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) {
cx.add_model(|cx| {
let text = "fn a() { b(|c| {}) }";
let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
let snapshot = buffer.snapshot();
assert_eq!(
snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")),
Some(range_of(text, "|"))
);
assert_eq!(
snapshot.range_for_syntax_ancestor(range_of(text, "|")),
Some(range_of(text, "|c|"))
);
assert_eq!(
snapshot.range_for_syntax_ancestor(range_of(text, "|c|")),
Some(range_of(text, "|c| {}"))
);
assert_eq!(
snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")),
Some(range_of(text, "(|c| {})"))
);
buffer
});
fn empty_range_at(text: &str, part: &str) -> Range<usize> {
let start = text.find(part).unwrap();
start..start
}
fn range_of(text: &str, part: &str) -> Range<usize> {
let start = text.find(part).unwrap();
start..start + part.len()
}
}
#[gpui::test]
fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
cx.add_model(|cx| {
@ -839,7 +877,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
for buffer in &buffers {
let buffer = buffer.read(cx).snapshot();
let actual_remote_selections = buffer
.remote_selections_in_range(Anchor::min()..Anchor::max())
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
.map(|(replica_id, selections)| (replica_id, selections.collect::<Vec<_>>()))
.collect::<Vec<_>>();
let expected_remote_selections = active_selections

View file

@ -556,7 +556,14 @@ type FakeLanguageServerHandlers = Arc<
Mutex<
HashMap<
&'static str,
Box<dyn Send + FnMut(usize, &[u8], gpui::AsyncAppContext) -> Vec<u8>>,
Box<
dyn Send
+ FnMut(
usize,
&[u8],
gpui::AsyncAppContext,
) -> futures::future::BoxFuture<'static, Vec<u8>>,
>,
>,
>,
>;
@ -585,12 +592,17 @@ impl LanguageServer {
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx);
fake.handle_request::<request::Initialize, _>({
fake.handle_request::<request::Initialize, _, _>({
let capabilities = capabilities.clone();
move |_, _| InitializeResult {
capabilities: capabilities.clone(),
move |_, _| {
let capabilities = capabilities.clone();
async move {
InitializeResult {
capabilities,
..Default::default()
}
}
}
});
let executor = cx.background().clone();
@ -628,7 +640,8 @@ impl FakeLanguageServer {
let response;
if let Some(handler) = handlers.lock().get_mut(request.method) {
response =
handler(request.id, request.params.get().as_bytes(), cx.clone());
handler(request.id, request.params.get().as_bytes(), cx.clone())
.await;
log::debug!("handled lsp request. method:{}", request.method);
} else {
response = serde_json::to_vec(&AnyResponse {
@ -704,19 +717,25 @@ impl FakeLanguageServer {
}
}
pub fn handle_request<T, F>(
pub fn handle_request<T, F, Fut>(
&mut self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> T::Result,
F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = T::Result>,
{
use futures::FutureExt as _;
let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded();
self.handlers.lock().insert(
T::METHOD,
Box::new(move |id, params, cx| {
let result = handler(serde_json::from_slice::<T::Params>(params).unwrap(), cx);
let responded_tx = responded_tx.clone();
async move {
let result = result.await;
let result = serde_json::to_string(&result).unwrap();
let result = serde_json::from_str::<&RawValue>(&result).unwrap();
let response = AnyResponse {
@ -726,6 +745,8 @@ impl FakeLanguageServer {
};
responded_tx.unbounded_send(()).ok();
serde_json::to_vec(&response).unwrap()
}
.boxed()
}),
);
responded_rx
@ -844,7 +865,7 @@ mod tests {
"file://b/c"
);
fake.handle_request::<request::Shutdown, _>(|_, _| ());
fake.handle_request::<request::Shutdown, _, _>(|_, _| async move {});
drop(server);
fake.receive_notification::<notification::Exit>().await;

View file

@ -69,7 +69,7 @@ impl View for OutlineView {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
Flex::new(Axis::Vertical)
.with_child(
@ -124,9 +124,12 @@ impl OutlineView {
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
{
let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(Some(
cx.app_state::<Settings>().theme.editor.syntax.as_ref(),
));
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.read(cx)
.outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
if let Some(outline) = buffer {
workspace.toggle_modal(cx, |cx, _| {
let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
@ -221,7 +224,7 @@ impl OutlineView {
) {
match event {
editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::Edited => self.update_matches(cx),
editor::Event::BufferEdited { .. } => self.update_matches(cx),
_ => {}
}
}
@ -288,7 +291,7 @@ impl OutlineView {
fn render_matches(&self, cx: &AppContext) -> ElementBox {
if self.matches.is_empty() {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
return Container::new(
Label::new(
"No matches".into(),
@ -330,7 +333,7 @@ impl OutlineView {
index: usize,
cx: &AppContext,
) -> ElementBox {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
let style = if index == self.selected_match_index {
&settings.theme.selector.active_item
} else {

View file

@ -11,15 +11,15 @@ use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
UpgradeModelHandle, WeakModelHandle,
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
};
use language::{
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry,
LocalFile, OffsetRangeExt, Operation, PointUtf16, TextBufferSnapshot, ToLspPosition, ToOffset,
ToPointUtf16, Transaction,
LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition,
ToOffset, ToPointUtf16, Transaction,
};
use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
use lsp_command::*;
@ -39,7 +39,7 @@ use std::{
path::{Component, Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, AtomicUsize},
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
time::Instant,
@ -49,9 +49,13 @@ use util::{post_inc, ResultExt, TryFutureExt as _};
pub use fs::*;
pub use worktree::*;
pub trait Item: Entity {
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
}
pub struct Project {
worktrees: Vec<WorktreeHandle>,
active_entry: Option<ProjectEntry>,
active_entry: Option<ProjectEntryId>,
languages: Arc<LanguageRegistry>,
language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
@ -114,12 +118,14 @@ pub struct Collaborator {
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
ActiveEntryChanged(Option<ProjectEntry>),
ActiveEntryChanged(Option<ProjectEntryId>),
WorktreeRemoved(WorktreeId),
DiskBasedDiagnosticsStarted,
DiskBasedDiagnosticsUpdated,
DiskBasedDiagnosticsFinished,
DiagnosticsUpdated(ProjectPath),
RemoteIdChanged(Option<u64>),
CollaboratorLeft(PeerId),
}
enum LanguageServerEvent {
@ -226,42 +232,58 @@ impl DiagnosticSummary {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ProjectEntry {
pub worktree_id: WorktreeId,
pub entry_id: usize,
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProjectEntryId(usize);
impl ProjectEntryId {
pub fn new(counter: &AtomicUsize) -> Self {
Self(counter.fetch_add(1, SeqCst))
}
pub fn from_proto(id: u64) -> Self {
Self(id as usize)
}
pub fn to_proto(&self) -> u64 {
self.0 as u64
}
pub fn to_usize(&self) -> usize {
self.0
}
}
impl Project {
pub fn init(client: &Arc<Client>) {
client.add_entity_message_handler(Self::handle_add_collaborator);
client.add_entity_message_handler(Self::handle_buffer_reloaded);
client.add_entity_message_handler(Self::handle_buffer_saved);
client.add_entity_message_handler(Self::handle_start_language_server);
client.add_entity_message_handler(Self::handle_update_language_server);
client.add_entity_message_handler(Self::handle_remove_collaborator);
client.add_entity_message_handler(Self::handle_register_worktree);
client.add_entity_message_handler(Self::handle_unregister_worktree);
client.add_entity_message_handler(Self::handle_unshare_project);
client.add_entity_message_handler(Self::handle_update_buffer_file);
client.add_entity_message_handler(Self::handle_update_buffer);
client.add_entity_message_handler(Self::handle_update_diagnostic_summary);
client.add_entity_message_handler(Self::handle_update_worktree);
client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_entity_request_handler(Self::handle_apply_code_action);
client.add_entity_request_handler(Self::handle_format_buffers);
client.add_entity_request_handler(Self::handle_get_code_actions);
client.add_entity_request_handler(Self::handle_get_completions);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
client.add_entity_request_handler(Self::handle_search_project);
client.add_entity_request_handler(Self::handle_get_project_symbols);
client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
client.add_entity_request_handler(Self::handle_open_buffer);
client.add_entity_request_handler(Self::handle_save_buffer);
client.add_model_message_handler(Self::handle_add_collaborator);
client.add_model_message_handler(Self::handle_buffer_reloaded);
client.add_model_message_handler(Self::handle_buffer_saved);
client.add_model_message_handler(Self::handle_start_language_server);
client.add_model_message_handler(Self::handle_update_language_server);
client.add_model_message_handler(Self::handle_remove_collaborator);
client.add_model_message_handler(Self::handle_register_worktree);
client.add_model_message_handler(Self::handle_unregister_worktree);
client.add_model_message_handler(Self::handle_unshare_project);
client.add_model_message_handler(Self::handle_update_buffer_file);
client.add_model_message_handler(Self::handle_update_buffer);
client.add_model_message_handler(Self::handle_update_diagnostic_summary);
client.add_model_message_handler(Self::handle_update_worktree);
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_model_request_handler(Self::handle_apply_code_action);
client.add_model_request_handler(Self::handle_format_buffers);
client.add_model_request_handler(Self::handle_get_code_actions);
client.add_model_request_handler(Self::handle_get_completions);
client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
client.add_model_request_handler(Self::handle_search_project);
client.add_model_request_handler(Self::handle_get_project_symbols);
client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
client.add_model_request_handler(Self::handle_open_buffer_by_id);
client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_save_buffer);
}
pub fn local(
@ -280,31 +302,11 @@ impl Project {
let mut status = rpc.status();
while let Some(status) = status.next().await {
if let Some(this) = this.upgrade(&cx) {
let remote_id = if status.is_connected() {
let response = rpc.request(proto::RegisterProject {}).await?;
Some(response.project_id)
if status.is_connected() {
this.update(&mut cx, |this, cx| this.register(cx)).await?;
} else {
None
};
if let Some(project_id) = remote_id {
let mut registrations = Vec::new();
this.update(&mut cx, |this, cx| {
for worktree in this.worktrees(cx).collect::<Vec<_>>() {
registrations.push(worktree.update(
cx,
|worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
worktree.register(project_id, cx)
},
));
this.update(&mut cx, |this, cx| this.unregister(cx));
}
});
for registration in registrations {
registration.await?;
}
}
this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx));
}
}
Ok(())
@ -355,7 +357,7 @@ impl Project {
fs: Arc<dyn Fs>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
client.authenticate_and_connect(&cx).await?;
client.authenticate_and_connect(true, &cx).await?;
let response = client
.request(proto::JoinProject {
@ -468,7 +470,6 @@ impl Project {
cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
}
#[cfg(any(test, feature = "test-support"))]
pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
self.opened_buffers
.get(&remote_id)
@ -537,16 +538,54 @@ impl Project {
&self.fs
}
fn set_remote_id(&mut self, remote_id: Option<u64>, cx: &mut ModelContext<Self>) {
fn unregister(&mut self, cx: &mut ModelContext<Self>) {
self.unshare(cx);
for worktree in &self.worktrees {
if let Some(worktree) = worktree.upgrade(cx) {
worktree.update(cx, |worktree, _| {
worktree.as_local_mut().unwrap().unregister();
});
}
}
if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
*remote_id_tx.borrow_mut() = remote_id;
*remote_id_tx.borrow_mut() = None;
}
self.subscriptions.clear();
if let Some(remote_id) = remote_id {
self.subscriptions
.push(self.client.add_model_for_remote_entity(remote_id, cx));
}
fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
self.unregister(cx);
let response = self.client.request(proto::RegisterProject {});
cx.spawn(|this, mut cx| async move {
let remote_id = response.await?.project_id;
let mut registrations = Vec::new();
this.update(&mut cx, |this, cx| {
if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state {
*remote_id_tx.borrow_mut() = Some(remote_id);
}
cx.emit(Event::RemoteIdChanged(Some(remote_id)));
this.subscriptions
.push(this.client.add_model_for_remote_entity(remote_id, cx));
for worktree in &this.worktrees {
if let Some(worktree) = worktree.upgrade(cx) {
registrations.push(worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
worktree.register(remote_id, cx)
}));
}
}
});
futures::future::try_join_all(registrations).await?;
Ok(())
})
}
pub fn remote_id(&self) -> Option<u64> {
@ -623,6 +662,24 @@ impl Project {
.find(|worktree| worktree.read(cx).id() == id)
}
pub fn worktree_for_entry(
&self,
entry_id: ProjectEntryId,
cx: &AppContext,
) -> Option<ModelHandle<Worktree>> {
self.worktrees(cx)
.find(|worktree| worktree.read(cx).contains_entry(entry_id))
}
pub fn worktree_id_for_entry(
&self,
entry_id: ProjectEntryId,
cx: &AppContext,
) -> Option<WorktreeId> {
self.worktree_for_entry(entry_id, cx)
.map(|worktree| worktree.read(cx).id())
}
pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let rpc = self.client.clone();
cx.spawn(|this, mut cx| async move {
@ -685,19 +742,35 @@ impl Project {
})
}
pub fn unshare(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn unshare(&mut self, cx: &mut ModelContext<Self>) {
let rpc = self.client.clone();
cx.spawn(|this, mut cx| async move {
let project_id = this.update(&mut cx, |this, cx| {
if let ProjectClientState::Local {
is_shared,
remote_id_rx,
..
} = &mut this.client_state
} = &mut self.client_state
{
*is_shared = false;
if !*is_shared {
return;
}
for open_buffer in this.opened_buffers.values_mut() {
*is_shared = false;
self.collaborators.clear();
self.shared_buffers.clear();
for worktree_handle in self.worktrees.iter_mut() {
if let WorktreeHandle::Strong(worktree) = worktree_handle {
let is_visible = worktree.update(cx, |worktree, _| {
worktree.as_local_mut().unwrap().unshare();
worktree.is_visible()
});
if !is_visible {
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
}
}
}
for open_buffer in self.opened_buffers.values_mut() {
match open_buffer {
OpenBuffer::Strong(buffer) => {
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
@ -706,38 +779,14 @@ impl Project {
}
}
for worktree_handle in this.worktrees.iter_mut() {
match worktree_handle {
WorktreeHandle::Strong(worktree) => {
if !worktree.read(cx).is_visible() {
*worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
}
}
_ => {}
}
if let Some(project_id) = *remote_id_rx.borrow() {
rpc.send(proto::UnshareProject { project_id }).log_err();
}
remote_id_rx
.borrow()
.ok_or_else(|| anyhow!("no project id"))
cx.notify();
} else {
Err(anyhow!("can't share a remote project"))
log::error!("attempted to unshare a remote project");
}
})?;
rpc.send(proto::UnshareProject { project_id })?;
this.update(&mut cx, |this, cx| {
this.collaborators.clear();
this.shared_buffers.clear();
for worktree in this.worktrees(cx).collect::<Vec<_>>() {
worktree.update(cx, |worktree, _| {
worktree.as_local_mut().unwrap().unshare();
});
}
cx.notify()
});
Ok(())
})
}
fn project_unshared(&mut self, cx: &mut ModelContext<Self>) {
@ -785,6 +834,23 @@ impl Project {
Ok(buffer)
}
pub fn open_path(
&mut self,
path: impl Into<ProjectPath>,
cx: &mut ModelContext<Self>,
) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
let task = self.open_buffer(path, cx);
cx.spawn_weak(|_, cx| async move {
let buffer = task.await?;
let project_entry_id = buffer
.read_with(&cx, |buffer, cx| {
File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
})
.ok_or_else(|| anyhow!("no project entry"))?;
Ok((project_entry_id, buffer.into()))
})
}
pub fn open_buffer(
&mut self,
path: impl Into<ProjectPath>,
@ -876,7 +942,7 @@ impl Project {
let path_string = path.to_string_lossy().to_string();
cx.spawn(|this, mut cx| async move {
let response = rpc
.request(proto::OpenBuffer {
.request(proto::OpenBufferByPath {
project_id,
worktree_id: remote_worktree_id.to_proto(),
path: path_string,
@ -925,6 +991,32 @@ impl Project {
})
}
pub fn open_buffer_by_id(
&mut self,
id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
if let Some(buffer) = self.buffer_for_id(id, cx) {
Task::ready(Ok(buffer))
} else if self.is_local() {
Task::ready(Err(anyhow!("buffer {} does not exist", id)))
} else if let Some(project_id) = self.remote_id() {
let request = self
.client
.request(proto::OpenBufferById { project_id, id });
cx.spawn(|this, mut cx| async move {
let buffer = request
.await?
.buffer
.ok_or_else(|| anyhow!("invalid buffer"))?;
this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
.await
})
} else {
Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
}
}
pub fn save_buffer_as(
&mut self,
buffer: ModelHandle<Buffer>,
@ -1096,7 +1188,7 @@ impl Project {
});
cx.background().spawn(request).detach_and_log_err(cx);
}
BufferEvent::Edited => {
BufferEvent::Edited { .. } => {
let language_server = self
.language_server_for_buffer(buffer.read(cx), cx)?
.clone();
@ -1783,38 +1875,23 @@ impl Project {
});
let mut sanitized_diagnostics = Vec::new();
let mut edits_since_save = snapshot
let edits_since_save = Patch::new(
snapshot
.edits_since::<PointUtf16>(buffer.read(cx).saved_version())
.peekable();
let mut last_edit_old_end = PointUtf16::zero();
let mut last_edit_new_end = PointUtf16::zero();
'outer: for entry in diagnostics {
let mut start = entry.range.start;
let mut end = entry.range.end;
.collect(),
);
for entry in diagnostics {
let start;
let end;
if entry.diagnostic.is_disk_based {
// Some diagnostics are based on files on disk instead of buffers'
// current contents. Adjust these diagnostics' ranges to reflect
// any unsaved edits.
if entry.diagnostic.is_disk_based {
while let Some(edit) = edits_since_save.peek() {
if edit.old.end <= start {
last_edit_old_end = edit.old.end;
last_edit_new_end = edit.new.end;
edits_since_save.next();
} else if edit.old.start <= end && edit.old.end >= start {
continue 'outer;
start = edits_since_save.old_to_new(entry.range.start);
end = edits_since_save.old_to_new(entry.range.end);
} else {
break;
}
}
let start_overshoot = start - last_edit_old_end;
start = last_edit_new_end;
start += start_overshoot;
let end_overshoot = end - last_edit_old_end;
end = last_edit_new_end;
end += end_overshoot;
start = entry.range.start;
end = entry.range.end;
}
let mut range = snapshot.clip_point_utf16(start, Bias::Left)
@ -3163,10 +3240,7 @@ impl Project {
let new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
let entry = worktree.read(cx).entry_for_path(project_path.path)?;
Some(ProjectEntry {
worktree_id: project_path.worktree_id,
entry_id: entry.id,
})
Some(entry.id)
});
if new_active_entry != self.active_entry {
self.active_entry = new_active_entry;
@ -3217,10 +3291,25 @@ impl Project {
}
}
pub fn active_entry(&self) -> Option<ProjectEntry> {
pub fn active_entry(&self) -> Option<ProjectEntryId> {
self.active_entry
}
pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<ProjectEntryId> {
self.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.entry_for_path(&path.path)
.map(|entry| entry.id)
}
pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option<ProjectPath> {
let worktree = self.worktree_for_entry(entry_id, cx)?;
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
let path = worktree.entry_for_id(entry_id)?.path.clone();
Some(ProjectPath { worktree_id, path })
}
// RPC message handlers
async fn handle_unshare_project(
@ -3274,6 +3363,7 @@ impl Project {
buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
}
}
cx.emit(Event::CollaboratorLeft(peer_id));
cx.notify();
Ok(())
})
@ -3821,9 +3911,28 @@ impl Project {
hasher.finalize().as_slice().try_into().unwrap()
}
async fn handle_open_buffer(
async fn handle_open_buffer_by_id(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::OpenBuffer>,
envelope: TypedEnvelope<proto::OpenBufferById>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
let peer_id = envelope.original_sender_id()?;
let buffer = this
.update(&mut cx, |this, cx| {
this.open_buffer_by_id(envelope.payload.id, cx)
})
.await?;
this.update(&mut cx, |this, cx| {
Ok(proto::OpenBufferResponse {
buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
})
})
}
async fn handle_open_buffer_by_path(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::OpenBufferByPath>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
@ -4477,6 +4586,12 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf {
components.iter().map(|c| c.as_os_str()).collect()
}
impl Item for Buffer {
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
}
}
#[cfg(test)]
mod tests {
use super::{Event, *};
@ -4897,7 +5012,7 @@ mod tests {
}
#[gpui::test]
async fn test_transforming_disk_based_diagnostics(cx: &mut gpui::TestAppContext) {
async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake();
@ -5122,11 +5237,13 @@ mod tests {
buffer.update(cx, |buffer, cx| {
buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx);
buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
buffer.edit(Some(Point::new(3, 10)..Point::new(3, 10)), "xxx", cx);
});
let change_notification_2 =
fake_server.receive_notification::<lsp::notification::DidChangeTextDocument>();
let change_notification_2 = fake_server
.receive_notification::<lsp::notification::DidChangeTextDocument>()
.await;
assert!(
change_notification_2.await.text_document.version
change_notification_2.text_document.version
> change_notification_1.text_document.version
);
@ -5134,7 +5251,7 @@ mod tests {
fake_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
version: Some(open_notification.text_document.version),
version: Some(change_notification_2.text_document.version),
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
@ -5174,7 +5291,7 @@ mod tests {
}
},
DiagnosticEntry {
range: Point::new(3, 9)..Point::new(3, 11),
range: Point::new(3, 9)..Point::new(3, 14),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
@ -5672,7 +5789,7 @@ mod tests {
.unwrap();
let mut fake_server = fake_servers.next().await.unwrap();
fake_server.handle_request::<lsp::request::GotoDefinition, _>(move |params, _| {
fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
let params = params.text_document_position_params;
assert_eq!(
params.text_document.uri.to_file_path().unwrap(),
@ -6607,7 +6724,7 @@ mod tests {
project.prepare_rename(buffer.clone(), 7, cx)
});
fake_server
.handle_request::<lsp::request::PrepareRenameRequest, _>(|params, _| {
.handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
assert_eq!(params.position, lsp::Position::new(0, 7));
Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
@ -6626,7 +6743,7 @@ mod tests {
project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
});
fake_server
.handle_request::<lsp::request::Rename, _>(|params, _| {
.handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri.as_str(),
"file:///dir/one.rs"

View file

@ -1,3 +1,5 @@
use crate::ProjectEntryId;
use super::{
fs::{self, Fs},
ignore::IgnoreStack,
@ -39,10 +41,7 @@ use std::{
future::Future,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
sync::{atomic::AtomicUsize, Arc},
time::{Duration, SystemTime},
};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap};
@ -101,7 +100,7 @@ pub struct LocalSnapshot {
abs_path: Arc<Path>,
scan_id: usize,
ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
removed_entry_ids: HashMap<u64, usize>,
removed_entry_ids: HashMap<u64, ProjectEntryId>,
next_entry_id: Arc<AtomicUsize>,
snapshot: Snapshot,
}
@ -712,7 +711,9 @@ impl LocalWorktree {
let worktree = this.as_local_mut().unwrap();
match response {
Ok(_) => {
if worktree.registration == Registration::Pending {
worktree.registration = Registration::Done { project_id };
}
Ok(())
}
Err(error) => {
@ -809,6 +810,11 @@ impl LocalWorktree {
})
}
pub fn unregister(&mut self) {
self.unshare();
self.registration = Registration::None;
}
pub fn unshare(&mut self) {
self.share.take();
}
@ -856,13 +862,16 @@ impl Snapshot {
self.id
}
pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
self.entries_by_id.get(&entry_id, &()).is_some()
}
pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
let mut entries_by_path_edits = Vec::new();
let mut entries_by_id_edits = Vec::new();
for entry_id in update.removed_entries {
let entry_id = entry_id as usize;
let entry = self
.entry_for_id(entry_id)
.entry_for_id(ProjectEntryId::from_proto(entry_id))
.ok_or_else(|| anyhow!("unknown entry"))?;
entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
entries_by_id_edits.push(Edit::Remove(entry.id));
@ -985,7 +994,7 @@ impl Snapshot {
})
}
pub fn entry_for_id(&self, id: usize) -> Option<&Entry> {
pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> {
let entry = self.entries_by_id.get(&id, &())?;
self.entry_for_path(&entry.path)
}
@ -1062,7 +1071,7 @@ impl LocalSnapshot {
other_entries.next();
}
Ordering::Greater => {
removed_entries.push(other_entry.id as u64);
removed_entries.push(other_entry.id.to_proto());
other_entries.next();
}
}
@ -1073,7 +1082,7 @@ impl LocalSnapshot {
self_entries.next();
}
(None, Some(other_entry)) => {
removed_entries.push(other_entry.id as u64);
removed_entries.push(other_entry.id.to_proto());
other_entries.next();
}
(None, None) => break,
@ -1326,7 +1335,7 @@ pub struct File {
pub worktree: ModelHandle<Worktree>,
pub path: Arc<Path>,
pub mtime: SystemTime,
pub(crate) entry_id: Option<usize>,
pub(crate) entry_id: Option<ProjectEntryId>,
pub(crate) is_local: bool,
}
@ -1423,7 +1432,7 @@ impl language::File for File {
fn to_proto(&self) -> rpc::proto::File {
rpc::proto::File {
worktree_id: self.worktree.id() as u64,
entry_id: self.entry_id.map(|entry_id| entry_id as u64),
entry_id: self.entry_id.map(|entry_id| entry_id.to_proto()),
path: self.path.to_string_lossy().into(),
mtime: Some(self.mtime.into()),
}
@ -1490,7 +1499,7 @@ impl File {
worktree,
path: Path::new(&proto.path).into(),
mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
entry_id: proto.entry_id.map(|entry_id| entry_id as usize),
entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
is_local: false,
})
}
@ -1502,11 +1511,15 @@ impl File {
pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId {
self.worktree.read(cx).id()
}
pub fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
self.entry_id
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry {
pub id: usize,
pub id: ProjectEntryId,
pub kind: EntryKind,
pub path: Arc<Path>,
pub inode: u64,
@ -1530,7 +1543,7 @@ impl Entry {
root_char_bag: CharBag,
) -> Self {
Self {
id: next_entry_id.fetch_add(1, SeqCst),
id: ProjectEntryId::new(next_entry_id),
kind: if metadata.is_dir {
EntryKind::PendingDir
} else {
@ -1620,7 +1633,7 @@ impl sum_tree::Summary for EntrySummary {
#[derive(Clone, Debug)]
struct PathEntry {
id: usize,
id: ProjectEntryId,
path: Arc<Path>,
is_ignored: bool,
scan_id: usize,
@ -1635,7 +1648,7 @@ impl sum_tree::Item for PathEntry {
}
impl sum_tree::KeyedItem for PathEntry {
type Key = usize;
type Key = ProjectEntryId;
fn key(&self) -> Self::Key {
self.id
@ -1644,7 +1657,7 @@ impl sum_tree::KeyedItem for PathEntry {
#[derive(Clone, Debug, Default)]
struct PathEntrySummary {
max_id: usize,
max_id: ProjectEntryId,
}
impl sum_tree::Summary for PathEntrySummary {
@ -1655,7 +1668,7 @@ impl sum_tree::Summary for PathEntrySummary {
}
}
impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for usize {
impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) {
*self = summary.max_id;
}
@ -2345,7 +2358,7 @@ impl<'a> Iterator for ChildEntriesIter<'a> {
impl<'a> From<&'a Entry> for proto::Entry {
fn from(entry: &'a Entry) -> Self {
Self {
id: entry.id as u64,
id: entry.id.to_proto(),
is_dir: entry.is_dir(),
path: entry.path.to_string_lossy().to_string(),
inode: entry.inode,
@ -2370,7 +2383,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
};
let path: Arc<Path> = Arc::from(Path::new(&entry.path));
Ok(Entry {
id: entry.id as usize,
id: ProjectEntryId::from_proto(entry.id),
kind,
path: path.clone(),
inode: entry.inode,

View file

@ -9,7 +9,7 @@ use gpui::{
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use project::{Project, ProjectEntry, ProjectPath, Worktree, WorktreeId};
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use std::{
collections::{hash_map, HashMap},
ffi::OsStr,
@ -24,7 +24,7 @@ pub struct ProjectPanel {
project: ModelHandle<Project>,
list: UniformListState,
visible_entries: Vec<(WorktreeId, Vec<usize>)>,
expanded_dir_ids: HashMap<WorktreeId, Vec<usize>>,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
selection: Option<Selection>,
handle: WeakViewHandle<Self>,
}
@ -32,7 +32,7 @@ pub struct ProjectPanel {
#[derive(Copy, Clone)]
struct Selection {
worktree_id: WorktreeId,
entry_id: usize,
entry_id: ProjectEntryId,
index: usize,
}
@ -47,8 +47,8 @@ struct EntryDetails {
action!(ExpandSelectedEntry);
action!(CollapseSelectedEntry);
action!(ToggleExpanded, ProjectEntry);
action!(Open, ProjectEntry);
action!(ToggleExpanded, ProjectEntryId);
action!(Open, ProjectEntryId);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectPanel::expand_selected_entry);
@ -64,10 +64,7 @@ pub fn init(cx: &mut MutableAppContext) {
}
pub enum Event {
OpenedEntry {
worktree_id: WorktreeId,
entry_id: usize,
},
OpenedEntry(ProjectEntryId),
}
impl ProjectPanel {
@ -78,16 +75,16 @@ impl ProjectPanel {
cx.notify();
})
.detach();
cx.subscribe(&project, |this, _, event, cx| match event {
project::Event::ActiveEntryChanged(Some(ProjectEntry {
worktree_id,
entry_id,
})) => {
this.expand_entry(*worktree_id, *entry_id, cx);
this.update_visible_entries(Some((*worktree_id, *entry_id)), cx);
cx.subscribe(&project, |this, project, event, cx| match event {
project::Event::ActiveEntryChanged(Some(entry_id)) => {
if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
{
this.expand_entry(worktree_id, *entry_id, cx);
this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
this.autoscroll();
cx.notify();
}
}
project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id);
this.update_visible_entries(None, cx);
@ -109,16 +106,13 @@ impl ProjectPanel {
this
});
cx.subscribe(&project_panel, move |workspace, _, event, cx| match event {
&Event::OpenedEntry {
worktree_id,
entry_id,
} => {
if let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) {
&Event::OpenedEntry(entry_id) => {
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
workspace
.open_path(
ProjectPath {
worktree_id,
worktree_id: worktree.read(cx).id(),
path: entry.path.clone(),
},
cx,
@ -152,10 +146,7 @@ impl ProjectPanel {
}
}
} else {
let event = Event::OpenedEntry {
worktree_id: worktree.id(),
entry_id: entry.id,
};
let event = Event::OpenedEntry(entry.id);
cx.emit(event);
}
}
@ -193,11 +184,8 @@ impl ProjectPanel {
}
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
let ProjectEntry {
worktree_id,
entry_id,
} = action.0;
let entry_id = action.0;
if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
match expanded_dir_ids.binary_search(&entry_id) {
Ok(ix) => {
@ -211,6 +199,7 @@ impl ProjectPanel {
cx.focus_self();
}
}
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(selection) = self.selection {
@ -229,10 +218,7 @@ impl ProjectPanel {
}
fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) {
cx.emit(Event::OpenedEntry {
worktree_id: action.0.worktree_id,
entry_id: action.0.entry_id,
});
cx.emit(Event::OpenedEntry(action.0));
}
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
@ -313,7 +299,7 @@ impl ProjectPanel {
fn update_visible_entries(
&mut self,
new_selected_entry: Option<(WorktreeId, usize)>,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>,
) {
let worktrees = self
@ -379,7 +365,7 @@ impl ProjectPanel {
fn expand_entry(
&mut self,
worktree_id: WorktreeId,
entry_id: usize,
entry_id: ProjectEntryId,
cx: &mut ViewContext<Self>,
) {
let project = self.project.read(cx);
@ -411,7 +397,7 @@ impl ProjectPanel {
&self,
range: Range<usize>,
cx: &mut ViewContext<ProjectPanel>,
mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext<ProjectPanel>),
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
) {
let mut ix = 0;
for (worktree_id, visible_worktree_entries) in &self.visible_entries {
@ -450,11 +436,7 @@ impl ProjectPanel {
e.worktree_id == snapshot.id() && e.entry_id == entry.id
}),
};
let entry = ProjectEntry {
worktree_id: snapshot.id(),
entry_id: entry.id,
};
callback(entry, details, cx);
callback(entry.id, details, cx);
}
}
}
@ -463,13 +445,13 @@ impl ProjectPanel {
}
fn render_entry(
entry: ProjectEntry,
entry_id: ProjectEntryId,
details: EntryDetails,
theme: &theme::ProjectPanel,
cx: &mut ViewContext<Self>,
) -> ElementBox {
let is_dir = details.is_dir;
MouseEventHandler::new::<Self, _, _>(entry.entry_id, cx, |state, _| {
MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
let style = match (details.is_selected, state.hovered) {
(false, false) => &theme.entry,
(false, true) => &theme.hovered_entry,
@ -519,9 +501,9 @@ impl ProjectPanel {
})
.on_click(move |cx| {
if is_dir {
cx.dispatch_action(ToggleExpanded(entry))
cx.dispatch_action(ToggleExpanded(entry_id))
} else {
cx.dispatch_action(Open(entry))
cx.dispatch_action(Open(entry_id))
}
})
.with_cursor_style(CursorStyle::PointingHand)
@ -535,7 +517,7 @@ impl View for ProjectPanel {
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
let theme = &cx.app_state::<Settings>().theme.project_panel;
let theme = &cx.global::<Settings>().theme.project_panel;
let mut container_style = theme.container;
let padding = std::mem::take(&mut container_style.padding);
let handle = self.handle.clone();
@ -546,7 +528,7 @@ impl View for ProjectPanel {
.map(|(_, worktree_entries)| worktree_entries.len())
.sum(),
move |range, items, cx| {
let theme = cx.app_state::<Settings>().theme.clone();
let theme = cx.global::<Settings>().theme.clone();
let this = handle.upgrade(cx).unwrap();
this.update(cx.app, |this, cx| {
this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| {
@ -830,13 +812,7 @@ mod tests {
let worktree = worktree.read(cx);
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
panel.toggle_expanded(
&ToggleExpanded(ProjectEntry {
worktree_id: worktree.id(),
entry_id,
}),
cx,
);
panel.toggle_expanded(&ToggleExpanded(entry_id), cx);
return;
}
}

View file

@ -1,6 +1,5 @@
use editor::{
combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label,
Autoscroll, Bias, Editor,
combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor,
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@ -70,7 +69,7 @@ impl View for ProjectSymbolsView {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
Flex::new(Axis::Vertical)
.with_child(
Container::new(ChildView::new(&self.query_editor).boxed())
@ -234,7 +233,7 @@ impl ProjectSymbolsView {
fn render_matches(&self, cx: &AppContext) -> ElementBox {
if self.matches.is_empty() {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
return Container::new(
Label::new(
"No matches".into(),
@ -277,7 +276,7 @@ impl ProjectSymbolsView {
show_worktree_root_name: bool,
cx: &AppContext,
) -> ElementBox {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
let style = if index == self.selected_match_index {
&settings.theme.selector.active_item
} else {
@ -329,7 +328,7 @@ impl ProjectSymbolsView {
) {
match event {
editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::Edited => self.update_matches(cx),
editor::Event::BufferEdited { .. } => self.update_matches(cx),
_ => {}
}
}
@ -346,6 +345,7 @@ impl ProjectSymbolsView {
let buffer = workspace
.project()
.update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
let symbol = symbol.clone();
cx.spawn(|workspace, mut cx| async move {
let buffer = buffer.await?;
@ -353,10 +353,8 @@ impl ProjectSymbolsView {
let position = buffer
.read(cx)
.clip_point_utf16(symbol.range.start, Bias::Left);
let editor = workspace
.open_item(BufferItemHandle(buffer), cx)
.downcast::<Editor>()
.unwrap();
let editor = workspace.open_project_item::<Editor>(buffer, cx);
editor.update(cx, |editor, cx| {
editor.select_ranges(
[position..position],

View file

@ -40,8 +40,9 @@ message Envelope {
StartLanguageServer start_language_server = 33;
UpdateLanguageServer update_language_server = 34;
OpenBuffer open_buffer = 35;
OpenBufferResponse open_buffer_response = 36;
OpenBufferById open_buffer_by_id = 35;
OpenBufferByPath open_buffer_by_path = 36;
OpenBufferResponse open_buffer_response = 37;
UpdateBuffer update_buffer = 38;
UpdateBufferFile update_buffer_file = 39;
SaveBuffer save_buffer = 40;
@ -79,6 +80,11 @@ message Envelope {
GetUsers get_users = 70;
GetUsersResponse get_users_response = 71;
Follow follow = 72;
FollowResponse follow_response = 73;
UpdateFollowers update_followers = 74;
Unfollow unfollow = 75;
}
}
@ -241,12 +247,17 @@ message OpenBufferForSymbolResponse {
Buffer buffer = 1;
}
message OpenBuffer {
message OpenBufferByPath {
uint64 project_id = 1;
uint64 worktree_id = 2;
string path = 3;
}
message OpenBufferById {
uint64 project_id = 1;
uint64 id = 2;
}
message OpenBufferResponse {
Buffer buffer = 1;
}
@ -521,8 +532,77 @@ message UpdateContacts {
repeated Contact contacts = 1;
}
message UpdateDiagnostics {
uint32 replica_id = 1;
uint32 lamport_timestamp = 2;
repeated Diagnostic diagnostics = 3;
}
message Follow {
uint64 project_id = 1;
uint32 leader_id = 2;
}
message FollowResponse {
optional uint64 active_view_id = 1;
repeated View views = 2;
}
message UpdateFollowers {
uint64 project_id = 1;
repeated uint32 follower_ids = 2;
oneof variant {
UpdateActiveView update_active_view = 3;
View create_view = 4;
UpdateView update_view = 5;
}
}
message Unfollow {
uint64 project_id = 1;
uint32 leader_id = 2;
}
// Entities
message UpdateActiveView {
optional uint64 id = 1;
optional uint32 leader_id = 2;
}
message UpdateView {
uint64 id = 1;
optional uint32 leader_id = 2;
oneof variant {
Editor editor = 3;
}
message Editor {
repeated Selection selections = 1;
Anchor scroll_top_anchor = 2;
float scroll_x = 3;
float scroll_y = 4;
}
}
message View {
uint64 id = 1;
optional uint32 leader_id = 2;
oneof variant {
Editor editor = 3;
}
message Editor {
uint64 buffer_id = 1;
repeated Selection selections = 2;
Anchor scroll_top_anchor = 3;
float scroll_x = 4;
float scroll_y = 5;
}
}
message Collaborator {
uint32 peer_id = 1;
uint32 replica_id = 2;
@ -578,17 +658,6 @@ message BufferState {
repeated string completion_triggers = 8;
}
message BufferFragment {
uint32 replica_id = 1;
uint32 local_timestamp = 2;
uint32 lamport_timestamp = 3;
uint32 insertion_offset = 4;
uint32 len = 5;
bool visible = 6;
repeated VectorClockEntry deletions = 7;
repeated VectorClockEntry max_undos = 8;
}
message SelectionSet {
uint32 replica_id = 1;
repeated Selection selections = 2;
@ -614,12 +683,6 @@ enum Bias {
Right = 1;
}
message UpdateDiagnostics {
uint32 replica_id = 1;
uint32 lamport_timestamp = 2;
repeated Diagnostic diagnostics = 3;
}
message Diagnostic {
Anchor start = 1;
Anchor end = 2;

View file

@ -96,7 +96,7 @@ pub struct ConnectionState {
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(30);
pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5);
impl Peer {
pub fn new() -> Arc<Self> {

View file

@ -147,6 +147,8 @@ messages!(
(BufferSaved, Foreground),
(ChannelMessageSent, Foreground),
(Error, Foreground),
(Follow, Foreground),
(FollowResponse, Foreground),
(FormatBuffers, Foreground),
(FormatBuffersResponse, Foreground),
(GetChannelMessages, Foreground),
@ -175,7 +177,8 @@ messages!(
(UpdateLanguageServer, Foreground),
(LeaveChannel, Foreground),
(LeaveProject, Foreground),
(OpenBuffer, Background),
(OpenBufferById, Background),
(OpenBufferByPath, Background),
(OpenBufferForSymbol, Background),
(OpenBufferForSymbolResponse, Background),
(OpenBufferResponse, Background),
@ -195,13 +198,15 @@ messages!(
(SendChannelMessageResponse, Foreground),
(ShareProject, Foreground),
(Test, Foreground),
(Unfollow, Foreground),
(UnregisterProject, Foreground),
(UnregisterWorktree, Foreground),
(UnshareProject, Foreground),
(UpdateBuffer, Background),
(UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground),
(UpdateContacts, Foreground),
(UpdateDiagnosticSummary, Foreground),
(UpdateFollowers, Foreground),
(UpdateWorktree, Foreground),
);
@ -211,6 +216,7 @@ request_messages!(
ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse
),
(Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse),
(GetChannelMessages, GetChannelMessagesResponse),
(GetChannels, GetChannelsResponse),
@ -223,7 +229,8 @@ request_messages!(
(GetUsers, GetUsersResponse),
(JoinChannel, JoinChannelResponse),
(JoinProject, JoinProjectResponse),
(OpenBuffer, OpenBufferResponse),
(OpenBufferById, OpenBufferResponse),
(OpenBufferByPath, OpenBufferResponse),
(OpenBufferForSymbol, OpenBufferForSymbolResponse),
(Ping, Ack),
(PerformRename, PerformRenameResponse),
@ -246,6 +253,7 @@ entity_messages!(
ApplyCompletionAdditionalEdits,
BufferReloaded,
BufferSaved,
Follow,
FormatBuffers,
GetCodeActions,
GetCompletions,
@ -255,7 +263,8 @@ entity_messages!(
GetProjectSymbols,
JoinProject,
LeaveProject,
OpenBuffer,
OpenBufferById,
OpenBufferByPath,
OpenBufferForSymbol,
PerformRename,
PrepareRename,
@ -263,11 +272,13 @@ entity_messages!(
SaveBuffer,
SearchProject,
StartLanguageServer,
Unfollow,
UnregisterWorktree,
UnshareProject,
UpdateBuffer,
UpdateBufferFile,
UpdateDiagnosticSummary,
UpdateFollowers,
UpdateLanguageServer,
RegisterWorktree,
UpdateWorktree,

View file

@ -5,4 +5,4 @@ pub mod proto;
pub use conn::Connection;
pub use peer::*;
pub const PROTOCOL_VERSION: u32 = 11;
pub const PROTOCOL_VERSION: u32 = 12;

View file

@ -8,7 +8,7 @@ use gpui::{
use language::OffsetRangeExt;
use project::search::SearchQuery;
use std::ops::Range;
use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace};
use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace};
action!(Deploy, bool);
action!(Dismiss);
@ -66,7 +66,7 @@ impl View for SearchBar {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.app_state::<Settings>().theme.clone();
let theme = cx.global::<Settings>().theme.clone();
let editor_container = if self.query_contains_error {
theme.search.invalid_editor
} else {
@ -126,7 +126,7 @@ impl View for SearchBar {
impl Toolbar for SearchBar {
fn active_item_changed(
&mut self,
item: Option<Box<dyn ItemViewHandle>>,
item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> bool {
self.active_editor_subscription.take();
@ -197,7 +197,7 @@ impl SearchBar {
) -> ElementBox {
let is_active = self.is_search_option_enabled(search_option);
MouseEventHandler::new::<Self, _, _>(search_option as usize, cx, |state, cx| {
let theme = &cx.app_state::<Settings>().theme.search;
let theme = &cx.global::<Settings>().theme.search;
let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button,
@ -222,7 +222,7 @@ impl SearchBar {
) -> ElementBox {
enum NavButton {}
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
let theme = &cx.app_state::<Settings>().theme.search;
let theme = &cx.global::<Settings>().theme.search;
let style = if state.hovered {
&theme.hovered_option_button
} else {
@ -336,11 +336,9 @@ impl SearchBar {
direction,
&editor.buffer().read(cx).read(cx),
);
editor.select_ranges(
[ranges[new_index].clone()],
Some(Autoscroll::Fit),
cx,
);
let range_to_select = ranges[new_index].clone();
editor.unfold_ranges([range_to_select.clone()], false, cx);
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
}
});
}
@ -360,7 +358,7 @@ impl SearchBar {
cx: &mut ViewContext<Self>,
) {
match event {
editor::Event::Edited => {
editor::Event::BufferEdited { .. } => {
self.query_contains_error = false;
self.clear_matches(cx);
self.update_matches(true, cx);
@ -377,8 +375,8 @@ impl SearchBar {
cx: &mut ViewContext<Self>,
) {
match event {
editor::Event::Edited => self.update_matches(false, cx),
editor::Event::SelectionsChanged => self.update_match_index(cx),
editor::Event::BufferEdited { .. } => self.update_matches(false, cx),
editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
_ => {}
}
}
@ -475,7 +473,7 @@ impl SearchBar {
}
}
let theme = &cx.app_state::<Settings>().theme.search;
let theme = &cx.global::<Settings>().theme.search;
editor.highlight_background::<Self>(
ranges,
theme.match_background,
@ -510,8 +508,9 @@ impl SearchBar {
#[cfg(test)]
mod tests {
use super::*;
use editor::{DisplayPoint, Editor, MultiBuffer};
use editor::{DisplayPoint, Editor};
use gpui::{color::Color, TestAppContext};
use language::Buffer;
use std::sync::Arc;
use unindent::Unindent as _;
@ -521,11 +520,12 @@ mod tests {
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
theme.search.match_background = Color::red();
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
cx.update(|cx| cx.add_app_state(settings));
cx.update(|cx| cx.set_global(settings));
let buffer = cx.update(|cx| {
MultiBuffer::build_simple(
&r#"
let buffer = cx.add_model(|cx| {
Buffer::new(
0,
r#"
A regular expression (shortened as regex or regexp;[1] also referred to as
rational expression[2][3]) is a sequence of characters that specifies a search
pattern in text. Usually such patterns are used by string-searching algorithms

View file

@ -7,7 +7,7 @@ use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
use gpui::{
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
ViewHandle, WeakModelHandle,
ViewHandle, WeakModelHandle, WeakViewHandle,
};
use project::{search::SearchQuery, Project};
use std::{
@ -16,7 +16,7 @@ use std::{
path::PathBuf,
};
use util::ResultExt as _;
use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace};
use workspace::{Item, ItemNavHistory, Settings, Workspace};
action!(Deploy);
action!(Search);
@ -26,10 +26,10 @@ action!(ToggleFocus);
const MAX_TAB_TITLE_LEN: usize = 24;
#[derive(Default)]
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakModelHandle<ProjectSearch>>);
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
pub fn init(cx: &mut MutableAppContext) {
cx.add_app_state(ActiveSearches::default());
cx.set_global(ActiveSearches::default());
cx.add_bindings([
Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
@ -139,23 +139,6 @@ impl ProjectSearch {
}
}
impl Item for ProjectSearch {
type View = ProjectSearchView;
fn build_view(
model: ModelHandle<Self>,
_: &Workspace,
nav_history: ItemNavHistory,
cx: &mut gpui::ViewContext<Self::View>,
) -> Self::View {
ProjectSearchView::new(model, Some(nav_history), cx)
}
fn project_path(&self) -> Option<project::ProjectPath> {
None
}
}
enum ViewEvent {
UpdateTab,
}
@ -172,7 +155,7 @@ impl View for ProjectSearchView {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let model = &self.model.read(cx);
let results = if model.match_ranges.is_empty() {
let theme = &cx.app_state::<Settings>().theme;
let theme = &cx.global::<Settings>().theme;
let text = if self.query_editor.read(cx).text(cx).is_empty() {
""
} else if model.pending_search.is_some() {
@ -199,11 +182,11 @@ impl View for ProjectSearchView {
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.update_app_state(|state: &mut ActiveSearches, cx| {
state.0.insert(
self.model.read(cx).project.downgrade(),
self.model.downgrade(),
)
let handle = cx.weak_handle();
cx.update_global(|state: &mut ActiveSearches, cx| {
state
.0
.insert(self.model.read(cx).project.downgrade(), handle)
});
if self.model.read(cx).match_ranges.is_empty() {
@ -214,7 +197,7 @@ impl View for ProjectSearchView {
}
}
impl ItemView for ProjectSearchView {
impl Item for ProjectSearchView {
fn act_as_type(
&self,
type_id: TypeId,
@ -235,12 +218,8 @@ impl ItemView for ProjectSearchView {
.update(cx, |editor, cx| editor.deactivated(cx));
}
fn item(&self, _: &gpui::AppContext) -> Box<dyn ItemHandle> {
Box::new(self.model.clone())
}
fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
let search_theme = &settings.theme.search;
Flex::row()
.with_child(
@ -271,6 +250,10 @@ impl ItemView for ProjectSearchView {
None
}
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
None
}
fn can_save(&self, _: &gpui::AppContext) -> bool {
true
}
@ -305,21 +288,23 @@ impl ItemView for ProjectSearchView {
unreachable!("save_as should not have been called")
}
fn clone_on_split(
&self,
nav_history: ItemNavHistory,
cx: &mut ViewContext<Self>,
) -> Option<Self>
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
let model = self.model.update(cx, |model, cx| model.clone(cx));
Some(Self::new(model, Some(nav_history), cx))
Some(Self::new(model, cx))
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
self.results_editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
self.results_editor
.update(cx, |editor, cx| editor.navigate(data, cx));
.update(cx, |editor, cx| editor.navigate(data, cx))
}
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
@ -328,11 +313,7 @@ impl ItemView for ProjectSearchView {
}
impl ProjectSearchView {
fn new(
model: ModelHandle<ProjectSearch>,
nav_history: Option<ItemNavHistory>,
cx: &mut ViewContext<Self>,
) -> Self {
fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
let project;
let excerpts;
let mut query_text = String::new();
@ -362,15 +343,14 @@ impl ProjectSearchView {
});
let results_editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(excerpts, Some(project), cx);
let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx);
editor.set_searchable(false);
editor.set_nav_history(nav_history);
editor
});
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
.detach();
cx.subscribe(&results_editor, |this, _, event, cx| {
if matches!(event, editor::Event::SelectionsChanged) {
if matches!(event, editor::Event::SelectionsChanged { .. }) {
this.update_match_index(cx);
}
})
@ -394,28 +374,31 @@ impl ProjectSearchView {
// If no search exists in the workspace, create a new one.
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
// Clean up entries for dropped projects
cx.update_app_state(|state: &mut ActiveSearches, cx| {
cx.update_global(|state: &mut ActiveSearches, cx| {
state.0.retain(|project, _| project.is_upgradable(cx))
});
let active_search = cx
.app_state::<ActiveSearches>()
.global::<ActiveSearches>()
.0
.get(&workspace.project().downgrade());
let existing = active_search
.and_then(|active_search| {
workspace
.items_of_type::<ProjectSearch>(cx)
.items_of_type::<ProjectSearchView>(cx)
.find(|search| search == active_search)
})
.or_else(|| workspace.item_of_type::<ProjectSearch>(cx));
.or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
if let Some(existing) = existing {
workspace.activate_item(&existing, cx);
} else {
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
workspace.open_item(model, cx);
workspace.add_item(
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
cx,
);
}
}
@ -450,7 +433,10 @@ impl ProjectSearchView {
model.search(new_query, cx);
model
});
workspace.open_item(model, cx);
workspace.add_item(
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
cx,
);
}
}
}
@ -503,6 +489,7 @@ impl ProjectSearchView {
);
let range_to_select = model.match_ranges[new_index].clone();
self.results_editor.update(cx, |editor, cx| {
editor.unfold_ranges([range_to_select.clone()], false, cx);
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
});
}
@ -552,7 +539,7 @@ impl ProjectSearchView {
if reset_selections {
editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
}
let theme = &cx.app_state::<Settings>().theme.search;
let theme = &cx.global::<Settings>().theme.search;
editor.highlight_background::<Self>(match_ranges, theme.match_background, cx);
});
if self.query_editor.is_focused(cx) {
@ -578,7 +565,7 @@ impl ProjectSearchView {
}
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.app_state::<Settings>().theme.clone();
let theme = cx.global::<Settings>().theme.clone();
let editor_container = if self.query_contains_error {
theme.search.invalid_editor
} else {
@ -642,7 +629,7 @@ impl ProjectSearchView {
) -> ElementBox {
let is_active = self.is_option_enabled(option);
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
let theme = &cx.app_state::<Settings>().theme.search;
let theme = &cx.global::<Settings>().theme.search;
let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button,
@ -675,7 +662,7 @@ impl ProjectSearchView {
) -> ElementBox {
enum NavButton {}
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
let theme = &cx.app_state::<Settings>().theme.search;
let theme = &cx.global::<Settings>().theme.search;
let style = if state.hovered {
&theme.hovered_option_button
} else {
@ -707,7 +694,7 @@ mod tests {
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
theme.search.match_background = Color::red();
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
cx.update(|cx| cx.add_app_state(settings));
cx.update(|cx| cx.set_global(settings));
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@ -732,7 +719,7 @@ mod tests {
let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
let search_view = cx.add_view(Default::default(), |cx| {
ProjectSearchView::new(search.clone(), None, cx)
ProjectSearchView::new(search.clone(), cx)
});
search_view.update(cx, |search_view, cx| {

View file

@ -39,9 +39,9 @@ pub(crate) fn active_match_index(
None
} else {
match ranges.binary_search_by(|probe| {
if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() {
if probe.end.cmp(&cursor, &*buffer).is_lt() {
Ordering::Less
} else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() {
} else if probe.start.cmp(&cursor, &*buffer).is_gt() {
Ordering::Greater
} else {
Ordering::Equal
@ -59,7 +59,7 @@ pub(crate) fn match_index_for_direction(
direction: Direction,
buffer: &MultiBufferSnapshot,
) -> usize {
if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() {
if ranges[index].start.cmp(&cursor, &buffer).is_gt() {
if direction == Direction::Prev {
if index == 0 {
index = ranges.len() - 1;
@ -67,7 +67,7 @@ pub(crate) fn match_index_for_direction(
index -= 1;
}
}
} else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() {
} else if ranges[index].end.cmp(&cursor, &buffer).is_lt() {
if direction == Direction::Next {
index = 0;
}

File diff suppressed because it is too large Load diff

View file

@ -12,23 +12,19 @@ pub struct Anchor {
}
impl Anchor {
pub fn min() -> Self {
Self {
pub const MIN: Self = Self {
timestamp: clock::Local::MIN,
offset: usize::MIN,
bias: Bias::Left,
}
}
};
pub fn max() -> Self {
Self {
pub const MAX: Self = Self {
timestamp: clock::Local::MAX,
offset: usize::MAX,
bias: Bias::Right,
}
}
};
pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Result<Ordering> {
pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering {
let fragment_id_comparison = if self.timestamp == other.timestamp {
Ordering::Equal
} else {
@ -37,9 +33,25 @@ impl Anchor {
.cmp(&buffer.fragment_id_for_anchor(other))
};
Ok(fragment_id_comparison
fragment_id_comparison
.then_with(|| self.offset.cmp(&other.offset))
.then_with(|| self.bias.cmp(&other.bias)))
.then_with(|| self.bias.cmp(&other.bias))
}
pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
if self.cmp(other, buffer).is_le() {
self.clone()
} else {
other.clone()
}
}
pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
if self.cmp(other, buffer).is_ge() {
self.clone()
} else {
other.clone()
}
}
pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor {
@ -105,8 +117,8 @@ pub trait AnchorRangeExt {
impl AnchorRangeExt for Range<Anchor> {
fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> {
Ok(match self.start.cmp(&other.start, buffer)? {
Ordering::Equal => other.end.cmp(&self.end, buffer)?,
Ok(match self.start.cmp(&other.start, buffer) {
Ordering::Equal => other.end.cmp(&self.end, buffer),
ord @ _ => ord,
})
}

View file

@ -199,6 +199,28 @@ where
self.0.push(edit);
}
}
pub fn old_to_new(&self, old: T) -> T {
let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) {
Ok(ix) => ix,
Err(ix) => {
if ix == 0 {
return old;
} else {
ix - 1
}
}
};
if let Some(edit) = self.0.get(ix) {
if old >= edit.old.end {
edit.new.end + (old - edit.old.end)
} else {
edit.new.start
}
} else {
old
}
}
}
impl<T: Clone> IntoIterator for Patch<T> {
@ -399,26 +421,6 @@ mod tests {
);
}
// #[test]
// fn test_compose_edits() {
// assert_eq!(
// compose_edits(
// &Edit {
// old: 3..3,
// new: 3..6,
// },
// &Edit {
// old: 2..7,
// new: 2..4,
// },
// ),
// Edit {
// old: 2..4,
// new: 2..4
// }
// );
// }
#[gpui::test]
fn test_two_new_edits_touching_one_old_edit() {
assert_patch_composition(
@ -455,6 +457,30 @@ mod tests {
);
}
#[gpui::test]
fn test_old_to_new() {
let patch = Patch(vec![
Edit {
old: 2..4,
new: 2..4,
},
Edit {
old: 7..8,
new: 7..11,
},
]);
assert_eq!(patch.old_to_new(0), 0);
assert_eq!(patch.old_to_new(1), 1);
assert_eq!(patch.old_to_new(2), 2);
assert_eq!(patch.old_to_new(3), 2);
assert_eq!(patch.old_to_new(4), 4);
assert_eq!(patch.old_to_new(5), 5);
assert_eq!(patch.old_to_new(6), 6);
assert_eq!(patch.old_to_new(7), 7);
assert_eq!(patch.old_to_new(8), 11);
assert_eq!(patch.old_to_new(9), 12);
}
#[gpui::test(iterations = 100)]
fn test_random_patch_compositions(mut rng: StdRng) {
let operations = env::var("OPERATIONS")

View file

@ -1,5 +1,5 @@
use crate::Anchor;
use crate::{rope::TextDimension, BufferSnapshot, ToOffset, ToPoint};
use crate::{rope::TextDimension, BufferSnapshot};
use std::cmp::Ordering;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
@ -18,6 +18,12 @@ pub struct Selection<T> {
pub goal: SelectionGoal,
}
impl Default for SelectionGoal {
fn default() -> Self {
Self::None
}
}
impl<T: Clone> Selection<T> {
pub fn head(&self) -> T {
if self.reversed {
@ -34,14 +40,27 @@ impl<T: Clone> Selection<T> {
self.start.clone()
}
}
pub fn map<F, S>(&self, f: F) -> Selection<S>
where
F: Fn(T) -> S,
{
Selection::<S> {
id: self.id,
start: f(self.start.clone()),
end: f(self.end.clone()),
reversed: self.reversed,
goal: self.goal,
}
}
}
impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
impl<T: Copy + Ord> Selection<T> {
pub fn is_empty(&self) -> bool {
self.start == self.end
}
pub fn set_head(&mut self, head: T) {
pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) {
if head.cmp(&self.tail()) < Ordering::Equal {
if !self.reversed {
self.end = self.start;
@ -55,6 +74,14 @@ impl<T: ToOffset + ToPoint + Copy + Ord> Selection<T> {
}
self.end = head;
}
self.goal = new_goal;
}
pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) {
self.start = point;
self.end = point;
self.goal = new_goal;
self.reversed = false;
}
}

View file

@ -340,59 +340,41 @@ fn test_anchors() {
let anchor_at_offset_2 = buffer.anchor_before(2);
assert_eq!(
anchor_at_offset_0
.cmp(&anchor_at_offset_0, &buffer)
.unwrap(),
anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer),
Ordering::Equal
);
assert_eq!(
anchor_at_offset_1
.cmp(&anchor_at_offset_1, &buffer)
.unwrap(),
anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer),
Ordering::Equal
);
assert_eq!(
anchor_at_offset_2
.cmp(&anchor_at_offset_2, &buffer)
.unwrap(),
anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer),
Ordering::Equal
);
assert_eq!(
anchor_at_offset_0
.cmp(&anchor_at_offset_1, &buffer)
.unwrap(),
anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer),
Ordering::Less
);
assert_eq!(
anchor_at_offset_1
.cmp(&anchor_at_offset_2, &buffer)
.unwrap(),
anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer),
Ordering::Less
);
assert_eq!(
anchor_at_offset_0
.cmp(&anchor_at_offset_2, &buffer)
.unwrap(),
anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer),
Ordering::Less
);
assert_eq!(
anchor_at_offset_1
.cmp(&anchor_at_offset_0, &buffer)
.unwrap(),
anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer),
Ordering::Greater
);
assert_eq!(
anchor_at_offset_2
.cmp(&anchor_at_offset_1, &buffer)
.unwrap(),
anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer),
Ordering::Greater
);
assert_eq!(
anchor_at_offset_2
.cmp(&anchor_at_offset_0, &buffer)
.unwrap(),
anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer),
Ordering::Greater
);
}

View file

@ -1318,8 +1318,8 @@ impl Buffer {
let mut futures = Vec::new();
for anchor in anchors {
if !self.version.observed(anchor.timestamp)
&& *anchor != Anchor::max()
&& *anchor != Anchor::min()
&& *anchor != Anchor::MAX
&& *anchor != Anchor::MIN
{
let (tx, rx) = oneshot::channel();
self.edit_id_resolvers
@ -1638,9 +1638,9 @@ impl BufferSnapshot {
let mut position = D::default();
anchors.map(move |anchor| {
if *anchor == Anchor::min() {
if *anchor == Anchor::MIN {
return D::default();
} else if *anchor == Anchor::max() {
} else if *anchor == Anchor::MAX {
return D::from_text_summary(&self.visible_text.summary());
}
@ -1680,9 +1680,9 @@ impl BufferSnapshot {
where
D: TextDimension,
{
if *anchor == Anchor::min() {
if *anchor == Anchor::MIN {
D::default()
} else if *anchor == Anchor::max() {
} else if *anchor == Anchor::MAX {
D::from_text_summary(&self.visible_text.summary())
} else {
let anchor_key = InsertionFragmentKey {
@ -1718,9 +1718,9 @@ impl BufferSnapshot {
}
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
if *anchor == Anchor::min() {
if *anchor == Anchor::MIN {
&locator::MIN
} else if *anchor == Anchor::max() {
} else if *anchor == Anchor::MAX {
&locator::MAX
} else {
let anchor_key = InsertionFragmentKey {
@ -1758,9 +1758,9 @@ impl BufferSnapshot {
pub fn anchor_at<T: ToOffset>(&self, position: T, bias: Bias) -> Anchor {
let offset = position.to_offset(self);
if bias == Bias::Left && offset == 0 {
Anchor::min()
Anchor::MIN
} else if bias == Bias::Right && offset == self.len() {
Anchor::max()
Anchor::MAX
} else {
let mut fragment_cursor = self.fragments.cursor::<usize>();
fragment_cursor.seek(&offset, bias, &None);
@ -1775,9 +1775,7 @@ impl BufferSnapshot {
}
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
*anchor == Anchor::min()
|| *anchor == Anchor::max()
|| self.version.observed(anchor.timestamp)
*anchor == Anchor::MIN || *anchor == Anchor::MAX || self.version.observed(anchor.timestamp)
}
pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
@ -1799,7 +1797,7 @@ impl BufferSnapshot {
where
D: TextDimension + Ord,
{
self.edits_since_in_range(since, Anchor::min()..Anchor::max())
self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX)
}
pub fn edited_ranges_for_transaction<'a, D>(

View file

@ -35,6 +35,8 @@ pub struct Workspace {
pub tab: Tab,
pub active_tab: Tab,
pub pane_divider: Border,
pub leader_border_opacity: f32,
pub leader_border_width: f32,
pub left_sidebar: Sidebar,
pub right_sidebar: Sidebar,
pub status_bar: StatusBar,

View file

@ -54,7 +54,7 @@ impl ThemeSelector {
cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach();
let original_theme = cx.app_state::<Settings>().theme.clone();
let original_theme = cx.global::<Settings>().theme.clone();
let mut this = Self {
themes: registry,
@ -82,7 +82,7 @@ impl ThemeSelector {
}
fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
let current_theme_name = cx.app_state::<Settings>().theme.name.clone();
let current_theme_name = cx.global::<Settings>().theme.name.clone();
action.0.clear();
match action.0.get(&current_theme_name) {
Ok(theme) => {
@ -204,9 +204,9 @@ impl ThemeSelector {
cx: &mut ViewContext<Self>,
) {
match event {
editor::Event::Edited => {
editor::Event::BufferEdited { .. } => {
self.update_matches(cx);
self.select_if_matching(&cx.app_state::<Settings>().theme.name);
self.select_if_matching(&cx.global::<Settings>().theme.name);
self.show_selected_theme(cx);
}
editor::Event::Blurred => cx.emit(Event::Dismissed),
@ -216,7 +216,7 @@ impl ThemeSelector {
fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.matches.is_empty() {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
return Container::new(
Label::new(
"No matches".into(),
@ -251,7 +251,7 @@ impl ThemeSelector {
}
fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox {
let settings = cx.app_state::<Settings>();
let settings = cx.global::<Settings>();
let theme = &settings.theme;
let container = Container::new(
@ -276,7 +276,7 @@ impl ThemeSelector {
}
fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
cx.update_app_state::<Settings, _, _>(|settings, cx| {
cx.update_global::<Settings, _, _>(|settings, cx| {
settings.theme = theme;
cx.refresh_windows();
});
@ -299,7 +299,7 @@ impl View for ThemeSelector {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.app_state::<Settings>().theme.clone();
let theme = cx.global::<Settings>().theme.clone();
Align::new(
ConstrainedBox::new(
Container::new(

View file

@ -1,4 +1,8 @@
use std::path::{Path, PathBuf};
use std::{
collections::HashMap,
ops::Range,
path::{Path, PathBuf},
};
use tempdir::TempDir;
pub fn temp_tree(tree: serde_json::Value) -> TempDir {
@ -48,3 +52,44 @@ pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
}
text
}
pub fn marked_text_by(
marked_text: &str,
markers: Vec<char>,
) -> (String, HashMap<char, Vec<usize>>) {
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
let mut unmarked_text = String::new();
for char in marked_text.chars() {
if markers.contains(&char) {
let char_offsets = extracted_markers.entry(char).or_insert(Vec::new());
char_offsets.push(unmarked_text.len());
} else {
unmarked_text.push(char);
}
}
(unmarked_text, extracted_markers)
}
pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']);
(unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new))
}
pub fn marked_text_ranges(marked_text: &str) -> (String, Vec<Range<usize>>) {
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['[', ']']);
let opens = markers.remove(&'[').unwrap_or_default();
let closes = markers.remove(&']').unwrap_or_default();
assert_eq!(opens.len(), closes.len(), "marked ranges are unbalanced");
let ranges = opens
.into_iter()
.zip(closes)
.map(|(open, close)| {
assert!(close >= open, "marked ranges must be disjoint");
open..close
})
.collect();
(unmarked_text, ranges)
}

25
crates/vim/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "vim"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/vim.rs"
doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
workspace = { path = "../workspace" }
log = "0.4"
[dev-dependencies]
indoc = "1.0.4"
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -0,0 +1,53 @@
use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased};
use gpui::MutableAppContext;
use crate::{mode::Mode, SwitchMode, VimState};
pub fn init(cx: &mut MutableAppContext) {
cx.subscribe_global(editor_created).detach();
cx.subscribe_global(editor_focused).detach();
cx.subscribe_global(editor_blurred).detach();
cx.subscribe_global(editor_released).detach();
}
fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
cx.update_default_global(|vim_state: &mut VimState, cx| {
vim_state.editors.insert(editor.id(), editor.downgrade());
vim_state.sync_editor_options(cx);
})
}
fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) {
let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
Mode::Insert
} else {
Mode::Normal
};
VimState::update_global(cx, |state, cx| {
state.active_editor = Some(editor.downgrade());
state.switch_mode(&SwitchMode(mode), cx);
});
}
fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
VimState::update_global(cx, |state, cx| {
if let Some(previous_editor) = state.active_editor.clone() {
if previous_editor == editor.clone() {
state.active_editor = None;
}
}
state.sync_editor_options(cx);
})
}
fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) {
cx.update_default_global(|vim_state: &mut VimState, _| {
vim_state.editors.remove(&editor.id());
if let Some(previous_editor) = vim_state.active_editor.clone() {
if previous_editor == editor.clone() {
vim_state.active_editor = None;
}
}
});
}

30
crates/vim/src/insert.rs Normal file
View file

@ -0,0 +1,30 @@
use editor::Bias;
use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
use crate::{mode::Mode, SwitchMode, VimState};
action!(NormalBefore);
pub fn init(cx: &mut MutableAppContext) {
let context = Some("Editor && vim_mode == insert");
cx.add_bindings(vec![
Binding::new("escape", NormalBefore, context),
Binding::new("ctrl-c", NormalBefore, context),
]);
cx.add_action(normal_before);
}
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
state.switch_mode(&SwitchMode(Mode::Normal), cx);
})
}

36
crates/vim/src/mode.rs Normal file
View file

@ -0,0 +1,36 @@
use editor::CursorShape;
use gpui::keymap::Context;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mode {
Normal,
Insert,
}
impl Mode {
pub fn cursor_shape(&self) -> CursorShape {
match self {
Mode::Normal => CursorShape::Block,
Mode::Insert => CursorShape::Bar,
}
}
pub fn keymap_context_layer(&self) -> Context {
let mut context = Context::default();
context.map.insert(
"vim_mode".to_string(),
match self {
Self::Normal => "normal",
Self::Insert => "insert",
}
.to_string(),
);
context
}
}
impl Default for Mode {
fn default() -> Self {
Self::Normal
}
}

66
crates/vim/src/normal.rs Normal file
View file

@ -0,0 +1,66 @@
use editor::{movement, Bias};
use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
use crate::{Mode, SwitchMode, VimState};
action!(InsertBefore);
action!(MoveLeft);
action!(MoveDown);
action!(MoveUp);
action!(MoveRight);
pub fn init(cx: &mut MutableAppContext) {
let context = Some("Editor && vim_mode == normal");
cx.add_bindings(vec![
Binding::new("i", SwitchMode(Mode::Insert), context),
Binding::new("h", MoveLeft, context),
Binding::new("j", MoveDown, context),
Binding::new("k", MoveUp, context),
Binding::new("l", MoveRight, context),
]);
cx.add_action(move_left);
cx.add_action(move_down);
cx.add_action(move_up);
cx.add_action(move_right);
}
fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
})
}
fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, movement::down);
});
});
}
fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, movement::up);
});
});
}
fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.column_mut() += 1;
(map.clip_point(cursor, Bias::Right), SelectionGoal::None)
});
});
});
}

97
crates/vim/src/vim.rs Normal file
View file

@ -0,0 +1,97 @@
mod editor_events;
mod insert;
mod mode;
mod normal;
#[cfg(test)]
mod vim_tests;
use collections::HashMap;
use editor::{CursorShape, Editor};
use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
use mode::Mode;
use workspace::{self, Settings, Workspace};
action!(SwitchMode, Mode);
pub fn init(cx: &mut MutableAppContext) {
editor_events::init(cx);
insert::init(cx);
normal::init(cx);
cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| {
VimState::update_global(cx, |state, cx| state.switch_mode(action, cx))
});
cx.observe_global::<Settings, _>(|settings, cx| {
VimState::update_global(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
})
.detach();
}
#[derive(Default)]
pub struct VimState {
editors: HashMap<usize, WeakViewHandle<Editor>>,
active_editor: Option<WeakViewHandle<Editor>>,
enabled: bool,
mode: Mode,
}
impl VimState {
fn update_global<F, S>(cx: &mut MutableAppContext, update: F) -> S
where
F: FnOnce(&mut Self, &mut MutableAppContext) -> S,
{
cx.update_default_global(update)
}
fn update_active_editor<S>(
&self,
cx: &mut MutableAppContext,
update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
) -> Option<S> {
self.active_editor
.clone()
.and_then(|ae| ae.upgrade(cx))
.map(|ae| ae.update(cx, update))
}
fn switch_mode(&mut self, SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) {
self.mode = *mode;
self.sync_editor_options(cx);
}
fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
if self.enabled != enabled {
self.enabled = enabled;
if enabled {
self.mode = Mode::Normal;
}
self.sync_editor_options(cx);
}
}
fn sync_editor_options(&self, cx: &mut MutableAppContext) {
let mode = self.mode;
let cursor_shape = mode.cursor_shape();
for editor in self.editors.values() {
if let Some(editor) = editor.upgrade(cx) {
editor.update(cx, |editor, cx| {
if self.enabled {
editor.set_cursor_shape(cursor_shape, cx);
editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
editor.set_input_enabled(mode == Mode::Insert);
let context_layer = mode.keymap_context_layer();
editor.set_keymap_context_layer::<Self>(context_layer);
} else {
editor.set_cursor_shape(CursorShape::Bar, cx);
editor.set_clip_at_line_ends(false, cx);
editor.set_input_enabled(true);
editor.remove_keymap_context_layer::<Self>();
}
});
}
}
}
}

253
crates/vim/src/vim_tests.rs Normal file
View file

@ -0,0 +1,253 @@
use indoc::indoc;
use std::ops::Deref;
use editor::{display_map::ToDisplayPoint, DisplayPoint};
use gpui::{json::json, keymap::Keystroke, ViewHandle};
use language::{Point, Selection};
use util::test::marked_text;
use workspace::{WorkspaceHandle, WorkspaceParams};
use crate::*;
#[gpui::test]
async fn test_insert_mode(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestAppContext::new(cx, true, "").await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);
cx.simulate_keystrokes(&["T", "e", "s", "t"]);
cx.assert_newest_selection_head("Test|");
cx.simulate_keystroke("escape");
assert_eq!(cx.mode(), Mode::Normal);
cx.assert_newest_selection_head("Tes|t");
}
#[gpui::test]
async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestAppContext::new(cx, true, "Test\nTestTest\nTest").await;
cx.simulate_keystroke("l");
cx.assert_newest_selection_head(indoc! {"
T|est
TestTest
Test"});
cx.simulate_keystroke("h");
cx.assert_newest_selection_head(indoc! {"
|Test
TestTest
Test"});
cx.simulate_keystroke("j");
cx.assert_newest_selection_head(indoc! {"
Test
|TestTest
Test"});
cx.simulate_keystroke("k");
cx.assert_newest_selection_head(indoc! {"
|Test
TestTest
Test"});
cx.simulate_keystroke("j");
cx.assert_newest_selection_head(indoc! {"
Test
|TestTest
Test"});
// When moving left, cursor does not wrap to the previous line
cx.simulate_keystroke("h");
cx.assert_newest_selection_head(indoc! {"
Test
|TestTest
Test"});
// When moving right, cursor does not reach the line end or wrap to the next line
for _ in 0..9 {
cx.simulate_keystroke("l");
}
cx.assert_newest_selection_head(indoc! {"
Test
TestTes|t
Test"});
// Goal column respects the inability to reach the end of the line
cx.simulate_keystroke("k");
cx.assert_newest_selection_head(indoc! {"
Tes|t
TestTest
Test"});
cx.simulate_keystroke("j");
cx.assert_newest_selection_head(indoc! {"
Test
TestTes|t
Test"});
}
#[gpui::test]
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestAppContext::new(cx, true, "").await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);
// Editor acts as though vim is disabled
cx.disable_vim();
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
cx.assert_newest_selection_head("hjkl|");
// Enabling dynamically sets vim mode again and restores normal mode
cx.enable_vim();
assert_eq!(cx.mode(), Mode::Normal);
cx.simulate_keystrokes(&["h", "h", "h", "l"]);
assert_eq!(cx.editor_text(), "hjkl".to_owned());
cx.assert_newest_selection_head("hj|kl");
cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
cx.assert_newest_selection_head("hjTest|kl");
// Disabling and enabling resets to normal mode
assert_eq!(cx.mode(), Mode::Insert);
cx.disable_vim();
assert_eq!(cx.mode(), Mode::Insert);
cx.enable_vim();
assert_eq!(cx.mode(), Mode::Normal);
}
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestAppContext::new(cx, false, "").await;
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
cx.assert_newest_selection_head("hjkl|");
}
struct VimTestAppContext<'a> {
cx: &'a mut gpui::TestAppContext,
window_id: usize,
editor: ViewHandle<Editor>,
}
impl<'a> VimTestAppContext<'a> {
async fn new(
cx: &'a mut gpui::TestAppContext,
enabled: bool,
initial_editor_text: &str,
) -> VimTestAppContext<'a> {
cx.update(|cx| {
editor::init(cx);
crate::init(cx);
});
let params = cx.update(WorkspaceParams::test);
cx.update(|cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.vim_mode = enabled;
});
});
params
.fs
.as_fake()
.insert_tree(
"/root",
json!({ "dir": { "test.txt": initial_editor_text } }),
)
.await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
params
.project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, cx))
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(cx, |_, cx| cx.focus_self());
Self {
cx,
window_id,
editor,
}
}
fn enable_vim(&mut self) {
self.cx.update(|cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.vim_mode = true;
});
})
}
fn disable_vim(&mut self) {
self.cx.update(|cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.vim_mode = false;
});
})
}
fn newest_selection(&mut self) -> Selection<DisplayPoint> {
self.editor.update(self.cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
editor
.newest_selection::<Point>(cx)
.map(|point| point.to_display_point(&snapshot.display_snapshot))
})
}
fn mode(&mut self) -> Mode {
self.cx.update(|cx| cx.global::<VimState>().mode)
}
fn editor_text(&mut self) -> String {
self.editor
.update(self.cx, |editor, cx| editor.snapshot(cx).text())
}
fn simulate_keystroke(&mut self, keystroke_text: &str) {
let keystroke = Keystroke::parse(keystroke_text).unwrap();
let input = if keystroke.modified() {
None
} else {
Some(keystroke.key.clone())
};
self.cx
.dispatch_keystroke(self.window_id, keystroke, input, false);
}
fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
}
fn assert_newest_selection_head(&mut self, text: &str) {
let (unmarked_text, markers) = marked_text(&text);
assert_eq!(
self.editor_text(),
unmarked_text,
"Unmarked text doesn't match editor text"
);
let newest_selection = self.newest_selection();
let expected_head = self.editor.update(self.cx, |editor, cx| {
markers[0].to_display_point(&editor.snapshot(cx))
});
assert_eq!(newest_selection.head(), expected_head)
}
}
impl<'a> Deref for VimTestAppContext<'a> {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
self.cx
}
}

View file

@ -1,4 +1,4 @@
use crate::{ItemViewHandle, Settings, StatusItemView};
use crate::{ItemHandle, Settings, StatusItemView};
use futures::StreamExt;
use gpui::AppContext;
use gpui::{
@ -116,7 +116,7 @@ impl View for LspStatus {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.app_state::<Settings>().theme;
let theme = &cx.global::<Settings>().theme;
let mut pending_work = self.pending_language_server_work(cx);
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
@ -166,7 +166,7 @@ impl View for LspStatus {
} else if !self.failed.is_empty() {
drop(pending_work);
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
let theme = &cx.app_state::<Settings>().theme;
let theme = &cx.global::<Settings>().theme;
Label::new(
format!(
"Failed to download {} language server{}. Click to dismiss.",
@ -187,5 +187,5 @@ impl View for LspStatus {
}
impl StatusItemView for LspStatus {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemViewHandle>, _: &mut ViewContext<Self>) {}
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
}

View file

@ -1,5 +1,5 @@
use super::{ItemViewHandle, SplitDirection};
use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace};
use super::{ItemHandle, SplitDirection};
use crate::{Item, Settings, WeakItemHandle, Workspace};
use collections::{HashMap, VecDeque};
use gpui::{
action,
@ -7,10 +7,10 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f},
keymap::Binding,
platform::{CursorStyle, NavigationDirection},
AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use project::ProjectPath;
use project::{ProjectEntryId, ProjectPath};
use std::{
any::{Any, TypeId},
cell::RefCell,
@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
pane.activate_item(action.0, cx);
pane.activate_item(action.0, true, cx);
});
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
pane.activate_prev_item(cx);
@ -92,12 +92,13 @@ pub fn init(cx: &mut MutableAppContext) {
pub enum Event {
Activate,
ActivateItem { local: bool },
Remove,
Split(SplitDirection),
}
pub struct Pane {
item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
items: Vec<Box<dyn ItemHandle>>,
active_item_index: usize,
nav_history: Rc<RefCell<NavHistory>>,
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
@ -108,7 +109,7 @@ pub struct Pane {
pub trait Toolbar: View {
fn active_item_changed(
&mut self,
item: Option<Box<dyn ItemViewHandle>>,
item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> bool;
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
@ -117,7 +118,7 @@ pub trait Toolbar: View {
trait ToolbarHandle {
fn active_item_changed(
&self,
item: Option<Box<dyn ItemViewHandle>>,
item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext,
) -> bool;
fn on_dismiss(&self, cx: &mut MutableAppContext);
@ -126,7 +127,7 @@ trait ToolbarHandle {
pub struct ItemNavHistory {
history: Rc<RefCell<NavHistory>>,
item_view: Rc<dyn WeakItemViewHandle>,
item: Rc<dyn WeakItemHandle>,
}
#[derive(Default)]
@ -152,14 +153,14 @@ impl Default for NavigationMode {
}
pub struct NavigationEntry {
pub item_view: Rc<dyn WeakItemViewHandle>,
pub item: Rc<dyn WeakItemHandle>,
pub data: Option<Box<dyn Any>>,
}
impl Pane {
pub fn new() -> Self {
Self {
item_views: Vec::new(),
items: Vec::new(),
active_item_index: 0,
nav_history: Default::default(),
toolbars: Default::default(),
@ -211,40 +212,47 @@ impl Pane {
workspace.activate_pane(pane.clone(), cx);
let to_load = pane.update(cx, |pane, cx| {
loop {
// Retrieve the weak item handle from the history.
let entry = pane.nav_history.borrow_mut().pop(mode)?;
// If the item is still present in this pane, then activate it.
if let Some(index) = entry
.item_view
.item
.upgrade(cx)
.and_then(|v| pane.index_for_item_view(v.as_ref()))
.and_then(|v| pane.index_for_item(v.as_ref()))
{
if let Some(item_view) = pane.active_item() {
if let Some(item) = pane.active_item() {
pane.nav_history.borrow_mut().set_mode(mode);
item_view.deactivated(cx);
item.deactivated(cx);
pane.nav_history
.borrow_mut()
.set_mode(NavigationMode::Normal);
}
pane.active_item_index = index;
let prev_active_index = mem::replace(&mut pane.active_item_index, index);
pane.focus_active_item(cx);
let mut navigated = prev_active_index != pane.active_item_index;
if let Some(data) = entry.data {
pane.active_item()?.navigate(data, cx);
navigated |= pane.active_item()?.navigate(data, cx);
}
if navigated {
cx.notify();
None
break None;
}
}
// If the item is no longer present in this pane, then retrieve its
// project path in order to reopen it.
else {
pane.nav_history
break pane
.nav_history
.borrow_mut()
.paths_by_item
.get(&entry.item_view.id())
.get(&entry.item.id())
.cloned()
.map(|project_path| (project_path, entry))
.map(|project_path| (project_path, entry));
}
}
});
@ -253,18 +261,27 @@ impl Pane {
let pane = pane.downgrade();
let task = workspace.load_path(project_path, cx);
cx.spawn(|workspace, mut cx| async move {
let item = task.await;
let task = task.await;
if let Some(pane) = pane.upgrade(&cx) {
if let Some(item) = item.log_err() {
workspace.update(&mut cx, |workspace, cx| {
pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode));
let item_view = workspace.open_item_in_pane(item, &pane, cx);
pane.update(cx, |p, _| {
p.nav_history.borrow_mut().set_mode(NavigationMode::Normal)
if let Some((project_entry_id, build_item)) = task.log_err() {
pane.update(&mut cx, |pane, _| {
pane.nav_history.borrow_mut().set_mode(mode);
});
let item = workspace.update(&mut cx, |workspace, cx| {
Self::open_item(
workspace,
pane.clone(),
project_entry_id,
cx,
build_item,
)
});
pane.update(&mut cx, |pane, cx| {
pane.nav_history
.borrow_mut()
.set_mode(NavigationMode::Normal);
if let Some(data) = entry.data {
item_view.navigate(data, cx);
item.navigate(data, cx);
}
});
} else {
@ -281,80 +298,115 @@ impl Pane {
}
}
pub fn open_item<T>(
&mut self,
item_handle: T,
workspace: &Workspace,
cx: &mut ViewContext<Self>,
) -> Box<dyn ItemViewHandle>
where
T: 'static + ItemHandle,
{
for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() {
if *item_id == item_handle.id() {
let item_view = item_view.boxed_clone();
self.activate_item(ix, cx);
return item_view;
pub(crate) fn open_item(
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
project_entry_id: ProjectEntryId,
cx: &mut ViewContext<Workspace>,
build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
let existing_item = pane.update(cx, |pane, cx| {
for (ix, item) in pane.items.iter().enumerate() {
if item.project_entry_id(cx) == Some(project_entry_id) {
let item = item.boxed_clone();
pane.activate_item(ix, true, cx);
return Some(item);
}
}
None
});
if let Some(existing_item) = existing_item {
existing_item
} else {
let item = build_item(cx);
Self::add_item(workspace, pane, item.boxed_clone(), true, cx);
item
}
}
let item_view =
item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx);
self.add_item_view(item_view.boxed_clone(), cx);
item_view
}
pub fn add_item_view(
&mut self,
mut item_view: Box<dyn ItemViewHandle>,
cx: &mut ViewContext<Self>,
pub(crate) fn add_item(
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
item: Box<dyn ItemHandle>,
local: bool,
cx: &mut ViewContext<Workspace>,
) {
item_view.added_to_pane(cx);
let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len());
self.item_views
.insert(item_idx, (item_view.item(cx).id(), item_view));
self.activate_item(item_idx, cx);
// Prevent adding the same item to the pane more than once.
if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
return;
}
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
item.added_to_pane(workspace, pane.clone(), cx);
pane.update(cx, |pane, cx| {
let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
pane.items.insert(item_idx, item);
pane.activate_item(item_idx, local, cx);
cx.notify();
});
}
pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
let item_id = item.id();
self.item_views
pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
self.items.iter()
}
pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
self.items
.iter()
.any(|(existing_item_id, _)| *existing_item_id == item_id)
.filter_map(|item| item.to_any().downcast())
}
pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
self.item_views.iter().map(|(_, view)| view)
pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
self.items.get(self.active_item_index).cloned()
}
pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
self.item_views
.get(self.active_item_index)
.map(|(_, view)| view.clone())
pub fn project_entry_id_for_item(
&self,
item: &dyn ItemHandle,
cx: &AppContext,
) -> Option<ProjectEntryId> {
self.items.iter().find_map(|existing| {
if existing.id() == item.id() {
existing.project_entry_id(cx)
} else {
None
}
})
}
pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
self.item_views
.iter()
.position(|(_, i)| i.id() == item_view.id())
pub fn item_for_entry(
&self,
entry_id: ProjectEntryId,
cx: &AppContext,
) -> Option<Box<dyn ItemHandle>> {
self.items.iter().find_map(|item| {
if item.project_entry_id(cx) == Some(entry_id) {
Some(item.boxed_clone())
} else {
None
}
})
}
pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
self.item_views.iter().position(|(id, _)| *id == item.id())
self.items.iter().position(|i| i.id() == item.id())
}
pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
if index < self.item_views.len() {
pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
if index < self.items.len() {
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
if prev_active_item_ix != self.active_item_index
&& prev_active_item_ix < self.item_views.len()
&& prev_active_item_ix < self.items.len()
{
self.item_views[prev_active_item_ix].1.deactivated(cx);
self.items[prev_active_item_ix].deactivated(cx);
cx.emit(Event::ActivateItem { local });
}
self.update_active_toolbar(cx);
if local {
self.focus_active_item(cx);
self.activate(cx);
}
cx.notify();
}
}
@ -363,31 +415,31 @@ impl Pane {
let mut index = self.active_item_index;
if index > 0 {
index -= 1;
} else if self.item_views.len() > 0 {
index = self.item_views.len() - 1;
} else if self.items.len() > 0 {
index = self.items.len() - 1;
}
self.activate_item(index, cx);
self.activate_item(index, true, cx);
}
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
let mut index = self.active_item_index;
if index + 1 < self.item_views.len() {
if index + 1 < self.items.len() {
index += 1;
} else {
index = 0;
}
self.activate_item(index, cx);
self.activate_item(index, true, cx);
}
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
if !self.item_views.is_empty() {
self.close_item(self.item_views[self.active_item_index].1.id(), cx)
if !self.items.is_empty() {
self.close_item(self.items[self.active_item_index].id(), cx)
}
}
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
if !self.item_views.is_empty() {
let active_item_id = self.item_views[self.active_item_index].1.id();
if !self.items.is_empty() {
let active_item_id = self.items[self.active_item_index].id();
self.close_items(cx, |id| id != active_item_id);
}
}
@ -403,10 +455,10 @@ impl Pane {
) {
let mut item_ix = 0;
let mut new_active_item_index = self.active_item_index;
self.item_views.retain(|(_, item_view)| {
if should_close(item_view.id()) {
self.items.retain(|item| {
if should_close(item.id()) {
if item_ix == self.active_item_index {
item_view.deactivated(cx);
item.deactivated(cx);
}
if item_ix < self.active_item_index {
@ -414,10 +466,10 @@ impl Pane {
}
let mut nav_history = self.nav_history.borrow_mut();
if let Some(path) = item_view.project_path(cx) {
nav_history.paths_by_item.insert(item_view.id(), path);
if let Some(path) = item.project_path(cx) {
nav_history.paths_by_item.insert(item.id(), path);
} else {
nav_history.paths_by_item.remove(&item_view.id());
nav_history.paths_by_item.remove(&item.id());
}
item_ix += 1;
@ -428,10 +480,10 @@ impl Pane {
}
});
if self.item_views.is_empty() {
if self.items.is_empty() {
cx.emit(Event::Remove);
} else {
self.active_item_index = cmp::min(new_active_item_index, self.item_views.len() - 1);
self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1);
self.focus_active_item(cx);
self.activate(cx);
}
@ -440,7 +492,7 @@ impl Pane {
cx.notify();
}
fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_item) = self.active_item() {
cx.focus(active_item);
}
@ -500,9 +552,9 @@ impl Pane {
}
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
let active_item = self.item_views.get(self.active_item_index);
let active_item = self.items.get(self.active_item_index);
for (toolbar_type_id, toolbar) in &self.toolbars {
let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx);
let visible = toolbar.active_item_changed(active_item.cloned(), cx);
if Some(*toolbar_type_id) == self.active_toolbar_type {
self.active_toolbar_visible = visible;
}
@ -510,12 +562,12 @@ impl Pane {
}
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.app_state::<Settings>().theme.clone();
let theme = cx.global::<Settings>().theme.clone();
enum Tabs {}
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
let mut row = Flex::row();
for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
for (ix, item) in self.items.iter().enumerate() {
let is_active = ix == self.active_item_index;
row.add_child({
@ -524,7 +576,7 @@ impl Pane {
} else {
theme.workspace.tab.clone()
};
let title = item_view.tab_content(&tab_style, cx);
let title = item.tab_content(&tab_style, cx);
let mut style = if is_active {
theme.workspace.active_tab.clone()
@ -541,9 +593,9 @@ impl Pane {
.with_child(
Align::new({
let diameter = 7.0;
let icon_color = if item_view.has_conflict(cx) {
let icon_color = if item.has_conflict(cx) {
Some(style.icon_conflict)
} else if item_view.is_dirty(cx) {
} else if item.is_dirty(cx) {
Some(style.icon_dirty)
} else {
None
@ -587,7 +639,7 @@ impl Pane {
.with_child(
Align::new(
ConstrainedBox::new(if mouse_state.hovered {
let item_id = item_view.id();
let item_id = item.id();
enum TabCloseButton {}
let icon = Svg::new("icons/x.svg");
MouseEventHandler::new::<TabCloseButton, _, _>(
@ -691,7 +743,7 @@ impl View for Pane {
impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
fn active_item_changed(
&self,
item: Option<Box<dyn ItemViewHandle>>,
item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext,
) -> bool {
self.update(cx, |this, cx| this.active_item_changed(item, cx))
@ -707,10 +759,10 @@ impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
}
impl ItemNavHistory {
pub fn new<T: ItemView>(history: Rc<RefCell<NavHistory>>, item_view: &ViewHandle<T>) -> Self {
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
Self {
history,
item_view: Rc::new(item_view.downgrade()),
item: Rc::new(item.downgrade()),
}
}
@ -719,7 +771,7 @@ impl ItemNavHistory {
}
pub fn push<D: 'static + Any>(&self, data: Option<D>) {
self.history.borrow_mut().push(data, self.item_view.clone());
self.history.borrow_mut().push(data, self.item.clone());
}
}
@ -752,11 +804,7 @@ impl NavHistory {
self.mode = mode;
}
pub fn push<D: 'static + Any>(
&mut self,
data: Option<D>,
item_view: Rc<dyn WeakItemViewHandle>,
) {
pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
match self.mode {
NavigationMode::Disabled => {}
NavigationMode::Normal => {
@ -764,7 +812,7 @@ impl NavHistory {
self.backward_stack.pop_front();
}
self.backward_stack.push_back(NavigationEntry {
item_view,
item,
data: data.map(|data| Box::new(data) as Box<dyn Any>),
});
self.forward_stack.clear();
@ -774,7 +822,7 @@ impl NavHistory {
self.forward_stack.pop_front();
}
self.forward_stack.push_back(NavigationEntry {
item_view,
item,
data: data.map(|data| Box::new(data) as Box<dyn Any>),
});
}
@ -783,7 +831,7 @@ impl NavHistory {
self.backward_stack.pop_front();
}
self.backward_stack.push_back(NavigationEntry {
item_view,
item,
data: data.map(|data| Box::new(data) as Box<dyn Any>),
});
}

View file

@ -1,9 +1,11 @@
use crate::{FollowerStatesByLeader, Pane};
use anyhow::{anyhow, Result};
use gpui::{elements::*, Axis, ViewHandle};
use client::PeerId;
use collections::HashMap;
use gpui::{elements::*, Axis, Border, ViewHandle};
use project::Collaborator;
use theme::Theme;
use crate::Pane;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PaneGroup {
root: Member,
@ -47,8 +49,19 @@ impl PaneGroup {
}
}
pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
self.root.render(theme)
pub(crate) fn render<'a>(
&self,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
) -> ElementBox {
self.root.render(theme, follower_states, collaborators)
}
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
let mut panes = Vec::new();
self.root.collect_panes(&mut panes);
panes
}
}
@ -80,10 +93,50 @@ impl Member {
Member::Axis(PaneAxis { axis, members })
}
pub fn render(&self, theme: &Theme) -> ElementBox {
pub fn render(
&self,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
) -> ElementBox {
match self {
Member::Pane(pane) => ChildView::new(pane).boxed(),
Member::Axis(axis) => axis.render(theme),
Member::Pane(pane) => {
let mut border = Border::default();
let leader = follower_states
.iter()
.find_map(|(leader_id, follower_states)| {
if follower_states.contains_key(pane) {
Some(leader_id)
} else {
None
}
})
.and_then(|leader_id| collaborators.get(leader_id));
if let Some(leader) = leader {
let leader_color = theme
.editor
.replica_selection_style(leader.replica_id)
.cursor;
border = Border::all(theme.workspace.leader_border_width, leader_color);
border
.color
.fade_out(1. - theme.workspace.leader_border_opacity);
border.overlay = true;
}
ChildView::new(pane).contained().with_border(border).boxed()
}
Member::Axis(axis) => axis.render(theme, follower_states, collaborators),
}
}
fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
match self {
Member::Axis(axis) => {
for member in &axis.members {
member.collect_panes(panes);
}
}
Member::Pane(pane) => panes.push(pane),
}
}
}
@ -172,11 +225,16 @@ impl PaneAxis {
}
}
fn render<'a>(&self, theme: &Theme) -> ElementBox {
fn render(
&self,
theme: &Theme,
follower_state: &FollowerStatesByLeader,
collaborators: &HashMap<PeerId, Collaborator>,
) -> ElementBox {
let last_member_ix = self.members.len() - 1;
Flex::new(self.axis)
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
let mut member = member.render(theme);
let mut member = member.render(theme, follower_state, collaborators);
if ix < last_member_ix {
let mut border = theme.workspace.pane_divider;
border.left = false;

View file

@ -17,6 +17,7 @@ use util::ResultExt;
pub struct Settings {
pub buffer_font_family: FamilyId,
pub buffer_font_size: f32,
pub vim_mode: bool,
pub tab_size: usize,
pub soft_wrap: SoftWrap,
pub preferred_line_length: u32,
@ -48,6 +49,8 @@ struct SettingsFileContent {
buffer_font_family: Option<String>,
#[serde(default)]
buffer_font_size: Option<f32>,
#[serde(default)]
vim_mode: Option<bool>,
#[serde(flatten)]
editor: LanguageOverride,
#[serde(default)]
@ -130,6 +133,7 @@ impl Settings {
Ok(Self {
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
buffer_font_size: 15.,
vim_mode: false,
tab_size: 4,
soft_wrap: SoftWrap::None,
preferred_line_length: 80,
@ -174,6 +178,7 @@ impl Settings {
Settings {
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
buffer_font_size: 14.,
vim_mode: false,
tab_size: 4,
soft_wrap: SoftWrap::None,
preferred_line_length: 80,
@ -200,6 +205,7 @@ impl Settings {
}
merge(&mut self.buffer_font_size, data.buffer_font_size);
merge(&mut self.vim_mode, data.vim_mode);
merge(&mut self.soft_wrap, data.editor.soft_wrap);
merge(&mut self.tab_size, data.editor.tab_size);
merge(

View file

@ -1,4 +1,4 @@
use crate::{ItemViewHandle, Pane, Settings};
use crate::{ItemHandle, Pane, Settings};
use gpui::{
elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
View, ViewContext, ViewHandle,
@ -7,7 +7,7 @@ use gpui::{
pub trait StatusItemView: View {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn crate::ItemViewHandle>,
active_pane_item: Option<&dyn crate::ItemHandle>,
cx: &mut ViewContext<Self>,
);
}
@ -16,7 +16,7 @@ trait StatusItemViewHandle {
fn to_any(&self) -> AnyViewHandle;
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemViewHandle>,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext,
);
}
@ -38,7 +38,7 @@ impl View for StatusBar {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
Flex::row()
.with_children(self.left_items.iter().map(|i| {
ChildView::new(i.as_ref())
@ -114,7 +114,7 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemViewHandle>,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext,
) {
self.update(cx, |this, cx| {

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.21.0"
version = "0.23.0"
[lib]
name = "zed"
@ -55,6 +55,7 @@ text = { path = "../text" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
util = { path = "../util" }
vim = { path = "../vim" }
workspace = { path = "../workspace" }
anyhow = "1.0.38"
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
@ -64,6 +65,7 @@ crossbeam-channel = "0.5.0"
ctor = "0.1.20"
dirs = "3.0"
easy-parallel = "3.1.0"
env_logger = "0.8"
futures = "0.3"
http-auth-basic = "0.1.3"
ignore = "0.4"

View file

@ -4,6 +4,8 @@ base = { family = "Zed Sans", size = 14 }
[workspace]
background = "$surface.0"
pane_divider = { width = 1, color = "$border.0" }
leader_border_opacity = 0.7
leader_border_width = 2.0
[workspace.titlebar]
height = 32

View file

@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task};
use log::LevelFilter;
use parking_lot::Mutex;
use project::Fs;
use simplelog::SimpleLogger;
use smol::process::Command;
use std::{env, fs, path::PathBuf, sync::Arc};
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
@ -61,7 +60,6 @@ fn main() {
app.run(move |cx| {
let http = http::client();
let client = client::Client::new(http.clone());
let mut path_openers = Vec::new();
let mut languages = language::build_language_registry(login_shell_env_loaded);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
let channel_list =
@ -70,8 +68,8 @@ fn main() {
project::Project::init(&client);
client::Channel::init(&client);
client::init(client.clone(), cx);
workspace::init(cx);
editor::init(cx, &mut path_openers);
workspace::init(&client, cx);
editor::init(cx);
go_to_line::init(cx);
file_finder::init(cx);
chat_panel::init(cx);
@ -80,11 +78,18 @@ fn main() {
project_panel::init(cx);
diagnostics::init(cx);
search::init(cx);
vim::init(cx);
cx.spawn({
let client = client.clone();
|cx| async move {
if stdout_is_a_pty() {
if client::IMPERSONATE_LOGIN.is_some() {
client.authenticate_and_connect(false, &cx).await?;
}
} else {
if client.has_keychain_credentials(&cx) {
client.authenticate_and_connect(&cx).await?;
client.authenticate_and_connect(true, &cx).await?;
}
}
Ok::<_, anyhow::Error>(())
}
@ -102,7 +107,7 @@ fn main() {
cx.spawn(|mut cx| async move {
while let Some(settings) = settings_rx.next().await {
cx.update(|cx| {
cx.update_app_state(|s, _| *s = settings);
cx.update_global(|s, _| *s = settings);
cx.refresh_windows();
});
}
@ -111,7 +116,7 @@ fn main() {
languages.set_language_server_download_dir(zed::ROOT_PATH.clone());
languages.set_theme(&settings.theme.editor.syntax);
cx.add_app_state(settings);
cx.set_global(settings);
let app_state = Arc::new(AppState {
languages: Arc::new(languages),
@ -120,7 +125,6 @@ fn main() {
client,
user_store,
fs,
path_openers: Arc::from(path_openers),
build_window_options: &build_window_options,
build_workspace: &build_workspace,
});
@ -144,11 +148,10 @@ fn main() {
}
fn init_logger() {
let level = LevelFilter::Info;
if stdout_is_a_pty() {
SimpleLogger::init(level, Default::default()).expect("could not initialize logger");
env_logger::init();
} else {
let level = LevelFilter::Info;
let log_dir_path = dirs::home_dir()
.expect("could not locate home directory for logging")
.join("Library/Logs/");

View file

@ -17,9 +17,8 @@ fn init_logger() {
pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
let settings = Settings::test(cx);
let mut path_openers = Vec::new();
editor::init(cx, &mut path_openers);
cx.add_app_state(settings);
editor::init(cx);
cx.set_global(settings);
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone());
@ -40,7 +39,6 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
client,
user_store,
fs: FakeFs::new(cx.background().clone()),
path_openers: Arc::from(path_openers),
build_window_options: &build_window_options,
build_workspace: &build_workspace,
})

View file

@ -43,7 +43,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_global_action(quit);
cx.add_global_action({
move |action: &AdjustBufferFontSize, cx| {
cx.update_app_state::<Settings, _, _>(|settings, cx| {
cx.update_global::<Settings, _, _>(|settings, cx| {
settings.buffer_font_size =
(settings.buffer_font_size + action.0).max(MIN_FONT_SIZE);
cx.refresh_windows();
@ -111,7 +111,6 @@ pub fn build_workspace(
languages: app_state.languages.clone(),
user_store: app_state.user_store.clone(),
channel_list: app_state.channel_list.clone(),
path_openers: app_state.path_openers.clone(),
};
let mut workspace = Workspace::new(&workspace_params, cx);
let project = workspace.project().clone();
@ -193,7 +192,7 @@ mod tests {
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
use util::test::temp_tree;
use workspace::{
open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
};
#[gpui::test]
@ -253,7 +252,7 @@ mod tests {
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
let app_state = cx.update(test_app_state);
cx.update(|cx| {
workspace::init(cx);
workspace::init(&app_state.client, cx);
});
cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
let window_id = *cx.window_ids().first().unwrap();
@ -325,7 +324,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx),
Some(file1.clone())
);
assert_eq!(pane.item_views().count(), 1);
assert_eq!(pane.items().count(), 1);
});
// Open the second entry
@ -339,7 +338,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx),
Some(file2.clone())
);
assert_eq!(pane.item_views().count(), 2);
assert_eq!(pane.items().count(), 2);
});
// Open the first entry again. The existing pane item is activated.
@ -355,7 +354,7 @@ mod tests {
pane.active_item().unwrap().project_path(cx),
Some(file1.clone())
);
assert_eq!(pane.item_views().count(), 2);
assert_eq!(pane.items().count(), 2);
});
// Split the pane with the first entry, then open the second entry again.
@ -394,7 +393,7 @@ mod tests {
Some(file3.clone())
);
let pane_entries = pane
.item_views()
.items()
.map(|i| i.project_path(cx).unwrap())
.collect::<Vec<_>>();
assert_eq!(pane_entries, &[file1, file2, file3]);
@ -894,6 +893,52 @@ mod tests {
(file3.clone(), DisplayPoint::new(0, 0))
);
// Modify file to remove nav history location, and ensure duplicates are skipped
editor1.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx)
});
for _ in 0..5 {
editor1.update(cx, |editor, cx| {
editor
.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx);
});
editor1.update(cx, |editor, cx| {
editor.select_display_ranges(
&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)],
cx,
)
});
}
editor1.update(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.select_display_ranges(
&[DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)],
cx,
);
editor.insert("", cx);
})
});
editor1.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx)
});
workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
.await;
assert_eq!(
active_location(&workspace, cx),
(file1.clone(), DisplayPoint::new(2, 0))
);
workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
.await;
assert_eq!(
active_location(&workspace, cx),
(file1.clone(), DisplayPoint::new(3, 0))
);
fn active_location(
workspace: &ViewHandle<Workspace>,
cx: &mut TestAppContext,