From 327a2f99673d3b54c24265cf74d9df876b2adbdd Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 30 Oct 2023 17:43:07 -0700 Subject: [PATCH 1/5] Add the entity trait and implement for models, views, subscriptions, and observations --- crates/call2/src/call2.rs | 4 +- crates/client2/src/user.rs | 6 +-- crates/copilot2/src/copilot2.rs | 4 +- crates/gpui2/src/app.rs | 2 +- crates/gpui2/src/app/async_context.rs | 8 ++-- crates/gpui2/src/app/entity_map.rs | 52 +++++++++++++++----- crates/gpui2/src/app/model_context.rs | 69 +++++++++++++++------------ crates/gpui2/src/app/test_context.rs | 4 +- crates/gpui2/src/gpui2.rs | 23 +++++++-- crates/gpui2/src/view.rs | 44 ++++++++++++----- crates/gpui2/src/window.rs | 58 ++++++++++++---------- crates/project2/src/project2.rs | 8 ++-- crates/project2/src/terminals.rs | 2 +- 13 files changed, 183 insertions(+), 101 deletions(-) diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index d8678b7ed4..fd09dc3180 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -67,8 +67,8 @@ impl ActiveCall { incoming_call: watch::channel(), _subscriptions: vec![ - client.add_request_handler(cx.weak_handle(), Self::handle_incoming_call), - client.add_message_handler(cx.weak_handle(), Self::handle_call_canceled), + client.add_request_handler(cx.weak_model(), Self::handle_incoming_call), + client.add_message_handler(cx.weak_model(), Self::handle_call_canceled), ], client, user_store, diff --git a/crates/client2/src/user.rs b/crates/client2/src/user.rs index a8be4b6401..2a8cf34af4 100644 --- a/crates/client2/src/user.rs +++ b/crates/client2/src/user.rs @@ -122,9 +122,9 @@ impl UserStore { let (mut current_user_tx, current_user_rx) = watch::channel(); let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscriptions = vec![ - client.add_message_handler(cx.weak_handle(), Self::handle_update_contacts), - client.add_message_handler(cx.weak_handle(), Self::handle_update_invite_info), - client.add_message_handler(cx.weak_handle(), Self::handle_show_contacts), + client.add_message_handler(cx.weak_model(), Self::handle_update_contacts), + client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info), + client.add_message_handler(cx.weak_model(), Self::handle_show_contacts), ]; Self { users: Default::default(), diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index c3107a2f47..083c491656 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -7,8 +7,8 @@ use async_tar::Archive; use collections::{HashMap, HashSet}; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui2::{ - AppContext, AsyncAppContext, Context, EntityId, EventEmitter, Model, ModelContext, Task, - WeakModel, + AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Model, ModelContext, + Task, WeakModel, }; use language2::{ language_settings::{all_language_settings, language_settings}, diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 1d2c17d357..8e7a652b4b 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -726,7 +726,7 @@ impl Context for AppContext { /// Update the entity referenced by the given model. The function is passed a mutable reference to the /// entity along with a `ModelContext` for the entity. - fn update_entity( + fn update_model( &mut self, model: &Model, update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 71417f2a5e..042a75848e 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -32,7 +32,7 @@ impl Context for AsyncAppContext { Ok(lock.build_model(build_model)) } - fn update_entity( + fn update_model( &mut self, handle: &Model, update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, @@ -42,7 +42,7 @@ impl Context for AsyncAppContext { .upgrade() .ok_or_else(|| anyhow!("app was released"))?; let mut lock = app.lock(); // Need this to compile - Ok(lock.update_entity(handle, update)) + Ok(lock.update_model(handle, update)) } } @@ -230,13 +230,13 @@ impl Context for AsyncWindowContext { .update_window(self.window, |cx| cx.build_model(build_model)) } - fn update_entity( + fn update_model( &mut self, handle: &Model, update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, ) -> Result { self.app - .update_window(self.window, |cx| cx.update_entity(handle, update)) + .update_window(self.window, |cx| cx.update_model(handle, update)) } } diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index 7e0c5626a5..7b1bc0d000 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -1,4 +1,4 @@ -use crate::{AnyBox, AppContext, Context}; +use crate::{private::Sealed, AnyBox, AppContext, Context, Entity}; use anyhow::{anyhow, Result}; use derive_more::{Deref, DerefMut}; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; @@ -253,6 +253,32 @@ pub struct Model { unsafe impl Send for Model {} unsafe impl Sync for Model {} +impl Sealed for Model {} + +impl Entity for Model { + type Weak = WeakModel; + + fn entity_id(&self) -> EntityId { + self.any_model.entity_id + } + + fn downgrade(&self) -> Self::Weak { + WeakModel { + any_model: self.any_model.downgrade(), + entity_type: self.entity_type, + } + } + + fn upgrade_from(weak: &Self::Weak) -> Option + where + Self: Sized, + { + Some(Model { + any_model: weak.any_model.upgrade()?, + entity_type: weak.entity_type, + }) + } +} impl Model { fn new(id: EntityId, entity_map: Weak>) -> Self @@ -265,11 +291,12 @@ impl Model { } } + /// Downgrade the this to a weak model reference pub fn downgrade(&self) -> WeakModel { - WeakModel { - any_model: self.any_model.downgrade(), - entity_type: self.entity_type, - } + // Delegate to the trait implementation to keep behavior in one place. + // This method was included to improve method resolution in the presence of + // the Model's deref + Entity::downgrade(self) } /// Convert this into a dynamically typed model. @@ -294,7 +321,7 @@ impl Model { where C: Context, { - cx.update_entity(self, update) + cx.update_model(self, update) } } @@ -334,7 +361,7 @@ impl Eq for Model {} impl PartialEq> for Model { fn eq(&self, other: &WeakModel) -> bool { - self.entity_id() == other.entity_id() + self.any_model.entity_id() == other.entity_id() } } @@ -415,11 +442,10 @@ impl Clone for WeakModel { } impl WeakModel { + /// Upgrade this weak model reference into a strong model reference pub fn upgrade(&self) -> Option> { - Some(Model { - any_model: self.any_model.upgrade()?, - entity_type: self.entity_type, - }) + // Delegate to the trait implementation to keep behavior in one place. + Model::upgrade_from(self) } /// Update the entity referenced by this model with the given function if @@ -441,7 +467,7 @@ impl WeakModel { crate::Flatten::flatten( self.upgrade() .ok_or_else(|| anyhow!("entity release")) - .map(|this| cx.update_entity(&this, update)), + .map(|this| cx.update_model(&this, update)), ) } } @@ -462,6 +488,6 @@ impl Eq for WeakModel {} impl PartialEq> for WeakModel { fn eq(&self, other: &Model) -> bool { - self.entity_id() == other.entity_id() + self.entity_id() == other.any_model.entity_id() } } diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 463652886b..8a4576c052 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -1,6 +1,6 @@ use crate::{ - AppContext, AsyncAppContext, Context, Effect, EntityId, EventEmitter, MainThread, Model, - Reference, Subscription, Task, WeakModel, + AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, EventEmitter, MainThread, + Model, Reference, Subscription, Task, WeakModel, }; use derive_more::{Deref, DerefMut}; use futures::FutureExt; @@ -31,29 +31,32 @@ impl<'a, T: 'static> ModelContext<'a, T> { } pub fn handle(&self) -> Model { - self.weak_handle() + self.weak_model() .upgrade() .expect("The entity must be alive if we have a model context") } - pub fn weak_handle(&self) -> WeakModel { + pub fn weak_model(&self) -> WeakModel { self.model_state.clone() } - pub fn observe( + pub fn observe( &mut self, - handle: &Model, - mut on_notify: impl FnMut(&mut T, Model, &mut ModelContext<'_, T>) + Send + 'static, + entity: &E, + mut on_notify: impl FnMut(&mut T, E, &mut ModelContext<'_, T>) + Send + 'static, ) -> Subscription where T: 'static + Send, + T2: 'static, + E: Entity, { - let this = self.weak_handle(); - let handle = handle.downgrade(); + let this = self.weak_model(); + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); self.app.observers.insert( - handle.entity_id, + entity_id, Box::new(move |cx| { - if let Some((this, handle)) = this.upgrade().zip(handle.upgrade()) { + if let Some((this, handle)) = this.upgrade().zip(E::upgrade_from(&handle)) { this.update(cx, |this, cx| on_notify(this, handle, cx)); true } else { @@ -63,21 +66,24 @@ impl<'a, T: 'static> ModelContext<'a, T> { ) } - pub fn subscribe( + pub fn subscribe( &mut self, - handle: &Model, - mut on_event: impl FnMut(&mut T, Model, &E::Event, &mut ModelContext<'_, T>) + Send + 'static, + entity: &E, + mut on_event: impl FnMut(&mut T, E, &T2::Event, &mut ModelContext<'_, T>) + Send + 'static, ) -> Subscription where T: 'static + Send, + T2: 'static + EventEmitter, + E: Entity, { - let this = self.weak_handle(); - let handle = handle.downgrade(); + let this = self.weak_model(); + let entity_id = entity.entity_id(); + let entity = entity.downgrade(); self.app.event_listeners.insert( - handle.entity_id, + entity_id, Box::new(move |event, cx| { - let event: &E::Event = event.downcast_ref().expect("invalid event type"); - if let Some((this, handle)) = this.upgrade().zip(handle.upgrade()) { + let event: &T2::Event = event.downcast_ref().expect("invalid event type"); + if let Some((this, handle)) = this.upgrade().zip(E::upgrade_from(&entity)) { this.update(cx, |this, cx| on_event(this, handle, event, cx)); true } else { @@ -103,17 +109,20 @@ impl<'a, T: 'static> ModelContext<'a, T> { ) } - pub fn observe_release( + pub fn observe_release( &mut self, - handle: &Model, - mut on_release: impl FnMut(&mut T, &mut E, &mut ModelContext<'_, T>) + Send + 'static, + entity: &E, + mut on_release: impl FnMut(&mut T, &mut T2, &mut ModelContext<'_, T>) + Send + 'static, ) -> Subscription where T: Any + Send, + T2: 'static, + E: Entity, { - let this = self.weak_handle(); + let entity_id = entity.entity_id(); + let this = self.weak_model(); self.app.release_listeners.insert( - handle.entity_id, + entity_id, Box::new(move |entity, cx| { let entity = entity.downcast_mut().expect("invalid entity type"); if let Some(this) = this.upgrade() { @@ -130,7 +139,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { where T: 'static + Send, { - let handle = self.weak_handle(); + let handle = self.weak_model(); self.global_observers.insert( TypeId::of::(), Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()), @@ -145,7 +154,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { Fut: 'static + Future + Send, T: 'static + Send, { - let handle = self.weak_handle(); + let handle = self.weak_model(); self.app.quit_observers.insert( (), Box::new(move |cx| { @@ -191,7 +200,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { Fut: Future + Send + 'static, R: Send + 'static, { - let this = self.weak_handle(); + let this = self.weak_model(); self.app.spawn(|cx| f(this, cx)) } @@ -203,7 +212,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { Fut: Future + 'static, R: Send + 'static, { - let this = self.weak_handle(); + let this = self.weak_model(); self.app.spawn_on_main(|cx| f(this, cx)) } } @@ -235,12 +244,12 @@ impl<'a, T> Context for ModelContext<'a, T> { self.app.build_model(build_model) } - fn update_entity( + fn update_model( &mut self, handle: &Model, update: impl FnOnce(&mut U, &mut Self::ModelContext<'_, U>) -> R, ) -> R { - self.app.update_entity(handle, update) + self.app.update_model(handle, update) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index dc5896fe06..2b09a95a34 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -26,13 +26,13 @@ impl Context for TestAppContext { lock.build_model(build_model) } - fn update_entity( + fn update_model( &mut self, handle: &Model, update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, ) -> Self::Result { let mut lock = self.app.lock(); - lock.update_entity(handle, update) + lock.update_model(handle, update) } } diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 85300c1a4a..8625866a44 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -24,6 +24,12 @@ mod util; mod view; mod window; +mod private { + /// A mechanism for restricting implementations of a trait to only those in GPUI. + /// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/ + pub trait Sealed {} +} + pub use action::*; pub use anyhow::Result; pub use app::*; @@ -39,6 +45,7 @@ pub use image_cache::*; pub use interactive::*; pub use keymap::*; pub use platform::*; +use private::Sealed; pub use refineable::*; pub use scene::*; pub use serde; @@ -80,7 +87,7 @@ pub trait Context { where T: 'static + Send; - fn update_entity( + fn update_model( &mut self, handle: &Model, update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, @@ -104,6 +111,16 @@ pub trait VisualContext: Context { ) -> Self::Result; } +pub trait Entity: Sealed { + type Weak: 'static + Send; + + fn entity_id(&self) -> EntityId; + fn downgrade(&self) -> Self::Weak; + fn upgrade_from(weak: &Self::Weak) -> Option + where + Self: Sized; +} + pub enum GlobalKey { Numeric(usize), View(EntityId), @@ -149,12 +166,12 @@ impl Context for MainThread { }) } - fn update_entity( + fn update_model( &mut self, handle: &Model, update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, ) -> Self::Result { - self.0.update_entity(handle, |entity, cx| { + self.0.update_model(handle, |entity, cx| { let cx = unsafe { mem::transmute::< &mut C::ModelContext<'_, T>, diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index eef5819361..08e64261e1 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,7 +1,7 @@ use crate::{ - AnyBox, AnyElement, AnyModel, AppContext, AvailableSpace, BorrowWindow, Bounds, Component, - Element, ElementId, EntityId, LayoutId, Model, Pixels, Size, ViewContext, VisualContext, - WeakModel, WindowContext, + private::Sealed, AnyBox, AnyElement, AnyModel, AppContext, AvailableSpace, BorrowWindow, + Bounds, Component, Element, ElementId, Entity, EntityId, LayoutId, Model, Pixels, Size, + ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{any::TypeId, marker::PhantomData, sync::Arc}; @@ -16,19 +16,42 @@ pub struct View { pub(crate) model: Model, } +impl Sealed for View {} + impl View { pub fn into_any(self) -> AnyView { AnyView(Arc::new(self)) } } -impl View { - pub fn downgrade(&self) -> WeakView { +impl Entity for View { + type Weak = WeakView; + + fn entity_id(&self) -> EntityId { + self.model.entity_id + } + + fn downgrade(&self) -> Self::Weak { WeakView { model: self.model.downgrade(), } } + fn upgrade_from(weak: &Self::Weak) -> Option + where + Self: Sized, + { + let model = weak.model.upgrade()?; + Some(View { model }) + } +} + +impl View { + /// Convert this strong view reference into a weak view reference. + pub fn downgrade(&self) -> WeakView { + Entity::downgrade(self) + } + pub fn update( &self, cx: &mut C, @@ -111,8 +134,7 @@ pub struct WeakView { impl WeakView { pub fn upgrade(&self) -> Option> { - let model = self.model.upgrade()?; - Some(View { model }) + Entity::upgrade_from(self) } pub fn update( @@ -200,7 +222,7 @@ where } fn entity_id(&self) -> EntityId { - self.model.entity_id + Entity::entity_id(self) } fn model(&self) -> AnyModel { @@ -208,7 +230,7 @@ where } fn initialize(&self, cx: &mut WindowContext) -> AnyBox { - cx.with_element_id(self.entity_id(), |_global_id, cx| { + cx.with_element_id(ViewObject::entity_id(self), |_global_id, cx| { self.update(cx, |state, cx| { let mut any_element = Box::new(AnyElement::new(state.render(cx))); any_element.initialize(state, cx); @@ -218,7 +240,7 @@ where } fn layout(&self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId { - cx.with_element_id(self.entity_id(), |_global_id, cx| { + cx.with_element_id(ViewObject::entity_id(self), |_global_id, cx| { self.update(cx, |state, cx| { let element = element.downcast_mut::>().unwrap(); element.layout(state, cx) @@ -227,7 +249,7 @@ where } fn paint(&self, _: Bounds, element: &mut AnyBox, cx: &mut WindowContext) { - cx.with_element_id(self.entity_id(), |_global_id, cx| { + cx.with_element_id(ViewObject::entity_id(self), |_global_id, cx| { self.update(cx, |state, cx| { let element = element.downcast_mut::>().unwrap(); element.paint(state, cx); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index d5e18e1439..7898ac34fc 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,8 +1,8 @@ use crate::{ px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, DevicePixels, DispatchContext, DisplayId, Edges, Effect, - EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, - ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, + Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, + Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, MainThread, MainThreadOnly, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderImageParams, @@ -1253,7 +1253,7 @@ impl Context for WindowContext<'_, '_> { self.entities.insert(slot, model) } - fn update_entity( + fn update_model( &mut self, model: &Model, update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, @@ -1568,23 +1568,25 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { self.window_cx.on_next_frame(move |cx| view.update(cx, f)); } - pub fn observe( + pub fn observe( &mut self, - handle: &Model, - mut on_notify: impl FnMut(&mut V, Model, &mut ViewContext<'_, '_, V>) + Send + 'static, + entity: &E, + mut on_notify: impl FnMut(&mut V, E, &mut ViewContext<'_, '_, V>) + Send + 'static, ) -> Subscription where - E: 'static, + V2: 'static, V: Any + Send, + E: Entity, { let view = self.view(); - let handle = handle.downgrade(); + let entity_id = entity.entity_id(); + let entity = entity.downgrade(); let window_handle = self.window.handle; self.app.observers.insert( - handle.entity_id, + entity_id, Box::new(move |cx| { cx.update_window(window_handle.id, |cx| { - if let Some(handle) = handle.upgrade() { + if let Some(handle) = E::upgrade_from(&entity) { view.update(cx, |this, cx| on_notify(this, handle, cx)) .is_ok() } else { @@ -1596,21 +1598,24 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { ) } - pub fn subscribe( + pub fn subscribe( &mut self, - handle: &Model, - mut on_event: impl FnMut(&mut V, Model, &E::Event, &mut ViewContext<'_, '_, V>) - + Send - + 'static, - ) -> Subscription { + entity: &E, + mut on_event: impl FnMut(&mut V, E, &V2::Event, &mut ViewContext<'_, '_, V>) + Send + 'static, + ) -> Subscription + where + V2: EventEmitter, + E: Entity, + { let view = self.view(); - let handle = handle.downgrade(); + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); let window_handle = self.window.handle; self.app.event_listeners.insert( - handle.entity_id, + entity_id, Box::new(move |event, cx| { cx.update_window(window_handle.id, |cx| { - if let Some(handle) = handle.upgrade() { + if let Some(handle) = E::upgrade_from(&handle) { let event = event.downcast_ref().expect("invalid event type"); view.update(cx, |this, cx| on_event(this, handle, event, cx)) .is_ok() @@ -1638,18 +1643,21 @@ impl<'a, 'w, V: 'static> ViewContext<'a, 'w, V> { ) } - pub fn observe_release( + pub fn observe_release( &mut self, - handle: &Model, - mut on_release: impl FnMut(&mut V, &mut T, &mut ViewContext<'_, '_, V>) + Send + 'static, + entity: &E, + mut on_release: impl FnMut(&mut V, &mut V2, &mut ViewContext<'_, '_, V>) + Send + 'static, ) -> Subscription where V: Any + Send, + V2: 'static, + E: Entity, { let view = self.view(); + let entity_id = entity.entity_id(); let window_handle = self.window.handle; self.app.release_listeners.insert( - handle.entity_id, + entity_id, Box::new(move |entity, cx| { let entity = entity.downcast_mut().expect("invalid entity type"); let _ = cx.update_window(window_handle.id, |cx| { @@ -1864,12 +1872,12 @@ impl<'a, 'w, V> Context for ViewContext<'a, 'w, V> { self.window_cx.build_model(build_model) } - fn update_entity( + fn update_model( &mut self, model: &Model, update: impl FnOnce(&mut T, &mut Self::ModelContext<'_, T>) -> R, ) -> R { - self.window_cx.update_entity(model, update) + self.window_cx.update_model(model, update) } } diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index c2ee171866..3f3c5f1308 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -26,8 +26,8 @@ use futures::{ }; use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui2::{ - AnyModel, AppContext, AsyncAppContext, Context, EventEmitter, Executor, Model, ModelContext, - Task, WeakModel, + AnyModel, AppContext, AsyncAppContext, Context, Entity, EventEmitter, Executor, Model, + ModelContext, Task, WeakModel, }; use itertools::Itertools; use language2::{ @@ -2491,7 +2491,7 @@ impl Project { delay } else { if first_insertion { - let this = cx.weak_handle(); + let this = cx.weak_model(); cx.defer(move |cx| { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { @@ -8650,7 +8650,7 @@ fn subscribe_for_copilot_events( // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. if !copilot_server.has_notification_handler::() { let new_server_id = copilot_server.server_id(); - let weak_project = cx.weak_handle(); + let weak_project = cx.weak_model(); let copilot_log_subscription = copilot_server .on_notification::( move |params, mut cx| { diff --git a/crates/project2/src/terminals.rs b/crates/project2/src/terminals.rs index 5cd62d5ae6..ce89914dc6 100644 --- a/crates/project2/src/terminals.rs +++ b/crates/project2/src/terminals.rs @@ -1,5 +1,5 @@ use crate::Project; -use gpui2::{AnyWindowHandle, Context, Model, ModelContext, WeakModel}; +use gpui2::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel}; use settings2::Settings; use std::path::{Path, PathBuf}; use terminal2::{ From 6f1197e00c2650a68296f519d85f8a5039660b92 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 30 Oct 2023 18:00:37 -0700 Subject: [PATCH 2/5] Change model to downcast with ownership --- crates/gpui2/src/app/entity_map.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index 7b1bc0d000..840b0831cd 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -172,7 +172,7 @@ impl AnyModel { } } - pub fn downcast(&self) -> Option> { + pub fn downcast(self) -> Option> { if TypeId::of::() == self.entity_type { Some(Model { any_model: self.clone(), From f5b13071f17c075ac38c4efcf8f0ca18636bb13c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 30 Oct 2023 18:01:26 -0700 Subject: [PATCH 3/5] experiment with a way to recover the any entities when downcasting fails --- crates/gpui2/src/app/entity_map.rs | 16 ++++++++++++---- crates/gpui2/src/view.rs | 21 +++++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index 840b0831cd..bbeabd3e4f 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -172,14 +172,14 @@ impl AnyModel { } } - pub fn downcast(self) -> Option> { + pub fn downcast(self) -> Result, AnyModel> { if TypeId::of::() == self.entity_type { - Some(Model { - any_model: self.clone(), + Ok(Model { + any_model: self, entity_type: PhantomData, }) } else { - None + Err(self) } } } @@ -243,6 +243,14 @@ impl PartialEq for AnyModel { impl Eq for AnyModel {} +impl std::fmt::Debug for AnyModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AnyModel") + .field("entity_id", &self.entity_id.as_u64()) + .finish() + } +} + #[derive(Deref, DerefMut)] pub struct Model { #[deref] diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 08e64261e1..3c3ad034b4 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -211,6 +211,7 @@ trait ViewObject: Send + Sync { fn initialize(&self, cx: &mut WindowContext) -> AnyBox; fn layout(&self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId; fn paint(&self, bounds: Bounds, element: &mut AnyBox, cx: &mut WindowContext); + fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result; } impl ViewObject for View @@ -256,14 +257,24 @@ where }); }); } + + fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(&format!("AnyView<{}>", std::any::type_name::())) + .field("entity_id", &ViewObject::entity_id(self).as_u64()) + .finish() + } } #[derive(Clone)] pub struct AnyView(Arc); impl AnyView { - pub fn downcast(self) -> Option> { - self.0.model().downcast().map(|model| View { model }) + pub fn downcast(self) -> Result, AnyView> { + self.0 + .model() + .downcast() + .map(|model| View { model }) + .map_err(|_| self) } pub(crate) fn entity_type(&self) -> TypeId { @@ -326,6 +337,12 @@ impl Element<()> for AnyView { } } +impl std::fmt::Debug for AnyView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.debug(f) + } +} + struct EraseAnyViewState { view: AnyView, parent_view_state_type: PhantomData, From db34de6be469216196fb33ec456717ad31f86ad3 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 30 Oct 2023 21:41:17 -0400 Subject: [PATCH 4/5] Port `zed/src/languages` to zed2 --- Cargo.lock | 5 +- crates/gpui2/src/app.rs | 4 + crates/language2/src/language2.rs | 46 +- crates/prettier2/src/prettier2.rs | 244 ++++---- crates/project2/src/project2.rs | 36 +- crates/settings2/src/keymap_file.rs | 4 +- crates/theme2/src/theme2.rs | 18 + crates/zed2/Cargo.toml | 5 +- crates/zed2/src/languages.rs | 273 +++++++++ crates/zed2/src/languages/bash/brackets.scm | 3 + crates/zed2/src/languages/bash/config.toml | 9 + crates/zed2/src/languages/bash/highlights.scm | 59 ++ crates/zed2/src/languages/c.rs | 321 ++++++++++ crates/zed2/src/languages/c/brackets.scm | 3 + crates/zed2/src/languages/c/config.toml | 12 + crates/zed2/src/languages/c/embedding.scm | 43 ++ crates/zed2/src/languages/c/highlights.scm | 109 ++++ crates/zed2/src/languages/c/indents.scm | 9 + crates/zed2/src/languages/c/injections.scm | 7 + crates/zed2/src/languages/c/outline.scm | 70 +++ crates/zed2/src/languages/c/overrides.scm | 2 + crates/zed2/src/languages/cpp/brackets.scm | 3 + crates/zed2/src/languages/cpp/config.toml | 12 + crates/zed2/src/languages/cpp/embedding.scm | 61 ++ crates/zed2/src/languages/cpp/highlights.scm | 158 +++++ crates/zed2/src/languages/cpp/indents.scm | 7 + crates/zed2/src/languages/cpp/injections.scm | 7 + crates/zed2/src/languages/cpp/outline.scm | 149 +++++ crates/zed2/src/languages/cpp/overrides.scm | 2 + crates/zed2/src/languages/css.rs | 130 ++++ crates/zed2/src/languages/css/brackets.scm | 3 + crates/zed2/src/languages/css/config.toml | 13 + crates/zed2/src/languages/css/highlights.scm | 78 +++ crates/zed2/src/languages/css/indents.scm | 1 + crates/zed2/src/languages/css/overrides.scm | 2 + crates/zed2/src/languages/elixir.rs | 546 +++++++++++++++++ crates/zed2/src/languages/elixir/brackets.scm | 5 + crates/zed2/src/languages/elixir/config.toml | 16 + .../zed2/src/languages/elixir/embedding.scm | 27 + .../zed2/src/languages/elixir/highlights.scm | 153 +++++ crates/zed2/src/languages/elixir/indents.scm | 6 + .../zed2/src/languages/elixir/injections.scm | 7 + crates/zed2/src/languages/elixir/outline.scm | 26 + .../zed2/src/languages/elixir/overrides.scm | 2 + crates/zed2/src/languages/elm/config.toml | 11 + crates/zed2/src/languages/elm/highlights.scm | 72 +++ crates/zed2/src/languages/elm/injections.scm | 2 + crates/zed2/src/languages/elm/outline.scm | 22 + crates/zed2/src/languages/erb/config.toml | 8 + crates/zed2/src/languages/erb/highlights.scm | 12 + crates/zed2/src/languages/erb/injections.scm | 7 + crates/zed2/src/languages/glsl/config.toml | 9 + crates/zed2/src/languages/glsl/highlights.scm | 118 ++++ crates/zed2/src/languages/go.rs | 464 ++++++++++++++ crates/zed2/src/languages/go/brackets.scm | 3 + crates/zed2/src/languages/go/config.toml | 12 + crates/zed2/src/languages/go/embedding.scm | 24 + crates/zed2/src/languages/go/highlights.scm | 107 ++++ crates/zed2/src/languages/go/indents.scm | 9 + crates/zed2/src/languages/go/outline.scm | 43 ++ crates/zed2/src/languages/go/overrides.scm | 6 + crates/zed2/src/languages/heex/config.toml | 12 + crates/zed2/src/languages/heex/highlights.scm | 57 ++ crates/zed2/src/languages/heex/injections.scm | 13 + crates/zed2/src/languages/heex/overrides.scm | 4 + crates/zed2/src/languages/html.rs | 130 ++++ crates/zed2/src/languages/html/brackets.scm | 2 + crates/zed2/src/languages/html/config.toml | 14 + crates/zed2/src/languages/html/highlights.scm | 15 + crates/zed2/src/languages/html/indents.scm | 6 + crates/zed2/src/languages/html/injections.scm | 7 + crates/zed2/src/languages/html/outline.scm | 0 crates/zed2/src/languages/html/overrides.scm | 2 + .../src/languages/javascript/brackets.scm | 5 + .../zed2/src/languages/javascript/config.toml | 26 + .../src/languages/javascript/contexts.scm | 0 .../src/languages/javascript/embedding.scm | 71 +++ .../src/languages/javascript/highlights.scm | 217 +++++++ .../zed2/src/languages/javascript/indents.scm | 15 + .../zed2/src/languages/javascript/outline.scm | 62 ++ .../src/languages/javascript/overrides.scm | 13 + crates/zed2/src/languages/json.rs | 184 ++++++ crates/zed2/src/languages/json/brackets.scm | 3 + crates/zed2/src/languages/json/config.toml | 10 + crates/zed2/src/languages/json/embedding.scm | 14 + crates/zed2/src/languages/json/highlights.scm | 21 + crates/zed2/src/languages/json/indents.scm | 2 + crates/zed2/src/languages/json/outline.scm | 2 + crates/zed2/src/languages/json/overrides.scm | 1 + crates/zed2/src/languages/language_plugin.rs | 168 ++++++ crates/zed2/src/languages/lua.rs | 135 +++++ crates/zed2/src/languages/lua/brackets.scm | 3 + crates/zed2/src/languages/lua/config.toml | 10 + crates/zed2/src/languages/lua/embedding.scm | 10 + crates/zed2/src/languages/lua/highlights.scm | 198 ++++++ crates/zed2/src/languages/lua/indents.scm | 10 + crates/zed2/src/languages/lua/outline.scm | 3 + .../zed2/src/languages/markdown/config.toml | 11 + .../src/languages/markdown/highlights.scm | 24 + .../src/languages/markdown/injections.scm | 4 + crates/zed2/src/languages/nix/config.toml | 11 + crates/zed2/src/languages/nix/highlights.scm | 95 +++ crates/zed2/src/languages/nu/brackets.scm | 4 + crates/zed2/src/languages/nu/config.toml | 9 + crates/zed2/src/languages/nu/highlights.scm | 302 ++++++++++ crates/zed2/src/languages/nu/indents.scm | 3 + crates/zed2/src/languages/php.rs | 137 +++++ crates/zed2/src/languages/php/config.toml | 14 + crates/zed2/src/languages/php/embedding.scm | 36 ++ crates/zed2/src/languages/php/highlights.scm | 123 ++++ crates/zed2/src/languages/php/injections.scm | 3 + crates/zed2/src/languages/php/outline.scm | 29 + crates/zed2/src/languages/php/tags.scm | 40 ++ crates/zed2/src/languages/python.rs | 296 +++++++++ crates/zed2/src/languages/python/brackets.scm | 3 + crates/zed2/src/languages/python/config.toml | 16 + .../zed2/src/languages/python/embedding.scm | 9 + .../zed2/src/languages/python/highlights.scm | 125 ++++ crates/zed2/src/languages/python/indents.scm | 3 + crates/zed2/src/languages/python/outline.scm | 9 + .../zed2/src/languages/python/overrides.scm | 2 + crates/zed2/src/languages/racket/brackets.scm | 3 + crates/zed2/src/languages/racket/config.toml | 9 + .../zed2/src/languages/racket/highlights.scm | 40 ++ crates/zed2/src/languages/racket/indents.scm | 3 + crates/zed2/src/languages/racket/outline.scm | 10 + crates/zed2/src/languages/ruby.rs | 160 +++++ crates/zed2/src/languages/ruby/brackets.scm | 14 + crates/zed2/src/languages/ruby/config.toml | 13 + crates/zed2/src/languages/ruby/embedding.scm | 22 + crates/zed2/src/languages/ruby/highlights.scm | 181 ++++++ crates/zed2/src/languages/ruby/indents.scm | 17 + crates/zed2/src/languages/ruby/outline.scm | 17 + crates/zed2/src/languages/ruby/overrides.scm | 2 + crates/zed2/src/languages/rust.rs | 568 ++++++++++++++++++ crates/zed2/src/languages/rust/brackets.scm | 6 + crates/zed2/src/languages/rust/config.toml | 13 + crates/zed2/src/languages/rust/embedding.scm | 32 + crates/zed2/src/languages/rust/highlights.scm | 116 ++++ crates/zed2/src/languages/rust/indents.scm | 14 + crates/zed2/src/languages/rust/injections.scm | 7 + crates/zed2/src/languages/rust/outline.scm | 63 ++ crates/zed2/src/languages/rust/overrides.scm | 8 + crates/zed2/src/languages/scheme/brackets.scm | 3 + crates/zed2/src/languages/scheme/config.toml | 9 + .../zed2/src/languages/scheme/highlights.scm | 28 + crates/zed2/src/languages/scheme/indents.scm | 3 + crates/zed2/src/languages/scheme/outline.scm | 10 + .../zed2/src/languages/scheme/overrides.scm | 6 + crates/zed2/src/languages/svelte.rs | 133 ++++ crates/zed2/src/languages/svelte/config.toml | 20 + crates/zed2/src/languages/svelte/folds.scm | 9 + .../zed2/src/languages/svelte/highlights.scm | 42 ++ crates/zed2/src/languages/svelte/indents.scm | 8 + .../zed2/src/languages/svelte/injections.scm | 28 + .../zed2/src/languages/svelte/overrides.scm | 7 + crates/zed2/src/languages/tailwind.rs | 167 +++++ crates/zed2/src/languages/toml/brackets.scm | 3 + crates/zed2/src/languages/toml/config.toml | 10 + crates/zed2/src/languages/toml/highlights.scm | 37 ++ crates/zed2/src/languages/toml/indents.scm | 0 crates/zed2/src/languages/toml/outline.scm | 15 + crates/zed2/src/languages/toml/overrides.scm | 2 + crates/zed2/src/languages/tsx/brackets.scm | 1 + crates/zed2/src/languages/tsx/config.toml | 25 + crates/zed2/src/languages/tsx/embedding.scm | 85 +++ .../zed2/src/languages/tsx/highlights-jsx.scm | 0 crates/zed2/src/languages/tsx/highlights.scm | 1 + crates/zed2/src/languages/tsx/indents.scm | 1 + crates/zed2/src/languages/tsx/outline.scm | 1 + crates/zed2/src/languages/tsx/overrides.scm | 13 + crates/zed2/src/languages/typescript.rs | 384 ++++++++++++ .../src/languages/typescript/brackets.scm | 5 + .../zed2/src/languages/typescript/config.toml | 16 + .../src/languages/typescript/embedding.scm | 85 +++ .../src/languages/typescript/highlights.scm | 221 +++++++ .../zed2/src/languages/typescript/indents.scm | 15 + .../zed2/src/languages/typescript/outline.scm | 65 ++ .../src/languages/typescript/overrides.scm | 2 + crates/zed2/src/languages/vue.rs | 220 +++++++ crates/zed2/src/languages/vue/brackets.scm | 2 + crates/zed2/src/languages/vue/config.toml | 14 + crates/zed2/src/languages/vue/highlights.scm | 15 + crates/zed2/src/languages/vue/injections.scm | 7 + crates/zed2/src/languages/yaml.rs | 142 +++++ crates/zed2/src/languages/yaml/brackets.scm | 3 + crates/zed2/src/languages/yaml/config.toml | 12 + crates/zed2/src/languages/yaml/highlights.scm | 49 ++ crates/zed2/src/languages/yaml/outline.scm | 1 + crates/zed2/src/main.rs | 8 +- crates/zed2/src/zed2.rs | 1 + 191 files changed, 9448 insertions(+), 182 deletions(-) create mode 100644 crates/zed2/src/languages.rs create mode 100644 crates/zed2/src/languages/bash/brackets.scm create mode 100644 crates/zed2/src/languages/bash/config.toml create mode 100644 crates/zed2/src/languages/bash/highlights.scm create mode 100644 crates/zed2/src/languages/c.rs create mode 100644 crates/zed2/src/languages/c/brackets.scm create mode 100644 crates/zed2/src/languages/c/config.toml create mode 100644 crates/zed2/src/languages/c/embedding.scm create mode 100644 crates/zed2/src/languages/c/highlights.scm create mode 100644 crates/zed2/src/languages/c/indents.scm create mode 100644 crates/zed2/src/languages/c/injections.scm create mode 100644 crates/zed2/src/languages/c/outline.scm create mode 100644 crates/zed2/src/languages/c/overrides.scm create mode 100644 crates/zed2/src/languages/cpp/brackets.scm create mode 100644 crates/zed2/src/languages/cpp/config.toml create mode 100644 crates/zed2/src/languages/cpp/embedding.scm create mode 100644 crates/zed2/src/languages/cpp/highlights.scm create mode 100644 crates/zed2/src/languages/cpp/indents.scm create mode 100644 crates/zed2/src/languages/cpp/injections.scm create mode 100644 crates/zed2/src/languages/cpp/outline.scm create mode 100644 crates/zed2/src/languages/cpp/overrides.scm create mode 100644 crates/zed2/src/languages/css.rs create mode 100644 crates/zed2/src/languages/css/brackets.scm create mode 100644 crates/zed2/src/languages/css/config.toml create mode 100644 crates/zed2/src/languages/css/highlights.scm create mode 100644 crates/zed2/src/languages/css/indents.scm create mode 100644 crates/zed2/src/languages/css/overrides.scm create mode 100644 crates/zed2/src/languages/elixir.rs create mode 100644 crates/zed2/src/languages/elixir/brackets.scm create mode 100644 crates/zed2/src/languages/elixir/config.toml create mode 100644 crates/zed2/src/languages/elixir/embedding.scm create mode 100644 crates/zed2/src/languages/elixir/highlights.scm create mode 100644 crates/zed2/src/languages/elixir/indents.scm create mode 100644 crates/zed2/src/languages/elixir/injections.scm create mode 100644 crates/zed2/src/languages/elixir/outline.scm create mode 100644 crates/zed2/src/languages/elixir/overrides.scm create mode 100644 crates/zed2/src/languages/elm/config.toml create mode 100644 crates/zed2/src/languages/elm/highlights.scm create mode 100644 crates/zed2/src/languages/elm/injections.scm create mode 100644 crates/zed2/src/languages/elm/outline.scm create mode 100644 crates/zed2/src/languages/erb/config.toml create mode 100644 crates/zed2/src/languages/erb/highlights.scm create mode 100644 crates/zed2/src/languages/erb/injections.scm create mode 100644 crates/zed2/src/languages/glsl/config.toml create mode 100644 crates/zed2/src/languages/glsl/highlights.scm create mode 100644 crates/zed2/src/languages/go.rs create mode 100644 crates/zed2/src/languages/go/brackets.scm create mode 100644 crates/zed2/src/languages/go/config.toml create mode 100644 crates/zed2/src/languages/go/embedding.scm create mode 100644 crates/zed2/src/languages/go/highlights.scm create mode 100644 crates/zed2/src/languages/go/indents.scm create mode 100644 crates/zed2/src/languages/go/outline.scm create mode 100644 crates/zed2/src/languages/go/overrides.scm create mode 100644 crates/zed2/src/languages/heex/config.toml create mode 100644 crates/zed2/src/languages/heex/highlights.scm create mode 100644 crates/zed2/src/languages/heex/injections.scm create mode 100644 crates/zed2/src/languages/heex/overrides.scm create mode 100644 crates/zed2/src/languages/html.rs create mode 100644 crates/zed2/src/languages/html/brackets.scm create mode 100644 crates/zed2/src/languages/html/config.toml create mode 100644 crates/zed2/src/languages/html/highlights.scm create mode 100644 crates/zed2/src/languages/html/indents.scm create mode 100644 crates/zed2/src/languages/html/injections.scm create mode 100644 crates/zed2/src/languages/html/outline.scm create mode 100644 crates/zed2/src/languages/html/overrides.scm create mode 100644 crates/zed2/src/languages/javascript/brackets.scm create mode 100644 crates/zed2/src/languages/javascript/config.toml create mode 100644 crates/zed2/src/languages/javascript/contexts.scm create mode 100644 crates/zed2/src/languages/javascript/embedding.scm create mode 100644 crates/zed2/src/languages/javascript/highlights.scm create mode 100644 crates/zed2/src/languages/javascript/indents.scm create mode 100644 crates/zed2/src/languages/javascript/outline.scm create mode 100644 crates/zed2/src/languages/javascript/overrides.scm create mode 100644 crates/zed2/src/languages/json.rs create mode 100644 crates/zed2/src/languages/json/brackets.scm create mode 100644 crates/zed2/src/languages/json/config.toml create mode 100644 crates/zed2/src/languages/json/embedding.scm create mode 100644 crates/zed2/src/languages/json/highlights.scm create mode 100644 crates/zed2/src/languages/json/indents.scm create mode 100644 crates/zed2/src/languages/json/outline.scm create mode 100644 crates/zed2/src/languages/json/overrides.scm create mode 100644 crates/zed2/src/languages/language_plugin.rs create mode 100644 crates/zed2/src/languages/lua.rs create mode 100644 crates/zed2/src/languages/lua/brackets.scm create mode 100644 crates/zed2/src/languages/lua/config.toml create mode 100644 crates/zed2/src/languages/lua/embedding.scm create mode 100644 crates/zed2/src/languages/lua/highlights.scm create mode 100644 crates/zed2/src/languages/lua/indents.scm create mode 100644 crates/zed2/src/languages/lua/outline.scm create mode 100644 crates/zed2/src/languages/markdown/config.toml create mode 100644 crates/zed2/src/languages/markdown/highlights.scm create mode 100644 crates/zed2/src/languages/markdown/injections.scm create mode 100644 crates/zed2/src/languages/nix/config.toml create mode 100644 crates/zed2/src/languages/nix/highlights.scm create mode 100644 crates/zed2/src/languages/nu/brackets.scm create mode 100644 crates/zed2/src/languages/nu/config.toml create mode 100644 crates/zed2/src/languages/nu/highlights.scm create mode 100644 crates/zed2/src/languages/nu/indents.scm create mode 100644 crates/zed2/src/languages/php.rs create mode 100644 crates/zed2/src/languages/php/config.toml create mode 100644 crates/zed2/src/languages/php/embedding.scm create mode 100644 crates/zed2/src/languages/php/highlights.scm create mode 100644 crates/zed2/src/languages/php/injections.scm create mode 100644 crates/zed2/src/languages/php/outline.scm create mode 100644 crates/zed2/src/languages/php/tags.scm create mode 100644 crates/zed2/src/languages/python.rs create mode 100644 crates/zed2/src/languages/python/brackets.scm create mode 100644 crates/zed2/src/languages/python/config.toml create mode 100644 crates/zed2/src/languages/python/embedding.scm create mode 100644 crates/zed2/src/languages/python/highlights.scm create mode 100644 crates/zed2/src/languages/python/indents.scm create mode 100644 crates/zed2/src/languages/python/outline.scm create mode 100644 crates/zed2/src/languages/python/overrides.scm create mode 100644 crates/zed2/src/languages/racket/brackets.scm create mode 100644 crates/zed2/src/languages/racket/config.toml create mode 100644 crates/zed2/src/languages/racket/highlights.scm create mode 100644 crates/zed2/src/languages/racket/indents.scm create mode 100644 crates/zed2/src/languages/racket/outline.scm create mode 100644 crates/zed2/src/languages/ruby.rs create mode 100644 crates/zed2/src/languages/ruby/brackets.scm create mode 100644 crates/zed2/src/languages/ruby/config.toml create mode 100644 crates/zed2/src/languages/ruby/embedding.scm create mode 100644 crates/zed2/src/languages/ruby/highlights.scm create mode 100644 crates/zed2/src/languages/ruby/indents.scm create mode 100644 crates/zed2/src/languages/ruby/outline.scm create mode 100644 crates/zed2/src/languages/ruby/overrides.scm create mode 100644 crates/zed2/src/languages/rust.rs create mode 100644 crates/zed2/src/languages/rust/brackets.scm create mode 100644 crates/zed2/src/languages/rust/config.toml create mode 100644 crates/zed2/src/languages/rust/embedding.scm create mode 100644 crates/zed2/src/languages/rust/highlights.scm create mode 100644 crates/zed2/src/languages/rust/indents.scm create mode 100644 crates/zed2/src/languages/rust/injections.scm create mode 100644 crates/zed2/src/languages/rust/outline.scm create mode 100644 crates/zed2/src/languages/rust/overrides.scm create mode 100644 crates/zed2/src/languages/scheme/brackets.scm create mode 100644 crates/zed2/src/languages/scheme/config.toml create mode 100644 crates/zed2/src/languages/scheme/highlights.scm create mode 100644 crates/zed2/src/languages/scheme/indents.scm create mode 100644 crates/zed2/src/languages/scheme/outline.scm create mode 100644 crates/zed2/src/languages/scheme/overrides.scm create mode 100644 crates/zed2/src/languages/svelte.rs create mode 100644 crates/zed2/src/languages/svelte/config.toml create mode 100755 crates/zed2/src/languages/svelte/folds.scm create mode 100755 crates/zed2/src/languages/svelte/highlights.scm create mode 100755 crates/zed2/src/languages/svelte/indents.scm create mode 100755 crates/zed2/src/languages/svelte/injections.scm create mode 100644 crates/zed2/src/languages/svelte/overrides.scm create mode 100644 crates/zed2/src/languages/tailwind.rs create mode 100644 crates/zed2/src/languages/toml/brackets.scm create mode 100644 crates/zed2/src/languages/toml/config.toml create mode 100644 crates/zed2/src/languages/toml/highlights.scm create mode 100644 crates/zed2/src/languages/toml/indents.scm create mode 100644 crates/zed2/src/languages/toml/outline.scm create mode 100644 crates/zed2/src/languages/toml/overrides.scm create mode 120000 crates/zed2/src/languages/tsx/brackets.scm create mode 100644 crates/zed2/src/languages/tsx/config.toml create mode 100644 crates/zed2/src/languages/tsx/embedding.scm create mode 100644 crates/zed2/src/languages/tsx/highlights-jsx.scm create mode 120000 crates/zed2/src/languages/tsx/highlights.scm create mode 120000 crates/zed2/src/languages/tsx/indents.scm create mode 120000 crates/zed2/src/languages/tsx/outline.scm create mode 100644 crates/zed2/src/languages/tsx/overrides.scm create mode 100644 crates/zed2/src/languages/typescript.rs create mode 100644 crates/zed2/src/languages/typescript/brackets.scm create mode 100644 crates/zed2/src/languages/typescript/config.toml create mode 100644 crates/zed2/src/languages/typescript/embedding.scm create mode 100644 crates/zed2/src/languages/typescript/highlights.scm create mode 100644 crates/zed2/src/languages/typescript/indents.scm create mode 100644 crates/zed2/src/languages/typescript/outline.scm create mode 100644 crates/zed2/src/languages/typescript/overrides.scm create mode 100644 crates/zed2/src/languages/vue.rs create mode 100644 crates/zed2/src/languages/vue/brackets.scm create mode 100644 crates/zed2/src/languages/vue/config.toml create mode 100644 crates/zed2/src/languages/vue/highlights.scm create mode 100644 crates/zed2/src/languages/vue/injections.scm create mode 100644 crates/zed2/src/languages/yaml.rs create mode 100644 crates/zed2/src/languages/yaml/brackets.scm create mode 100644 crates/zed2/src/languages/yaml/config.toml create mode 100644 crates/zed2/src/languages/yaml/highlights.scm create mode 100644 crates/zed2/src/languages/yaml/outline.scm diff --git a/Cargo.lock b/Cargo.lock index 85eb0fde85..3aca27106c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10946,7 +10946,7 @@ dependencies = [ "ctor", "db2", "env_logger 0.9.3", - "feature_flags", + "feature_flags2", "fs2", "fsevent", "futures 0.3.28", @@ -10963,7 +10963,7 @@ dependencies = [ "lazy_static", "libc", "log", - "lsp", + "lsp2", "node_runtime", "num_cpus", "parking_lot 0.11.2", @@ -11016,6 +11016,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-vue", "tree-sitter-yaml", "unindent", "url", diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 1d2c17d357..48ef097357 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -637,6 +637,10 @@ impl AppContext { ) } + pub fn all_action_names<'a>(&'a self) -> impl Iterator + 'a { + self.action_builders.keys().cloned() + } + /// Move the global of the given type to the stack. pub(crate) fn lease_global(&mut self) -> GlobalLease { GlobalLease::new( diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 89e98e57ca..717a80619b 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -230,8 +230,8 @@ impl CachedLspAdapter { self.adapter.label_for_symbol(name, kind, language).await } - pub fn enabled_formatters(&self) -> Vec { - self.adapter.enabled_formatters() + pub fn prettier_plugins(&self) -> &[&'static str] { + self.adapter.prettier_plugins() } } @@ -340,31 +340,8 @@ pub trait LspAdapter: 'static + Send + Sync { Default::default() } - fn enabled_formatters(&self) -> Vec { - Vec::new() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum BundledFormatter { - Prettier { - // See https://prettier.io/docs/en/options.html#parser for a list of valid values. - // Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used. - // There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins. - // - // But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed. - // For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict. - parser_name: Option<&'static str>, - plugin_names: Vec<&'static str>, - }, -} - -impl BundledFormatter { - pub fn prettier(parser_name: &'static str) -> Self { - Self::Prettier { - parser_name: Some(parser_name), - plugin_names: Vec::new(), - } + fn prettier_plugins(&self) -> &[&'static str] { + &[] } } @@ -402,6 +379,8 @@ pub struct LanguageConfig { pub overrides: HashMap, #[serde(default)] pub word_characters: HashSet, + #[serde(default)] + pub prettier_parser_name: Option, } #[derive(Debug, Default)] @@ -475,6 +454,7 @@ impl Default for LanguageConfig { overrides: Default::default(), collapsed_placeholder: Default::default(), word_characters: Default::default(), + prettier_parser_name: None, } } } @@ -500,7 +480,7 @@ pub struct FakeLspAdapter { pub initializer: Option>, pub disk_based_diagnostics_progress_token: Option, pub disk_based_diagnostics_sources: Vec, - pub enabled_formatters: Vec, + pub prettier_plugins: Vec<&'static str>, } #[derive(Clone, Debug, Default)] @@ -1604,6 +1584,10 @@ impl Language { override_id: None, } } + + pub fn prettier_parser_name(&self) -> Option<&str> { + self.config.prettier_parser_name.as_deref() + } } impl LanguageScope { @@ -1766,7 +1750,7 @@ impl Default for FakeLspAdapter { disk_based_diagnostics_progress_token: None, initialization_options: None, disk_based_diagnostics_sources: Vec::new(), - enabled_formatters: Vec::new(), + prettier_plugins: Vec::new(), } } } @@ -1824,8 +1808,8 @@ impl LspAdapter for Arc { self.initialization_options.clone() } - fn enabled_formatters(&self) -> Vec { - self.enabled_formatters.clone() + fn prettier_plugins(&self) -> &[&'static str] { + &self.prettier_plugins } } diff --git a/crates/prettier2/src/prettier2.rs b/crates/prettier2/src/prettier2.rs index 804aeab594..6b13429eec 100644 --- a/crates/prettier2/src/prettier2.rs +++ b/crates/prettier2/src/prettier2.rs @@ -1,8 +1,8 @@ use anyhow::Context; -use collections::{HashMap, HashSet}; +use collections::HashMap; use fs2::Fs; use gpui2::{AsyncAppContext, Model}; -use language2::{language_settings::language_settings, Buffer, BundledFormatter, Diff}; +use language2::{language_settings::language_settings, Buffer, Diff}; use lsp2::{LanguageServer, LanguageServerId}; use node_runtime::NodeRuntime; use serde::{Deserialize, Serialize}; @@ -189,128 +189,134 @@ impl Prettier { ) -> anyhow::Result { match self { Self::Real(local) => { - let params = buffer.update(cx, |buffer, cx| { - let buffer_language = buffer.language(); - let parsers_with_plugins = buffer_language - .into_iter() - .flat_map(|language| { - language + let params = buffer + .update(cx, |buffer, cx| { + let buffer_language = buffer.language(); + let parser_with_plugins = buffer_language.and_then(|l| { + let prettier_parser = l.prettier_parser_name()?; + let mut prettier_plugins = l .lsp_adapters() .iter() - .flat_map(|adapter| adapter.enabled_formatters()) - .filter_map(|formatter| match formatter { - BundledFormatter::Prettier { - parser_name, - plugin_names, - } => Some((parser_name, plugin_names)), - }) - }) - .fold( - HashMap::default(), - |mut parsers_with_plugins, (parser_name, plugins)| { - match parser_name { - Some(parser_name) => parsers_with_plugins - .entry(parser_name) - .or_insert_with(HashSet::default) - .extend(plugins), - None => parsers_with_plugins.values_mut().for_each(|existing_plugins| { - existing_plugins.extend(plugins.iter()); - }), + .flat_map(|adapter| adapter.prettier_plugins()) + .collect::>(); + prettier_plugins.dedup(); + Some((prettier_parser, prettier_plugins)) + }); + + let prettier_node_modules = self.prettier_dir().join("node_modules"); + anyhow::ensure!( + prettier_node_modules.is_dir(), + "Prettier node_modules dir does not exist: {prettier_node_modules:?}" + ); + let plugin_name_into_path = |plugin_name: &str| { + let prettier_plugin_dir = prettier_node_modules.join(plugin_name); + for possible_plugin_path in [ + prettier_plugin_dir.join("dist").join("index.mjs"), + prettier_plugin_dir.join("dist").join("index.js"), + prettier_plugin_dir.join("dist").join("plugin.js"), + prettier_plugin_dir.join("index.mjs"), + prettier_plugin_dir.join("index.js"), + prettier_plugin_dir.join("plugin.js"), + prettier_plugin_dir, + ] { + if possible_plugin_path.is_file() { + return Some(possible_plugin_path); } - parsers_with_plugins - }, + } + None + }; + let (parser, located_plugins) = match parser_with_plugins { + Some((parser, plugins)) => { + // Tailwind plugin requires being added last + // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins + let mut add_tailwind_back = false; + + let mut plugins = plugins + .into_iter() + .filter(|&&plugin_name| { + if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME { + add_tailwind_back = true; + false + } else { + true + } + }) + .map(|plugin_name| { + (plugin_name, plugin_name_into_path(plugin_name)) + }) + .collect::>(); + if add_tailwind_back { + plugins.push(( + &TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, + plugin_name_into_path( + TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, + ), + )); + } + (Some(parser.to_string()), plugins) + } + None => (None, Vec::new()), + }; + + let prettier_options = if self.is_default() { + let language_settings = + language_settings(buffer_language, buffer.file(), cx); + let mut options = language_settings.prettier.clone(); + if !options.contains_key("tabWidth") { + options.insert( + "tabWidth".to_string(), + serde_json::Value::Number(serde_json::Number::from( + language_settings.tab_size.get(), + )), + ); + } + if !options.contains_key("printWidth") { + options.insert( + "printWidth".to_string(), + serde_json::Value::Number(serde_json::Number::from( + language_settings.preferred_line_length, + )), + ); + } + Some(options) + } else { + None + }; + + let plugins = located_plugins + .into_iter() + .filter_map(|(plugin_name, located_plugin_path)| { + match located_plugin_path { + Some(path) => Some(path), + None => { + log::error!( + "Have not found plugin path for {:?} inside {:?}", + plugin_name, + prettier_node_modules + ); + None + } + } + }) + .collect(); + log::debug!( + "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}", + plugins, + prettier_options, + buffer.file().map(|f| f.full_path(cx)) ); - let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len()); - if parsers_with_plugins.len() > 1 { - log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}"); - } - - let prettier_node_modules = self.prettier_dir().join("node_modules"); - anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}"); - let plugin_name_into_path = |plugin_name: &str| { - let prettier_plugin_dir = prettier_node_modules.join(plugin_name); - for possible_plugin_path in [ - prettier_plugin_dir.join("dist").join("index.mjs"), - prettier_plugin_dir.join("dist").join("index.js"), - prettier_plugin_dir.join("dist").join("plugin.js"), - prettier_plugin_dir.join("index.mjs"), - prettier_plugin_dir.join("index.js"), - prettier_plugin_dir.join("plugin.js"), - prettier_plugin_dir, - ] { - if possible_plugin_path.is_file() { - return Some(possible_plugin_path); - } - } - None - }; - let (parser, located_plugins) = match selected_parser_with_plugins { - Some((parser, plugins)) => { - // Tailwind plugin requires being added last - // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins - let mut add_tailwind_back = false; - - let mut plugins = plugins.into_iter().filter(|&&plugin_name| { - if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME { - add_tailwind_back = true; - false - } else { - true - } - }).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::>(); - if add_tailwind_back { - plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME))); - } - (Some(parser.to_string()), plugins) - }, - None => (None, Vec::new()), - }; - - let prettier_options = if self.is_default() { - let language_settings = language_settings(buffer_language, buffer.file(), cx); - let mut options = language_settings.prettier.clone(); - if !options.contains_key("tabWidth") { - options.insert( - "tabWidth".to_string(), - serde_json::Value::Number(serde_json::Number::from( - language_settings.tab_size.get(), - )), - ); - } - if !options.contains_key("printWidth") { - options.insert( - "printWidth".to_string(), - serde_json::Value::Number(serde_json::Number::from( - language_settings.preferred_line_length, - )), - ); - } - Some(options) - } else { - None - }; - - let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| { - match located_plugin_path { - Some(path) => Some(path), - None => { - log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}"); - None}, - } - }).collect(); - log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx))); - - anyhow::Ok(FormatParams { - text: buffer.text(), - options: FormatOptions { - parser, - plugins, - path: buffer_path, - prettier_options, - }, - }) - })?.context("prettier params calculation")?; + anyhow::Ok(FormatParams { + text: buffer.text(), + options: FormatOptions { + parser, + plugins, + path: buffer_path, + prettier_options, + }, + }) + })? + .context("prettier params calculation")?; let response = local .server .request::(params) diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index c2ee171866..5d3b19301c 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -39,11 +39,11 @@ use language2::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, split_operations, }, - range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter, - CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, - Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, - LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, - TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, + CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, + File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, + OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, + ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp2::{ @@ -8410,12 +8410,7 @@ impl Project { let Some(buffer_language) = buffer.language() else { return Task::ready(None); }; - if !buffer_language - .lsp_adapters() - .iter() - .flat_map(|adapter| adapter.enabled_formatters()) - .any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. })) - { + if buffer_language.prettier_parser_name().is_none() { return Task::ready(None); } @@ -8574,16 +8569,15 @@ impl Project { }; let mut prettier_plugins = None; - for formatter in new_language - .lsp_adapters() - .into_iter() - .flat_map(|adapter| adapter.enabled_formatters()) - { - match formatter { - BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins - .get_or_insert_with(|| HashSet::default()) - .extend(plugin_names), - } + if new_language.prettier_parser_name().is_some() { + prettier_plugins + .get_or_insert_with(|| HashSet::default()) + .extend( + new_language + .lsp_adapters() + .iter() + .flat_map(|adapter| adapter.prettier_plugins()), + ) } let Some(prettier_plugins) = prettier_plugins else { return Task::ready(Ok(())); diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index be2a11c401..d0a32131b5 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -1,7 +1,7 @@ use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use anyhow::{anyhow, Context, Result}; use collections::BTreeMap; -use gpui2::{AppContext, KeyBinding}; +use gpui2::{AppContext, KeyBinding, SharedString}; use schemars::{ gen::{SchemaGenerator, SchemaSettings}, schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, @@ -96,7 +96,7 @@ impl KeymapFile { Ok(()) } - pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value { + pub fn generate_json_schema(action_names: &[SharedString]) -> serde_json::Value { let mut root_schema = SchemaSettings::draft07() .with(|settings| settings.option_add_null_type = false) .into_generator() diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 9425593070..b96a23c338 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -108,6 +108,24 @@ pub struct SyntaxTheme { } impl SyntaxTheme { + // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? + pub fn new_test(colors: impl IntoIterator) -> Self { + SyntaxTheme { + highlights: colors + .into_iter() + .map(|(key, color)| { + ( + key.to_owned(), + HighlightStyle { + color: Some(color), + ..Default::default() + }, + ) + }) + .collect(), + } + } + pub fn get(&self, name: &str) -> HighlightStyle { self.highlights .iter() diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 9f681a49e9..b75e2d881f 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -47,7 +47,7 @@ install_cli = { path = "../install_cli" } journal2 = { path = "../journal2" } language2 = { path = "../language2" } # language_selector = { path = "../language_selector" } -lsp = { path = "../lsp" } +lsp2 = { path = "../lsp2" } language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } # assistant = { path = "../assistant" } @@ -60,7 +60,7 @@ project2 = { path = "../project2" } # recent_projects = { path = "../recent_projects" } rpc2 = { path = "../rpc2" } settings2 = { path = "../settings2" } -feature_flags = { path = "../feature_flags" } +feature_flags2 = { path = "../feature_flags2" } sum_tree = { path = "../sum_tree" } shellexpand = "2.1.0" text = { path = "../text" } @@ -135,6 +135,7 @@ tree-sitter-yaml.workspace = true tree-sitter-lua.workspace = true tree-sitter-nix.workspace = true tree-sitter-nu.workspace = true +tree-sitter-vue.workspace = true url = "2.2" urlencoding = "2.1.2" diff --git a/crates/zed2/src/languages.rs b/crates/zed2/src/languages.rs new file mode 100644 index 0000000000..4f7a97cb97 --- /dev/null +++ b/crates/zed2/src/languages.rs @@ -0,0 +1,273 @@ +use anyhow::Context; +use gpui2::AppContext; +pub use language2::*; +use node_runtime::NodeRuntime; +use rust_embed::RustEmbed; +use settings2::Settings; +use std::{borrow::Cow, str, sync::Arc}; +use util::asset_str; + +use self::elixir::ElixirSettings; + +mod c; +mod css; +mod elixir; +mod go; +mod html; +mod json; +#[cfg(feature = "plugin_runtime")] +mod language_plugin; +mod lua; +mod php; +mod python; +mod ruby; +mod rust; +mod svelte; +mod tailwind; +mod typescript; +mod vue; +mod yaml; + +// 1. Add tree-sitter-{language} parser to zed crate +// 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below +// 3. Add config.toml to the newly created language directory using existing languages as a template +// 4. Copy highlights from tree sitter repo for the language into a highlights.scm file. +// Note: github highlights take the last match while zed takes the first +// 5. Add indents.scm, outline.scm, and brackets.scm to implement indent on newline, outline/breadcrumbs, +// and autoclosing brackets respectively +// 6. If the language has injections add an injections.scm query file + +#[derive(RustEmbed)] +#[folder = "src/languages"] +#[exclude = "*.rs"] +struct LanguageDir; + +pub fn init( + languages: Arc, + node_runtime: Arc, + cx: &mut AppContext, +) { + ElixirSettings::register(cx); + + let language = |name, grammar, adapters| { + languages.register(name, load_config(name), grammar, adapters, load_queries) + }; + + language("bash", tree_sitter_bash::language(), vec![]); + language( + "c", + tree_sitter_c::language(), + vec![Arc::new(c::CLspAdapter) as Arc], + ); + language( + "cpp", + tree_sitter_cpp::language(), + vec![Arc::new(c::CLspAdapter)], + ); + language( + "css", + tree_sitter_css::language(), + vec![ + Arc::new(css::CssLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + + match &ElixirSettings::get(None, cx).lsp { + elixir::ElixirLspSetting::ElixirLs => language( + "elixir", + tree_sitter_elixir::language(), + vec![ + Arc::new(elixir::ElixirLspAdapter), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ), + elixir::ElixirLspSetting::NextLs => language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir::NextLspAdapter)], + ), + elixir::ElixirLspSetting::Local { path, arguments } => language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir::LocalLspAdapter { + path: path.clone(), + arguments: arguments.clone(), + })], + ), + } + + language( + "go", + tree_sitter_go::language(), + vec![Arc::new(go::GoLspAdapter)], + ); + language( + "heex", + tree_sitter_heex::language(), + vec![ + Arc::new(elixir::ElixirLspAdapter), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "json", + tree_sitter_json::language(), + vec![Arc::new(json::JsonLspAdapter::new( + node_runtime.clone(), + languages.clone(), + ))], + ); + language("markdown", tree_sitter_markdown::language(), vec![]); + language( + "python", + tree_sitter_python::language(), + vec![Arc::new(python::PythonLspAdapter::new( + node_runtime.clone(), + ))], + ); + language( + "rust", + tree_sitter_rust::language(), + vec![Arc::new(rust::RustLspAdapter)], + ); + language("toml", tree_sitter_toml::language(), vec![]); + language( + "tsx", + tree_sitter_typescript::language_tsx(), + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "typescript", + tree_sitter_typescript::language_typescript(), + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "javascript", + tree_sitter_typescript::language_tsx(), + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "html", + tree_sitter_html::language(), + vec![ + Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "ruby", + tree_sitter_ruby::language(), + vec![Arc::new(ruby::RubyLanguageServer)], + ); + language( + "erb", + tree_sitter_embedded_template::language(), + vec![ + Arc::new(ruby::RubyLanguageServer), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + language("scheme", tree_sitter_scheme::language(), vec![]); + language("racket", tree_sitter_racket::language(), vec![]); + language( + "lua", + tree_sitter_lua::language(), + vec![Arc::new(lua::LuaLspAdapter)], + ); + language( + "yaml", + tree_sitter_yaml::language(), + vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))], + ); + language( + "svelte", + tree_sitter_svelte::language(), + vec![ + Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + language( + "php", + tree_sitter_php::language(), + vec![ + Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); + + language("elm", tree_sitter_elm::language(), vec![]); + language("glsl", tree_sitter_glsl::language(), vec![]); + language("nix", tree_sitter_nix::language(), vec![]); + language("nu", tree_sitter_nu::language(), vec![]); + language( + "vue", + tree_sitter_vue::language(), + vec![Arc::new(vue::VueLspAdapter::new(node_runtime))], + ); +} + +#[cfg(any(test, feature = "test-support"))] +pub async fn language( + name: &str, + grammar: tree_sitter::Language, + lsp_adapter: Option>, +) -> Arc { + Arc::new( + Language::new(load_config(name), Some(grammar)) + .with_lsp_adapters(lsp_adapter.into_iter().collect()) + .await + .with_queries(load_queries(name)) + .unwrap(), + ) +} + +fn load_config(name: &str) -> LanguageConfig { + toml::from_slice( + &LanguageDir::get(&format!("{}/config.toml", name)) + .unwrap() + .data, + ) + .with_context(|| format!("failed to load config.toml for language {name:?}")) + .unwrap() +} + +fn load_queries(name: &str) -> LanguageQueries { + LanguageQueries { + highlights: load_query(name, "/highlights"), + brackets: load_query(name, "/brackets"), + indents: load_query(name, "/indents"), + outline: load_query(name, "/outline"), + embedding: load_query(name, "/embedding"), + injections: load_query(name, "/injections"), + overrides: load_query(name, "/overrides"), + } +} + +fn load_query(name: &str, filename_prefix: &str) -> Option> { + let mut result = None; + for path in LanguageDir::iter() { + if let Some(remainder) = path.strip_prefix(name) { + if remainder.starts_with(filename_prefix) { + let contents = asset_str::(path.as_ref()); + match &mut result { + None => result = Some(contents), + Some(r) => r.to_mut().push_str(contents.as_ref()), + } + } + } + } + result +} diff --git a/crates/zed2/src/languages/bash/brackets.scm b/crates/zed2/src/languages/bash/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed2/src/languages/bash/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed2/src/languages/bash/config.toml b/crates/zed2/src/languages/bash/config.toml new file mode 100644 index 0000000000..8c4513b250 --- /dev/null +++ b/crates/zed2/src/languages/bash/config.toml @@ -0,0 +1,9 @@ +name = "Shell Script" +path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"] +line_comment = "# " +first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b" +brackets = [ + { start = "[", end = "]", close = true, newline = false }, + { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/crates/zed2/src/languages/bash/highlights.scm b/crates/zed2/src/languages/bash/highlights.scm new file mode 100644 index 0000000000..5cb5dad6a0 --- /dev/null +++ b/crates/zed2/src/languages/bash/highlights.scm @@ -0,0 +1,59 @@ +[ + (string) + (raw_string) + (heredoc_body) + (heredoc_start) + (ansi_c_string) +] @string + +(command_name) @function + +(variable_name) @property + +[ + "case" + "do" + "done" + "elif" + "else" + "esac" + "export" + "fi" + "for" + "function" + "if" + "in" + "select" + "then" + "unset" + "until" + "while" + "local" + "declare" +] @keyword + +(comment) @comment + +(function_definition name: (word) @function) + +(file_descriptor) @number + +[ + (command_substitution) + (process_substitution) + (expansion) +]@embedded + +[ + "$" + "&&" + ">" + ">>" + "<" + "|" +] @operator + +( + (command (_) @constant) + (#match? @constant "^-") +) diff --git a/crates/zed2/src/languages/c.rs b/crates/zed2/src/languages/c.rs new file mode 100644 index 0000000000..c836fdc740 --- /dev/null +++ b/crates/zed2/src/languages/c.rs @@ -0,0 +1,321 @@ +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use futures::StreamExt; +pub use language2::*; +use lsp2::LanguageServerBinary; +use smol::fs::{self, File}; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ + fs::remove_matching, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; + +pub struct CLspAdapter; + +#[async_trait] +impl super::LspAdapter for CLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("clangd".into()) + } + + fn short_name(&self) -> &'static str { + "clangd" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = latest_github_release("clangd/clangd", false, delegate.http_client()).await?; + let asset_name = format!("clangd-mac-{}.zip", release.name); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); + let version_dir = container_dir.join(format!("clangd_{}", version.name)); + let binary_path = version_dir.join("bin/clangd"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .context("error downloading release")?; + let mut file = File::create(&zip_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + let unzip_status = smol::process::Command::new("unzip") + .current_dir(&container_dir) + .arg(&zip_path) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip clangd archive"))?; + } + + remove_matching(&container_dir, |entry| entry != version_dir).await; + } + + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec![], + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } + + async fn label_for_completion( + &self, + completion: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + let label = completion + .label + .strip_prefix('•') + .unwrap_or(&completion.label) + .trim(); + + match completion.kind { + Some(lsp2::CompletionItemKind::FIELD) if completion.detail.is_some() => { + let detail = completion.detail.as_ref().unwrap(); + let text = format!("{} {}", detail, label); + let source = Rope::from(format!("struct S {{ {} }}", text).as_str()); + let runs = language.highlight_text(&source, 11..11 + text.len()); + return Some(CodeLabel { + filter_range: detail.len() + 1..text.len(), + text, + runs, + }); + } + Some(lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE) + if completion.detail.is_some() => + { + let detail = completion.detail.as_ref().unwrap(); + let text = format!("{} {}", detail, label); + let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len()); + return Some(CodeLabel { + filter_range: detail.len() + 1..text.len(), + text, + runs, + }); + } + Some(lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD) + if completion.detail.is_some() => + { + let detail = completion.detail.as_ref().unwrap(); + let text = format!("{} {}", detail, label); + let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len()); + return Some(CodeLabel { + filter_range: detail.len() + 1..text.rfind('(').unwrap_or(text.len()), + text, + runs, + }); + } + Some(kind) => { + let highlight_name = match kind { + lsp2::CompletionItemKind::STRUCT + | lsp2::CompletionItemKind::INTERFACE + | lsp2::CompletionItemKind::CLASS + | lsp2::CompletionItemKind::ENUM => Some("type"), + lsp2::CompletionItemKind::ENUM_MEMBER => Some("variant"), + lsp2::CompletionItemKind::KEYWORD => Some("keyword"), + lsp2::CompletionItemKind::VALUE | lsp2::CompletionItemKind::CONSTANT => { + Some("constant") + } + _ => None, + }; + if let Some(highlight_id) = language + .grammar() + .and_then(|g| g.highlight_id_for_name(highlight_name?)) + { + let mut label = CodeLabel::plain(label.to_string(), None); + label.runs.push(( + 0..label.text.rfind('(').unwrap_or(label.text.len()), + highlight_id, + )); + return Some(label); + } + } + _ => {} + } + Some(CodeLabel::plain(label.to_string(), None)) + } + + async fn label_for_symbol( + &self, + name: &str, + kind: lsp2::SymbolKind, + language: &Arc, + ) -> Option { + let (text, filter_range, display_range) = match kind { + lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => { + let text = format!("void {} () {{}}", name); + let filter_range = 0..name.len(); + let display_range = 5..5 + name.len(); + (text, filter_range, display_range) + } + lsp2::SymbolKind::STRUCT => { + let text = format!("struct {} {{}}", name); + let filter_range = 7..7 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::ENUM => { + let text = format!("enum {} {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::INTERFACE | lsp2::SymbolKind::CLASS => { + let text = format!("class {} {{}}", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::CONSTANT => { + let text = format!("const int {} = 0;", name); + let filter_range = 10..10 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::MODULE => { + let text = format!("namespace {} {{}}", name); + let filter_range = 10..10 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::TYPE_PARAMETER => { + let text = format!("typename {} {{}};", name); + let filter_range = 9..9 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + (|| async move { + let mut last_clangd_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_clangd_dir = Some(entry.path()); + } + } + let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let clangd_bin = clangd_dir.join("bin/clangd"); + if clangd_bin.exists() { + Ok(LanguageServerBinary { + path: clangd_bin, + arguments: vec![], + }) + } else { + Err(anyhow!( + "missing clangd binary in directory {:?}", + clangd_dir + )) + } + })() + .await + .log_err() +} + +#[cfg(test)] +mod tests { + use gpui2::{Context, TestAppContext}; + use language2::{language_settings::AllLanguageSettings, AutoindentMode, Buffer}; + use settings2::SettingsStore; + use std::num::NonZeroU32; + + #[gpui2::test] + async fn test_c_autoindent(cx: &mut TestAppContext) { + // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX); + cx.update(|cx| { + let test_settings = SettingsStore::test(cx); + cx.set_global(test_settings); + language2::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); + }); + let language = crate::languages::language("c", tree_sitter_c::language(), None).await; + + cx.build_model(|cx| { + let mut buffer = + Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx); + + // empty function + buffer.edit([(0..0, "int main() {}")], None, cx); + + // indent inside braces + let ix = buffer.len() - 1; + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "int main() {\n \n}"); + + // indent body of single-statement if statement + let ix = buffer.len() - 2; + buffer.edit([(ix..ix, "if (a)\nb;")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "int main() {\n if (a)\n b;\n}"); + + // indent inside field expression + let ix = buffer.len() - 3; + buffer.edit([(ix..ix, "\n.c")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "int main() {\n if (a)\n b\n .c;\n}"); + + buffer + }); + } +} diff --git a/crates/zed2/src/languages/c/brackets.scm b/crates/zed2/src/languages/c/brackets.scm new file mode 100644 index 0000000000..9e8c9cd93c --- /dev/null +++ b/crates/zed2/src/languages/c/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/c/config.toml b/crates/zed2/src/languages/c/config.toml new file mode 100644 index 0000000000..f986f4b834 --- /dev/null +++ b/crates/zed2/src/languages/c/config.toml @@ -0,0 +1,12 @@ +name = "C" +path_suffixes = ["c"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, +] diff --git a/crates/zed2/src/languages/c/embedding.scm b/crates/zed2/src/languages/c/embedding.scm new file mode 100644 index 0000000000..0178abeb18 --- /dev/null +++ b/crates/zed2/src/languages/c/embedding.scm @@ -0,0 +1,43 @@ +( + (comment)* @context + . + (declaration + declarator: [ + (function_declarator + declarator: (_) @name) + (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name)) + (pointer_declarator + "*" @name + declarator: (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name))) + ] + ) @item + ) + +( + (comment)* @context + . + (function_definition + declarator: [ + (function_declarator + declarator: (_) @name + ) + (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name + )) + (pointer_declarator + "*" @name + declarator: (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name))) + ] + ) @item + ) diff --git a/crates/zed2/src/languages/c/highlights.scm b/crates/zed2/src/languages/c/highlights.scm new file mode 100644 index 0000000000..064ec61a37 --- /dev/null +++ b/crates/zed2/src/languages/c/highlights.scm @@ -0,0 +1,109 @@ +[ + "break" + "case" + "const" + "continue" + "default" + "do" + "else" + "enum" + "extern" + "for" + "if" + "inline" + "return" + "sizeof" + "static" + "struct" + "switch" + "typedef" + "union" + "volatile" + "while" +] @keyword + +[ + "#define" + "#elif" + "#else" + "#endif" + "#if" + "#ifdef" + "#ifndef" + "#include" + (preproc_directive) +] @keyword + +[ + "--" + "-" + "-=" + "->" + "=" + "!=" + "*" + "&" + "&&" + "+" + "++" + "+=" + "<" + "==" + ">" + "||" +] @operator + +[ + "." + ";" +] @punctuation.delimiter + +[ + "{" + "}" + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + (string_literal) + (system_lib_string) + (char_literal) +] @string + +(comment) @comment + +(number_literal) @number + +[ + (true) + (false) + (null) +] @constant + +(identifier) @variable + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) + +(call_expression + function: (identifier) @function) +(call_expression + function: (field_expression + field: (field_identifier) @function)) +(function_declarator + declarator: (identifier) @function) +(preproc_function_def + name: (identifier) @function.special) + +(field_identifier) @property +(statement_identifier) @label + +[ + (type_identifier) + (primitive_type) + (sized_type_specifier) +] @type + diff --git a/crates/zed2/src/languages/c/indents.scm b/crates/zed2/src/languages/c/indents.scm new file mode 100644 index 0000000000..fa40ce215e --- /dev/null +++ b/crates/zed2/src/languages/c/indents.scm @@ -0,0 +1,9 @@ +[ + (field_expression) + (assignment_expression) + (if_statement) + (for_statement) +] @indent + +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/c/injections.scm b/crates/zed2/src/languages/c/injections.scm new file mode 100644 index 0000000000..845a63bd1b --- /dev/null +++ b/crates/zed2/src/languages/c/injections.scm @@ -0,0 +1,7 @@ +(preproc_def + value: (preproc_arg) @content + (#set! "language" "c")) + +(preproc_function_def + value: (preproc_arg) @content + (#set! "language" "c")) \ No newline at end of file diff --git a/crates/zed2/src/languages/c/outline.scm b/crates/zed2/src/languages/c/outline.scm new file mode 100644 index 0000000000..ef80b7af8c --- /dev/null +++ b/crates/zed2/src/languages/c/outline.scm @@ -0,0 +1,70 @@ +(preproc_def + "#define" @context + name: (_) @name) @item + +(preproc_function_def + "#define" @context + name: (_) @name + parameters: (preproc_params + "(" @context + ")" @context)) @item + +(type_definition + "typedef" @context + declarator: (_) @name) @item + +(declaration + (type_qualifier)? @context + type: (_)? @context + declarator: [ + (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)) + (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context))) + (pointer_declarator + "*" @context + declarator: (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)))) + ] +) @item + +(function_definition + (type_qualifier)? @context + type: (_)? @context + declarator: [ + (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)) + (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context))) + (pointer_declarator + "*" @context + declarator: (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)))) + ] +) @item diff --git a/crates/zed2/src/languages/c/overrides.scm b/crates/zed2/src/languages/c/overrides.scm new file mode 100644 index 0000000000..178355c67c --- /dev/null +++ b/crates/zed2/src/languages/c/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +(string_literal) @string diff --git a/crates/zed2/src/languages/cpp/brackets.scm b/crates/zed2/src/languages/cpp/brackets.scm new file mode 100644 index 0000000000..9e8c9cd93c --- /dev/null +++ b/crates/zed2/src/languages/cpp/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/cpp/config.toml b/crates/zed2/src/languages/cpp/config.toml new file mode 100644 index 0000000000..d9b38bca06 --- /dev/null +++ b/crates/zed2/src/languages/cpp/config.toml @@ -0,0 +1,12 @@ +name = "C++" +path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, +] diff --git a/crates/zed2/src/languages/cpp/embedding.scm b/crates/zed2/src/languages/cpp/embedding.scm new file mode 100644 index 0000000000..bbd93f20db --- /dev/null +++ b/crates/zed2/src/languages/cpp/embedding.scm @@ -0,0 +1,61 @@ +( + (comment)* @context + . + (function_definition + (type_qualifier)? @name + type: (_)? @name + declarator: [ + (function_declarator + declarator: (_) @name) + (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name)) + (pointer_declarator + "*" @name + declarator: (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name))) + (reference_declarator + ["&" "&&"] @name + (function_declarator + declarator: (_) @name)) + ] + (type_qualifier)? @name) @item + ) + +( + (comment)* @context + . + (template_declaration + (class_specifier + "class" @name + name: (_) @name) + ) @item +) + +( + (comment)* @context + . + (class_specifier + "class" @name + name: (_) @name) @item + ) + +( + (comment)* @context + . + (enum_specifier + "enum" @name + name: (_) @name) @item + ) + +( + (comment)* @context + . + (declaration + type: (struct_specifier + "struct" @name) + declarator: (_) @name) @item +) diff --git a/crates/zed2/src/languages/cpp/highlights.scm b/crates/zed2/src/languages/cpp/highlights.scm new file mode 100644 index 0000000000..bcfa01ca5c --- /dev/null +++ b/crates/zed2/src/languages/cpp/highlights.scm @@ -0,0 +1,158 @@ +(identifier) @variable + +(call_expression + function: (qualified_identifier + name: (identifier) @function)) + +(call_expression + function: (identifier) @function) + +(call_expression + function: (field_expression + field: (field_identifier) @function)) + +(preproc_function_def + name: (identifier) @function.special) + +(template_function + name: (identifier) @function) + +(template_method + name: (field_identifier) @function) + +(function_declarator + declarator: (identifier) @function) + +(function_declarator + declarator: (qualified_identifier + name: (identifier) @function)) + +(function_declarator + declarator: (field_identifier) @function) + +((namespace_identifier) @type + (#match? @type "^[A-Z]")) + +(auto) @type +(type_identifier) @type + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) + +(field_identifier) @property +(statement_identifier) @label +(this) @variable.special + +[ + "break" + "case" + "catch" + "class" + "co_await" + "co_return" + "co_yield" + "const" + "constexpr" + "continue" + "default" + "delete" + "do" + "else" + "enum" + "explicit" + "extern" + "final" + "for" + "friend" + "if" + "if" + "inline" + "mutable" + "namespace" + "new" + "noexcept" + "override" + "private" + "protected" + "public" + "return" + "sizeof" + "static" + "struct" + "switch" + "template" + "throw" + "try" + "typedef" + "typename" + "union" + "using" + "virtual" + "volatile" + "while" + (primitive_type) + (type_qualifier) +] @keyword + +[ + "#define" + "#elif" + "#else" + "#endif" + "#if" + "#ifdef" + "#ifndef" + "#include" + (preproc_directive) +] @keyword + +(comment) @comment + +[ + (true) + (false) + (null) + (nullptr) +] @constant + +(number_literal) @number + +[ + (string_literal) + (system_lib_string) + (char_literal) + (raw_string_literal) +] @string + +[ + "." + ";" +] @punctuation.delimiter + +[ + "{" + "}" + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + "--" + "-" + "-=" + "->" + "=" + "!=" + "*" + "&" + "&&" + "+" + "++" + "+=" + "<" + "==" + ">" + "||" +] @operator diff --git a/crates/zed2/src/languages/cpp/indents.scm b/crates/zed2/src/languages/cpp/indents.scm new file mode 100644 index 0000000000..a17f4c4821 --- /dev/null +++ b/crates/zed2/src/languages/cpp/indents.scm @@ -0,0 +1,7 @@ +[ + (field_expression) + (assignment_expression) +] @indent + +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/cpp/injections.scm b/crates/zed2/src/languages/cpp/injections.scm new file mode 100644 index 0000000000..eca372d577 --- /dev/null +++ b/crates/zed2/src/languages/cpp/injections.scm @@ -0,0 +1,7 @@ +(preproc_def + value: (preproc_arg) @content + (#set! "language" "c++")) + +(preproc_function_def + value: (preproc_arg) @content + (#set! "language" "c++")) \ No newline at end of file diff --git a/crates/zed2/src/languages/cpp/outline.scm b/crates/zed2/src/languages/cpp/outline.scm new file mode 100644 index 0000000000..38e75f193f --- /dev/null +++ b/crates/zed2/src/languages/cpp/outline.scm @@ -0,0 +1,149 @@ +(preproc_def + "#define" @context + name: (_) @name) @item + +(preproc_function_def + "#define" @context + name: (_) @name + parameters: (preproc_params + "(" @context + ")" @context)) @item + +(type_definition + "typedef" @context + declarator: (_) @name) @item + +(struct_specifier + "struct" @context + name: (_) @name) @item + +(class_specifier + "class" @context + name: (_) @name) @item + +(enum_specifier + "enum" @context + name: (_) @name) @item + +(enumerator + name: (_) @name) @item + +(declaration + (storage_class_specifier) @context + (type_qualifier)? @context + type: (_) @context + declarator: (init_declarator + declarator: (_) @name)) @item + +(function_definition + (type_qualifier)? @context + type: (_)? @context + declarator: [ + (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)) + (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context))) + (pointer_declarator + "*" @context + declarator: (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)))) + (reference_declarator + ["&" "&&"] @context + (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context))) + ] + (type_qualifier)? @context) @item + +(declaration + (type_qualifier)? @context + type: (_)? @context + declarator: [ + (field_identifier) @name + (pointer_declarator + "*" @context + declarator: (field_identifier) @name) + (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)) + (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context))) + (pointer_declarator + "*" @context + declarator: (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)))) + (reference_declarator + ["&" "&&"] @context + (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context))) + ] + (type_qualifier)? @context) @item + +(field_declaration + (type_qualifier)? @context + type: (_) @context + declarator: [ + (field_identifier) @name + (pointer_declarator + "*" @context + declarator: (field_identifier) @name) + (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)) + (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context))) + (pointer_declarator + "*" @context + declarator: (pointer_declarator + "*" @context + declarator: (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)))) + (reference_declarator + ["&" "&&"] @context + (function_declarator + declarator: (_) @name + parameters: (parameter_list + "(" @context + ")" @context))) + ] + (type_qualifier)? @context) @item diff --git a/crates/zed2/src/languages/cpp/overrides.scm b/crates/zed2/src/languages/cpp/overrides.scm new file mode 100644 index 0000000000..178355c67c --- /dev/null +++ b/crates/zed2/src/languages/cpp/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +(string_literal) @string diff --git a/crates/zed2/src/languages/css.rs b/crates/zed2/src/languages/css.rs new file mode 100644 index 0000000000..fb6fcabe8e --- /dev/null +++ b/crates/zed2/src/languages/css.rs @@ -0,0 +1,130 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::json; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = + "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct CssLspAdapter { + node: Arc, +} + +impl CssLspAdapter { + pub fn new(node: Arc) -> Self { + CssLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for CssLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("vscode-css-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "css" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("vscode-langservers-extracted") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("vscode-langservers-extracted", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed2/src/languages/css/brackets.scm b/crates/zed2/src/languages/css/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed2/src/languages/css/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed2/src/languages/css/config.toml b/crates/zed2/src/languages/css/config.toml new file mode 100644 index 0000000000..24a844c239 --- /dev/null +++ b/crates/zed2/src/languages/css/config.toml @@ -0,0 +1,13 @@ +name = "CSS" +path_suffixes = ["css"] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, +] +word_characters = ["-"] +block_comment = ["/* ", " */"] +prettier_parser_name = "css" diff --git a/crates/zed2/src/languages/css/highlights.scm b/crates/zed2/src/languages/css/highlights.scm new file mode 100644 index 0000000000..e271d8583c --- /dev/null +++ b/crates/zed2/src/languages/css/highlights.scm @@ -0,0 +1,78 @@ +(comment) @comment + +[ + (tag_name) + (nesting_selector) + (universal_selector) +] @tag + +[ + "~" + ">" + "+" + "-" + "*" + "/" + "=" + "^=" + "|=" + "~=" + "$=" + "*=" + "and" + "or" + "not" + "only" +] @operator + +(attribute_selector (plain_value) @string) + +(attribute_name) @attribute +(pseudo_element_selector (tag_name) @attribute) +(pseudo_class_selector (class_name) @attribute) + +[ + (class_name) + (id_name) + (namespace_name) + (property_name) + (feature_name) +] @property + +(function_name) @function + +( + [ + (property_name) + (plain_value) + ] @variable.special + (#match? @variable.special "^--") +) + +[ + "@media" + "@import" + "@charset" + "@namespace" + "@supports" + "@keyframes" + (at_keyword) + (to) + (from) + (important) +] @keyword + +(string_value) @string +(color_value) @string.special + +[ + (integer_value) + (float_value) +] @number + +(unit) @type + +[ + "," + ":" +] @punctuation.delimiter diff --git a/crates/zed2/src/languages/css/indents.scm b/crates/zed2/src/languages/css/indents.scm new file mode 100644 index 0000000000..e975469092 --- /dev/null +++ b/crates/zed2/src/languages/css/indents.scm @@ -0,0 +1 @@ +(_ "{" "}" @end) @indent diff --git a/crates/zed2/src/languages/css/overrides.scm b/crates/zed2/src/languages/css/overrides.scm new file mode 100644 index 0000000000..c0db9fe327 --- /dev/null +++ b/crates/zed2/src/languages/css/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +(string_value) @string diff --git a/crates/zed2/src/languages/elixir.rs b/crates/zed2/src/languages/elixir.rs new file mode 100644 index 0000000000..09c7305fb0 --- /dev/null +++ b/crates/zed2/src/languages/elixir.rs @@ -0,0 +1,546 @@ +use anyhow::{anyhow, bail, Context, Result}; +use async_trait::async_trait; +use futures::StreamExt; +use gpui2::{AsyncAppContext, Task}; +pub use language2::*; +use lsp2::{CompletionItemKind, LanguageServerBinary, SymbolKind}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings2::Settings; +use smol::fs::{self, File}; +use std::{ + any::Any, + env::consts, + ops::Deref, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, +}; +use util::{ + async_maybe, + fs::remove_matching, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct ElixirSettings { + pub lsp: ElixirLspSetting, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ElixirLspSetting { + ElixirLs, + NextLs, + Local { + path: String, + arguments: Vec, + }, +} + +#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] +pub struct ElixirSettingsContent { + lsp: Option, +} + +impl Settings for ElixirSettings { + const KEY: Option<&'static str> = Some("elixir"); + + type FileContent = ElixirSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui2::AppContext, + ) -> Result + where + Self: Sized, + { + Self::load_via_json_merge(default_value, user_values) + } +} + +pub struct ElixirLspAdapter; + +#[async_trait] +impl LspAdapter for ElixirLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("elixir-ls".into()) + } + + fn short_name(&self) -> &'static str { + "elixir-ls" + } + + fn will_start_server( + &self, + delegate: &Arc, + cx: &mut AsyncAppContext, + ) -> Option>> { + static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false); + + const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found."; + + let delegate = delegate.clone(); + Some(cx.spawn(|cx| async move { + let elixir_output = smol::process::Command::new("elixir") + .args(["--version"]) + .output() + .await; + if elixir_output.is_err() { + if DID_SHOW_NOTIFICATION + .compare_exchange(false, true, SeqCst, SeqCst) + .is_ok() + { + cx.update(|cx| { + delegate.show_notification(NOTIFICATION_MESSAGE, cx); + })? + } + return Err(anyhow!("cannot run elixir-ls")); + } + + Ok(()) + })) + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let http = delegate.http_client(); + let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?; + let version_name = release + .name + .strip_prefix("Release ") + .context("Elixir-ls release name does not start with prefix")? + .to_owned(); + + let asset_name = format!("elixir-ls-{}.zip", &version_name); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + + let version = GitHubLspBinaryVersion { + name: version_name, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name)); + let version_dir = container_dir.join(format!("elixir-ls_{}", version.name)); + let binary_path = version_dir.join("language_server.sh"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .context("error downloading release")?; + let mut file = File::create(&zip_path) + .await + .with_context(|| format!("failed to create file {}", zip_path.display()))?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + fs::create_dir_all(&version_dir) + .await + .with_context(|| format!("failed to create directory {}", version_dir.display()))?; + let unzip_status = smol::process::Command::new("unzip") + .arg(&zip_path) + .arg("-d") + .arg(&version_dir) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip elixir-ls archive"))?; + } + + remove_matching(&container_dir, |entry| entry != version_dir).await; + } + + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec![], + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary_elixir_ls(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary_elixir_ls(container_dir).await + } + + async fn label_for_completion( + &self, + completion: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + match completion.kind.zip(completion.detail.as_ref()) { + Some((_, detail)) if detail.starts_with("(function)") => { + let text = detail.strip_prefix("(function) ")?; + let filter_range = 0..text.find('(').unwrap_or(text.len()); + let source = Rope::from(format!("def {text}").as_str()); + let runs = language.highlight_text(&source, 4..4 + text.len()); + return Some(CodeLabel { + text: text.to_string(), + runs, + filter_range, + }); + } + Some((_, detail)) if detail.starts_with("(macro)") => { + let text = detail.strip_prefix("(macro) ")?; + let filter_range = 0..text.find('(').unwrap_or(text.len()); + let source = Rope::from(format!("defmacro {text}").as_str()); + let runs = language.highlight_text(&source, 9..9 + text.len()); + return Some(CodeLabel { + text: text.to_string(), + runs, + filter_range, + }); + } + Some(( + CompletionItemKind::CLASS + | CompletionItemKind::MODULE + | CompletionItemKind::INTERFACE + | CompletionItemKind::STRUCT, + _, + )) => { + let filter_range = 0..completion + .label + .find(" (") + .unwrap_or(completion.label.len()); + let text = &completion.label[filter_range.clone()]; + let source = Rope::from(format!("defmodule {text}").as_str()); + let runs = language.highlight_text(&source, 10..10 + text.len()); + return Some(CodeLabel { + text: completion.label.clone(), + runs, + filter_range, + }); + } + _ => {} + } + + None + } + + async fn label_for_symbol( + &self, + name: &str, + kind: SymbolKind, + language: &Arc, + ) -> Option { + let (text, filter_range, display_range) = match kind { + SymbolKind::METHOD | SymbolKind::FUNCTION => { + let text = format!("def {}", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => { + let text = format!("defmodule {}", name); + let filter_range = 10..10 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } +} + +async fn get_cached_server_binary_elixir_ls( + container_dir: PathBuf, +) -> Option { + (|| async move { + let mut last = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + last.map(|path| LanguageServerBinary { + path, + arguments: vec![], + }) + .ok_or_else(|| anyhow!("no cached binary")) + })() + .await + .log_err() +} + +pub struct NextLspAdapter; + +#[async_trait] +impl LspAdapter for NextLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("next-ls".into()) + } + + fn short_name(&self) -> &'static str { + "next-ls" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = + latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?; + let version = release.name.clone(); + let platform = match consts::ARCH { + "x86_64" => "darwin_amd64", + "aarch64" => "darwin_arm64", + other => bail!("Running on unsupported platform: {other}"), + }; + let asset_name = format!("next_ls_{}", platform); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: version, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + + let binary_path = container_dir.join("next-ls"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + + let mut file = smol::fs::File::create(&binary_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } + + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec!["--stdio".into()], + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary_next(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--stdio".into()]; + binary + }) + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary_next(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } + + async fn label_for_completion( + &self, + completion: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + label_for_completion_elixir(completion, language) + } + + async fn label_for_symbol( + &self, + name: &str, + symbol_kind: SymbolKind, + language: &Arc, + ) -> Option { + label_for_symbol_elixir(name, symbol_kind, language) + } +} + +async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option { + async_maybe!({ + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "next-ls") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: Vec::new(), + }) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() +} + +pub struct LocalLspAdapter { + pub path: String, + pub arguments: Vec, +} + +#[async_trait] +impl LspAdapter for LocalLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("local-ls".into()) + } + + fn short_name(&self) -> &'static str { + "local-ls" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(()) as Box<_>) + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let path = shellexpand::full(&self.path)?; + Ok(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let path = shellexpand::full(&self.path).ok()?; + Some(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + let path = shellexpand::full(&self.path).ok()?; + Some(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn label_for_completion( + &self, + completion: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + label_for_completion_elixir(completion, language) + } + + async fn label_for_symbol( + &self, + name: &str, + symbol: SymbolKind, + language: &Arc, + ) -> Option { + label_for_symbol_elixir(name, symbol, language) + } +} + +fn label_for_completion_elixir( + completion: &lsp2::CompletionItem, + language: &Arc, +) -> Option { + return Some(CodeLabel { + runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()), + text: completion.label.clone(), + filter_range: 0..completion.label.len(), + }); +} + +fn label_for_symbol_elixir( + name: &str, + _: SymbolKind, + language: &Arc, +) -> Option { + Some(CodeLabel { + runs: language.highlight_text(&name.into(), 0..name.len()), + text: name.to_string(), + filter_range: 0..name.len(), + }) +} diff --git a/crates/zed2/src/languages/elixir/brackets.scm b/crates/zed2/src/languages/elixir/brackets.scm new file mode 100644 index 0000000000..d8713187e2 --- /dev/null +++ b/crates/zed2/src/languages/elixir/brackets.scm @@ -0,0 +1,5 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) +("do" @open "end" @close) diff --git a/crates/zed2/src/languages/elixir/config.toml b/crates/zed2/src/languages/elixir/config.toml new file mode 100644 index 0000000000..8983c0e49b --- /dev/null +++ b/crates/zed2/src/languages/elixir/config.toml @@ -0,0 +1,16 @@ +name = "Elixir" +path_suffixes = ["ex", "exs"] +line_comment = "# " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, +] +scope_opt_in_language_servers = ["tailwindcss-language-server"] + +[overrides.string] +word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed2/src/languages/elixir/embedding.scm b/crates/zed2/src/languages/elixir/embedding.scm new file mode 100644 index 0000000000..16ad20746d --- /dev/null +++ b/crates/zed2/src/languages/elixir/embedding.scm @@ -0,0 +1,27 @@ +( + (unary_operator + operator: "@" + operand: (call + target: (identifier) @unary + (#match? @unary "^(doc)$")) + ) @context + . + (call + target: (identifier) @name + (arguments + [ + (identifier) @name + (call + target: (identifier) @name) + (binary_operator + left: (call + target: (identifier) @name) + operator: "when") + ]) + (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + ) + + (call + target: (identifier) @name + (arguments (alias) @name) + (#match? @name "^(defmodule|defprotocol)$")) @item diff --git a/crates/zed2/src/languages/elixir/highlights.scm b/crates/zed2/src/languages/elixir/highlights.scm new file mode 100644 index 0000000000..0e779d195c --- /dev/null +++ b/crates/zed2/src/languages/elixir/highlights.scm @@ -0,0 +1,153 @@ +["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword + +(unary_operator + operator: "&" + operand: (integer) @operator) + +(operator_identifier) @operator + +(unary_operator + operator: _ @operator) + +(binary_operator + operator: _ @operator) + +(dot + operator: _ @operator) + +(stab_clause + operator: _ @operator) + +[ + (boolean) + (nil) +] @constant + +[ + (integer) + (float) +] @number + +(alias) @type + +(call + target: (dot + left: (atom) @type)) + +(char) @constant + +(escape_sequence) @string.escape + +[ + (atom) + (quoted_atom) + (keyword) + (quoted_keyword) +] @string.special.symbol + +[ + (string) + (charlist) +] @string + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string + quoted_end: _ @string + (#match? @__name__ "^[sS]$")) @string + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string.regex + quoted_end: _ @string.regex + (#match? @__name__ "^[rR]$")) @string.regex + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string.special + quoted_end: _ @string.special) @string.special + +( + (identifier) @comment.unused + (#match? @comment.unused "^_") +) + +(call + target: [ + (identifier) @function + (dot + right: (identifier) @function) + ]) + +(call + target: (identifier) @keyword + (arguments + [ + (identifier) @function + (binary_operator + left: (identifier) @function + operator: "when") + (binary_operator + operator: "|>" + right: (identifier)) + ]) + (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) + +(binary_operator + operator: "|>" + right: (identifier) @function) + +(call + target: (identifier) @keyword + (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$")) + +(call + target: (identifier) @keyword + (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$")) + +( + (identifier) @constant.builtin + (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$") +) + +(unary_operator + operator: "@" @comment.doc + operand: (call + target: (identifier) @__attribute__ @comment.doc + (arguments + [ + (string) + (charlist) + (sigil) + (boolean) + ] @comment.doc)) + (#match? @__attribute__ "^(moduledoc|typedoc|doc)$")) + +(comment) @comment + +[ + "%" +] @punctuation + +[ + "," + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<<" + ">>" +] @punctuation.bracket + +(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded + +((sigil + (sigil_name) @_sigil_name + (quoted_content) @embedded) + (#eq? @_sigil_name "H")) diff --git a/crates/zed2/src/languages/elixir/indents.scm b/crates/zed2/src/languages/elixir/indents.scm new file mode 100644 index 0000000000..ab6fc4da67 --- /dev/null +++ b/crates/zed2/src/languages/elixir/indents.scm @@ -0,0 +1,6 @@ +(call) @indent + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent +(_ "do" "end" @end) @indent diff --git a/crates/zed2/src/languages/elixir/injections.scm b/crates/zed2/src/languages/elixir/injections.scm new file mode 100644 index 0000000000..4de229f104 --- /dev/null +++ b/crates/zed2/src/languages/elixir/injections.scm @@ -0,0 +1,7 @@ +; Phoenix HTML template + +((sigil + (sigil_name) @_sigil_name + (quoted_content) @content) + (#eq? @_sigil_name "H") + (#set! language "heex")) diff --git a/crates/zed2/src/languages/elixir/outline.scm b/crates/zed2/src/languages/elixir/outline.scm new file mode 100644 index 0000000000..a3311fb6d4 --- /dev/null +++ b/crates/zed2/src/languages/elixir/outline.scm @@ -0,0 +1,26 @@ +(call + target: (identifier) @context + (arguments (alias) @name) + (#match? @context "^(defmodule|defprotocol)$")) @item + +(call + target: (identifier) @context + (arguments + [ + (identifier) @name + (call + target: (identifier) @name + (arguments + "(" @context.extra + _* @context.extra + ")" @context.extra)) + (binary_operator + left: (call + target: (identifier) @name + (arguments + "(" @context.extra + _* @context.extra + ")" @context.extra)) + operator: "when") + ]) + (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item diff --git a/crates/zed2/src/languages/elixir/overrides.scm b/crates/zed2/src/languages/elixir/overrides.scm new file mode 100644 index 0000000000..1812540181 --- /dev/null +++ b/crates/zed2/src/languages/elixir/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +[(string) (charlist)] @string diff --git a/crates/zed2/src/languages/elm/config.toml b/crates/zed2/src/languages/elm/config.toml new file mode 100644 index 0000000000..5051427a93 --- /dev/null +++ b/crates/zed2/src/languages/elm/config.toml @@ -0,0 +1,11 @@ +name = "Elm" +path_suffixes = ["elm"] +line_comment = "-- " +block_comment = ["{- ", " -}"] +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, +] diff --git a/crates/zed2/src/languages/elm/highlights.scm b/crates/zed2/src/languages/elm/highlights.scm new file mode 100644 index 0000000000..5723c7eecb --- /dev/null +++ b/crates/zed2/src/languages/elm/highlights.scm @@ -0,0 +1,72 @@ +[ + "if" + "then" + "else" + "let" + "in" + (case) + (of) + (backslash) + (as) + (port) + (exposing) + (alias) + (import) + (module) + (type) + (arrow) + ] @keyword + +[ + (eq) + (operator_identifier) + (colon) +] @operator + +(type_annotation(lower_case_identifier) @function) +(port_annotation(lower_case_identifier) @function) +(function_declaration_left(lower_case_identifier) @function.definition) + +(function_call_expr + target: (value_expr + name: (value_qid (lower_case_identifier) @function))) + +(exposed_value(lower_case_identifier) @function) +(exposed_type(upper_case_identifier) @type) + +(field_access_expr(value_expr(value_qid)) @identifier) +(lower_pattern) @variable +(record_base_identifier) @identifier + +[ + "(" + ")" +] @punctuation.bracket + +[ + "|" + "," +] @punctuation.delimiter + +(number_constant_expr) @constant + +(type_declaration(upper_case_identifier) @type) +(type_ref) @type +(type_alias_declaration name: (upper_case_identifier) @type) + +(value_expr(upper_case_qid(upper_case_identifier)) @type) + +[ + (line_comment) + (block_comment) +] @comment + +(string_escape) @string.escape + +[ + (open_quote) + (close_quote) + (regular_string_part) + (open_char) + (close_char) +] @string diff --git a/crates/zed2/src/languages/elm/injections.scm b/crates/zed2/src/languages/elm/injections.scm new file mode 100644 index 0000000000..0567320675 --- /dev/null +++ b/crates/zed2/src/languages/elm/injections.scm @@ -0,0 +1,2 @@ +((glsl_content) @content + (#set! "language" "glsl")) diff --git a/crates/zed2/src/languages/elm/outline.scm b/crates/zed2/src/languages/elm/outline.scm new file mode 100644 index 0000000000..1d7d5a70b0 --- /dev/null +++ b/crates/zed2/src/languages/elm/outline.scm @@ -0,0 +1,22 @@ +(type_declaration + (type) @context + (upper_case_identifier) @name) @item + +(type_alias_declaration + (type) @context + (alias) @context + name: (upper_case_identifier) @name) @item + +(type_alias_declaration + typeExpression: + (type_expression + part: (record_type + (field_type + name: (lower_case_identifier) @name) @item))) + +(union_variant + name: (upper_case_identifier) @name) @item + +(value_declaration + functionDeclarationLeft: + (function_declaration_left(lower_case_identifier) @name)) @item diff --git a/crates/zed2/src/languages/erb/config.toml b/crates/zed2/src/languages/erb/config.toml new file mode 100644 index 0000000000..ebc45e9984 --- /dev/null +++ b/crates/zed2/src/languages/erb/config.toml @@ -0,0 +1,8 @@ +name = "ERB" +path_suffixes = ["erb"] +autoclose_before = ">})" +brackets = [ + { start = "<", end = ">", close = true, newline = true }, +] +block_comment = ["<%#", "%>"] +scope_opt_in_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed2/src/languages/erb/highlights.scm b/crates/zed2/src/languages/erb/highlights.scm new file mode 100644 index 0000000000..0bf76a7d49 --- /dev/null +++ b/crates/zed2/src/languages/erb/highlights.scm @@ -0,0 +1,12 @@ +(comment_directive) @comment + +[ + "<%#" + "<%" + "<%=" + "<%_" + "<%-" + "%>" + "-%>" + "_%>" +] @keyword diff --git a/crates/zed2/src/languages/erb/injections.scm b/crates/zed2/src/languages/erb/injections.scm new file mode 100644 index 0000000000..7a69a818ef --- /dev/null +++ b/crates/zed2/src/languages/erb/injections.scm @@ -0,0 +1,7 @@ +((code) @content + (#set! "language" "ruby") + (#set! "combined")) + +((content) @content + (#set! "language" "html") + (#set! "combined")) diff --git a/crates/zed2/src/languages/glsl/config.toml b/crates/zed2/src/languages/glsl/config.toml new file mode 100644 index 0000000000..4081a6381f --- /dev/null +++ b/crates/zed2/src/languages/glsl/config.toml @@ -0,0 +1,9 @@ +name = "GLSL" +path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"] +line_comment = "// " +block_comment = ["/* ", " */"] +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, +] diff --git a/crates/zed2/src/languages/glsl/highlights.scm b/crates/zed2/src/languages/glsl/highlights.scm new file mode 100644 index 0000000000..e4503c6fbb --- /dev/null +++ b/crates/zed2/src/languages/glsl/highlights.scm @@ -0,0 +1,118 @@ +"break" @keyword +"case" @keyword +"const" @keyword +"continue" @keyword +"default" @keyword +"do" @keyword +"else" @keyword +"enum" @keyword +"extern" @keyword +"for" @keyword +"if" @keyword +"inline" @keyword +"return" @keyword +"sizeof" @keyword +"static" @keyword +"struct" @keyword +"switch" @keyword +"typedef" @keyword +"union" @keyword +"volatile" @keyword +"while" @keyword + +"#define" @keyword +"#elif" @keyword +"#else" @keyword +"#endif" @keyword +"#if" @keyword +"#ifdef" @keyword +"#ifndef" @keyword +"#include" @keyword +(preproc_directive) @keyword + +"--" @operator +"-" @operator +"-=" @operator +"->" @operator +"=" @operator +"!=" @operator +"*" @operator +"&" @operator +"&&" @operator +"+" @operator +"++" @operator +"+=" @operator +"<" @operator +"==" @operator +">" @operator +"||" @operator + +"." @delimiter +";" @delimiter + +(string_literal) @string +(system_lib_string) @string + +(null) @constant +(number_literal) @number +(char_literal) @number + +(call_expression + function: (identifier) @function) +(call_expression + function: (field_expression + field: (field_identifier) @function)) +(function_declarator + declarator: (identifier) @function) +(preproc_function_def + name: (identifier) @function.special) + +(field_identifier) @property +(statement_identifier) @label +(type_identifier) @type +(primitive_type) @type +(sized_type_specifier) @type + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]*$")) + +(identifier) @variable + +(comment) @comment +; inherits: c + +[ + "in" + "out" + "inout" + "uniform" + "shared" + "layout" + "attribute" + "varying" + "buffer" + "coherent" + "readonly" + "writeonly" + "precision" + "highp" + "mediump" + "lowp" + "centroid" + "sample" + "patch" + "smooth" + "flat" + "noperspective" + "invariant" + "precise" +] @type.qualifier + +"subroutine" @keyword.function + +(extension_storage_class) @storageclass + +( + (identifier) @variable.builtin + (#match? @variable.builtin "^gl_") +) diff --git a/crates/zed2/src/languages/go.rs b/crates/zed2/src/languages/go.rs new file mode 100644 index 0000000000..21001015b9 --- /dev/null +++ b/crates/zed2/src/languages/go.rs @@ -0,0 +1,464 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +use gpui2::{AsyncAppContext, Task}; +pub use language2::*; +use lazy_static::lazy_static; +use lsp2::LanguageServerBinary; +use regex::Regex; +use smol::{fs, process}; +use std::{ + any::Any, + ffi::{OsStr, OsString}, + ops::Range, + path::PathBuf, + str, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, +}; +use util::{fs::remove_matching, github::latest_github_release, ResultExt}; + +fn server_binary_arguments() -> Vec { + vec!["-mode=stdio".into()] +} + +#[derive(Copy, Clone)] +pub struct GoLspAdapter; + +lazy_static! { + static ref GOPLS_VERSION_REGEX: Regex = Regex::new(r"\d+\.\d+\.\d+").unwrap(); +} + +#[async_trait] +impl super::LspAdapter for GoLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("gopls".into()) + } + + fn short_name(&self) -> &'static str { + "gopls" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = latest_github_release("golang/tools", false, delegate.http_client()).await?; + let version: Option = release.name.strip_prefix("gopls/v").map(str::to_string); + if version.is_none() { + log::warn!( + "couldn't infer gopls version from github release name '{}'", + release.name + ); + } + Ok(Box::new(version) as Box<_>) + } + + fn will_fetch_server( + &self, + delegate: &Arc, + cx: &mut AsyncAppContext, + ) -> Option>> { + static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false); + + const NOTIFICATION_MESSAGE: &str = + "Could not install the Go language server `gopls`, because `go` was not found."; + + let delegate = delegate.clone(); + Some(cx.spawn(|cx| async move { + let install_output = process::Command::new("go").args(["version"]).output().await; + if install_output.is_err() { + if DID_SHOW_NOTIFICATION + .compare_exchange(false, true, SeqCst, SeqCst) + .is_ok() + { + cx.update(|cx| { + delegate.show_notification(NOTIFICATION_MESSAGE, cx); + })? + } + return Err(anyhow!("cannot install gopls")); + } + Ok(()) + })) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::>().unwrap(); + let this = *self; + + if let Some(version) = *version { + let binary_path = container_dir.join(&format!("gopls_{version}")); + if let Ok(metadata) = fs::metadata(&binary_path).await { + if metadata.is_file() { + remove_matching(&container_dir, |entry| { + entry != binary_path && entry.file_name() != Some(OsStr::new("gobin")) + }) + .await; + + return Ok(LanguageServerBinary { + path: binary_path.to_path_buf(), + arguments: server_binary_arguments(), + }); + } + } + } else if let Some(path) = this + .cached_server_binary(container_dir.clone(), delegate) + .await + { + return Ok(path); + } + + let gobin_dir = container_dir.join("gobin"); + fs::create_dir_all(&gobin_dir).await?; + let install_output = process::Command::new("go") + .env("GO111MODULE", "on") + .env("GOBIN", &gobin_dir) + .args(["install", "golang.org/x/tools/gopls@latest"]) + .output() + .await?; + if !install_output.status.success() { + Err(anyhow!("failed to install gopls. Is go installed?"))?; + } + + let installed_binary_path = gobin_dir.join("gopls"); + let version_output = process::Command::new(&installed_binary_path) + .arg("version") + .output() + .await + .map_err(|e| anyhow!("failed to run installed gopls binary {:?}", e))?; + let version_stdout = str::from_utf8(&version_output.stdout) + .map_err(|_| anyhow!("gopls version produced invalid utf8"))?; + let version = GOPLS_VERSION_REGEX + .find(version_stdout) + .ok_or_else(|| anyhow!("failed to parse gopls version output"))? + .as_str(); + let binary_path = container_dir.join(&format!("gopls_{version}")); + fs::rename(&installed_binary_path, &binary_path).await?; + + Ok(LanguageServerBinary { + path: binary_path.to_path_buf(), + arguments: server_binary_arguments(), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } + + async fn label_for_completion( + &self, + completion: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + let label = &completion.label; + + // Gopls returns nested fields and methods as completions. + // To syntax highlight these, combine their final component + // with their detail. + let name_offset = label.rfind('.').unwrap_or(0); + + match completion.kind.zip(completion.detail.as_ref()) { + Some((lsp2::CompletionItemKind::MODULE, detail)) => { + let text = format!("{label} {detail}"); + let source = Rope::from(format!("import {text}").as_str()); + let runs = language.highlight_text(&source, 7..7 + text.len()); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some(( + lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE, + detail, + )) => { + let text = format!("{label} {detail}"); + let source = + Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 4..4 + text.len()), + ); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some((lsp2::CompletionItemKind::STRUCT, _)) => { + let text = format!("{label} struct {{}}"); + let source = Rope::from(format!("type {}", &text[name_offset..]).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 5..5 + text.len()), + ); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some((lsp2::CompletionItemKind::INTERFACE, _)) => { + let text = format!("{label} interface {{}}"); + let source = Rope::from(format!("type {}", &text[name_offset..]).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 5..5 + text.len()), + ); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some((lsp2::CompletionItemKind::FIELD, detail)) => { + let text = format!("{label} {detail}"); + let source = + Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 16..16 + text.len()), + ); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some(( + lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD, + detail, + )) => { + if let Some(signature) = detail.strip_prefix("func") { + let text = format!("{label}{signature}"); + let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 5..5 + text.len()), + ); + return Some(CodeLabel { + filter_range: 0..label.len(), + text, + runs, + }); + } + } + _ => {} + } + None + } + + async fn label_for_symbol( + &self, + name: &str, + kind: lsp2::SymbolKind, + language: &Arc, + ) -> Option { + let (text, filter_range, display_range) = match kind { + lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => { + let text = format!("func {} () {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::STRUCT => { + let text = format!("type {} struct {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..text.len(); + (text, filter_range, display_range) + } + lsp2::SymbolKind::INTERFACE => { + let text = format!("type {} interface {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..text.len(); + (text, filter_range, display_range) + } + lsp2::SymbolKind::CLASS => { + let text = format!("type {} T", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::CONSTANT => { + let text = format!("const {} = nil", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::VARIABLE => { + let text = format!("var {} = nil", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::MODULE => { + let text = format!("package {}", name); + let filter_range = 8..8 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + (|| async move { + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name.starts_with("gopls_")) + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: server_binary_arguments(), + }) + } else { + Err(anyhow!("no cached binary")) + } + })() + .await + .log_err() +} + +fn adjust_runs( + delta: usize, + mut runs: Vec<(Range, HighlightId)>, +) -> Vec<(Range, HighlightId)> { + for (range, _) in &mut runs { + range.start += delta; + range.end += delta; + } + runs +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::languages::language; + use gpui2::Hsla; + use theme2::SyntaxTheme; + + #[gpui2::test] + async fn test_go_label_for_completion() { + let language = language( + "go", + tree_sitter_go::language(), + Some(Arc::new(GoLspAdapter)), + ) + .await; + + let theme = SyntaxTheme::new_test([ + ("type", Hsla::default()), + ("keyword", Hsla::default()), + ("function", Hsla::default()), + ("number", Hsla::default()), + ("property", Hsla::default()), + ]); + language.set_theme(&theme); + + let grammar = language.grammar().unwrap(); + let highlight_function = grammar.highlight_id_for_name("function").unwrap(); + let highlight_type = grammar.highlight_id_for_name("type").unwrap(); + let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap(); + let highlight_number = grammar.highlight_id_for_name("number").unwrap(); + let highlight_field = grammar.highlight_id_for_name("property").unwrap(); + + assert_eq!( + language + .label_for_completion(&lsp2::CompletionItem { + kind: Some(lsp2::CompletionItemKind::FUNCTION), + label: "Hello".to_string(), + detail: Some("func(a B) c.D".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "Hello(a B) c.D".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (8..9, highlight_type), + (13..14, highlight_type), + ], + }) + ); + + // Nested methods + assert_eq!( + language + .label_for_completion(&lsp2::CompletionItem { + kind: Some(lsp2::CompletionItemKind::METHOD), + label: "one.two.Three".to_string(), + detail: Some("func() [3]interface{}".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "one.two.Three() [3]interface{}".to_string(), + filter_range: 0..13, + runs: vec![ + (8..13, highlight_function), + (17..18, highlight_number), + (19..28, highlight_keyword), + ], + }) + ); + + // Nested fields + assert_eq!( + language + .label_for_completion(&lsp2::CompletionItem { + kind: Some(lsp2::CompletionItemKind::FIELD), + label: "two.Three".to_string(), + detail: Some("a.Bcd".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "two.Three a.Bcd".to_string(), + filter_range: 0..9, + runs: vec![(4..9, highlight_field), (12..15, highlight_type)], + }) + ); + } +} diff --git a/crates/zed2/src/languages/go/brackets.scm b/crates/zed2/src/languages/go/brackets.scm new file mode 100644 index 0000000000..9e8c9cd93c --- /dev/null +++ b/crates/zed2/src/languages/go/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/go/config.toml b/crates/zed2/src/languages/go/config.toml new file mode 100644 index 0000000000..1951e193f0 --- /dev/null +++ b/crates/zed2/src/languages/go/config.toml @@ -0,0 +1,12 @@ +name = "Go" +path_suffixes = ["go"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/crates/zed2/src/languages/go/embedding.scm b/crates/zed2/src/languages/go/embedding.scm new file mode 100644 index 0000000000..9d8700cdfb --- /dev/null +++ b/crates/zed2/src/languages/go/embedding.scm @@ -0,0 +1,24 @@ +( + (comment)* @context + . + (type_declaration + (type_spec + name: (_) @name) + ) @item +) + +( + (comment)* @context + . + (function_declaration + name: (_) @name + ) @item +) + +( + (comment)* @context + . + (method_declaration + name: (_) @name + ) @item +) diff --git a/crates/zed2/src/languages/go/highlights.scm b/crates/zed2/src/languages/go/highlights.scm new file mode 100644 index 0000000000..6a9be8aae0 --- /dev/null +++ b/crates/zed2/src/languages/go/highlights.scm @@ -0,0 +1,107 @@ +(identifier) @variable +(type_identifier) @type +(field_identifier) @property + +(call_expression + function: (identifier) @function) + +(call_expression + function: (selector_expression + field: (field_identifier) @function.method)) + +(function_declaration + name: (identifier) @function) + +(method_declaration + name: (field_identifier) @function.method) + +[ + "--" + "-" + "-=" + ":=" + "!" + "!=" + "..." + "*" + "*" + "*=" + "/" + "/=" + "&" + "&&" + "&=" + "%" + "%=" + "^" + "^=" + "+" + "++" + "+=" + "<-" + "<" + "<<" + "<<=" + "<=" + "=" + "==" + ">" + ">=" + ">>" + ">>=" + "|" + "|=" + "||" + "~" +] @operator + +[ + "break" + "case" + "chan" + "const" + "continue" + "default" + "defer" + "else" + "fallthrough" + "for" + "func" + "go" + "goto" + "if" + "import" + "interface" + "map" + "package" + "range" + "return" + "select" + "struct" + "switch" + "type" + "var" +] @keyword + +[ + (interpreted_string_literal) + (raw_string_literal) + (rune_literal) +] @string + +(escape_sequence) @escape + +[ + (int_literal) + (float_literal) + (imaginary_literal) +] @number + +[ + (true) + (false) + (nil) + (iota) +] @constant.builtin + +(comment) @comment diff --git a/crates/zed2/src/languages/go/indents.scm b/crates/zed2/src/languages/go/indents.scm new file mode 100644 index 0000000000..abbb72eb37 --- /dev/null +++ b/crates/zed2/src/languages/go/indents.scm @@ -0,0 +1,9 @@ +[ + (assignment_statement) + (call_expression) + (selector_expression) +] @indent + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/go/outline.scm b/crates/zed2/src/languages/go/outline.scm new file mode 100644 index 0000000000..2ff7ef25a0 --- /dev/null +++ b/crates/zed2/src/languages/go/outline.scm @@ -0,0 +1,43 @@ +(type_declaration + "type" @context + (type_spec + name: (_) @name)) @item + +(function_declaration + "func" @context + name: (identifier) @name + parameters: (parameter_list + "(" @context + ")" @context)) @item + +(method_declaration + "func" @context + receiver: (parameter_list + "(" @context + (parameter_declaration + type: (_) @context) + ")" @context) + name: (field_identifier) @name + parameters: (parameter_list + "(" @context + ")" @context)) @item + +(const_declaration + "const" @context + (const_spec + name: (identifier) @name) @item) + +(source_file + (var_declaration + "var" @context + (var_spec + name: (identifier) @name) @item)) + +(method_spec + name: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)) @item + +(field_declaration + name: (_) @name) @item \ No newline at end of file diff --git a/crates/zed2/src/languages/go/overrides.scm b/crates/zed2/src/languages/go/overrides.scm new file mode 100644 index 0000000000..9eb287df3f --- /dev/null +++ b/crates/zed2/src/languages/go/overrides.scm @@ -0,0 +1,6 @@ +(comment) @comment +[ + (interpreted_string_literal) + (raw_string_literal) + (rune_literal) +] @string diff --git a/crates/zed2/src/languages/heex/config.toml b/crates/zed2/src/languages/heex/config.toml new file mode 100644 index 0000000000..74cb5ac9ff --- /dev/null +++ b/crates/zed2/src/languages/heex/config.toml @@ -0,0 +1,12 @@ +name = "HEEX" +path_suffixes = ["heex"] +autoclose_before = ">})" +brackets = [ + { start = "<", end = ">", close = true, newline = true }, +] +block_comment = ["<%!-- ", " --%>"] +scope_opt_in_language_servers = ["tailwindcss-language-server"] + +[overrides.string] +word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed2/src/languages/heex/highlights.scm b/crates/zed2/src/languages/heex/highlights.scm new file mode 100644 index 0000000000..5252b71fac --- /dev/null +++ b/crates/zed2/src/languages/heex/highlights.scm @@ -0,0 +1,57 @@ +; HEEx delimiters +[ + "/>" + "" + "{" + "}" +] @punctuation.bracket + +[ + "<%!--" + "<%" + "<%#" + "<%%=" + "<%=" + "%>" + "--%>" + "-->" + ""] +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] }, + { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, +] +word_characters = ["-"] +prettier_parser_name = "html" diff --git a/crates/zed2/src/languages/html/highlights.scm b/crates/zed2/src/languages/html/highlights.scm new file mode 100644 index 0000000000..0ce535fad4 --- /dev/null +++ b/crates/zed2/src/languages/html/highlights.scm @@ -0,0 +1,15 @@ +(tag_name) @keyword +(erroneous_end_tag_name) @keyword +(doctype) @constant +(attribute_name) @property +(attribute_value) @string +(comment) @comment + +"=" @operator + +[ + "<" + ">" + "" +] @punctuation.bracket \ No newline at end of file diff --git a/crates/zed2/src/languages/html/indents.scm b/crates/zed2/src/languages/html/indents.scm new file mode 100644 index 0000000000..436663dba3 --- /dev/null +++ b/crates/zed2/src/languages/html/indents.scm @@ -0,0 +1,6 @@ +(start_tag ">" @end) @indent +(self_closing_tag "/>" @end) @indent + +(element + (start_tag) @start + (end_tag)? @end) @indent diff --git a/crates/zed2/src/languages/html/injections.scm b/crates/zed2/src/languages/html/injections.scm new file mode 100644 index 0000000000..9084e373f2 --- /dev/null +++ b/crates/zed2/src/languages/html/injections.scm @@ -0,0 +1,7 @@ +(script_element + (raw_text) @content + (#set! "language" "javascript")) + +(style_element + (raw_text) @content + (#set! "language" "css")) diff --git a/crates/zed2/src/languages/html/outline.scm b/crates/zed2/src/languages/html/outline.scm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed2/src/languages/html/overrides.scm b/crates/zed2/src/languages/html/overrides.scm new file mode 100644 index 0000000000..97accffd67 --- /dev/null +++ b/crates/zed2/src/languages/html/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +(quoted_attribute_value) @string \ No newline at end of file diff --git a/crates/zed2/src/languages/javascript/brackets.scm b/crates/zed2/src/languages/javascript/brackets.scm new file mode 100644 index 0000000000..63395f81d8 --- /dev/null +++ b/crates/zed2/src/languages/javascript/brackets.scm @@ -0,0 +1,5 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/javascript/config.toml b/crates/zed2/src/languages/javascript/config.toml new file mode 100644 index 0000000000..3b8862e358 --- /dev/null +++ b/crates/zed2/src/languages/javascript/config.toml @@ -0,0 +1,26 @@ +name = "JavaScript" +path_suffixes = ["js", "jsx", "mjs", "cjs"] +first_line_pattern = '^#!.*\bnode\b' +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true, not_in = ["comment", "string"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "`", end = "`", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, +] +word_characters = ["$", "#"] +scope_opt_in_language_servers = ["tailwindcss-language-server"] +prettier_parser_name = "babel" + +[overrides.element] +line_comment = { remove = true } +block_comment = ["{/* ", " */}"] + +[overrides.string] +word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed2/src/languages/javascript/contexts.scm b/crates/zed2/src/languages/javascript/contexts.scm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed2/src/languages/javascript/embedding.scm b/crates/zed2/src/languages/javascript/embedding.scm new file mode 100644 index 0000000000..ab1a3b6b06 --- /dev/null +++ b/crates/zed2/src/languages/javascript/embedding.scm @@ -0,0 +1,71 @@ +( + (comment)* @context + . + [ + (export_statement + (function_declaration + "async"? @name + "function" @name + name: (_) @name)) + (function_declaration + "async"? @name + "function" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + [ + (export_statement + (class_declaration + "class" @name + name: (_) @name)) + (class_declaration + "class" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + [ + (export_statement + (interface_declaration + "interface" @name + name: (_) @name)) + (interface_declaration + "interface" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + [ + (export_statement + (enum_declaration + "enum" @name + name: (_) @name)) + (enum_declaration + "enum" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + (method_definition + [ + "get" + "set" + "async" + "*" + "static" + ]* @name + name: (_) @name) @item +) diff --git a/crates/zed2/src/languages/javascript/highlights.scm b/crates/zed2/src/languages/javascript/highlights.scm new file mode 100644 index 0000000000..36ab21ca1e --- /dev/null +++ b/crates/zed2/src/languages/javascript/highlights.scm @@ -0,0 +1,217 @@ +; Variables + +(identifier) @variable + +; Properties + +(property_identifier) @property + +; Function and method calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Function and method definitions + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function) (arrow_function)]) + +; Special identifiers + +((identifier) @type + (#match? @type "^[A-Z]")) +(type_identifier) @type +(predefined_type) @type.builtin + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^_*[A-Z_][A-Z\\d_]*$")) + +; Literals + +(this) @variable.special +(super) @variable.special + +[ + (null) + (undefined) +] @constant.builtin + +[ + (true) + (false) +] @boolean + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.regex +(number) @number + +; Tokens + +[ + ";" + "?." + "." + "," + ":" +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) + +; Keywords + +[ "abstract" + "declare" + "enum" + "export" + "implements" + "interface" + "keyof" + "namespace" + "private" + "protected" + "public" + "type" + "readonly" + "override" +] @keyword \ No newline at end of file diff --git a/crates/zed2/src/languages/javascript/indents.scm b/crates/zed2/src/languages/javascript/indents.scm new file mode 100644 index 0000000000..107e6ff8e0 --- /dev/null +++ b/crates/zed2/src/languages/javascript/indents.scm @@ -0,0 +1,15 @@ +[ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/javascript/outline.scm b/crates/zed2/src/languages/javascript/outline.scm new file mode 100644 index 0000000000..a1d4d339e8 --- /dev/null +++ b/crates/zed2/src/languages/javascript/outline.scm @@ -0,0 +1,62 @@ +(internal_module + "namespace" @context + name: (_) @name) @item + +(enum_declaration + "enum" @context + name: (_) @name) @item + +(function_declaration + "async"? @context + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(interface_declaration + "interface" @context + name: (_) @name) @item + +(program + (export_statement + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (_) @name) @item))) + +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (_) @name) @item)) + +(class_declaration + "class" @context + name: (_) @name) @item + +(method_definition + [ + "get" + "set" + "async" + "*" + "readonly" + "static" + (override_modifier) + (accessibility_modifier) + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(public_field_definition + [ + "declare" + "readonly" + "abstract" + "static" + (accessibility_modifier) + ]* @context + name: (_) @name) @item diff --git a/crates/zed2/src/languages/javascript/overrides.scm b/crates/zed2/src/languages/javascript/overrides.scm new file mode 100644 index 0000000000..8b43fdcfc5 --- /dev/null +++ b/crates/zed2/src/languages/javascript/overrides.scm @@ -0,0 +1,13 @@ +(comment) @comment + +[ + (string) + (template_string) +] @string + +[ + (jsx_element) + (jsx_fragment) + (jsx_self_closing_element) + (jsx_expression) +] @element diff --git a/crates/zed2/src/languages/json.rs b/crates/zed2/src/languages/json.rs new file mode 100644 index 0000000000..cb912f1042 --- /dev/null +++ b/crates/zed2/src/languages/json.rs @@ -0,0 +1,184 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use collections::HashMap; +use feature_flags2::FeatureFlagAppExt; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use gpui2::AppContext; +use language2::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::json; +use settings2::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + future, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::{paths, ResultExt}; + +const SERVER_PATH: &'static str = + "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct JsonLspAdapter { + node: Arc, + languages: Arc, +} + +impl JsonLspAdapter { + pub fn new(node: Arc, languages: Arc) -> Self { + JsonLspAdapter { node, languages } + } +} + +#[async_trait] +impl LspAdapter for JsonLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("json-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "json" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("vscode-json-languageserver") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("vscode-json-languageserver", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } + + fn workspace_configuration( + &self, + cx: &mut AppContext, + ) -> BoxFuture<'static, serde_json::Value> { + let action_names = cx.all_action_names().collect::>(); + let staff_mode = cx.is_staff(); + let language_names = &self.languages.language_names(); + let settings_schema = cx.global::().json_schema( + &SettingsJsonSchemaParams { + language_names, + staff_mode, + }, + cx, + ); + + future::ready(serde_json::json!({ + "json": { + "format": { + "enable": true, + }, + "schemas": [ + { + "fileMatch": [ + schema_file_match(&paths::SETTINGS), + &*paths::LOCAL_SETTINGS_RELATIVE_PATH, + ], + "schema": settings_schema, + }, + { + "fileMatch": [schema_file_match(&paths::KEYMAP)], + "schema": KeymapFile::generate_json_schema(&action_names), + } + ] + } + })) + .boxed() + } + + async fn language_ids(&self) -> HashMap { + [("JSON".into(), "jsonc".into())].into_iter().collect() + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} + +fn schema_file_match(path: &Path) -> &Path { + path.strip_prefix(path.parent().unwrap().parent().unwrap()) + .unwrap() +} diff --git a/crates/zed2/src/languages/json/brackets.scm b/crates/zed2/src/languages/json/brackets.scm new file mode 100644 index 0000000000..9e8c9cd93c --- /dev/null +++ b/crates/zed2/src/languages/json/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/json/config.toml b/crates/zed2/src/languages/json/config.toml new file mode 100644 index 0000000000..37a6d3a54c --- /dev/null +++ b/crates/zed2/src/languages/json/config.toml @@ -0,0 +1,10 @@ +name = "JSON" +path_suffixes = ["json"] +line_comment = "// " +autoclose_before = ",]}" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, +] +prettier_parser_name = "json" diff --git a/crates/zed2/src/languages/json/embedding.scm b/crates/zed2/src/languages/json/embedding.scm new file mode 100644 index 0000000000..fa286e3880 --- /dev/null +++ b/crates/zed2/src/languages/json/embedding.scm @@ -0,0 +1,14 @@ +; Only produce one embedding for the entire file. +(document) @item + +; Collapse arrays, except for the first object. +(array + "[" @keep + . + (object)? @keep + "]" @keep) @collapse + +; Collapse string values (but not keys). +(pair value: (string + "\"" @keep + "\"" @keep) @collapse) diff --git a/crates/zed2/src/languages/json/highlights.scm b/crates/zed2/src/languages/json/highlights.scm new file mode 100644 index 0000000000..b5c64e9634 --- /dev/null +++ b/crates/zed2/src/languages/json/highlights.scm @@ -0,0 +1,21 @@ +(comment) @comment + +(string) @string + +(pair + key: (string) @property) + +(number) @number + +[ + (true) + (false) + (null) +] @constant + +[ + "{" + "}" + "[" + "]" +] @punctuation.bracket \ No newline at end of file diff --git a/crates/zed2/src/languages/json/indents.scm b/crates/zed2/src/languages/json/indents.scm new file mode 100644 index 0000000000..b7b2a2e767 --- /dev/null +++ b/crates/zed2/src/languages/json/indents.scm @@ -0,0 +1,2 @@ +(array "]" @end) @indent +(object "}" @end) @indent diff --git a/crates/zed2/src/languages/json/outline.scm b/crates/zed2/src/languages/json/outline.scm new file mode 100644 index 0000000000..43e2743478 --- /dev/null +++ b/crates/zed2/src/languages/json/outline.scm @@ -0,0 +1,2 @@ +(pair + key: (string (string_content) @name)) @item diff --git a/crates/zed2/src/languages/json/overrides.scm b/crates/zed2/src/languages/json/overrides.scm new file mode 100644 index 0000000000..746dbc5cd9 --- /dev/null +++ b/crates/zed2/src/languages/json/overrides.scm @@ -0,0 +1 @@ +(string) @string \ No newline at end of file diff --git a/crates/zed2/src/languages/language_plugin.rs b/crates/zed2/src/languages/language_plugin.rs new file mode 100644 index 0000000000..a160cca228 --- /dev/null +++ b/crates/zed2/src/languages/language_plugin.rs @@ -0,0 +1,168 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use collections::HashMap; +use futures::lock::Mutex; +use gpui2::executor::Background; +use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn}; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::ResultExt; + +#[allow(dead_code)] +pub async fn new_json(executor: Arc) -> Result { + let plugin = PluginBuilder::new_default()? + .host_function_async("command", |command: String| async move { + let mut args = command.split(' '); + let command = args.next().unwrap(); + smol::process::Command::new(command) + .args(args) + .output() + .await + .log_err() + .map(|output| output.stdout) + })? + .init(PluginBinary::Precompiled(include_bytes!( + "../../../../plugins/bin/json_language.wasm.pre", + ))) + .await?; + + PluginLspAdapter::new(plugin, executor).await +} + +pub struct PluginLspAdapter { + name: WasiFn<(), String>, + fetch_latest_server_version: WasiFn<(), Option>, + fetch_server_binary: WasiFn<(PathBuf, String), Result>, + cached_server_binary: WasiFn>, + initialization_options: WasiFn<(), String>, + language_ids: WasiFn<(), Vec<(String, String)>>, + executor: Arc, + runtime: Arc>, +} + +impl PluginLspAdapter { + #[allow(unused)] + pub async fn new(mut plugin: Plugin, executor: Arc) -> Result { + Ok(Self { + name: plugin.function("name")?, + fetch_latest_server_version: plugin.function("fetch_latest_server_version")?, + fetch_server_binary: plugin.function("fetch_server_binary")?, + cached_server_binary: plugin.function("cached_server_binary")?, + initialization_options: plugin.function("initialization_options")?, + language_ids: plugin.function("language_ids")?, + executor, + runtime: Arc::new(Mutex::new(plugin)), + }) + } +} + +#[async_trait] +impl LspAdapter for PluginLspAdapter { + async fn name(&self) -> LanguageServerName { + let name: String = self + .runtime + .lock() + .await + .call(&self.name, ()) + .await + .unwrap(); + LanguageServerName(name.into()) + } + + fn short_name(&self) -> &'static str { + "PluginLspAdapter" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + let runtime = self.runtime.clone(); + let function = self.fetch_latest_server_version; + self.executor + .spawn(async move { + let mut runtime = runtime.lock().await; + let versions: Result> = + runtime.call::<_, Option>(&function, ()).await; + versions + .map_err(|e| anyhow!("{}", e))? + .ok_or_else(|| anyhow!("Could not fetch latest server version")) + .map(|v| Box::new(v) as Box<_>) + }) + .await + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = *version.downcast::().unwrap(); + let runtime = self.runtime.clone(); + let function = self.fetch_server_binary; + self.executor + .spawn(async move { + let mut runtime = runtime.lock().await; + let handle = runtime.attach_path(&container_dir)?; + let result: Result = + runtime.call(&function, (container_dir, version)).await?; + runtime.remove_resource(handle)?; + result.map_err(|e| anyhow!("{}", e)) + }) + .await + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let runtime = self.runtime.clone(); + let function = self.cached_server_binary; + + self.executor + .spawn(async move { + let mut runtime = runtime.lock().await; + let handle = runtime.attach_path(&container_dir).ok()?; + let result: Option = + runtime.call(&function, container_dir).await.ok()?; + runtime.remove_resource(handle).ok()?; + result + }) + .await + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } + + async fn initialization_options(&self) -> Option { + let string: String = self + .runtime + .lock() + .await + .call(&self.initialization_options, ()) + .await + .log_err()?; + + serde_json::from_str(&string).ok() + } + + async fn language_ids(&self) -> HashMap { + self.runtime + .lock() + .await + .call(&self.language_ids, ()) + .await + .log_err() + .unwrap_or_default() + .into_iter() + .collect() + } +} diff --git a/crates/zed2/src/languages/lua.rs b/crates/zed2/src/languages/lua.rs new file mode 100644 index 0000000000..c92534925c --- /dev/null +++ b/crates/zed2/src/languages/lua.rs @@ -0,0 +1,135 @@ +use anyhow::{anyhow, bail, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::{io::BufReader, StreamExt}; +use language2::{LanguageServerName, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use smol::fs; +use std::{any::Any, env::consts, path::PathBuf}; +use util::{ + async_maybe, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; + +#[derive(Copy, Clone)] +pub struct LuaLspAdapter; + +#[async_trait] +impl super::LspAdapter for LuaLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("lua-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "lua" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = + latest_github_release("LuaLS/lua-language-server", false, delegate.http_client()) + .await?; + let version = release.name.clone(); + let platform = match consts::ARCH { + "x86_64" => "x64", + "aarch64" => "arm64", + other => bail!("Running on unsupported platform: {other}"), + }; + let asset_name = format!("lua-language-server-{version}-darwin-{platform}.tar.gz"); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name.clone(), + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + + let binary_path = container_dir.join("bin/lua-language-server"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(container_dir).await?; + } + + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + Ok(LanguageServerBinary { + path: binary_path, + arguments: Vec::new(), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--version".into()]; + binary + }) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + async_maybe!({ + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "lua-language-server") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: Vec::new(), + }) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() +} diff --git a/crates/zed2/src/languages/lua/brackets.scm b/crates/zed2/src/languages/lua/brackets.scm new file mode 100644 index 0000000000..5f5bd60b93 --- /dev/null +++ b/crates/zed2/src/languages/lua/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("(" @open ")" @close) \ No newline at end of file diff --git a/crates/zed2/src/languages/lua/config.toml b/crates/zed2/src/languages/lua/config.toml new file mode 100644 index 0000000000..d3e44edfe9 --- /dev/null +++ b/crates/zed2/src/languages/lua/config.toml @@ -0,0 +1,10 @@ +name = "Lua" +path_suffixes = ["lua"] +line_comment = "-- " +autoclose_before = ",]}" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, +] +collapsed_placeholder = "--[ ... ]--" diff --git a/crates/zed2/src/languages/lua/embedding.scm b/crates/zed2/src/languages/lua/embedding.scm new file mode 100644 index 0000000000..0d1065089f --- /dev/null +++ b/crates/zed2/src/languages/lua/embedding.scm @@ -0,0 +1,10 @@ +( + (comment)* @context + . + (function_declaration + "function" @name + name: (_) @name + (comment)* @collapse + body: (block) @collapse + ) @item +) diff --git a/crates/zed2/src/languages/lua/highlights.scm b/crates/zed2/src/languages/lua/highlights.scm new file mode 100644 index 0000000000..f061bbf8f9 --- /dev/null +++ b/crates/zed2/src/languages/lua/highlights.scm @@ -0,0 +1,198 @@ +;; Keywords + +"return" @keyword + +[ + "goto" + "in" + "local" +] @keyword + +(break_statement) @keyword + +(do_statement +[ + "do" + "end" +] @keyword) + +(while_statement +[ + "while" + "do" + "end" +] @keyword) + +(repeat_statement +[ + "repeat" + "until" +] @keyword) + +(if_statement +[ + "if" + "elseif" + "else" + "then" + "end" +] @keyword) + +(elseif_statement +[ + "elseif" + "then" + "end" +] @keyword) + +(else_statement +[ + "else" + "end" +] @keyword) + +(for_statement +[ + "for" + "do" + "end" +] @keyword) + +(function_declaration +[ + "function" + "end" +] @keyword) + +(function_definition +[ + "function" + "end" +] @keyword) + +;; Operators + +[ + "and" + "not" + "or" +] @operator + +[ + "+" + "-" + "*" + "/" + "%" + "^" + "#" + "==" + "~=" + "<=" + ">=" + "<" + ">" + "=" + "&" + "~" + "|" + "<<" + ">>" + "//" + ".." +] @operator + +;; Punctuations + +[ + ";" + ":" + "," + "." +] @punctuation.delimiter + +;; Brackets + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +;; Variables + +(identifier) @variable + +((identifier) @variable.special + (#eq? @variable.special "self")) + +(variable_list + attribute: (attribute + (["<" ">"] @punctuation.bracket + (identifier) @attribute))) + +;; Constants + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z_0-9]*$")) + +(vararg_expression) @constant + +(nil) @constant.builtin + +[ + (false) + (true) +] @boolean + +;; Tables + +(field name: (identifier) @field) + +(dot_index_expression field: (identifier) @field) + +(table_constructor +[ + "{" + "}" +] @constructor) + +;; Functions + +(parameters (identifier) @parameter) + +(function_call + name: [ + (identifier) @function + (dot_index_expression field: (identifier) @function) + ]) + +(function_declaration + name: [ + (identifier) @function.definition + (dot_index_expression field: (identifier) @function.definition) + ]) + +(method_index_expression method: (identifier) @method) + +(function_call + (identifier) @function.builtin + (#any-of? @function.builtin + ;; built-in functions in Lua 5.1 + "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs" + "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print" + "rawequal" "rawget" "rawset" "require" "select" "setfenv" "setmetatable" + "tonumber" "tostring" "type" "unpack" "xpcall")) + +;; Others + +(comment) @comment + +(hash_bang_line) @preproc + +(number) @number + +(string) @string \ No newline at end of file diff --git a/crates/zed2/src/languages/lua/indents.scm b/crates/zed2/src/languages/lua/indents.scm new file mode 100644 index 0000000000..71e15a0c33 --- /dev/null +++ b/crates/zed2/src/languages/lua/indents.scm @@ -0,0 +1,10 @@ +(if_statement "end" @end) @indent +(do_statement "end" @end) @indent +(while_statement "end" @end) @indent +(for_statement "end" @end) @indent +(repeat_statement "until" @end) @indent +(function_declaration "end" @end) @indent + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent \ No newline at end of file diff --git a/crates/zed2/src/languages/lua/outline.scm b/crates/zed2/src/languages/lua/outline.scm new file mode 100644 index 0000000000..8bd8d88070 --- /dev/null +++ b/crates/zed2/src/languages/lua/outline.scm @@ -0,0 +1,3 @@ +(function_declaration + "function" @context + name: (_) @name) @item \ No newline at end of file diff --git a/crates/zed2/src/languages/markdown/config.toml b/crates/zed2/src/languages/markdown/config.toml new file mode 100644 index 0000000000..2fa3ff3cf2 --- /dev/null +++ b/crates/zed2/src/languages/markdown/config.toml @@ -0,0 +1,11 @@ +name = "Markdown" +path_suffixes = ["md", "mdx"] +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true }, + { start = "\"", end = "\"", close = false, newline = false }, + { start = "'", end = "'", close = false, newline = false }, + { start = "`", end = "`", close = false, newline = false }, +] diff --git a/crates/zed2/src/languages/markdown/highlights.scm b/crates/zed2/src/languages/markdown/highlights.scm new file mode 100644 index 0000000000..971c276868 --- /dev/null +++ b/crates/zed2/src/languages/markdown/highlights.scm @@ -0,0 +1,24 @@ +(emphasis) @emphasis +(strong_emphasis) @emphasis.strong + +[ + (atx_heading) + (setext_heading) +] @title + +[ + (list_marker_plus) + (list_marker_minus) + (list_marker_star) + (list_marker_dot) + (list_marker_parenthesis) +] @punctuation.list_marker + +(code_span) @text.literal + +(fenced_code_block + (info_string + (language) @text.literal)) + +(link_destination) @link_uri +(link_text) @link_text diff --git a/crates/zed2/src/languages/markdown/injections.scm b/crates/zed2/src/languages/markdown/injections.scm new file mode 100644 index 0000000000..577054b404 --- /dev/null +++ b/crates/zed2/src/languages/markdown/injections.scm @@ -0,0 +1,4 @@ +(fenced_code_block + (info_string + (language) @language) + (code_fence_content) @content) diff --git a/crates/zed2/src/languages/nix/config.toml b/crates/zed2/src/languages/nix/config.toml new file mode 100644 index 0000000000..778f0a6f05 --- /dev/null +++ b/crates/zed2/src/languages/nix/config.toml @@ -0,0 +1,11 @@ +name = "Nix" +path_suffixes = ["nix"] +line_comment = "# " +block_comment = ["/* ", " */"] +autoclose_before = ";:.,=}])>` \n\t\"" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true }, +] diff --git a/crates/zed2/src/languages/nix/highlights.scm b/crates/zed2/src/languages/nix/highlights.scm new file mode 100644 index 0000000000..d63a46411a --- /dev/null +++ b/crates/zed2/src/languages/nix/highlights.scm @@ -0,0 +1,95 @@ +(comment) @comment + +[ + "if" + "then" + "else" + "let" + "inherit" + "in" + "rec" + "with" + "assert" + "or" +] @keyword + +[ + (string_expression) + (indented_string_expression) +] @string + +[ + (path_expression) + (hpath_expression) + (spath_expression) +] @string.special.path + +(uri_expression) @link_uri + +[ + (integer_expression) + (float_expression) +] @number + +(interpolation + "${" @punctuation.special + "}" @punctuation.special) @embedded + +(escape_sequence) @escape +(dollar_escape) @escape + +(function_expression + universal: (identifier) @parameter +) + +(formal + name: (identifier) @parameter + "?"? @punctuation.delimiter) + +(select_expression + attrpath: (attrpath (identifier)) @property) + +(apply_expression + function: [ + (variable_expression (identifier)) @function + (select_expression + attrpath: (attrpath + attr: (identifier) @function .))]) + +(unary_expression + operator: _ @operator) + +(binary_expression + operator: _ @operator) + +(variable_expression (identifier) @variable) + +(binding + attrpath: (attrpath (identifier)) @property) + +"=" @operator + +[ + ";" + "." + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(identifier) @variable + +((identifier) @function.builtin + (#match? @function.builtin "^(__add|__addErrorContext|__all|__any|__appendContext|__attrNames|__attrValues|__bitAnd|__bitOr|__bitXor|__catAttrs|__compareVersions|__concatLists|__concatMap|__concatStringsSep|__deepSeq|__div|__elem|__elemAt|__fetchurl|__filter|__filterSource|__findFile|__foldl'|__fromJSON|__functionArgs|__genList|__genericClosure|__getAttr|__getContext|__getEnv|__hasAttr|__hasContext|__hashFile|__hashString|__head|__intersectAttrs|__isAttrs|__isBool|__isFloat|__isFunction|__isInt|__isList|__isPath|__isString|__langVersion|__length|__lessThan|__listToAttrs|__mapAttrs|__match|__mul|__parseDrvName|__partition|__path|__pathExists|__readDir|__readFile|__replaceStrings|__seq|__sort|__split|__splitVersion|__storePath|__stringLength|__sub|__substring|__tail|__toFile|__toJSON|__toPath|__toXML|__trace|__tryEval|__typeOf|__unsafeDiscardOutputDependency|__unsafeDiscardStringContext|__unsafeGetAttrPos|__valueSize|abort|baseNameOf|derivation|derivationStrict|dirOf|fetchGit|fetchMercurial|fetchTarball|fromTOML|import|isNull|map|placeholder|removeAttrs|scopedImport|throw|toString)$") + (#is-not? local)) + +((identifier) @variable.builtin + (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins|false|null|true)$") + (#is-not? local)) diff --git a/crates/zed2/src/languages/nu/brackets.scm b/crates/zed2/src/languages/nu/brackets.scm new file mode 100644 index 0000000000..7ede7a6192 --- /dev/null +++ b/crates/zed2/src/languages/nu/brackets.scm @@ -0,0 +1,4 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +(parameter_pipes "|" @open "|" @close) diff --git a/crates/zed2/src/languages/nu/config.toml b/crates/zed2/src/languages/nu/config.toml new file mode 100644 index 0000000000..d382b0705a --- /dev/null +++ b/crates/zed2/src/languages/nu/config.toml @@ -0,0 +1,9 @@ +name = "Nu" +path_suffixes = ["nu"] +line_comment = "# " +autoclose_before = ";:.,=}])>` \n\t\"" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, +] diff --git a/crates/zed2/src/languages/nu/highlights.scm b/crates/zed2/src/languages/nu/highlights.scm new file mode 100644 index 0000000000..97f46d3879 --- /dev/null +++ b/crates/zed2/src/languages/nu/highlights.scm @@ -0,0 +1,302 @@ +;;; --- +;;; keywords +[ + "def" + "def-env" + "alias" + "export-env" + "export" + "extern" + "module" + + "let" + "let-env" + "mut" + "const" + + "hide-env" + + "source" + "source-env" + + "overlay" + "register" + + "loop" + "while" + "error" + + "do" + "if" + "else" + "try" + "catch" + "match" + + "break" + "continue" + "return" + +] @keyword + +(hide_mod "hide" @keyword) +(decl_use "use" @keyword) + +(ctrl_for + "for" @keyword + "in" @keyword +) +(overlay_list "list" @keyword) +(overlay_hide "hide" @keyword) +(overlay_new "new" @keyword) +(overlay_use + "use" @keyword + "as" @keyword +) +(ctrl_error "make" @keyword) + +;;; --- +;;; literals +(val_number) @constant +(val_duration + unit: [ + "ns" "µs" "us" "ms" "sec" "min" "hr" "day" "wk" + ] @variable +) +(val_filesize + unit: [ + "b" "B" + + "kb" "kB" "Kb" "KB" + "mb" "mB" "Mb" "MB" + "gb" "gB" "Gb" "GB" + "tb" "tB" "Tb" "TB" + "pb" "pB" "Pb" "PB" + "eb" "eB" "Eb" "EB" + "zb" "zB" "Zb" "ZB" + + "kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB" + "mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB" + "gib" "giB" "gIB" "gIb" "Gib" "GIb" "GIB" + "tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB" + "pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB" + "eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB" + "zib" "ziB" "zIB" "zIb" "Zib" "ZIb" "ZIB" + ] @variable +) +(val_binary + [ + "0b" + "0o" + "0x" + ] @constant + "[" @punctuation.bracket + digit: [ + "," @punctuation.delimiter + (hex_digit) @constant + ] + "]" @punctuation.bracket +) @constant +(val_bool) @constant.builtin +(val_nothing) @constant.builtin +(val_string) @string +(val_date) @constant +(inter_escape_sequence) @constant +(escape_sequence) @constant +(val_interpolated [ + "$\"" + "$\'" + "\"" + "\'" +] @string) +(unescaped_interpolated_content) @string +(escaped_interpolated_content) @string +(expr_interpolated ["(" ")"] @variable) + +;;; --- +;;; operators +(expr_binary [ + "+" + "-" + "*" + "/" + "mod" + "//" + "++" + "**" + "==" + "!=" + "<" + "<=" + ">" + ">=" + "=~" + "!~" + "and" + "or" + "xor" + "bit-or" + "bit-xor" + "bit-and" + "bit-shl" + "bit-shr" + "in" + "not-in" + "starts-with" + "ends-with" +] @operator) + +(expr_binary opr: ([ + "and" + "or" + "xor" + "bit-or" + "bit-xor" + "bit-and" + "bit-shl" + "bit-shr" + "in" + "not-in" + "starts-with" + "ends-with" +]) @keyword) + +(where_command [ + "+" + "-" + "*" + "/" + "mod" + "//" + "++" + "**" + "==" + "!=" + "<" + "<=" + ">" + ">=" + "=~" + "!~" + "and" + "or" + "xor" + "bit-or" + "bit-xor" + "bit-and" + "bit-shl" + "bit-shr" + "in" + "not-in" + "starts-with" + "ends-with" +] @operator) + +(assignment [ + "=" + "+=" + "-=" + "*=" + "/=" + "++=" +] @operator) + +(expr_unary ["not" "-"] @operator) + +(val_range [ + ".." + "..=" + "..<" +] @operator) + +["=>" "=" "|"] @operator + +[ + "o>" "out>" + "e>" "err>" + "e+o>" "err+out>" + "o+e>" "out+err>" +] @special + +;;; --- +;;; punctuation +[ + "," + ";" +] @punctuation.delimiter + +(param_short_flag "-" @punctuation.delimiter) +(param_long_flag ["--"] @punctuation.delimiter) +(long_flag ["--"] @punctuation.delimiter) +(param_rest "..." @punctuation.delimiter) +(param_type [":"] @punctuation.special) +(param_value ["="] @punctuation.special) +(param_cmd ["@"] @punctuation.special) +(param_opt ["?"] @punctuation.special) + +[ + "(" ")" + "{" "}" + "[" "]" +] @punctuation.bracket + +(val_record + (record_entry ":" @punctuation.delimiter)) +;;; --- +;;; identifiers +(param_rest + name: (_) @variable) +(param_opt + name: (_) @variable) +(parameter + param_name: (_) @variable) +(param_cmd + (cmd_identifier) @string) +(param_long_flag) @variable +(param_short_flag) @variable + +(short_flag) @variable +(long_flag) @variable + +(scope_pattern [(wild_card) @function]) + +(cmd_identifier) @function + +(command + "^" @punctuation.delimiter + head: (_) @function +) + +"where" @function + +(path + ["." "?"] @punctuation.delimiter +) @variable + +(val_variable + "$" @operator + [ + (identifier) @variable + "in" @type.builtin + "nu" @type.builtin + "env" @type.builtin + "nothing" @type.builtin + ] ; If we have a special styling, use it here +) +;;; --- +;;; types +(flat_type) @type.builtin +(list_type + "list" @type + ["<" ">"] @punctuation.bracket +) +(collection_type + ["record" "table"] @type + "<" @punctuation.bracket + key: (_) @variable + ["," ":"] @punctuation.delimiter + ">" @punctuation.bracket +) + +(shebang) @comment +(comment) @comment diff --git a/crates/zed2/src/languages/nu/indents.scm b/crates/zed2/src/languages/nu/indents.scm new file mode 100644 index 0000000000..112b414aa4 --- /dev/null +++ b/crates/zed2/src/languages/nu/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/php.rs b/crates/zed2/src/languages/php.rs new file mode 100644 index 0000000000..d6e462e186 --- /dev/null +++ b/crates/zed2/src/languages/php.rs @@ -0,0 +1,137 @@ +use anyhow::{anyhow, Result}; + +use async_trait::async_trait; +use collections::HashMap; + +use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use node_runtime::NodeRuntime; + +use smol::{fs, stream::StreamExt}; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +fn intelephense_server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct IntelephenseVersion(String); + +pub struct IntelephenseLspAdapter { + node: Arc, +} + +impl IntelephenseLspAdapter { + const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js"; + + #[allow(unused)] + pub fn new(node: Arc) -> Self { + Self { node } + } +} + +#[async_trait] +impl LspAdapter for IntelephenseLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("intelephense".into()) + } + + fn short_name(&self) -> &'static str { + "php" + } + + async fn fetch_latest_server_version( + &self, + _delegate: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(IntelephenseVersion( + self.node.npm_package_latest_version("intelephense").await?, + )) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(Self::SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages(&container_dir, &[("intelephense", version.0.as_str())]) + .await?; + } + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: intelephense_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn label_for_completion( + &self, + _item: &lsp2::CompletionItem, + _language: &Arc, + ) -> Option { + None + } + + async fn initialization_options(&self) -> Option { + None + } + async fn language_ids(&self) -> HashMap { + HashMap::from_iter([("PHP".into(), "php".into())]) + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(IntelephenseLspAdapter::SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: intelephense_server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed2/src/languages/php/config.toml b/crates/zed2/src/languages/php/config.toml new file mode 100644 index 0000000000..f5ad67c12d --- /dev/null +++ b/crates/zed2/src/languages/php/config.toml @@ -0,0 +1,14 @@ +name = "PHP" +path_suffixes = ["php"] +first_line_pattern = '^#!.*php' +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, +] +collapsed_placeholder = "/* ... */" +word_characters = ["$"] +scope_opt_in_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed2/src/languages/php/embedding.scm b/crates/zed2/src/languages/php/embedding.scm new file mode 100644 index 0000000000..db277775b3 --- /dev/null +++ b/crates/zed2/src/languages/php/embedding.scm @@ -0,0 +1,36 @@ +( + (comment)* @context + . + [ + (function_definition + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (trait_declaration + "trait" @name + name: (_) @name) + + (method_declaration + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (interface_declaration + "interface" @name + name: (_) @name + ) + + (enum_declaration + "enum" @name + name: (_) @name + ) + + ] @item + ) diff --git a/crates/zed2/src/languages/php/highlights.scm b/crates/zed2/src/languages/php/highlights.scm new file mode 100644 index 0000000000..fcb087c47d --- /dev/null +++ b/crates/zed2/src/languages/php/highlights.scm @@ -0,0 +1,123 @@ +(php_tag) @tag +"?>" @tag + +; Types + +(primitive_type) @type.builtin +(cast_type) @type.builtin +(named_type (name) @type) @type +(named_type (qualified_name) @type) @type + +; Functions + +(array_creation_expression "array" @function.builtin) +(list_literal "list" @function.builtin) + +(method_declaration + name: (name) @function.method) + +(function_call_expression + function: [(qualified_name (name)) (name)] @function) + +(scoped_call_expression + name: (name) @function) + +(member_call_expression + name: (name) @function.method) + +(function_definition + name: (name) @function) + +; Member + +(property_element + (variable_name) @property) + +(member_access_expression + name: (variable_name (name)) @property) +(member_access_expression + name: (name) @property) + +; Variables + +(relative_scope) @variable.builtin + +((name) @constant + (#match? @constant "^_?[A-Z][A-Z\\d_]+$")) +((name) @constant.builtin + (#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$")) + +((name) @constructor + (#match? @constructor "^[A-Z]")) + +((name) @variable.builtin + (#eq? @variable.builtin "this")) + +(variable_name) @variable + +; Basic tokens +[ + (string) + (string_value) + (encapsed_string) + (heredoc) + (heredoc_body) + (nowdoc_body) +] @string +(boolean) @constant.builtin +(null) @constant.builtin +(integer) @number +(float) @number +(comment) @comment + +"$" @operator + +; Keywords + +"abstract" @keyword +"as" @keyword +"break" @keyword +"case" @keyword +"catch" @keyword +"class" @keyword +"const" @keyword +"continue" @keyword +"declare" @keyword +"default" @keyword +"do" @keyword +"echo" @keyword +"else" @keyword +"elseif" @keyword +"enum" @keyword +"enddeclare" @keyword +"endforeach" @keyword +"endif" @keyword +"endswitch" @keyword +"endwhile" @keyword +"extends" @keyword +"final" @keyword +"finally" @keyword +"foreach" @keyword +"function" @keyword +"global" @keyword +"if" @keyword +"implements" @keyword +"include_once" @keyword +"include" @keyword +"insteadof" @keyword +"interface" @keyword +"namespace" @keyword +"new" @keyword +"private" @keyword +"protected" @keyword +"public" @keyword +"require_once" @keyword +"require" @keyword +"return" @keyword +"static" @keyword +"switch" @keyword +"throw" @keyword +"trait" @keyword +"try" @keyword +"use" @keyword +"while" @keyword diff --git a/crates/zed2/src/languages/php/injections.scm b/crates/zed2/src/languages/php/injections.scm new file mode 100644 index 0000000000..57abd8ea2b --- /dev/null +++ b/crates/zed2/src/languages/php/injections.scm @@ -0,0 +1,3 @@ +((text) @content + (#set! "language" "html") + (#set! "combined")) diff --git a/crates/zed2/src/languages/php/outline.scm b/crates/zed2/src/languages/php/outline.scm new file mode 100644 index 0000000000..87986f1032 --- /dev/null +++ b/crates/zed2/src/languages/php/outline.scm @@ -0,0 +1,29 @@ +(class_declaration + "class" @context + name: (name) @name + ) @item + +(function_definition + "function" @context + name: (_) @name + ) @item + +(method_declaration + "function" @context + name: (_) @name + ) @item + +(interface_declaration + "interface" @context + name: (_) @name + ) @item + +(enum_declaration + "enum" @context + name: (_) @name + ) @item + +(trait_declaration + "trait" @context + name: (_) @name + ) @item diff --git a/crates/zed2/src/languages/php/tags.scm b/crates/zed2/src/languages/php/tags.scm new file mode 100644 index 0000000000..66d594c254 --- /dev/null +++ b/crates/zed2/src/languages/php/tags.scm @@ -0,0 +1,40 @@ +(namespace_definition + name: (namespace_name) @name) @module + +(interface_declaration + name: (name) @name) @definition.interface + +(trait_declaration + name: (name) @name) @definition.interface + +(class_declaration + name: (name) @name) @definition.class + +(class_interface_clause [(name) (qualified_name)] @name) @impl + +(property_declaration + (property_element (variable_name (name) @name))) @definition.field + +(function_definition + name: (name) @name) @definition.function + +(method_declaration + name: (name) @name) @definition.function + +(object_creation_expression + [ + (qualified_name (name) @name) + (variable_name (name) @name) + ]) @reference.class + +(function_call_expression + function: [ + (qualified_name (name) @name) + (variable_name (name)) @name + ]) @reference.call + +(scoped_call_expression + name: (name) @name) @reference.call + +(member_call_expression + name: (name) @name) @reference.call diff --git a/crates/zed2/src/languages/python.rs b/crates/zed2/src/languages/python.rs new file mode 100644 index 0000000000..8bbf022a17 --- /dev/null +++ b/crates/zed2/src/languages/python.rs @@ -0,0 +1,296 @@ +use anyhow::Result; +use async_trait::async_trait; +use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use node_runtime::NodeRuntime; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct PythonLspAdapter { + node: Arc, +} + +impl PythonLspAdapter { + pub fn new(node: Arc) -> Self { + PythonLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for PythonLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("pyright".into()) + } + + fn short_name(&self) -> &'static str { + "pyright" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages(&container_dir, &[("pyright", version.as_str())]) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn process_completion(&self, item: &mut lsp2::CompletionItem) { + // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. + // Where `XX` is the sorting category, `YYYY` is based on most recent usage, + // and `name` is the symbol name itself. + // + // Because the the symbol name is included, there generally are not ties when + // sorting by the `sortText`, so the symbol's fuzzy match score is not taken + // into account. Here, we remove the symbol name from the sortText in order + // to allow our own fuzzy score to be used to break ties. + // + // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 + let Some(sort_text) = &mut item.sort_text else { + return; + }; + let mut parts = sort_text.split('.'); + let Some(first) = parts.next() else { return }; + let Some(second) = parts.next() else { return }; + let Some(_) = parts.next() else { return }; + sort_text.replace_range(first.len() + second.len() + 1.., ""); + } + + async fn label_for_completion( + &self, + item: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + let label = &item.label; + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + lsp2::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, + lsp2::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?, + lsp2::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?, + lsp2::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, + _ => return None, + }; + Some(language2::CodeLabel { + text: label.clone(), + runs: vec![(0..label.len(), highlight_id)], + filter_range: 0..label.len(), + }) + } + + async fn label_for_symbol( + &self, + name: &str, + kind: lsp2::SymbolKind, + language: &Arc, + ) -> Option { + let (text, filter_range, display_range) = match kind { + lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => { + let text = format!("def {}():\n", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::CLASS => { + let text = format!("class {}:", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::CONSTANT => { + let text = format!("{} = 0", name); + let filter_range = 0..name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(language2::CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + let server_path = container_dir.join(SERVER_PATH); + if server_path.exists() { + Some(LanguageServerBinary { + path: node.binary_path().await.log_err()?, + arguments: server_binary_arguments(&server_path), + }) + } else { + log::error!("missing executable in directory {:?}", server_path); + None + } +} + +#[cfg(test)] +mod tests { + use gpui2::{Context, ModelContext, TestAppContext}; + use language2::{language_settings::AllLanguageSettings, AutoindentMode, Buffer}; + use settings2::SettingsStore; + use std::num::NonZeroU32; + + #[gpui2::test] + async fn test_python_autoindent(cx: &mut TestAppContext) { + // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX); + let language = + crate::languages::language("python", tree_sitter_python::language(), None).await; + cx.update(|cx| { + let test_settings = SettingsStore::test(cx); + cx.set_global(test_settings); + language2::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); + }); + + cx.build_model(|cx| { + let mut buffer = + Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx); + let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { + let ix = buffer.len(); + buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); + }; + + // indent after "def():" + append(&mut buffer, "def a():\n", cx); + assert_eq!(buffer.text(), "def a():\n "); + + // preserve indent after blank line + append(&mut buffer, "\n ", cx); + assert_eq!(buffer.text(), "def a():\n \n "); + + // indent after "if" + append(&mut buffer, "if a:\n ", cx); + assert_eq!(buffer.text(), "def a():\n \n if a:\n "); + + // preserve indent after statement + append(&mut buffer, "b()\n", cx); + assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n "); + + // preserve indent after statement + append(&mut buffer, "else", cx); + assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else"); + + // dedent "else"" + append(&mut buffer, ":", cx); + assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:"); + + // indent lines after else + append(&mut buffer, "\n", cx); + assert_eq!( + buffer.text(), + "def a():\n \n if a:\n b()\n else:\n " + ); + + // indent after an open paren. the closing paren is not indented + // because there is another token before it on the same line. + append(&mut buffer, "foo(\n1)", cx); + assert_eq!( + buffer.text(), + "def a():\n \n if a:\n b()\n else:\n foo(\n 1)" + ); + + // dedent the closing paren if it is shifted to the beginning of the line + let argument_ix = buffer.text().find('1').unwrap(); + buffer.edit( + [(argument_ix..argument_ix + 1, "")], + Some(AutoindentMode::EachLine), + cx, + ); + assert_eq!( + buffer.text(), + "def a():\n \n if a:\n b()\n else:\n foo(\n )" + ); + + // preserve indent after the close paren + append(&mut buffer, "\n", cx); + assert_eq!( + buffer.text(), + "def a():\n \n if a:\n b()\n else:\n foo(\n )\n " + ); + + // manually outdent the last line + let end_whitespace_ix = buffer.len() - 4; + buffer.edit( + [(end_whitespace_ix..buffer.len(), "")], + Some(AutoindentMode::EachLine), + cx, + ); + assert_eq!( + buffer.text(), + "def a():\n \n if a:\n b()\n else:\n foo(\n )\n" + ); + + // preserve the newly reduced indentation on the next newline + append(&mut buffer, "\n", cx); + assert_eq!( + buffer.text(), + "def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n" + ); + + // reset to a simple if statement + buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx); + + // dedent "else" on the line after a closing paren + append(&mut buffer, "\n else:\n", cx); + assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n "); + + buffer + }); + } +} diff --git a/crates/zed2/src/languages/python/brackets.scm b/crates/zed2/src/languages/python/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed2/src/languages/python/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed2/src/languages/python/config.toml b/crates/zed2/src/languages/python/config.toml new file mode 100644 index 0000000000..6777f6e60d --- /dev/null +++ b/crates/zed2/src/languages/python/config.toml @@ -0,0 +1,16 @@ +name = "Python" +path_suffixes = ["py", "pyi", "mpy"] +first_line_pattern = '^#!.*\bpython[0-9.]*\b' +line_comment = "# " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = false, newline = false, not_in = ["string"] }, +] + +auto_indent_using_last_non_empty_line = false +increase_indent_pattern = ":\\s*$" +decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" diff --git a/crates/zed2/src/languages/python/embedding.scm b/crates/zed2/src/languages/python/embedding.scm new file mode 100644 index 0000000000..e3efb3dbf6 --- /dev/null +++ b/crates/zed2/src/languages/python/embedding.scm @@ -0,0 +1,9 @@ +(class_definition + "class" @context + name: (identifier) @name + ) @item + +(function_definition + "async"? @context + "def" @context + name: (_) @name) @item diff --git a/crates/zed2/src/languages/python/highlights.scm b/crates/zed2/src/languages/python/highlights.scm new file mode 100644 index 0000000000..71ab963d82 --- /dev/null +++ b/crates/zed2/src/languages/python/highlights.scm @@ -0,0 +1,125 @@ +(attribute attribute: (identifier) @property) +(type (identifier) @type) + +; Function calls + +(decorator) @function + +(call + function: (attribute attribute: (identifier) @function.method)) +(call + function: (identifier) @function) + +; Function definitions + +(function_definition + name: (identifier) @function) + +; Identifier naming conventions + +((identifier) @type + (#match? @type "^[A-Z]")) + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) + +; Builtin functions + +((call + function: (identifier) @function.builtin) + (#match? + @function.builtin + "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$")) + +; Literals + +[ + (none) + (true) + (false) +] @constant.builtin + +[ + (integer) + (float) +] @number + +(comment) @comment +(string) @string +(escape_sequence) @escape + +(interpolation + "{" @punctuation.special + "}" @punctuation.special) @embedded + +[ + "-" + "-=" + "!=" + "*" + "**" + "**=" + "*=" + "/" + "//" + "//=" + "/=" + "&" + "%" + "%=" + "^" + "+" + "->" + "+=" + "<" + "<<" + "<=" + "<>" + "=" + ":=" + "==" + ">" + ">=" + ">>" + "|" + "~" + "and" + "in" + "is" + "not" + "or" +] @operator + +[ + "as" + "assert" + "async" + "await" + "break" + "class" + "continue" + "def" + "del" + "elif" + "else" + "except" + "exec" + "finally" + "for" + "from" + "global" + "if" + "import" + "lambda" + "nonlocal" + "pass" + "print" + "raise" + "return" + "try" + "while" + "with" + "yield" + "match" + "case" +] @keyword \ No newline at end of file diff --git a/crates/zed2/src/languages/python/indents.scm b/crates/zed2/src/languages/python/indents.scm new file mode 100644 index 0000000000..112b414aa4 --- /dev/null +++ b/crates/zed2/src/languages/python/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/python/outline.scm b/crates/zed2/src/languages/python/outline.scm new file mode 100644 index 0000000000..e3efb3dbf6 --- /dev/null +++ b/crates/zed2/src/languages/python/outline.scm @@ -0,0 +1,9 @@ +(class_definition + "class" @context + name: (identifier) @name + ) @item + +(function_definition + "async"? @context + "def" @context + name: (_) @name) @item diff --git a/crates/zed2/src/languages/python/overrides.scm b/crates/zed2/src/languages/python/overrides.scm new file mode 100644 index 0000000000..8a58e304e5 --- /dev/null +++ b/crates/zed2/src/languages/python/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +(string) @string diff --git a/crates/zed2/src/languages/racket/brackets.scm b/crates/zed2/src/languages/racket/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed2/src/languages/racket/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed2/src/languages/racket/config.toml b/crates/zed2/src/languages/racket/config.toml new file mode 100644 index 0000000000..0177e6ef6d --- /dev/null +++ b/crates/zed2/src/languages/racket/config.toml @@ -0,0 +1,9 @@ +name = "Racket" +path_suffixes = ["rkt"] +line_comment = "; " +autoclose_before = "])" +brackets = [ + { start = "[", end = "]", close = true, newline = false }, + { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false }, +] diff --git a/crates/zed2/src/languages/racket/highlights.scm b/crates/zed2/src/languages/racket/highlights.scm new file mode 100644 index 0000000000..2c0caf8935 --- /dev/null +++ b/crates/zed2/src/languages/racket/highlights.scm @@ -0,0 +1,40 @@ +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +[(string) + (here_string) + (byte_string)] @string +(regex) @string.regex +(escape_sequence) @escape + +[(comment) + (block_comment) + (sexp_comment)] @comment + +(symbol) @variable + +(number) @number +(character) @constant.builtin +(boolean) @constant.builtin +(keyword) @constant +(quote . (symbol)) @constant + +(extension) @keyword +(lang_name) @variable.special + +((symbol) @operator + (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$")) + +(list + . + (symbol) @function) + +(list + . + (symbol) @keyword + (#match? @keyword + "^(unit-from-context|for/last|syntax-case|match-let\\*-values|define-for-syntax|define/subexpression-pos-prop|set-field!|class-field-accessor|invoke-unit|#%stratified-body|for\\*/and|for\\*/weak-set|flat-rec-contract|for\\*/stream|planet|for/mutable-seteqv|log-error|delay|#%declare|prop:dict/contract|->d|lib|override\\*|define-local-member-name|send-generic|for\\*/hasheq|define-syntax|submod|except|include-at/relative-to/reader|public\\*|define-member-name|define/public|let\\*|for/and|for\\*/first|for|delay/strict|define-values-for-export|==|match-define-values|for/weak-seteq|for\\*/async|for/stream|for/weak-seteqv|set!-values|lambda|for\\*/product|augment-final\\*|pubment\\*|command-line|contract|case|struct-field-index|contract-struct|unless|for/hasheq|for/seteqv|with-method|define-values-for-syntax|for-template|pubment|for\\*/list|syntax-case\\*|init-field|define-serializable-class|=>|for/foldr/derived|letrec-syntaxes|overment\\*|unquote-splicing|_|inherit-field|for\\*|stream-lazy|match-lambda\\*|contract-pos/neg-doubling|unit/c|match-define|for\\*/set|unit/s|nor|#%expression|class/c|this%|place/context|super-make-object|when|set!|parametric->/c|syntax-id-rules|include/reader|compound-unit|override-final|get-field|gen:dict|for\\*/seteqv|for\\*/hash|#%provide|combine-out|link|with-contract-continuation-mark|define-struct/derived|stream\\*|λ|rename-out|define-serializable-class\\*|augment|define/augment|let|define-signature-form|letrec-syntax|abstract|define-namespace-anchor|#%module-begin|#%top-interaction|for\\*/weak-seteqv|do|define/subexpression-pos-prop/name|absent|send/apply|with-handlers\\*|all-from-out|provide-signature-elements|gen:stream|define/override-final|for\\*/mutable-seteqv|rename|quasisyntax/loc|instantiate|for/list|extends|include-at/relative-to|mixin|define/pubment|#%plain-lambda|except-out|#%plain-module-begin|init|for\\*/last|relative-in|define-unit/new-import-export|->dm|member-name-key|nand|interface\\*|struct|define/override|else|define/augment-final|failure-cont|open|log-info|define/final-prop|all-defined-out|for/sum|for\\*/sum|recursive-contract|define|define-logger|match\\*|log-debug|rename-inner|->|struct/derived|unit|class\\*|prefix-out|any|define/overment|define-signature|match-letrec-values|let-syntaxes|for/mutable-set|define/match|cond|super-instantiate|define-contract-struct|import|hash/dc|define-custom-set-types|public-final|for/vector|for-label|prefix-in|for\\*/foldr/derived|define-unit-binding|object-contract|syntax-rules|augride|for\\*/mutable-seteq|quasisyntax|inner|for-syntax|overment|send/keyword-apply|generic|let\\*-values|->m|define-values|struct-copy|init-depend|struct/ctc|match-lambda|#%printing-module-begin|match\\*/derived|case->m|this|file|stream-cons|inspect|field|for/weak-set|struct\\*|gen:custom-write|thunk\\*|combine-in|unquote|for/lists|define/private|for\\*/foldr|define-unit/s|with-continuation-mark|begin|prefix|quote-syntax/prune|object/c|interface|match/derived|for/hasheqv|current-contract-region|define-compound-unit|override|define/public-final|recontract-out|let/cc|augride\\*|inherit|send|define-values/invoke-unit|for/mutable-seteq|#%datum|for/first|match-let\\*|invoke-unit/infer|define/contract|syntax/loc|for\\*/hasheqv|define-sequence-syntax|let/ec|for/product|for\\*/fold/derived|define-syntax-rule|lazy|unconstrained-domain->|augment-final|private|class|define-splicing-for-clause-syntax|for\\*/fold|prompt-tag/c|contract-out|match/values|public-final\\*|case-lambda|for/fold|unsyntax|for/set|begin0|#%require|time|public|define-struct|include|define-values/invoke-unit/infer|only-space-in|struct/c|only-meta-in|unit/new-import-export|place|begin-for-syntax|shared|inherit/super|quote|for/or|struct/contract|export|inherit/inner|struct-out|let-syntax|augment\\*|for\\*/vector|rename-in|match-let|define-unit|:do-in|~@|for\\*/weak-seteq|private\\*|and|except-in|log-fatal|gen:equal\\+hash|provide|require|thunk|invariant-assertion|define-match-expander|init-rest|->\\*|class/derived|super-new|for/fold/derived|for\\*/mutable-set|match-lambda\\*\\*|only|with-contract|~\\?|opt/c|let-values|delay/thread|->i|for/foldr|for-meta|only-in|send\\+|\\.\\.\\.|struct-guard/c|->\\*m|gen:set|struct/dc|define-syntaxes|if|parameterize|module\\*|module|send\\*|#%variable-reference|compound-unit/infer|#%plain-app|for/hash|contracted|case->|match|for\\*/lists|#%app|letrec-values|log-warning|super|define/augride|local-require|provide/contract|define-struct/contract|match-let-values|quote-syntax|for\\*/seteq|define-compound-unit/infer|parameterize\\*|values/drop|for/seteq|tag|stream|delay/idle|module\\+|define-custom-hash-types|cons/dc|define-module-boundary-contract|or|protect-out|define-opt/c|implies|letrec-syntaxes\\+values|for\\*/or|unsyntax-splicing|override-final\\*|for/async|parameterize-break|syntax|place\\*|for-space|quasiquote|with-handlers|delay/sync|define-unit-from-context|match-letrec|#%top|define-unit/contract|delay/name|new|field-bound\\?|letrec|class-field-mutator|with-syntax|flat-murec-contract|rename-super|local)$" + )) + +((symbol) @comment + (#match? @comment "^#[cC][iIsS]$")) + diff --git a/crates/zed2/src/languages/racket/indents.scm b/crates/zed2/src/languages/racket/indents.scm new file mode 100644 index 0000000000..9a1cbad161 --- /dev/null +++ b/crates/zed2/src/languages/racket/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]") @indent +(_ "{" "}") @indent +(_ "(" ")") @indent diff --git a/crates/zed2/src/languages/racket/outline.scm b/crates/zed2/src/languages/racket/outline.scm new file mode 100644 index 0000000000..604e052a63 --- /dev/null +++ b/crates/zed2/src/languages/racket/outline.scm @@ -0,0 +1,10 @@ +(list + . + (symbol) @start-symbol @context + . + [ + (symbol) @name + (list . (symbol) @name) + ] + (#match? @start-symbol "^define") +) @item \ No newline at end of file diff --git a/crates/zed2/src/languages/ruby.rs b/crates/zed2/src/languages/ruby.rs new file mode 100644 index 0000000000..8718f1c757 --- /dev/null +++ b/crates/zed2/src/languages/ruby.rs @@ -0,0 +1,160 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use std::{any::Any, path::PathBuf, sync::Arc}; + +pub struct RubyLanguageServer; + +#[async_trait] +impl LspAdapter for RubyLanguageServer { + async fn name(&self) -> LanguageServerName { + LanguageServerName("solargraph".into()) + } + + fn short_name(&self) -> &'static str { + "solargraph" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(())) + } + + async fn fetch_server_binary( + &self, + _version: Box, + _container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Err(anyhow!("solargraph must be installed manually")) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: "solargraph".into(), + arguments: vec!["stdio".into()], + }) + } + + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } + + async fn label_for_completion( + &self, + item: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + let label = &item.label; + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + lsp2::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, + lsp2::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, + lsp2::CompletionItemKind::CLASS | lsp2::CompletionItemKind::MODULE => { + grammar.highlight_id_for_name("type")? + } + lsp2::CompletionItemKind::KEYWORD => { + if label.starts_with(':') { + grammar.highlight_id_for_name("string.special.symbol")? + } else { + grammar.highlight_id_for_name("keyword")? + } + } + lsp2::CompletionItemKind::VARIABLE => { + if label.starts_with('@') { + grammar.highlight_id_for_name("property")? + } else { + return None; + } + } + _ => return None, + }; + Some(language2::CodeLabel { + text: label.clone(), + runs: vec![(0..label.len(), highlight_id)], + filter_range: 0..label.len(), + }) + } + + async fn label_for_symbol( + &self, + label: &str, + kind: lsp2::SymbolKind, + language: &Arc, + ) -> Option { + let grammar = language.grammar()?; + match kind { + lsp2::SymbolKind::METHOD => { + let mut parts = label.split('#'); + let classes = parts.next()?; + let method = parts.next()?; + if parts.next().is_some() { + return None; + } + + let class_id = grammar.highlight_id_for_name("type")?; + let method_id = grammar.highlight_id_for_name("function.method")?; + + let mut ix = 0; + let mut runs = Vec::new(); + for (i, class) in classes.split("::").enumerate() { + if i > 0 { + ix += 2; + } + let end_ix = ix + class.len(); + runs.push((ix..end_ix, class_id)); + ix = end_ix; + } + + ix += 1; + let end_ix = ix + method.len(); + runs.push((ix..end_ix, method_id)); + Some(language2::CodeLabel { + text: label.to_string(), + runs, + filter_range: 0..label.len(), + }) + } + lsp2::SymbolKind::CONSTANT => { + let constant_id = grammar.highlight_id_for_name("constant")?; + Some(language2::CodeLabel { + text: label.to_string(), + runs: vec![(0..label.len(), constant_id)], + filter_range: 0..label.len(), + }) + } + lsp2::SymbolKind::CLASS | lsp2::SymbolKind::MODULE => { + let class_id = grammar.highlight_id_for_name("type")?; + + let mut ix = 0; + let mut runs = Vec::new(); + for (i, class) in label.split("::").enumerate() { + if i > 0 { + ix += "::".len(); + } + let end_ix = ix + class.len(); + runs.push((ix..end_ix, class_id)); + ix = end_ix; + } + + Some(language2::CodeLabel { + text: label.to_string(), + runs, + filter_range: 0..label.len(), + }) + } + _ => return None, + } + } +} diff --git a/crates/zed2/src/languages/ruby/brackets.scm b/crates/zed2/src/languages/ruby/brackets.scm new file mode 100644 index 0000000000..957b20ecdb --- /dev/null +++ b/crates/zed2/src/languages/ruby/brackets.scm @@ -0,0 +1,14 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) +("do" @open "end" @close) + +(block_parameters "|" @open "|" @close) +(interpolation "#{" @open "}" @close) + +(if "if" @open "end" @close) +(unless "unless" @open "end" @close) +(begin "begin" @open "end" @close) +(module "module" @open "end" @close) +(_ . "def" @open "end" @close) +(_ . "class" @open "end" @close) \ No newline at end of file diff --git a/crates/zed2/src/languages/ruby/config.toml b/crates/zed2/src/languages/ruby/config.toml new file mode 100644 index 0000000000..6c8c615015 --- /dev/null +++ b/crates/zed2/src/languages/ruby/config.toml @@ -0,0 +1,13 @@ +name = "Ruby" +path_suffixes = ["rb", "Gemfile"] +first_line_pattern = '^#!.*\bruby\b' +line_comment = "# " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, +] +collapsed_placeholder = "# ..." diff --git a/crates/zed2/src/languages/ruby/embedding.scm b/crates/zed2/src/languages/ruby/embedding.scm new file mode 100644 index 0000000000..7a101e6b09 --- /dev/null +++ b/crates/zed2/src/languages/ruby/embedding.scm @@ -0,0 +1,22 @@ +( + (comment)* @context + . + [ + (module + "module" @name + name: (_) @name) + (method + "def" @name + name: (_) @name + body: (body_statement) @collapse) + (class + "class" @name + name: (_) @name) + (singleton_method + "def" @name + object: (_) @name + "." @name + name: (_) @name + body: (body_statement) @collapse) + ] @item + ) diff --git a/crates/zed2/src/languages/ruby/highlights.scm b/crates/zed2/src/languages/ruby/highlights.scm new file mode 100644 index 0000000000..2610cfa1cc --- /dev/null +++ b/crates/zed2/src/languages/ruby/highlights.scm @@ -0,0 +1,181 @@ +; Keywords + +[ + "alias" + "and" + "begin" + "break" + "case" + "class" + "def" + "do" + "else" + "elsif" + "end" + "ensure" + "for" + "if" + "in" + "module" + "next" + "or" + "rescue" + "retry" + "return" + "then" + "unless" + "until" + "when" + "while" + "yield" +] @keyword + +(identifier) @variable + +((identifier) @keyword + (#match? @keyword "^(private|protected|public)$")) + +; Function calls + +((identifier) @function.method.builtin + (#eq? @function.method.builtin "require")) + +"defined?" @function.method.builtin + +(call + method: [(identifier) (constant)] @function.method) + +; Function definitions + +(alias (identifier) @function.method) +(setter (identifier) @function.method) +(method name: [(identifier) (constant)] @function.method) +(singleton_method name: [(identifier) (constant)] @function.method) + +; Identifiers + +[ + (class_variable) + (instance_variable) +] @property + +((identifier) @constant.builtin + (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) + +(file) @constant.builtin +(line) @constant.builtin +(encoding) @constant.builtin + +(hash_splat_nil + "**" @operator +) @constant.builtin + +((constant) @constant + (#match? @constant "^[A-Z\\d_]+$")) + +(constant) @type + +(self) @variable.special +(super) @variable.special + +; Literals + +[ + (string) + (bare_string) + (subshell) + (heredoc_body) + (heredoc_beginning) +] @string + +[ + (simple_symbol) + (delimited_symbol) + (hash_key_symbol) + (bare_symbol) +] @string.special.symbol + +(regex) @string.regex +(escape_sequence) @escape + +[ + (integer) + (float) +] @number + +[ + (nil) + (true) + (false) +] @constant.builtin + +(comment) @comment + +; Operators + +[ + "!" + "~" + "+" + "-" + "**" + "*" + "/" + "%" + "<<" + ">>" + "&" + "|" + "^" + ">" + "<" + "<=" + ">=" + "==" + "!=" + "=~" + "!~" + "<=>" + "||" + "&&" + ".." + "..." + "=" + "**=" + "*=" + "/=" + "%=" + "+=" + "-=" + "<<=" + ">>=" + "&&=" + "&=" + "||=" + "|=" + "^=" + "=>" + "->" + (operator) +] @operator + +[ + "," + ";" + "." +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "%w(" + "%i(" +] @punctuation.bracket + +(interpolation + "#{" @punctuation.special + "}" @punctuation.special) @embedded diff --git a/crates/zed2/src/languages/ruby/indents.scm b/crates/zed2/src/languages/ruby/indents.scm new file mode 100644 index 0000000000..ac5175fa6f --- /dev/null +++ b/crates/zed2/src/languages/ruby/indents.scm @@ -0,0 +1,17 @@ +(method "end" @end) @indent +(class "end" @end) @indent +(module "end" @end) @indent +(begin "end" @end) @indent +(do_block "end" @end) @indent + +(then) @indent +(call) @indent + +(ensure) @outdent +(rescue) @outdent +(else) @outdent + + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/ruby/outline.scm b/crates/zed2/src/languages/ruby/outline.scm new file mode 100644 index 0000000000..0b36dabadb --- /dev/null +++ b/crates/zed2/src/languages/ruby/outline.scm @@ -0,0 +1,17 @@ +(class + "class" @context + name: (_) @name) @item + +(method + "def" @context + name: (_) @name) @item + +(singleton_method + "def" @context + object: (_) @context + "." @context + name: (_) @name) @item + +(module + "module" @context + name: (_) @name) @item diff --git a/crates/zed2/src/languages/ruby/overrides.scm b/crates/zed2/src/languages/ruby/overrides.scm new file mode 100644 index 0000000000..8a58e304e5 --- /dev/null +++ b/crates/zed2/src/languages/ruby/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +(string) @string diff --git a/crates/zed2/src/languages/rust.rs b/crates/zed2/src/languages/rust.rs new file mode 100644 index 0000000000..a0abcedd07 --- /dev/null +++ b/crates/zed2/src/languages/rust.rs @@ -0,0 +1,568 @@ +use anyhow::{anyhow, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_trait::async_trait; +use futures::{io::BufReader, StreamExt}; +pub use language2::*; +use lazy_static::lazy_static; +use lsp2::LanguageServerBinary; +use regex::Regex; +use smol::fs::{self, File}; +use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use util::{ + fs::remove_matching, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; + +pub struct RustLspAdapter; + +#[async_trait] +impl LspAdapter for RustLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("rust-analyzer".into()) + } + + fn short_name(&self) -> &'static str { + "rust" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = + latest_github_release("rust-analyzer/rust-analyzer", false, delegate.http_client()) + .await?; + let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + Ok(Box::new(GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + })) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); + + if fs::metadata(&destination_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let mut file = File::create(&destination_path).await?; + futures::io::copy(decompressed_bytes, &mut file).await?; + fs::set_permissions( + &destination_path, + ::from_mode(0o755), + ) + .await?; + + remove_matching(&container_dir, |entry| entry != destination_path).await; + } + + Ok(LanguageServerBinary { + path: destination_path, + arguments: Default::default(), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } + + async fn disk_based_diagnostic_sources(&self) -> Vec { + vec!["rustc".into()] + } + + async fn disk_based_diagnostics_progress_token(&self) -> Option { + Some("rust-analyzer/flycheck".into()) + } + + fn process_diagnostics(&self, params: &mut lsp2::PublishDiagnosticsParams) { + lazy_static! { + static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap(); + } + + for diagnostic in &mut params.diagnostics { + for message in diagnostic + .related_information + .iter_mut() + .flatten() + .map(|info| &mut info.message) + .chain([&mut diagnostic.message]) + { + if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") { + *message = sanitized; + } + } + } + } + + async fn label_for_completion( + &self, + completion: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + match completion.kind { + Some(lsp2::CompletionItemKind::FIELD) if completion.detail.is_some() => { + let detail = completion.detail.as_ref().unwrap(); + let name = &completion.label; + let text = format!("{}: {}", name, detail); + let source = Rope::from(format!("struct S {{ {} }}", text).as_str()); + let runs = language.highlight_text(&source, 11..11 + text.len()); + return Some(CodeLabel { + text, + runs, + filter_range: 0..name.len(), + }); + } + Some(lsp2::CompletionItemKind::CONSTANT | lsp2::CompletionItemKind::VARIABLE) + if completion.detail.is_some() + && completion.insert_text_format != Some(lsp2::InsertTextFormat::SNIPPET) => + { + let detail = completion.detail.as_ref().unwrap(); + let name = &completion.label; + let text = format!("{}: {}", name, detail); + let source = Rope::from(format!("let {} = ();", text).as_str()); + let runs = language.highlight_text(&source, 4..4 + text.len()); + return Some(CodeLabel { + text, + runs, + filter_range: 0..name.len(), + }); + } + Some(lsp2::CompletionItemKind::FUNCTION | lsp2::CompletionItemKind::METHOD) + if completion.detail.is_some() => + { + lazy_static! { + static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap(); + } + let detail = completion.detail.as_ref().unwrap(); + const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"]; + let prefix = FUNCTION_PREFIXES + .iter() + .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix))); + // fn keyword should be followed by opening parenthesis. + if let Some((prefix, suffix)) = prefix { + if suffix.starts_with('(') { + let text = REGEX.replace(&completion.label, suffix).to_string(); + let source = Rope::from(format!("{prefix} {} {{}}", text).as_str()); + let run_start = prefix.len() + 1; + let runs = + language.highlight_text(&source, run_start..run_start + text.len()); + return Some(CodeLabel { + filter_range: 0..completion.label.find('(').unwrap_or(text.len()), + text, + runs, + }); + } + } + } + Some(kind) => { + let highlight_name = match kind { + lsp2::CompletionItemKind::STRUCT + | lsp2::CompletionItemKind::INTERFACE + | lsp2::CompletionItemKind::ENUM => Some("type"), + lsp2::CompletionItemKind::ENUM_MEMBER => Some("variant"), + lsp2::CompletionItemKind::KEYWORD => Some("keyword"), + lsp2::CompletionItemKind::VALUE | lsp2::CompletionItemKind::CONSTANT => { + Some("constant") + } + _ => None, + }; + let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?; + let mut label = CodeLabel::plain(completion.label.clone(), None); + label.runs.push(( + 0..label.text.rfind('(').unwrap_or(label.text.len()), + highlight_id, + )); + return Some(label); + } + _ => {} + } + None + } + + async fn label_for_symbol( + &self, + name: &str, + kind: lsp2::SymbolKind, + language: &Arc, + ) -> Option { + let (text, filter_range, display_range) = match kind { + lsp2::SymbolKind::METHOD | lsp2::SymbolKind::FUNCTION => { + let text = format!("fn {} () {{}}", name); + let filter_range = 3..3 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::STRUCT => { + let text = format!("struct {} {{}}", name); + let filter_range = 7..7 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::ENUM => { + let text = format!("enum {} {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::INTERFACE => { + let text = format!("trait {} {{}}", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::CONSTANT => { + let text = format!("const {}: () = ();", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::MODULE => { + let text = format!("mod {} {{}}", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp2::SymbolKind::TYPE_PARAMETER => { + let text = format!("type {} {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + (|| async move { + let mut last = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + + anyhow::Ok(LanguageServerBinary { + path: last.ok_or_else(|| anyhow!("no cached binary"))?, + arguments: Default::default(), + }) + })() + .await + .log_err() +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU32; + + use super::*; + use crate::languages::language; + use gpui2::{Context, Hsla, TestAppContext}; + use language2::language_settings::AllLanguageSettings; + use settings2::SettingsStore; + use theme2::SyntaxTheme; + + #[gpui2::test] + async fn test_process_rust_diagnostics() { + let mut params = lsp2::PublishDiagnosticsParams { + uri: lsp2::Url::from_file_path("/a").unwrap(), + version: None, + diagnostics: vec![ + // no newlines + lsp2::Diagnostic { + message: "use of moved value `a`".to_string(), + ..Default::default() + }, + // newline at the end of a code span + lsp2::Diagnostic { + message: "consider importing this struct: `use b::c;\n`".to_string(), + ..Default::default() + }, + // code span starting right after a newline + lsp2::Diagnostic { + message: "cannot borrow `self.d` as mutable\n`self` is a `&` reference" + .to_string(), + ..Default::default() + }, + ], + }; + RustLspAdapter.process_diagnostics(&mut params); + + assert_eq!(params.diagnostics[0].message, "use of moved value `a`"); + + // remove trailing newline from code span + assert_eq!( + params.diagnostics[1].message, + "consider importing this struct: `use b::c;`" + ); + + // do not remove newline before the start of code span + assert_eq!( + params.diagnostics[2].message, + "cannot borrow `self.d` as mutable\n`self` is a `&` reference" + ); + } + + #[gpui2::test] + async fn test_rust_label_for_completion() { + let language = language( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ) + .await; + let grammar = language.grammar().unwrap(); + let theme = SyntaxTheme::new_test([ + ("type", Hsla::default()), + ("keyword", Hsla::default()), + ("function", Hsla::default()), + ("property", Hsla::default()), + ]); + + language.set_theme(&theme); + + let highlight_function = grammar.highlight_id_for_name("function").unwrap(); + let highlight_type = grammar.highlight_id_for_name("type").unwrap(); + let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap(); + let highlight_field = grammar.highlight_id_for_name("property").unwrap(); + + assert_eq!( + language + .label_for_completion(&lsp2::CompletionItem { + kind: Some(lsp2::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + }) + ); + assert_eq!( + language + .label_for_completion(&lsp2::CompletionItem { + kind: Some(lsp2::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("async fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + }) + ); + assert_eq!( + language + .label_for_completion(&lsp2::CompletionItem { + kind: Some(lsp2::CompletionItemKind::FIELD), + label: "len".to_string(), + detail: Some("usize".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "len: usize".to_string(), + filter_range: 0..3, + runs: vec![(0..3, highlight_field), (5..10, highlight_type),], + }) + ); + + assert_eq!( + language + .label_for_completion(&lsp2::CompletionItem { + kind: Some(lsp2::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + }) + ); + } + + #[gpui2::test] + async fn test_rust_label_for_symbol() { + let language = language( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ) + .await; + let grammar = language.grammar().unwrap(); + let theme = SyntaxTheme::new_test([ + ("type", Hsla::default()), + ("keyword", Hsla::default()), + ("function", Hsla::default()), + ("property", Hsla::default()), + ]); + + language.set_theme(&theme); + + let highlight_function = grammar.highlight_id_for_name("function").unwrap(); + let highlight_type = grammar.highlight_id_for_name("type").unwrap(); + let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap(); + + assert_eq!( + language + .label_for_symbol("hello", lsp2::SymbolKind::FUNCTION) + .await, + Some(CodeLabel { + text: "fn hello".to_string(), + filter_range: 3..8, + runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)], + }) + ); + + assert_eq!( + language + .label_for_symbol("World", lsp2::SymbolKind::TYPE_PARAMETER) + .await, + Some(CodeLabel { + text: "type World".to_string(), + filter_range: 5..10, + runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)], + }) + ); + } + + #[gpui2::test] + async fn test_rust_autoindent(cx: &mut TestAppContext) { + // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX); + cx.update(|cx| { + let test_settings = SettingsStore::test(cx); + cx.set_global(test_settings); + language2::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); + }); + + let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; + + cx.build_model(|cx| { + let mut buffer = + Buffer::new(0, cx.entity_id().as_u64(), "").with_language(language, cx); + + // indent between braces + buffer.set_text("fn a() {}", cx); + let ix = buffer.len() - 1; + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "fn a() {\n \n}"); + + // indent between braces, even after empty lines + buffer.set_text("fn a() {\n\n\n}", cx); + let ix = buffer.len() - 2; + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "fn a() {\n\n\n \n}"); + + // indent a line that continues a field expression + buffer.set_text("fn a() {\n \n}", cx); + let ix = buffer.len() - 2; + buffer.edit([(ix..ix, "b\n.c")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "fn a() {\n b\n .c\n}"); + + // indent further lines that continue the field expression, even after empty lines + let ix = buffer.len() - 2; + buffer.edit([(ix..ix, "\n\n.d")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "fn a() {\n b\n .c\n \n .d\n}"); + + // dedent the line after the field expression + let ix = buffer.len() - 2; + buffer.edit([(ix..ix, ";\ne")], Some(AutoindentMode::EachLine), cx); + assert_eq!( + buffer.text(), + "fn a() {\n b\n .c\n \n .d;\n e\n}" + ); + + // indent inside a struct within a call + buffer.set_text("const a: B = c(D {});", cx); + let ix = buffer.len() - 3; + buffer.edit([(ix..ix, "\n\n")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "const a: B = c(D {\n \n});"); + + // indent further inside a nested call + let ix = buffer.len() - 4; + buffer.edit([(ix..ix, "e: f(\n\n)")], Some(AutoindentMode::EachLine), cx); + assert_eq!(buffer.text(), "const a: B = c(D {\n e: f(\n \n )\n});"); + + // keep that indent after an empty line + let ix = buffer.len() - 8; + buffer.edit([(ix..ix, "\n")], Some(AutoindentMode::EachLine), cx); + assert_eq!( + buffer.text(), + "const a: B = c(D {\n e: f(\n \n \n )\n});" + ); + + buffer + }); + } +} diff --git a/crates/zed2/src/languages/rust/brackets.scm b/crates/zed2/src/languages/rust/brackets.scm new file mode 100644 index 0000000000..0be534c48c --- /dev/null +++ b/crates/zed2/src/languages/rust/brackets.scm @@ -0,0 +1,6 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) +(closure_parameters "|" @open "|" @close) \ No newline at end of file diff --git a/crates/zed2/src/languages/rust/config.toml b/crates/zed2/src/languages/rust/config.toml new file mode 100644 index 0000000000..8216ba0a74 --- /dev/null +++ b/crates/zed2/src/languages/rust/config.toml @@ -0,0 +1,13 @@ +name = "Rust" +path_suffixes = ["rs"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true, not_in = ["string", "comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, +] +collapsed_placeholder = " /* ... */ " diff --git a/crates/zed2/src/languages/rust/embedding.scm b/crates/zed2/src/languages/rust/embedding.scm new file mode 100644 index 0000000000..286b1d1357 --- /dev/null +++ b/crates/zed2/src/languages/rust/embedding.scm @@ -0,0 +1,32 @@ +( + [(line_comment) (attribute_item)]* @context + . + [ + + (struct_item + name: (_) @name) + + (enum_item + name: (_) @name) + + (impl_item + trait: (_)? @name + "for"? @name + type: (_) @name) + + (trait_item + name: (_) @name) + + (function_item + name: (_) @name + body: (block + "{" @keep + "}" @keep) @collapse) + + (macro_definition + name: (_) @name) + ] @item + ) + +(attribute_item) @collapse +(use_declaration) @collapse diff --git a/crates/zed2/src/languages/rust/highlights.scm b/crates/zed2/src/languages/rust/highlights.scm new file mode 100644 index 0000000000..7240173a89 --- /dev/null +++ b/crates/zed2/src/languages/rust/highlights.scm @@ -0,0 +1,116 @@ +(type_identifier) @type +(primitive_type) @type.builtin +(self) @variable.special +(field_identifier) @property + +(call_expression + function: [ + (identifier) @function + (scoped_identifier + name: (identifier) @function) + (field_expression + field: (field_identifier) @function.method) + ]) + +(generic_function + function: [ + (identifier) @function + (scoped_identifier + name: (identifier) @function) + (field_expression + field: (field_identifier) @function.method) + ]) + +(function_item name: (identifier) @function.definition) +(function_signature_item name: (identifier) @function.definition) + +(macro_invocation + macro: [ + (identifier) @function.special + (scoped_identifier + name: (identifier) @function.special) + ]) + +(macro_definition + name: (identifier) @function.special.definition) + +; Identifier conventions + +; Assume uppercase names are types/enum-constructors +((identifier) @type + (#match? @type "^[A-Z]")) + +; Assume all-caps names are constants +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +(_ + . + "<" @punctuation.bracket + ">" @punctuation.bracket) + +[ + "as" + "async" + "await" + "break" + "const" + "continue" + "default" + "dyn" + "else" + "enum" + "extern" + "for" + "fn" + "if" + "in" + "impl" + "let" + "loop" + "macro_rules!" + "match" + "mod" + "move" + "pub" + "ref" + "return" + "static" + "struct" + "trait" + "type" + "use" + "where" + "while" + "union" + "unsafe" + (mutable_specifier) + (super) +] @keyword + +[ + (string_literal) + (raw_string_literal) + (char_literal) +] @string + +[ + (integer_literal) + (float_literal) +] @number + +(boolean_literal) @constant + +[ + (line_comment) + (block_comment) +] @comment diff --git a/crates/zed2/src/languages/rust/indents.scm b/crates/zed2/src/languages/rust/indents.scm new file mode 100644 index 0000000000..9ab6b02908 --- /dev/null +++ b/crates/zed2/src/languages/rust/indents.scm @@ -0,0 +1,14 @@ +[ + ((where_clause) _ @end) + (field_expression) + (call_expression) + (assignment_expression) + (let_declaration) + (let_chain) + (await_expression) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/rust/injections.scm b/crates/zed2/src/languages/rust/injections.scm new file mode 100644 index 0000000000..57ebea8539 --- /dev/null +++ b/crates/zed2/src/languages/rust/injections.scm @@ -0,0 +1,7 @@ +(macro_invocation + (token_tree) @content + (#set! "language" "rust")) + +(macro_rule + (token_tree) @content + (#set! "language" "rust")) \ No newline at end of file diff --git a/crates/zed2/src/languages/rust/outline.scm b/crates/zed2/src/languages/rust/outline.scm new file mode 100644 index 0000000000..5c89087ac0 --- /dev/null +++ b/crates/zed2/src/languages/rust/outline.scm @@ -0,0 +1,63 @@ +(struct_item + (visibility_modifier)? @context + "struct" @context + name: (_) @name) @item + +(enum_item + (visibility_modifier)? @context + "enum" @context + name: (_) @name) @item + +(enum_variant + (visibility_modifier)? @context + name: (_) @name) @item + +(impl_item + "impl" @context + trait: (_)? @name + "for"? @context + type: (_) @name) @item + +(trait_item + (visibility_modifier)? @context + "trait" @context + name: (_) @name) @item + +(function_item + (visibility_modifier)? @context + (function_modifiers)? @context + "fn" @context + name: (_) @name) @item + +(function_signature_item + (visibility_modifier)? @context + (function_modifiers)? @context + "fn" @context + name: (_) @name) @item + +(macro_definition + . "macro_rules!" @context + name: (_) @name) @item + +(mod_item + (visibility_modifier)? @context + "mod" @context + name: (_) @name) @item + +(type_item + (visibility_modifier)? @context + "type" @context + name: (_) @name) @item + +(associated_type + "type" @context + name: (_) @name) @item + +(const_item + (visibility_modifier)? @context + "const" @context + name: (_) @name) @item + +(field_declaration + (visibility_modifier)? @context + name: (_) @name) @item diff --git a/crates/zed2/src/languages/rust/overrides.scm b/crates/zed2/src/languages/rust/overrides.scm new file mode 100644 index 0000000000..216a395147 --- /dev/null +++ b/crates/zed2/src/languages/rust/overrides.scm @@ -0,0 +1,8 @@ +[ + (string_literal) + (raw_string_literal) +] @string +[ + (line_comment) + (block_comment) +] @comment diff --git a/crates/zed2/src/languages/scheme/brackets.scm b/crates/zed2/src/languages/scheme/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed2/src/languages/scheme/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed2/src/languages/scheme/config.toml b/crates/zed2/src/languages/scheme/config.toml new file mode 100644 index 0000000000..7b47698833 --- /dev/null +++ b/crates/zed2/src/languages/scheme/config.toml @@ -0,0 +1,9 @@ +name = "Scheme" +path_suffixes = ["scm", "ss"] +line_comment = "; " +autoclose_before = "])" +brackets = [ + { start = "[", end = "]", close = true, newline = false }, + { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/crates/zed2/src/languages/scheme/highlights.scm b/crates/zed2/src/languages/scheme/highlights.scm new file mode 100644 index 0000000000..40ba61cd05 --- /dev/null +++ b/crates/zed2/src/languages/scheme/highlights.scm @@ -0,0 +1,28 @@ +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +(number) @number +(character) @constant.builtin +(boolean) @constant.builtin + +(symbol) @variable +(string) @string + +(escape_sequence) @escape + +[(comment) + (block_comment) + (directive)] @comment + +((symbol) @operator + (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$")) + +(list + . + (symbol) @function) + +(list + . + (symbol) @keyword + (#match? @keyword + "^(define-syntax|let\\*|lambda|λ|case|=>|quote-splicing|unquote-splicing|set!|let|letrec|letrec-syntax|let-values|let\\*-values|do|else|define|cond|syntax-rules|unquote|begin|quote|let-syntax|and|if|quasiquote|letrec|delay|or|when|unless|identifier-syntax|assert|library|export|import|rename|only|except|prefix)$" + )) diff --git a/crates/zed2/src/languages/scheme/indents.scm b/crates/zed2/src/languages/scheme/indents.scm new file mode 100644 index 0000000000..9a1cbad161 --- /dev/null +++ b/crates/zed2/src/languages/scheme/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]") @indent +(_ "{" "}") @indent +(_ "(" ")") @indent diff --git a/crates/zed2/src/languages/scheme/outline.scm b/crates/zed2/src/languages/scheme/outline.scm new file mode 100644 index 0000000000..604e052a63 --- /dev/null +++ b/crates/zed2/src/languages/scheme/outline.scm @@ -0,0 +1,10 @@ +(list + . + (symbol) @start-symbol @context + . + [ + (symbol) @name + (list . (symbol) @name) + ] + (#match? @start-symbol "^define") +) @item \ No newline at end of file diff --git a/crates/zed2/src/languages/scheme/overrides.scm b/crates/zed2/src/languages/scheme/overrides.scm new file mode 100644 index 0000000000..8c0d41b046 --- /dev/null +++ b/crates/zed2/src/languages/scheme/overrides.scm @@ -0,0 +1,6 @@ +[ + (comment) + (block_comment) + (directive) +] @comment +(string) @string diff --git a/crates/zed2/src/languages/svelte.rs b/crates/zed2/src/languages/svelte.rs new file mode 100644 index 0000000000..53f52a6a30 --- /dev/null +++ b/crates/zed2/src/languages/svelte.rs @@ -0,0 +1,133 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::json; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = "node_modules/svelte-language-server/bin/server.js"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct SvelteLspAdapter { + node: Arc, +} + +impl SvelteLspAdapter { + pub fn new(node: Arc) -> Self { + SvelteLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for SvelteLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("svelte-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "svelte" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("svelte-language-server") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("svelte-language-server", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } + + fn prettier_plugins(&self) -> &[&'static str] { + &["prettier-plugin-svelte"] + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed2/src/languages/svelte/config.toml b/crates/zed2/src/languages/svelte/config.toml new file mode 100644 index 0000000000..76f03493b5 --- /dev/null +++ b/crates/zed2/src/languages/svelte/config.toml @@ -0,0 +1,20 @@ +name = "Svelte" +path_suffixes = ["svelte"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true, not_in = ["string", "comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, +] +scope_opt_in_language_servers = ["tailwindcss-language-server"] +prettier_parser_name = "svelte" + +[overrides.string] +word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed2/src/languages/svelte/folds.scm b/crates/zed2/src/languages/svelte/folds.scm new file mode 100755 index 0000000000..795c32fc4a --- /dev/null +++ b/crates/zed2/src/languages/svelte/folds.scm @@ -0,0 +1,9 @@ +[ + (style_element) + (script_element) + (element) + (if_statement) + (else_statement) + (each_statement) + (await_statement) +] @fold diff --git a/crates/zed2/src/languages/svelte/highlights.scm b/crates/zed2/src/languages/svelte/highlights.scm new file mode 100755 index 0000000000..de873684e4 --- /dev/null +++ b/crates/zed2/src/languages/svelte/highlights.scm @@ -0,0 +1,42 @@ +; Special identifiers +;-------------------- + +; TODO: +(tag_name) @tag +(attribute_name) @property +(erroneous_end_tag_name) @keyword +(comment) @comment + +[ + (attribute_value) + (quoted_attribute_value) +] @string + +[ + (text) + (raw_text_expr) +] @none + +[ + (special_block_keyword) + (then) + (as) +] @keyword + +[ + "{" + "}" +] @punctuation.bracket + +"=" @operator + +[ + "<" + ">" + "" + "#" + ":" + "/" + "@" +] @tag.delimiter diff --git a/crates/zed2/src/languages/svelte/indents.scm b/crates/zed2/src/languages/svelte/indents.scm new file mode 100755 index 0000000000..886d8ca867 --- /dev/null +++ b/crates/zed2/src/languages/svelte/indents.scm @@ -0,0 +1,8 @@ +[ + (element) + (if_statement) + (each_statement) + (await_statement) + (script_element) + (style_element) +] @indent diff --git a/crates/zed2/src/languages/svelte/injections.scm b/crates/zed2/src/languages/svelte/injections.scm new file mode 100755 index 0000000000..8c1ac9fcd0 --- /dev/null +++ b/crates/zed2/src/languages/svelte/injections.scm @@ -0,0 +1,28 @@ +; injections.scm +; -------------- +(script_element + (raw_text) @content + (#set! "language" "javascript")) + + ((script_element + (start_tag + (attribute + (quoted_attribute_value (attribute_value) @_language))) + (raw_text) @content) + (#eq? @_language "ts") + (#set! "language" "typescript")) + +((script_element + (start_tag + (attribute + (quoted_attribute_value (attribute_value) @_language))) + (raw_text) @content) + (#eq? @_language "typescript") + (#set! "language" "typescript")) + +(style_element + (raw_text) @content + (#set! "language" "css")) + +((raw_text_expr) @content + (#set! "language" "javascript")) diff --git a/crates/zed2/src/languages/svelte/overrides.scm b/crates/zed2/src/languages/svelte/overrides.scm new file mode 100644 index 0000000000..2a76410297 --- /dev/null +++ b/crates/zed2/src/languages/svelte/overrides.scm @@ -0,0 +1,7 @@ +(comment) @comment + +[ + (raw_text) + (attribute_value) + (quoted_attribute_value) +] @string diff --git a/crates/zed2/src/languages/tailwind.rs b/crates/zed2/src/languages/tailwind.rs new file mode 100644 index 0000000000..0aa2154f1e --- /dev/null +++ b/crates/zed2/src/languages/tailwind.rs @@ -0,0 +1,167 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use collections::HashMap; +use futures::{ + future::{self, BoxFuture}, + FutureExt, StreamExt, +}; +use gpui2::AppContext; +use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::{json, Value}; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = "node_modules/.bin/tailwindcss-language-server"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct TailwindLspAdapter { + node: Arc, +} + +impl TailwindLspAdapter { + pub fn new(node: Arc) -> Self { + TailwindLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for TailwindLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("tailwindcss-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "tailwind" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("@tailwindcss/language-server") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("@tailwindcss/language-server", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true, + "userLanguages": { + "html": "html", + "css": "css", + "javascript": "javascript", + "typescriptreact": "typescriptreact", + }, + })) + } + + fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + future::ready(json!({ + "tailwindCSS": { + "emmetCompletions": true, + } + })) + .boxed() + } + + async fn language_ids(&self) -> HashMap { + HashMap::from_iter([ + ("HTML".to_string(), "html".to_string()), + ("CSS".to_string(), "css".to_string()), + ("JavaScript".to_string(), "javascript".to_string()), + ("TSX".to_string(), "typescriptreact".to_string()), + ("Svelte".to_string(), "svelte".to_string()), + ("Elixir".to_string(), "phoenix-heex".to_string()), + ("HEEX".to_string(), "phoenix-heex".to_string()), + ("ERB".to_string(), "erb".to_string()), + ("PHP".to_string(), "php".to_string()), + ]) + } + + fn prettier_plugins(&self) -> &[&'static str] { + &["prettier-plugin-tailwindcss"] + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed2/src/languages/toml/brackets.scm b/crates/zed2/src/languages/toml/brackets.scm new file mode 100644 index 0000000000..9e8c9cd93c --- /dev/null +++ b/crates/zed2/src/languages/toml/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/toml/config.toml b/crates/zed2/src/languages/toml/config.toml new file mode 100644 index 0000000000..188239a8e0 --- /dev/null +++ b/crates/zed2/src/languages/toml/config.toml @@ -0,0 +1,10 @@ +name = "TOML" +path_suffixes = ["Cargo.lock", "toml"] +line_comment = "# " +autoclose_before = ",]}" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/crates/zed2/src/languages/toml/highlights.scm b/crates/zed2/src/languages/toml/highlights.scm new file mode 100644 index 0000000000..04d83b5459 --- /dev/null +++ b/crates/zed2/src/languages/toml/highlights.scm @@ -0,0 +1,37 @@ +; Properties +;----------- + +(bare_key) @property +(quoted_key) @property + +; Literals +;--------- + +(boolean) @constant +(comment) @comment +(string) @string +(integer) @number +(float) @number +(offset_date_time) @string.special +(local_date_time) @string.special +(local_date) @string.special +(local_time) @string.special + +; Punctuation +;------------ + +[ + "." + "," +] @punctuation.delimiter + +"=" @operator + +[ + "[" + "]" + "[[" + "]]" + "{" + "}" +] @punctuation.bracket diff --git a/crates/zed2/src/languages/toml/indents.scm b/crates/zed2/src/languages/toml/indents.scm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed2/src/languages/toml/outline.scm b/crates/zed2/src/languages/toml/outline.scm new file mode 100644 index 0000000000..d232d489b6 --- /dev/null +++ b/crates/zed2/src/languages/toml/outline.scm @@ -0,0 +1,15 @@ +(table + . + "[" + . + (_) @name) @item + +(table_array_element + . + "[[" + . + (_) @name) @item + +(pair + . + (_) @name) @item \ No newline at end of file diff --git a/crates/zed2/src/languages/toml/overrides.scm b/crates/zed2/src/languages/toml/overrides.scm new file mode 100644 index 0000000000..8a58e304e5 --- /dev/null +++ b/crates/zed2/src/languages/toml/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +(string) @string diff --git a/crates/zed2/src/languages/tsx/brackets.scm b/crates/zed2/src/languages/tsx/brackets.scm new file mode 120000 index 0000000000..e6835c943b --- /dev/null +++ b/crates/zed2/src/languages/tsx/brackets.scm @@ -0,0 +1 @@ +../typescript/brackets.scm \ No newline at end of file diff --git a/crates/zed2/src/languages/tsx/config.toml b/crates/zed2/src/languages/tsx/config.toml new file mode 100644 index 0000000000..0dae25d779 --- /dev/null +++ b/crates/zed2/src/languages/tsx/config.toml @@ -0,0 +1,25 @@ +name = "TSX" +path_suffixes = ["tsx"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true, not_in = ["string", "comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, +] +word_characters = ["#", "$"] +scope_opt_in_language_servers = ["tailwindcss-language-server"] +prettier_parser_name = "typescript" + +[overrides.element] +line_comment = { remove = true } +block_comment = ["{/* ", " */}"] + +[overrides.string] +word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed2/src/languages/tsx/embedding.scm b/crates/zed2/src/languages/tsx/embedding.scm new file mode 100644 index 0000000000..ddcff66584 --- /dev/null +++ b/crates/zed2/src/languages/tsx/embedding.scm @@ -0,0 +1,85 @@ +( + (comment)* @context + . + [ + (export_statement + (function_declaration + "async"? @name + "function" @name + name: (_) @name)) + (function_declaration + "async"? @name + "function" @name + name: (_) @name) + ] @item + ) + +( + (comment)* @context + . + [ + (export_statement + (class_declaration + "class" @name + name: (_) @name)) + (class_declaration + "class" @name + name: (_) @name) + ] @item + ) + +( + (comment)* @context + . + [ + (export_statement + (interface_declaration + "interface" @name + name: (_) @name)) + (interface_declaration + "interface" @name + name: (_) @name) + ] @item + ) + +( + (comment)* @context + . + [ + (export_statement + (enum_declaration + "enum" @name + name: (_) @name)) + (enum_declaration + "enum" @name + name: (_) @name) + ] @item + ) + +( + (comment)* @context + . + [ + (export_statement + (type_alias_declaration + "type" @name + name: (_) @name)) + (type_alias_declaration + "type" @name + name: (_) @name) + ] @item + ) + +( + (comment)* @context + . + (method_definition + [ + "get" + "set" + "async" + "*" + "static" + ]* @name + name: (_) @name) @item + ) diff --git a/crates/zed2/src/languages/tsx/highlights-jsx.scm b/crates/zed2/src/languages/tsx/highlights-jsx.scm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed2/src/languages/tsx/highlights.scm b/crates/zed2/src/languages/tsx/highlights.scm new file mode 120000 index 0000000000..226302a5d1 --- /dev/null +++ b/crates/zed2/src/languages/tsx/highlights.scm @@ -0,0 +1 @@ +../typescript/highlights.scm \ No newline at end of file diff --git a/crates/zed2/src/languages/tsx/indents.scm b/crates/zed2/src/languages/tsx/indents.scm new file mode 120000 index 0000000000..502c2a060a --- /dev/null +++ b/crates/zed2/src/languages/tsx/indents.scm @@ -0,0 +1 @@ +../typescript/indents.scm \ No newline at end of file diff --git a/crates/zed2/src/languages/tsx/outline.scm b/crates/zed2/src/languages/tsx/outline.scm new file mode 120000 index 0000000000..a0df409fda --- /dev/null +++ b/crates/zed2/src/languages/tsx/outline.scm @@ -0,0 +1 @@ +../typescript/outline.scm \ No newline at end of file diff --git a/crates/zed2/src/languages/tsx/overrides.scm b/crates/zed2/src/languages/tsx/overrides.scm new file mode 100644 index 0000000000..8b43fdcfc5 --- /dev/null +++ b/crates/zed2/src/languages/tsx/overrides.scm @@ -0,0 +1,13 @@ +(comment) @comment + +[ + (string) + (template_string) +] @string + +[ + (jsx_element) + (jsx_fragment) + (jsx_self_closing_element) + (jsx_expression) +] @element diff --git a/crates/zed2/src/languages/typescript.rs b/crates/zed2/src/languages/typescript.rs new file mode 100644 index 0000000000..8eecf25540 --- /dev/null +++ b/crates/zed2/src/languages/typescript.rs @@ -0,0 +1,384 @@ +use anyhow::{anyhow, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::{future::BoxFuture, FutureExt}; +use gpui2::AppContext; +use language2::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp2::{CodeActionKind, LanguageServerBinary}; +use node_runtime::NodeRuntime; +use serde_json::{json, Value}; +use smol::{fs, io::BufReader, stream::StreamExt}; +use std::{ + any::Any, + ffi::OsString, + future, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::{fs::remove_matching, github::latest_github_release}; +use util::{github::GitHubLspBinaryVersion, ResultExt}; + +fn typescript_server_binary_arguments(server_path: &Path) -> Vec { + vec![ + server_path.into(), + "--stdio".into(), + "--tsserver-path".into(), + "node_modules/typescript/lib".into(), + ] +} + +fn eslint_server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct TypeScriptLspAdapter { + node: Arc, +} + +impl TypeScriptLspAdapter { + const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js"; + const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs"; + + pub fn new(node: Arc) -> Self { + TypeScriptLspAdapter { node } + } +} + +struct TypeScriptVersions { + typescript_version: String, + server_version: String, +} + +#[async_trait] +impl LspAdapter for TypeScriptLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("typescript-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "tsserver" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(TypeScriptVersions { + typescript_version: self.node.npm_package_latest_version("typescript").await?, + server_version: self + .node + .npm_package_latest_version("typescript-language-server") + .await?, + }) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(Self::NEW_SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[ + ("typescript", version.typescript_version.as_str()), + ( + "typescript-language-server", + version.server_version.as_str(), + ), + ], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: typescript_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_ts_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_ts_server_binary(container_dir, &*self.node).await + } + + fn code_action_kinds(&self) -> Option> { + Some(vec![ + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR, + CodeActionKind::REFACTOR_EXTRACT, + CodeActionKind::SOURCE, + ]) + } + + async fn label_for_completion( + &self, + item: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + use lsp2::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"), + _ => None, + }?; + + let text = match &item.detail { + Some(detail) => format!("{} {}", item.label, detail), + None => item.label.clone(), + }; + + Some(language2::CodeLabel { + text, + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} + +async fn get_cached_ts_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + (|| async move { + let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH); + let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH); + if new_server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: typescript_server_binary_arguments(&new_server_path), + }) + } else if old_server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: typescript_server_binary_arguments(&old_server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + container_dir + )) + } + })() + .await + .log_err() +} + +pub struct EsLintLspAdapter { + node: Arc, +} + +impl EsLintLspAdapter { + const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js"; + + #[allow(unused)] + pub fn new(node: Arc) -> Self { + EsLintLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for EsLintLspAdapter { + fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + future::ready(json!({ + "": { + "validate": "on", + "rulesCustomizations": [], + "run": "onType", + "nodePath": null, + } + })) + .boxed() + } + + async fn name(&self) -> LanguageServerName { + LanguageServerName("eslint".into()) + } + + fn short_name(&self) -> &'static str { + "eslint" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + // At the time of writing the latest vscode-eslint release was released in 2020 and requires + // special custom LSP protocol extensions be handled to fully initialize. Download the latest + // prerelease instead to sidestep this issue + let release = + latest_github_release("microsoft/vscode-eslint", true, delegate.http_client()).await?; + Ok(Box::new(GitHubLspBinaryVersion { + name: release.name, + url: release.tarball_url, + })) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name)); + let server_path = destination_path.join(Self::SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + remove_matching(&container_dir, |entry| entry != destination_path).await; + + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(&destination_path).await?; + + let mut dir = fs::read_dir(&destination_path).await?; + let first = dir.next().await.ok_or(anyhow!("missing first file"))??; + let repo_root = destination_path.join("vscode-eslint"); + fs::rename(first.path(), &repo_root).await?; + + self.node + .run_npm_subcommand(Some(&repo_root), "install", &[]) + .await?; + + self.node + .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"]) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: eslint_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_eslint_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_eslint_server_binary(container_dir, &*self.node).await + } + + async fn label_for_completion( + &self, + _item: &lsp2::CompletionItem, + _language: &Arc, + ) -> Option { + None + } + + async fn initialization_options(&self) -> Option { + None + } +} + +async fn get_cached_eslint_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + (|| async move { + // This is unfortunate but we don't know what the version is to build a path directly + let mut dir = fs::read_dir(&container_dir).await?; + let first = dir.next().await.ok_or(anyhow!("missing first file"))??; + if !first.file_type().await?.is_dir() { + return Err(anyhow!("First entry is not a directory")); + } + let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH); + + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: eslint_server_binary_arguments(&server_path), + }) + })() + .await + .log_err() +} + +#[cfg(test)] +mod tests { + use gpui2::{Context, TestAppContext}; + use unindent::Unindent; + + #[gpui2::test] + async fn test_outline(cx: &mut TestAppContext) { + let language = crate::languages::language( + "typescript", + tree_sitter_typescript::language_typescript(), + None, + ) + .await; + + let text = r#" + function a() { + // local variables are omitted + let a1 = 1; + // all functions are included + async function a2() {} + } + // top-level variables are included + let b: C + function getB() {} + // exported variables are included + export const d = e; + "# + .unindent(); + + let buffer = cx.build_model(|cx| { + language2::Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) + }); + let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[ + ("function a()", 0), + ("async function a2()", 1), + ("let b", 0), + ("function getB()", 0), + ("const d", 0), + ] + ); + } +} diff --git a/crates/zed2/src/languages/typescript/brackets.scm b/crates/zed2/src/languages/typescript/brackets.scm new file mode 100644 index 0000000000..63395f81d8 --- /dev/null +++ b/crates/zed2/src/languages/typescript/brackets.scm @@ -0,0 +1,5 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/typescript/config.toml b/crates/zed2/src/languages/typescript/config.toml new file mode 100644 index 0000000000..d1ebffc559 --- /dev/null +++ b/crates/zed2/src/languages/typescript/config.toml @@ -0,0 +1,16 @@ +name = "TypeScript" +path_suffixes = ["ts", "cts", "d.cts", "d.mts", "mts"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true, not_in = ["string", "comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, + { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, +] +word_characters = ["#", "$"] +prettier_parser_name = "typescript" diff --git a/crates/zed2/src/languages/typescript/embedding.scm b/crates/zed2/src/languages/typescript/embedding.scm new file mode 100644 index 0000000000..3170cb7c95 --- /dev/null +++ b/crates/zed2/src/languages/typescript/embedding.scm @@ -0,0 +1,85 @@ +( + (comment)* @context + . + [ + (export_statement + (function_declaration + "async"? @name + "function" @name + name: (_) @name)) + (function_declaration + "async"? @name + "function" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + [ + (export_statement + (class_declaration + "class" @name + name: (_) @name)) + (class_declaration + "class" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + [ + (export_statement + (interface_declaration + "interface" @name + name: (_) @name)) + (interface_declaration + "interface" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + [ + (export_statement + (enum_declaration + "enum" @name + name: (_) @name)) + (enum_declaration + "enum" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + [ + (export_statement + (type_alias_declaration + "type" @name + name: (_) @name)) + (type_alias_declaration + "type" @name + name: (_) @name) + ] @item +) + +( + (comment)* @context + . + (method_definition + [ + "get" + "set" + "async" + "*" + "static" + ]* @name + name: (_) @name) @item +) diff --git a/crates/zed2/src/languages/typescript/highlights.scm b/crates/zed2/src/languages/typescript/highlights.scm new file mode 100644 index 0000000000..bf086ea156 --- /dev/null +++ b/crates/zed2/src/languages/typescript/highlights.scm @@ -0,0 +1,221 @@ +; Variables + +(identifier) @variable + +; Properties + +(property_identifier) @property + +; Function and method calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Function and method definitions + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function) (arrow_function)]) + +; Special identifiers + +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +((identifier) @type + (#match? @type "^[A-Z]")) +(type_identifier) @type +(predefined_type) @type.builtin + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^_*[A-Z_][A-Z\\d_]*$")) + +; Literals + +(this) @variable.special +(super) @variable.special + +[ + (null) + (undefined) +] @constant.builtin + +[ + (true) + (false) +] @boolean + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.regex +(number) @number + +; Tokens + +[ + ";" + "?." + "." + "," + ":" +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "satisfies" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) + +; Keywords + +[ "abstract" + "declare" + "enum" + "export" + "implements" + "interface" + "keyof" + "namespace" + "private" + "protected" + "public" + "type" + "readonly" + "override" +] @keyword \ No newline at end of file diff --git a/crates/zed2/src/languages/typescript/indents.scm b/crates/zed2/src/languages/typescript/indents.scm new file mode 100644 index 0000000000..107e6ff8e0 --- /dev/null +++ b/crates/zed2/src/languages/typescript/indents.scm @@ -0,0 +1,15 @@ +[ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed2/src/languages/typescript/outline.scm b/crates/zed2/src/languages/typescript/outline.scm new file mode 100644 index 0000000000..68d297653e --- /dev/null +++ b/crates/zed2/src/languages/typescript/outline.scm @@ -0,0 +1,65 @@ +(internal_module + "namespace" @context + name: (_) @name) @item + +(enum_declaration + "enum" @context + name: (_) @name) @item + +(type_alias_declaration + "type" @context + name: (_) @name) @item + +(function_declaration + "async"? @context + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(interface_declaration + "interface" @context + name: (_) @name) @item + +(export_statement + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (_) @name) @item)) + +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (_) @name) @item)) + +(class_declaration + "class" @context + name: (_) @name) @item + +(method_definition + [ + "get" + "set" + "async" + "*" + "readonly" + "static" + (override_modifier) + (accessibility_modifier) + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(public_field_definition + [ + "declare" + "readonly" + "abstract" + "static" + (accessibility_modifier) + ]* @context + name: (_) @name) @item diff --git a/crates/zed2/src/languages/typescript/overrides.scm b/crates/zed2/src/languages/typescript/overrides.scm new file mode 100644 index 0000000000..8a58e304e5 --- /dev/null +++ b/crates/zed2/src/languages/typescript/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment +(string) @string diff --git a/crates/zed2/src/languages/vue.rs b/crates/zed2/src/languages/vue.rs new file mode 100644 index 0000000000..0c87c4bee8 --- /dev/null +++ b/crates/zed2/src/languages/vue.rs @@ -0,0 +1,220 @@ +use anyhow::{anyhow, ensure, Result}; +use async_trait::async_trait; +use futures::StreamExt; +pub use language2::*; +use lsp2::{CodeActionKind, LanguageServerBinary}; +use node_runtime::NodeRuntime; +use parking_lot::Mutex; +use serde_json::Value; +use smol::fs::{self}; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +pub struct VueLspVersion { + vue_version: String, + ts_version: String, +} + +pub struct VueLspAdapter { + node: Arc, + typescript_install_path: Mutex>, +} + +impl VueLspAdapter { + const SERVER_PATH: &'static str = + "node_modules/@vue/language-server/bin/vue-language-server.js"; + // TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options. + const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib"; + pub fn new(node: Arc) -> Self { + let typescript_install_path = Mutex::new(None); + Self { + node, + typescript_install_path, + } + } +} +#[async_trait] +impl super::LspAdapter for VueLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("vue-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "vue-language-server" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(VueLspVersion { + vue_version: self + .node + .npm_package_latest_version("@vue/language-server") + .await?, + ts_version: self.node.npm_package_latest_version("typescript").await?, + }) as Box<_>) + } + async fn initialization_options(&self) -> Option { + let typescript_sdk_path = self.typescript_install_path.lock(); + let typescript_sdk_path = typescript_sdk_path + .as_ref() + .expect("initialization_options called without a container_dir for typescript"); + + Some(serde_json::json!({ + "typescript": { + "tsdk": typescript_sdk_path + } + })) + } + fn code_action_kinds(&self) -> Option> { + // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it + // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec. + Some(vec![ + CodeActionKind::EMPTY, + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR_REWRITE, + ]) + } + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(Self::SERVER_PATH); + let ts_path = container_dir.join(Self::TYPESCRIPT_PATH); + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("@vue/language-server", version.vue_version.as_str())], + ) + .await?; + } + ensure!( + fs::metadata(&server_path).await.is_ok(), + "@vue/language-server package installation failed" + ); + if fs::metadata(&ts_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("typescript", version.ts_version.as_str())], + ) + .await?; + } + + ensure!( + fs::metadata(&ts_path).await.is_ok(), + "typescript for Vue package installation failed" + ); + *self.typescript_install_path.lock() = Some(ts_path); + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: vue_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?; + *self.typescript_install_path.lock() = Some(ts_path); + Some(server) + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()) + .await + .map(|(mut binary, ts_path)| { + binary.arguments = vec!["--help".into()]; + (binary, ts_path) + })?; + *self.typescript_install_path.lock() = Some(ts_path); + Some(server) + } + + async fn label_for_completion( + &self, + item: &lsp2::CompletionItem, + language: &Arc, + ) -> Option { + use lsp2::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"), + Kind::VARIABLE => grammar.highlight_id_for_name("type"), + Kind::KEYWORD => grammar.highlight_id_for_name("keyword"), + Kind::VALUE => grammar.highlight_id_for_name("tag"), + _ => None, + }?; + + let text = match &item.detail { + Some(detail) => format!("{} {}", item.label, detail), + None => item.label.clone(), + }; + + Some(language2::CodeLabel { + text, + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } +} + +fn vue_server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +type TypescriptPath = PathBuf; +async fn get_cached_server_binary( + container_dir: PathBuf, + node: Arc, +) -> Option<(LanguageServerBinary, TypescriptPath)> { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH); + let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH); + if server_path.exists() && typescript_path.exists() { + Ok(( + LanguageServerBinary { + path: node.binary_path().await?, + arguments: vue_server_binary_arguments(&server_path), + }, + typescript_path, + )) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed2/src/languages/vue/brackets.scm b/crates/zed2/src/languages/vue/brackets.scm new file mode 100644 index 0000000000..2d12b17daa --- /dev/null +++ b/crates/zed2/src/languages/vue/brackets.scm @@ -0,0 +1,2 @@ +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/vue/config.toml b/crates/zed2/src/languages/vue/config.toml new file mode 100644 index 0000000000..c41a667b75 --- /dev/null +++ b/crates/zed2/src/languages/vue/config.toml @@ -0,0 +1,14 @@ +name = "Vue.js" +path_suffixes = ["vue"] +block_comment = [""] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true, not_in = ["string", "comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, +] +word_characters = ["-"] diff --git a/crates/zed2/src/languages/vue/highlights.scm b/crates/zed2/src/languages/vue/highlights.scm new file mode 100644 index 0000000000..1a80c84f68 --- /dev/null +++ b/crates/zed2/src/languages/vue/highlights.scm @@ -0,0 +1,15 @@ +(attribute) @property +(directive_attribute) @property +(quoted_attribute_value) @string +(interpolation) @punctuation.special +(raw_text) @embedded + +((tag_name) @type + (#match? @type "^[A-Z]")) + +((directive_name) @keyword + (#match? @keyword "^v-")) + +(start_tag) @tag +(end_tag) @tag +(self_closing_tag) @tag diff --git a/crates/zed2/src/languages/vue/injections.scm b/crates/zed2/src/languages/vue/injections.scm new file mode 100644 index 0000000000..9084e373f2 --- /dev/null +++ b/crates/zed2/src/languages/vue/injections.scm @@ -0,0 +1,7 @@ +(script_element + (raw_text) @content + (#set! "language" "javascript")) + +(style_element + (raw_text) @content + (#set! "language" "css")) diff --git a/crates/zed2/src/languages/yaml.rs b/crates/zed2/src/languages/yaml.rs new file mode 100644 index 0000000000..338a7a7ade --- /dev/null +++ b/crates/zed2/src/languages/yaml.rs @@ -0,0 +1,142 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use gpui2::AppContext; +use language2::{ + language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate, +}; +use lsp2::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::Value; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + future, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct YamlLspAdapter { + node: Arc, +} + +impl YamlLspAdapter { + pub fn new(node: Arc) -> Self { + YamlLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for YamlLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("yaml-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "yaml" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("yaml-language-server") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("yaml-language-server", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &*self.node).await + } + fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { + let tab_size = all_language_settings(None, cx) + .language(Some("YAML")) + .tab_size; + + future::ready(serde_json::json!({ + "yaml": { + "keyOrdering": false + }, + "[yaml]": { + "editor.tabSize": tab_size, + } + })) + .boxed() + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &dyn NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed2/src/languages/yaml/brackets.scm b/crates/zed2/src/languages/yaml/brackets.scm new file mode 100644 index 0000000000..9e8c9cd93c --- /dev/null +++ b/crates/zed2/src/languages/yaml/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) diff --git a/crates/zed2/src/languages/yaml/config.toml b/crates/zed2/src/languages/yaml/config.toml new file mode 100644 index 0000000000..4e91dd348b --- /dev/null +++ b/crates/zed2/src/languages/yaml/config.toml @@ -0,0 +1,12 @@ +name = "YAML" +path_suffixes = ["yml", "yaml"] +line_comment = "# " +autoclose_before = ",]}" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, +] + +increase_indent_pattern = ":\\s*[|>]?\\s*$" +prettier_parser_name = "yaml" diff --git a/crates/zed2/src/languages/yaml/highlights.scm b/crates/zed2/src/languages/yaml/highlights.scm new file mode 100644 index 0000000000..06081f63cb --- /dev/null +++ b/crates/zed2/src/languages/yaml/highlights.scm @@ -0,0 +1,49 @@ +(boolean_scalar) @boolean +(null_scalar) @constant.builtin + +[ + (double_quote_scalar) + (single_quote_scalar) + (block_scalar) + (string_scalar) +] @string + +(escape_sequence) @string.escape + +[ + (integer_scalar) + (float_scalar) +] @number + +(comment) @comment + +[ + (anchor_name) + (alias_name) + (tag) +] @type + +key: (flow_node (plain_scalar (string_scalar) @property)) + +[ + "," + "-" + ":" + ">" + "?" + "|" +] @punctuation.delimiter + +[ + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "*" + "&" + "---" + "..." +] @punctuation.special \ No newline at end of file diff --git a/crates/zed2/src/languages/yaml/outline.scm b/crates/zed2/src/languages/yaml/outline.scm new file mode 100644 index 0000000000..e85eb1bf8a --- /dev/null +++ b/crates/zed2/src/languages/yaml/outline.scm @@ -0,0 +1 @@ +(block_mapping_pair key: (flow_node (plain_scalar (string_scalar) @name))) @item \ No newline at end of file diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index a4270856b8..82eacd9710 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -45,12 +45,8 @@ use util::{ paths, ResultExt, }; use uuid::Uuid; +use zed2::languages; use zed2::{ensure_only_instance, AppState, Assets, IsOnlyInstance}; -// use zed2::{ -// assets::Assets, -// build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, -// only_instance::{ensure_only_instance, IsOnlyInstance}, -// }; mod open_listener; @@ -117,9 +113,11 @@ fn main() { let copilot_language_server_id = languages.next_language_server_id(); languages.set_executor(cx.executor().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); + let languages = Arc::new(languages); let node_runtime = RealNodeRuntime::new(http.clone()); language2::init(cx); + languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 45a9276b2b..4389f3012a 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,4 +1,5 @@ mod assets; +pub mod languages; mod only_instance; mod open_listener; From c98a811c0bd5a30ebb91e6a4425b0e89f25381d0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 31 Oct 2023 10:40:49 +0200 Subject: [PATCH 5/5] Only process diagnostics if corresponding project is alive Part of https://github.com/zed-industries/zed/pull/3128 that is possible to apply now. --- crates/project2/src/project2.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 5d3b19301c..9b17a88941 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -2986,8 +2986,8 @@ impl Project { let this = this.clone(); move |mut params, mut cx| { let adapter = adapter.clone(); - adapter.process_diagnostics(&mut params); if let Some(this) = this.upgrade() { + adapter.process_diagnostics(&mut params); this.update(&mut cx, |this, cx| { this.update_diagnostics( server_id,