Compare commits
21 Commits
fix-gpui-p
...
ime-pre-ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29611ac1cb | ||
|
|
d12b8c3945 | ||
|
|
356fcec337 | ||
|
|
08123a270a | ||
|
|
6eb8e83411 | ||
|
|
4c51ee7816 | ||
|
|
660cf214c7 | ||
|
|
b2565fadfb | ||
|
|
2cff075c53 | ||
|
|
819bb2663d | ||
|
|
dc141d0f61 | ||
|
|
22cf73acec | ||
|
|
1d46a52c62 | ||
|
|
fda975fb76 | ||
|
|
0f32145ecb | ||
|
|
6fe665ab94 | ||
|
|
279c5ab81f | ||
|
|
99901801f4 | ||
|
|
4dc98026c4 | ||
|
|
c83d1c23d7 | ||
|
|
39a2cdb13f |
@@ -17,10 +17,17 @@ impl RustdocSlashCommand {
|
||||
async fn build_message(
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
crate_name: String,
|
||||
module_path: Vec<String>,
|
||||
) -> Result<String> {
|
||||
let version = "latest";
|
||||
let path = format!(
|
||||
"{crate_name}/{version}/{crate_name}/{module_path}",
|
||||
module_path = module_path.join("/")
|
||||
);
|
||||
|
||||
let mut response = http_client
|
||||
.get(
|
||||
&format!("https://docs.rs/{crate_name}"),
|
||||
&format!("https://docs.rs/{path}"),
|
||||
AsyncBody::default(),
|
||||
true,
|
||||
)
|
||||
@@ -87,14 +94,28 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let crate_name = argument.to_string();
|
||||
let mut path_components = argument.split("::");
|
||||
let crate_name = match path_components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing crate name"))
|
||||
{
|
||||
Ok(crate_name) => crate_name.to_string(),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let crate_name = crate_name.clone();
|
||||
async move { Self::build_message(http_client, crate_name).await }
|
||||
let module_path = module_path.clone();
|
||||
async move { Self::build_message(http_client, crate_name, module_path).await }
|
||||
});
|
||||
|
||||
let crate_name = SharedString::from(crate_name);
|
||||
let module_path = if module_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::from(module_path.join("::")))
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
@@ -107,6 +128,7 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
id,
|
||||
unfold,
|
||||
crate_name: crate_name.clone(),
|
||||
module_path: module_path.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
@@ -121,17 +143,23 @@ struct RustdocPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub crate_name: SharedString,
|
||||
pub module_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl RenderOnce for RustdocPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
let crate_path = self
|
||||
.module_path
|
||||
.map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
|
||||
.unwrap_or(self.crate_name.to_string());
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileRust))
|
||||
.child(Label::new(format!("rustdoc: {}", self.crate_name)))
|
||||
.child(Label::new(format!("rustdoc: {crate_path}")))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ pub struct ChannelStore {
|
||||
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
||||
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
|
||||
client: Arc<Client>,
|
||||
did_subscribe: bool,
|
||||
user_store: Model<UserStore>,
|
||||
_rpc_subscriptions: [Subscription; 2],
|
||||
_watch_connection_status: Task<Option<()>>,
|
||||
@@ -243,6 +244,20 @@ impl ChannelStore {
|
||||
.log_err();
|
||||
}),
|
||||
channel_states: Default::default(),
|
||||
did_subscribe: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self) {
|
||||
if !self.did_subscribe {
|
||||
if self
|
||||
.client
|
||||
.send(proto::SubscribeToChannels {})
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
self.did_subscribe = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,7 +1050,7 @@ impl ChannelStore {
|
||||
|
||||
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
|
||||
cx.notify();
|
||||
|
||||
self.did_subscribe = false;
|
||||
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if wait_for_reconnect {
|
||||
|
||||
@@ -654,6 +654,7 @@ pub struct ChannelsForUser {
|
||||
pub channel_memberships: Vec<channel_member::Model>,
|
||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||
pub hosted_projects: Vec<proto::HostedProject>,
|
||||
pub invited_channels: Vec<Channel>,
|
||||
|
||||
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
||||
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
|
||||
|
||||
@@ -416,7 +416,9 @@ impl Database {
|
||||
user_id: UserId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<MembershipUpdated> {
|
||||
let new_channels = self.get_user_channels(user_id, Some(channel), tx).await?;
|
||||
let new_channels = self
|
||||
.get_user_channels(user_id, Some(channel), false, tx)
|
||||
.await?;
|
||||
let removed_channels = self
|
||||
.get_channel_descendants_excluding_self([channel], tx)
|
||||
.await?
|
||||
@@ -481,44 +483,10 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channel invites for the user with the given ID.
|
||||
pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut role_for_channel: HashMap<ChannelId, ChannelRole> = HashMap::default();
|
||||
|
||||
let channel_invites = channel_member::Entity::find()
|
||||
.filter(
|
||||
channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(false)),
|
||||
)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
for invite in channel_invites {
|
||||
role_for_channel.insert(invite.channel_id, invite.role);
|
||||
}
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(role_for_channel.keys().copied()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let channels = channels.into_iter().map(Channel::from_model).collect();
|
||||
|
||||
Ok(channels)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = tx;
|
||||
|
||||
self.get_user_channels(user_id, None, &tx).await
|
||||
})
|
||||
.await
|
||||
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID that are descendants
|
||||
@@ -527,25 +495,37 @@ impl Database {
|
||||
&self,
|
||||
user_id: UserId,
|
||||
ancestor_channel: Option<&channel::Model>,
|
||||
include_invites: bool,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<ChannelsForUser> {
|
||||
let mut filter = channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(true));
|
||||
|
||||
let mut filter = channel_member::Column::UserId.eq(user_id);
|
||||
if !include_invites {
|
||||
filter = filter.and(channel_member::Column::Accepted.eq(true))
|
||||
}
|
||||
if let Some(ancestor) = ancestor_channel {
|
||||
filter = filter.and(channel_member::Column::ChannelId.eq(ancestor.root_id()));
|
||||
}
|
||||
|
||||
let channel_memberships = channel_member::Entity::find()
|
||||
let mut channels = Vec::<channel::Model>::new();
|
||||
let mut invited_channels = Vec::<Channel>::new();
|
||||
let mut channel_memberships = Vec::<channel_member::Model>::new();
|
||||
let mut rows = channel_member::Entity::find()
|
||||
.filter(filter)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(channel_memberships.iter().map(|m| m.channel_id)))
|
||||
.all(tx)
|
||||
.inner_join(channel::Entity)
|
||||
.select_also(channel::Entity)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
if let (membership, Some(channel)) = row? {
|
||||
if membership.accepted {
|
||||
channel_memberships.push(membership);
|
||||
channels.push(channel);
|
||||
} else {
|
||||
invited_channels.push(Channel::from_model(channel));
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let mut descendants = self
|
||||
.get_channel_descendants_excluding_self(channels.iter(), tx)
|
||||
@@ -643,6 +623,7 @@ impl Database {
|
||||
Ok(ChannelsForUser {
|
||||
channel_memberships,
|
||||
channels,
|
||||
invited_channels,
|
||||
hosted_projects,
|
||||
channel_participants,
|
||||
latest_buffer_versions,
|
||||
|
||||
@@ -176,23 +176,23 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let user_2_invites = db
|
||||
.get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
|
||||
.get_channels_for_user(user_2)
|
||||
.await
|
||||
.unwrap()
|
||||
.invited_channels
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
|
||||
|
||||
let user_3_invites = db
|
||||
.get_channel_invites_for_user(user_3) // -> [channel_1_1]
|
||||
.get_channels_for_user(user_3)
|
||||
.await
|
||||
.unwrap()
|
||||
.invited_channels
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||
|
||||
let (mut members, _) = db
|
||||
|
||||
@@ -557,6 +557,7 @@ impl Server {
|
||||
.add_request_handler(user_handler(request_contact))
|
||||
.add_request_handler(user_handler(remove_contact))
|
||||
.add_request_handler(user_handler(respond_to_contact_request))
|
||||
.add_message_handler(subscribe_to_channels)
|
||||
.add_request_handler(user_handler(create_channel))
|
||||
.add_request_handler(user_handler(delete_channel))
|
||||
.add_request_handler(user_handler(invite_channel_member))
|
||||
@@ -1105,34 +1106,25 @@ impl Server {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let (contacts, channels_for_user, channel_invites, dev_server_projects) =
|
||||
future::try_join4(
|
||||
self.app_state.db.get_contacts(user.id),
|
||||
self.app_state.db.get_channels_for_user(user.id),
|
||||
self.app_state.db.get_channel_invites_for_user(user.id),
|
||||
self.app_state.db.dev_server_projects_update(user.id),
|
||||
)
|
||||
.await?;
|
||||
let (contacts, dev_server_projects) = future::try_join(
|
||||
self.app_state.db.get_contacts(user.id),
|
||||
self.app_state.db.dev_server_projects_update(user.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut pool = self.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user.id, user.admin, zed_version);
|
||||
for membership in &channels_for_user.channel_memberships {
|
||||
pool.subscribe_to_channel(user.id, membership.channel_id, membership.role)
|
||||
}
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_initial_contacts_update(contacts, &pool),
|
||||
)?;
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_update_user_channels(&channels_for_user),
|
||||
)?;
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_channels_update(channels_for_user, channel_invites),
|
||||
)?;
|
||||
}
|
||||
|
||||
if should_auto_subscribe_to_channels(zed_version) {
|
||||
subscribe_user_to_channels(user.id, session).await?;
|
||||
}
|
||||
|
||||
send_dev_server_projects_update(user.id, dev_server_projects, session).await;
|
||||
|
||||
if let Some(incoming_call) =
|
||||
@@ -3399,6 +3391,36 @@ async fn remove_contact(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
|
||||
version.0.minor() < 139
|
||||
}
|
||||
|
||||
async fn subscribe_to_channels(_: proto::SubscribeToChannels, session: Session) -> Result<()> {
|
||||
subscribe_user_to_channels(
|
||||
session.user_id().ok_or_else(|| anyhow!("must be a user"))?,
|
||||
&session,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_user_to_channels(user_id: UserId, session: &Session) -> Result<(), Error> {
|
||||
let channels_for_user = session.db().await.get_channels_for_user(user_id).await?;
|
||||
let mut pool = session.connection_pool().await;
|
||||
for membership in &channels_for_user.channel_memberships {
|
||||
pool.subscribe_to_channel(user_id, membership.channel_id, membership.role)
|
||||
}
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
build_update_user_channels(&channels_for_user),
|
||||
)?;
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
build_channels_update(channels_for_user),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new channel.
|
||||
async fn create_channel(
|
||||
request: proto::CreateChannel,
|
||||
@@ -5034,7 +5056,7 @@ fn notify_membership_updated(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut update = build_channels_update(result.new_channels, vec![]);
|
||||
let mut update = build_channels_update(result.new_channels);
|
||||
update.delete_channels = result
|
||||
.removed_channels
|
||||
.into_iter()
|
||||
@@ -5064,10 +5086,7 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
|
||||
}
|
||||
}
|
||||
|
||||
fn build_channels_update(
|
||||
channels: ChannelsForUser,
|
||||
channel_invites: Vec<db::Channel>,
|
||||
) -> proto::UpdateChannels {
|
||||
fn build_channels_update(channels: ChannelsForUser) -> proto::UpdateChannels {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
|
||||
for channel in channels.channels {
|
||||
@@ -5086,7 +5105,7 @@ fn build_channels_update(
|
||||
});
|
||||
}
|
||||
|
||||
for channel in channel_invites {
|
||||
for channel in channels.invited_channels {
|
||||
update.channel_invitations.push(channel.to_proto());
|
||||
}
|
||||
|
||||
|
||||
@@ -2161,6 +2161,9 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
||||
self.channel_store.update(cx, |channel_store, _| {
|
||||
channel_store.initialize();
|
||||
});
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(list(self.list_state.clone()).size_full())
|
||||
|
||||
@@ -11574,6 +11574,7 @@ impl ViewInputHandler for Editor {
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
|
||||
dbg!("unmark text");
|
||||
self.clear_highlights::<InputComposition>(cx);
|
||||
self.ime_transaction.take();
|
||||
}
|
||||
@@ -11584,6 +11585,7 @@ impl ViewInputHandler for Editor {
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
dbg!((&range_utf16, text));
|
||||
if !self.input_enabled {
|
||||
cx.emit(EditorEvent::InputIgnored { text: text.into() });
|
||||
return;
|
||||
@@ -11646,6 +11648,7 @@ impl ViewInputHandler for Editor {
|
||||
new_selected_range_utf16: Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
dbg!((&range_utf16, text, &new_selected_range_utf16));
|
||||
if !self.input_enabled {
|
||||
cx.emit(EditorEvent::InputIgnored { text: text.into() });
|
||||
return;
|
||||
|
||||
@@ -311,6 +311,7 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
||||
decl.register()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum ImeInput {
|
||||
InsertText(String, Option<Range<usize>>),
|
||||
@@ -1219,16 +1220,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
|
||||
let keydown = event.keystroke.clone();
|
||||
let fn_modifier = keydown.modifiers.function;
|
||||
// Ignore events from held-down keys after some of the initially-pressed keys
|
||||
// were released.
|
||||
if event.is_held {
|
||||
if lock.last_fresh_keydown.as_ref() != Some(&keydown) {
|
||||
return YES;
|
||||
}
|
||||
} else {
|
||||
lock.last_fresh_keydown = Some(keydown.clone());
|
||||
}
|
||||
lock.input_during_keydown = Some(SmallVec::new());
|
||||
|
||||
drop(lock);
|
||||
|
||||
// Send the event to the input context for IME handling, unless the `fn` modifier is
|
||||
@@ -1240,11 +1232,32 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
let _: BOOL = msg_send![input_context, handleEvent: native_event];
|
||||
}
|
||||
}
|
||||
let mut lock = window_state.lock();
|
||||
|
||||
println!("******************************************");
|
||||
///// THE PROBLEM AREA ///////
|
||||
// Ignore events from held-down keys after some of the initially-pressed keys
|
||||
// were released.
|
||||
if dbg!(event.is_held) {
|
||||
if dbg!(lock.last_fresh_keydown.as_ref()) != Some(&keydown) {
|
||||
return YES;
|
||||
}
|
||||
} else {
|
||||
//????
|
||||
lock.last_fresh_keydown = Some(dbg!(keydown.clone()));
|
||||
}
|
||||
lock.input_during_keydown = Some(SmallVec::new());
|
||||
///// THE PROBLEM AREA ///////
|
||||
|
||||
// Send the event to the input context for IME handling, unless the `fn` modifier is
|
||||
// being pressed.
|
||||
// this will call back into `insert_text`, etc.
|
||||
|
||||
let mut handled = false;
|
||||
let mut lock = window_state.lock();
|
||||
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
|
||||
|
||||
let mut input_during_keydown = lock.input_during_keydown.take().unwrap();
|
||||
dbg!(&input_during_keydown);
|
||||
let mut callback = lock.event_callback.take();
|
||||
drop(lock);
|
||||
|
||||
@@ -1260,7 +1273,10 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
dbg!(is_composing, &last_ime);
|
||||
|
||||
if let Some(ime) = last_ime {
|
||||
// Problem area
|
||||
if let ImeInput::InsertText(text, _) = &ime {
|
||||
if !is_composing {
|
||||
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
|
||||
@@ -1275,6 +1291,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
handled = true;
|
||||
send_to_input_handler(this, ime);
|
||||
}
|
||||
// Problem area
|
||||
} else if !is_composing {
|
||||
let is_held = event.is_held;
|
||||
|
||||
@@ -1655,21 +1672,27 @@ extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
|
||||
}
|
||||
|
||||
extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some() as BOOL
|
||||
dbg!("has marked range");
|
||||
with_input_handler(
|
||||
this,
|
||||
|input_handler| dbg!(input_handler.marked_text_range()),
|
||||
)
|
||||
.flatten()
|
||||
.is_some() as BOOL
|
||||
}
|
||||
|
||||
extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
|
||||
dbg!("querying marked range");
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.map_or(NSRange::invalid(), |range| range.into())
|
||||
.map_or(NSRange::invalid(), |range| dbg!(range).into())
|
||||
}
|
||||
|
||||
extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
|
||||
dbg!("asking for sel range");
|
||||
with_input_handler(this, |input_handler| input_handler.selected_text_range())
|
||||
.flatten()
|
||||
.map_or(NSRange::invalid(), |range| range.into())
|
||||
.map_or(NSRange::invalid(), |range| dbg!(range).into())
|
||||
}
|
||||
|
||||
extern "C" fn first_rect_for_character_range(
|
||||
|
||||
@@ -3153,10 +3153,7 @@ impl BufferSnapshot {
|
||||
range: Range<Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<IndentGuide> {
|
||||
fn tab_size_for_row(this: &BufferSnapshot, row: BufferRow, cx: &AppContext) -> u32 {
|
||||
let language = this.language_at(Point::new(row, 0));
|
||||
language_settings(language, None, cx).tab_size.get() as u32
|
||||
}
|
||||
let tab_size = language_settings(self.language(), None, cx).tab_size.get() as u32;
|
||||
|
||||
let start_row = range.start.to_point(self).row;
|
||||
let end_row = range.end.to_point(self).row;
|
||||
@@ -3167,9 +3164,6 @@ impl BufferSnapshot {
|
||||
let mut result_vec = Vec::new();
|
||||
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
|
||||
|
||||
// TODO: This should be calculated for every row but it is pretty expensive
|
||||
let tab_size = tab_size_for_row(self, start_row, cx);
|
||||
|
||||
while let Some((first_row, mut line_indent)) = row_indents.next() {
|
||||
let current_depth = indent_stack.len() as u32;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
; Properties
|
||||
|
||||
(property_identifier) @property
|
||||
(shorthand_property_identifier) @property
|
||||
(shorthand_property_identifier_pattern) @property
|
||||
|
||||
; Function and method calls
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (id, _))| !self.is_current_workspace(*id, cx))
|
||||
.map(|(id, (_, location))| {
|
||||
let combined_string = match location {
|
||||
SerializedWorkspaceLocation::Local(paths, _) => paths
|
||||
@@ -393,8 +394,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (workspace_id, location) = &self.workspaces[hit.candidate_id];
|
||||
let is_current_workspace = self.is_current_workspace(*workspace_id, cx);
|
||||
let (_, location) = self.workspaces.get(hit.candidate_id)?;
|
||||
|
||||
let is_remote = matches!(location, SerializedWorkspaceLocation::DevServer(_));
|
||||
let dev_server_status =
|
||||
@@ -487,7 +487,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
highlighted.render(cx)
|
||||
}),
|
||||
)
|
||||
.when(!is_current_workspace, |el| {
|
||||
.map(|el| {
|
||||
let delete_button = div()
|
||||
.child(
|
||||
IconButton::new("delete", IconName::Close)
|
||||
|
||||
@@ -159,6 +159,7 @@ message Envelope {
|
||||
SetChannelMemberRole set_channel_member_role = 123;
|
||||
RenameChannel rename_channel = 124;
|
||||
RenameChannelResponse rename_channel_response = 125;
|
||||
SubscribeToChannels subscribe_to_channels = 207; // current max
|
||||
|
||||
JoinChannelBuffer join_channel_buffer = 126;
|
||||
JoinChannelBufferResponse join_channel_buffer_response = 127;
|
||||
@@ -250,7 +251,7 @@ message Envelope {
|
||||
TaskContextForLocation task_context_for_location = 203;
|
||||
TaskContext task_context = 204;
|
||||
TaskTemplatesResponse task_templates_response = 205;
|
||||
TaskTemplates task_templates = 206; // Current max
|
||||
TaskTemplates task_templates = 206;
|
||||
}
|
||||
|
||||
reserved 158 to 161;
|
||||
@@ -1297,6 +1298,8 @@ message ChannelMember {
|
||||
}
|
||||
}
|
||||
|
||||
message SubscribeToChannels {}
|
||||
|
||||
message CreateChannel {
|
||||
string name = 1;
|
||||
optional uint64 parent_id = 2;
|
||||
|
||||
@@ -277,6 +277,7 @@ messages!(
|
||||
(ShareProjectResponse, Foreground),
|
||||
(ShowContacts, Foreground),
|
||||
(StartLanguageServer, Foreground),
|
||||
(SubscribeToChannels, Foreground),
|
||||
(SynchronizeBuffers, Foreground),
|
||||
(SynchronizeBuffersResponse, Foreground),
|
||||
(TaskContextForLocation, Background),
|
||||
|
||||
75
crates/rustdoc_to_markdown/src/html_element.rs
Normal file
75
crates/rustdoc_to_markdown/src/html_element.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use html5ever::Attribute;
|
||||
|
||||
/// Returns a [`HashSet`] containing the HTML elements that are inline by default.
|
||||
///
|
||||
/// [MDN: List of "inline" elements](https://yari-demos.prod.mdn.mozit.cloud/en-US/docs/Web/HTML/Inline_elements)
|
||||
fn inline_elements() -> &'static HashSet<&'static str> {
|
||||
static INLINE_ELEMENTS: OnceLock<HashSet<&str>> = OnceLock::new();
|
||||
&INLINE_ELEMENTS.get_or_init(|| {
|
||||
HashSet::from_iter([
|
||||
"a", "abbr", "acronym", "audio", "b", "bdi", "bdo", "big", "br", "button", "canvas",
|
||||
"cite", "code", "data", "datalist", "del", "dfn", "em", "embed", "i", "iframe", "img",
|
||||
"input", "ins", "kbd", "label", "map", "mark", "meter", "noscript", "object", "output",
|
||||
"picture", "progress", "q", "ruby", "s", "samp", "script", "select", "slot", "small",
|
||||
"span", "strong", "sub", "sup", "svg", "template", "textarea", "time", "tt", "u",
|
||||
"var", "video", "wbr",
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HtmlElement {
|
||||
pub(crate) tag: String,
|
||||
pub(crate) attrs: RefCell<Vec<Attribute>>,
|
||||
}
|
||||
|
||||
impl HtmlElement {
|
||||
/// Returns whether this [`HtmlElement`] is an inline element.
|
||||
pub fn is_inline(&self) -> bool {
|
||||
inline_elements().contains(self.tag.as_str())
|
||||
}
|
||||
|
||||
/// Returns the attribute with the specified name.
|
||||
pub fn attr(&self, name: &str) -> Option<String> {
|
||||
self.attrs
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|attr| attr.name.local.to_string() == name)
|
||||
.map(|attr| attr.value.to_string())
|
||||
}
|
||||
|
||||
/// Returns the list of classes on this [`HtmlElement`].
|
||||
pub fn classes(&self) -> Vec<String> {
|
||||
self.attrs
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|attr| attr.name.local.to_string() == "class")
|
||||
.map(|attr| {
|
||||
attr.value
|
||||
.split(' ')
|
||||
.map(|class| class.trim().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns whether this [`HtmlElement`] has the specified class.
|
||||
pub fn has_class(&self, class: &str) -> bool {
|
||||
self.has_any_classes(&[class])
|
||||
}
|
||||
|
||||
/// Returns whether this [`HtmlElement`] has any of the specified classes.
|
||||
pub fn has_any_classes(&self, classes: &[&str]) -> bool {
|
||||
self.attrs.borrow().iter().any(|attr| {
|
||||
attr.name.local.to_string() == "class"
|
||||
&& attr
|
||||
.value
|
||||
.split(' ')
|
||||
.any(|class| classes.contains(&class.trim()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use html5ever::Attribute;
|
||||
use markup5ever_rcdom::{Handle, NodeData};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::html_element::HtmlElement;
|
||||
|
||||
fn empty_line_regex() -> &'static Regex {
|
||||
static REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
REGEX.get_or_init(|| Regex::new(r"^\s*$").unwrap())
|
||||
@@ -17,11 +17,7 @@ fn more_than_three_newlines_regex() -> &'static Regex {
|
||||
REGEX.get_or_init(|| Regex::new(r"\n{3,}").unwrap())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct HtmlElement {
|
||||
tag: String,
|
||||
attrs: RefCell<Vec<Attribute>>,
|
||||
}
|
||||
const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name";
|
||||
|
||||
enum StartTagOutcome {
|
||||
Continue,
|
||||
@@ -135,6 +131,16 @@ impl MarkdownWriter {
|
||||
}
|
||||
|
||||
fn start_tag(&mut self, tag: &HtmlElement) -> StartTagOutcome {
|
||||
if tag.is_inline() && self.is_inside("p") {
|
||||
if let Some(parent) = self.current_element_stack.iter().last() {
|
||||
if !parent.is_inline() {
|
||||
if !(self.markdown.ends_with(' ') || self.markdown.ends_with('\n')) {
|
||||
self.push_str(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match tag.tag.as_str() {
|
||||
"head" | "script" | "nav" => return StartTagOutcome::Skip,
|
||||
"h1" => self.push_str("\n\n# "),
|
||||
@@ -143,24 +149,17 @@ impl MarkdownWriter {
|
||||
"h4" => self.push_str("\n\n#### "),
|
||||
"h5" => self.push_str("\n\n##### "),
|
||||
"h6" => self.push_str("\n\n###### "),
|
||||
"p" => self.push_blank_line(),
|
||||
"strong" => self.push_str("**"),
|
||||
"em" => self.push_str("_"),
|
||||
"code" => {
|
||||
if !self.is_inside("pre") {
|
||||
self.push_str("`")
|
||||
self.push_str("`");
|
||||
}
|
||||
}
|
||||
"pre" => {
|
||||
let attrs = tag.attrs.borrow();
|
||||
let classes = attrs
|
||||
.iter()
|
||||
.find(|attr| attr.name.local.to_string() == "class")
|
||||
.map(|attr| {
|
||||
attr.value
|
||||
.split(' ')
|
||||
.map(|class| class.trim())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let is_rust = classes.iter().any(|class| class == &"rust");
|
||||
let classes = tag.classes();
|
||||
let is_rust = classes.iter().any(|class| class == "rust");
|
||||
let language = is_rust
|
||||
.then(|| "rs")
|
||||
.or_else(|| {
|
||||
@@ -174,7 +173,7 @@ impl MarkdownWriter {
|
||||
})
|
||||
.unwrap_or("");
|
||||
|
||||
self.push_str(&format!("\n\n```{language}\n"))
|
||||
self.push_str(&format!("\n\n```{language}\n"));
|
||||
}
|
||||
"ul" | "ol" => self.push_newline(),
|
||||
"li" => self.push_str("- "),
|
||||
@@ -198,29 +197,23 @@ impl MarkdownWriter {
|
||||
self.push_str("| ");
|
||||
}
|
||||
"summary" => {
|
||||
if tag.attrs.borrow().iter().any(|attr| {
|
||||
attr.name.local.to_string() == "class" && attr.value.to_string() == "hideme"
|
||||
}) {
|
||||
if tag.has_class("hideme") {
|
||||
return StartTagOutcome::Skip;
|
||||
}
|
||||
}
|
||||
"button" => {
|
||||
if tag.attr("id").as_deref() == Some("copy-path") {
|
||||
return StartTagOutcome::Skip;
|
||||
}
|
||||
}
|
||||
"div" | "span" => {
|
||||
let classes_to_skip = ["nav-container", "sidebar-elems", "out-of-band"];
|
||||
|
||||
if tag.attrs.borrow().iter().any(|attr| {
|
||||
attr.name.local.to_string() == "class"
|
||||
&& attr
|
||||
.value
|
||||
.split(' ')
|
||||
.any(|class| classes_to_skip.contains(&class.trim()))
|
||||
}) {
|
||||
if tag.has_any_classes(&classes_to_skip) {
|
||||
return StartTagOutcome::Skip;
|
||||
}
|
||||
|
||||
if tag.attrs.borrow().iter().any(|attr| {
|
||||
attr.name.local.to_string() == "class" && attr.value.to_string() == "item-name"
|
||||
}) {
|
||||
self.push_str("`");
|
||||
if self.is_inside_item_name() && tag.has_class("stab") {
|
||||
self.push_str(" [");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -232,9 +225,11 @@ impl MarkdownWriter {
|
||||
fn end_tag(&mut self, tag: &HtmlElement) {
|
||||
match tag.tag.as_str() {
|
||||
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => self.push_str("\n\n"),
|
||||
"strong" => self.push_str("**"),
|
||||
"em" => self.push_str("_"),
|
||||
"code" => {
|
||||
if !self.is_inside("pre") {
|
||||
self.push_str("`")
|
||||
self.push_str("`");
|
||||
}
|
||||
}
|
||||
"pre" => self.push_str("\n```\n"),
|
||||
@@ -258,11 +253,13 @@ impl MarkdownWriter {
|
||||
"table" => {
|
||||
self.current_table_columns = 0;
|
||||
}
|
||||
"div" => {
|
||||
if tag.attrs.borrow().iter().any(|attr| {
|
||||
attr.name.local.to_string() == "class" && attr.value.to_string() == "item-name"
|
||||
}) {
|
||||
self.push_str("`: ");
|
||||
"div" | "span" => {
|
||||
if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) {
|
||||
self.push_str(": ");
|
||||
}
|
||||
|
||||
if self.is_inside_item_name() && tag.has_class("stab") {
|
||||
self.push_str("]");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -275,9 +272,25 @@ impl MarkdownWriter {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let trimmed_text = text.trim_matches(|char| char == '\n' || char == '\r' || char == '§');
|
||||
self.push_str(trimmed_text);
|
||||
let text = text
|
||||
.trim_matches(|char| char == '\n' || char == '\r' || char == '§')
|
||||
.replace('\n', " ");
|
||||
|
||||
if self.is_inside_item_name() && !self.is_inside("span") && !self.is_inside("code") {
|
||||
self.push_str(&format!("`{text}`"));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.push_str(&text);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether we're currently inside of an `.item-name` element, which
|
||||
/// rustdoc uses to display Rust items in a list.
|
||||
fn is_inside_item_name(&self) -> bool {
|
||||
self.current_element_stack
|
||||
.iter()
|
||||
.any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod html_element;
|
||||
mod markdown_writer;
|
||||
|
||||
use std::io::Read;
|
||||
@@ -44,6 +45,112 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_main_heading_buttons_get_removed() {
|
||||
let html = indoc! {r##"
|
||||
<div class="main-heading">
|
||||
<h1>Crate <a class="mod" href="#">serde</a><button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1>
|
||||
<span class="out-of-band">
|
||||
<a class="src" href="../src/serde/lib.rs.html#1-340">source</a> · <button id="toggle-all-docs" title="collapse all docs">[<span>−</span>]</button>
|
||||
</span>
|
||||
</div>
|
||||
"##};
|
||||
let expected = indoc! {"
|
||||
# Crate serde
|
||||
"}
|
||||
.trim();
|
||||
|
||||
assert_eq!(
|
||||
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
|
||||
expected
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_paragraph() {
|
||||
let html = indoc! {r#"
|
||||
<p>In particular, the last point is what sets <code>axum</code> apart from other frameworks.
|
||||
<code>axum</code> doesn’t have its own middleware system but instead uses
|
||||
<a href="https://docs.rs/tower-service/0.3.2/x86_64-unknown-linux-gnu/tower_service/trait.Service.html" title="trait tower_service::Service"><code>tower::Service</code></a>. This means <code>axum</code> gets timeouts, tracing, compression,
|
||||
authorization, and more, for free. It also enables you to share middleware with
|
||||
applications written using <a href="http://crates.io/crates/hyper"><code>hyper</code></a> or <a href="http://crates.io/crates/tonic"><code>tonic</code></a>.</p>
|
||||
"#};
|
||||
let expected = indoc! {"
|
||||
In particular, the last point is what sets `axum` apart from other frameworks. `axum` doesn’t have its own middleware system but instead uses `tower::Service`. This means `axum` gets timeouts, tracing, compression, authorization, and more, for free. It also enables you to share middleware with applications written using `hyper` or `tonic`.
|
||||
"}
|
||||
.trim();
|
||||
|
||||
assert_eq!(
|
||||
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
|
||||
expected
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_paragraphs() {
|
||||
let html = indoc! {r##"
|
||||
<h2 id="serde"><a class="doc-anchor" href="#serde">§</a>Serde</h2>
|
||||
<p>Serde is a framework for <em><strong>ser</strong></em>ializing and <em><strong>de</strong></em>serializing Rust data
|
||||
structures efficiently and generically.</p>
|
||||
<p>The Serde ecosystem consists of data structures that know how to serialize
|
||||
and deserialize themselves along with data formats that know how to
|
||||
serialize and deserialize other things. Serde provides the layer by which
|
||||
these two groups interact with each other, allowing any supported data
|
||||
structure to be serialized and deserialized using any supported data format.</p>
|
||||
<p>See the Serde website <a href="https://serde.rs/">https://serde.rs/</a> for additional documentation and
|
||||
usage examples.</p>
|
||||
<h3 id="design"><a class="doc-anchor" href="#design">§</a>Design</h3>
|
||||
<p>Where many other languages rely on runtime reflection for serializing data,
|
||||
Serde is instead built on Rust’s powerful trait system. A data structure
|
||||
that knows how to serialize and deserialize itself is one that implements
|
||||
Serde’s <code>Serialize</code> and <code>Deserialize</code> traits (or uses Serde’s derive
|
||||
attribute to automatically generate implementations at compile time). This
|
||||
avoids any overhead of reflection or runtime type information. In fact in
|
||||
many situations the interaction between data structure and data format can
|
||||
be completely optimized away by the Rust compiler, leaving Serde
|
||||
serialization to perform the same speed as a handwritten serializer for the
|
||||
specific selection of data structure and data format.</p>
|
||||
"##};
|
||||
let expected = indoc! {"
|
||||
## Serde
|
||||
|
||||
Serde is a framework for _**ser**_ializing and _**de**_serializing Rust data structures efficiently and generically.
|
||||
|
||||
The Serde ecosystem consists of data structures that know how to serialize and deserialize themselves along with data formats that know how to serialize and deserialize other things. Serde provides the layer by which these two groups interact with each other, allowing any supported data structure to be serialized and deserialized using any supported data format.
|
||||
|
||||
See the Serde website https://serde.rs/ for additional documentation and usage examples.
|
||||
|
||||
### Design
|
||||
|
||||
Where many other languages rely on runtime reflection for serializing data, Serde is instead built on Rust’s powerful trait system. A data structure that knows how to serialize and deserialize itself is one that implements Serde’s `Serialize` and `Deserialize` traits (or uses Serde’s derive attribute to automatically generate implementations at compile time). This avoids any overhead of reflection or runtime type information. In fact in many situations the interaction between data structure and data format can be completely optimized away by the Rust compiler, leaving Serde serialization to perform the same speed as a handwritten serializer for the specific selection of data structure and data format.
|
||||
"}
|
||||
.trim();
|
||||
|
||||
assert_eq!(
|
||||
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
|
||||
expected
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_styled_text() {
|
||||
let html = indoc! {r#"
|
||||
<p>This text is <strong>bolded</strong>.</p>
|
||||
<p>This text is <em>italicized</em>.</p>
|
||||
"#};
|
||||
let expected = indoc! {"
|
||||
This text is **bolded**.
|
||||
|
||||
This text is _italicized_.
|
||||
"}
|
||||
.trim();
|
||||
|
||||
assert_eq!(
|
||||
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
|
||||
expected
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rust_code_block() {
|
||||
let html = indoc! {r#"
|
||||
@@ -118,6 +225,42 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_item_table() {
|
||||
let html = indoc! {r##"
|
||||
<h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2>
|
||||
<ul class="item-table">
|
||||
<li><div class="item-name"><a class="struct" href="struct.Error.html" title="struct axum::Error">Error</a></div><div class="desc docblock-short">Errors that can happen when using axum.</div></li>
|
||||
<li><div class="item-name"><a class="struct" href="struct.Extension.html" title="struct axum::Extension">Extension</a></div><div class="desc docblock-short">Extractor and response for extensions.</div></li>
|
||||
<li><div class="item-name"><a class="struct" href="struct.Form.html" title="struct axum::Form">Form</a><span class="stab portability" title="Available on crate feature `form` only"><code>form</code></span></div><div class="desc docblock-short">URL encoded extractor and response.</div></li>
|
||||
<li><div class="item-name"><a class="struct" href="struct.Json.html" title="struct axum::Json">Json</a><span class="stab portability" title="Available on crate feature `json` only"><code>json</code></span></div><div class="desc docblock-short">JSON Extractor / Response.</div></li>
|
||||
<li><div class="item-name"><a class="struct" href="struct.Router.html" title="struct axum::Router">Router</a></div><div class="desc docblock-short">The router type for composing handlers and services.</div></li></ul>
|
||||
<h2 id="functions" class="section-header">Functions<a href="#functions" class="anchor">§</a></h2>
|
||||
<ul class="item-table">
|
||||
<li><div class="item-name"><a class="fn" href="fn.serve.html" title="fn axum::serve">serve</a><span class="stab portability" title="Available on crate feature `tokio` and (crate features `http1` or `http2`) only"><code>tokio</code> and (<code>http1</code> or <code>http2</code>)</span></div><div class="desc docblock-short">Serve the service with the supplied listener.</div></li>
|
||||
</ul>
|
||||
"##};
|
||||
let expected = indoc! {r#"
|
||||
## Structs
|
||||
|
||||
- `Error`: Errors that can happen when using axum.
|
||||
- `Extension`: Extractor and response for extensions.
|
||||
- `Form` [`form`]: URL encoded extractor and response.
|
||||
- `Json` [`json`]: JSON Extractor / Response.
|
||||
- `Router`: The router type for composing handlers and services.
|
||||
|
||||
## Functions
|
||||
|
||||
- `serve` [`tokio` and (`http1` or `http2`)]: Serve the service with the supplied listener.
|
||||
"#}
|
||||
.trim();
|
||||
|
||||
assert_eq!(
|
||||
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
|
||||
expected
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table() {
|
||||
let html = indoc! {r##"
|
||||
@@ -144,8 +287,9 @@ mod tests {
|
||||
let expected = indoc! {r#"
|
||||
## Feature flags
|
||||
|
||||
axum uses a set of feature flags to reduce the amount of compiled and
|
||||
optional dependencies.The following optional features are available:
|
||||
axum uses a set of feature flags to reduce the amount of compiled and optional dependencies.
|
||||
|
||||
The following optional features are available:
|
||||
|
||||
| Name | Description | Default? |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -78,8 +78,6 @@ pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
|
||||
|
||||
const GIT_STATUS_UPDATE_BATCH_SIZE: usize = 1024;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub struct WorktreeId(usize);
|
||||
|
||||
@@ -4293,7 +4291,7 @@ impl BackgroundScanner {
|
||||
async fn update_git_repositories(&self, dot_git_paths: Vec<PathBuf>) {
|
||||
log::debug!("reloading repositories: {dot_git_paths:?}");
|
||||
|
||||
let (update_job_tx, update_job_rx) = channel::unbounded();
|
||||
let mut repo_updates = Vec::new();
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
let scan_id = state.snapshot.scan_id;
|
||||
@@ -4308,7 +4306,7 @@ impl BackgroundScanner {
|
||||
.then(|| (*entry_id, repo.clone()))
|
||||
});
|
||||
|
||||
let (work_dir, repository) = match existing_repository_entry {
|
||||
let (work_directory, repository) = match existing_repository_entry {
|
||||
None => {
|
||||
match state.build_git_repository(dot_git_dir.into(), self.fs.as_ref()) {
|
||||
Some(output) => output,
|
||||
@@ -4327,7 +4325,6 @@ impl BackgroundScanner {
|
||||
continue;
|
||||
};
|
||||
|
||||
log::info!("reload git repository {dot_git_dir:?}");
|
||||
let repo = &repository.repo_ptr;
|
||||
let branch = repo.branch_name();
|
||||
repo.reload_index();
|
||||
@@ -4345,41 +4342,16 @@ impl BackgroundScanner {
|
||||
}
|
||||
};
|
||||
|
||||
let statuses = repository
|
||||
.statuses(Path::new(""))
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let entries = state.snapshot.entries_by_path.clone();
|
||||
let location_in_repo = state
|
||||
.snapshot
|
||||
.repository_entries
|
||||
.get(&work_dir)
|
||||
.and_then(|repo| repo.location_in_repo.clone());
|
||||
let mut files =
|
||||
state
|
||||
repo_updates.push(UpdateGitStatusesJob {
|
||||
location_in_repo: state
|
||||
.snapshot
|
||||
.traverse_from_path(true, false, false, work_dir.0.as_ref());
|
||||
let mut start_path = work_dir.0.clone();
|
||||
while start_path.starts_with(&work_dir.0) {
|
||||
files.advance_by(GIT_STATUS_UPDATE_BATCH_SIZE);
|
||||
let end_path = files.entry().map(|e| e.path.clone());
|
||||
smol::block_on(update_job_tx.send(UpdateGitStatusesJob {
|
||||
start_path: start_path.clone(),
|
||||
end_path: end_path.clone(),
|
||||
entries: entries.clone(),
|
||||
location_in_repo: location_in_repo.clone(),
|
||||
containing_repository: ScanJobContainingRepository {
|
||||
work_directory: work_dir.clone(),
|
||||
statuses: statuses.clone(),
|
||||
},
|
||||
}))
|
||||
.unwrap();
|
||||
if let Some(end_path) = end_path {
|
||||
start_path = end_path;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
.repository_entries
|
||||
.get(&work_directory)
|
||||
.and_then(|repo| repo.location_in_repo.clone())
|
||||
.clone(),
|
||||
work_directory,
|
||||
repository,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove any git repositories whose .git entry no longer exists.
|
||||
@@ -4414,87 +4386,92 @@ impl BackgroundScanner {
|
||||
.repository_entries
|
||||
.retain(|_, entry| ids_to_preserve.contains(&entry.work_directory.0));
|
||||
}
|
||||
drop(update_job_tx);
|
||||
|
||||
let (mut updates_done_tx, mut updates_done_rx) = barrier::channel();
|
||||
self.executor
|
||||
.scoped(|scope| {
|
||||
for _ in 0..self.executor.num_cpus() {
|
||||
scope.spawn(async {
|
||||
loop {
|
||||
select_biased! {
|
||||
// Process any path refresh requests before moving on to process
|
||||
// the queue of git statuses.
|
||||
request = self.scan_requests_rx.recv().fuse() => {
|
||||
let Ok(request) = request else { break };
|
||||
if !self.process_scan_request(request, true).await {
|
||||
return;
|
||||
}
|
||||
}
|
||||
scope.spawn(async {
|
||||
for repo_update in repo_updates {
|
||||
self.update_git_statuses(repo_update);
|
||||
}
|
||||
updates_done_tx.blocking_send(()).ok();
|
||||
});
|
||||
|
||||
// Process git status updates in batches.
|
||||
job = update_job_rx.recv().fuse() => {
|
||||
let Ok(job) = job else { break };
|
||||
self.update_git_statuses(job);
|
||||
scope.spawn(async {
|
||||
loop {
|
||||
select_biased! {
|
||||
// Process any path refresh requests before moving on to process
|
||||
// the queue of git statuses.
|
||||
request = self.scan_requests_rx.recv().fuse() => {
|
||||
let Ok(request) = request else { break };
|
||||
if !self.process_scan_request(request, true).await {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ = updates_done_rx.recv().fuse() => break,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Update the git statuses for a given batch of entries.
|
||||
fn update_git_statuses(&self, job: UpdateGitStatusesJob) {
|
||||
// Determine which entries in this batch have changed their git status.
|
||||
log::trace!("updating git statuses for repo {:?}", job.work_directory.0);
|
||||
let t0 = Instant::now();
|
||||
let mut edits = Vec::new();
|
||||
for entry in Traversal::new(&job.entries, true, false, false, &job.start_path) {
|
||||
if job
|
||||
.end_path
|
||||
.as_ref()
|
||||
.map_or(false, |end| &entry.path >= end)
|
||||
{
|
||||
let Some(statuses) = job.repository.statuses(Path::new("")).log_err() else {
|
||||
return;
|
||||
};
|
||||
log::trace!(
|
||||
"computed git statuses for repo {:?} in {:?}",
|
||||
job.work_directory.0,
|
||||
t0.elapsed()
|
||||
);
|
||||
|
||||
let t0 = Instant::now();
|
||||
let mut changes = Vec::new();
|
||||
let snapshot = self.state.lock().snapshot.snapshot.clone();
|
||||
for file in snapshot.traverse_from_path(true, false, false, job.work_directory.0.as_ref()) {
|
||||
let Ok(repo_path) = file.path.strip_prefix(&job.work_directory.0) else {
|
||||
break;
|
||||
}
|
||||
let Ok(repo_path) = entry
|
||||
.path
|
||||
.strip_prefix(&job.containing_repository.work_directory)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let repo_path = RepoPath(if let Some(location) = &job.location_in_repo {
|
||||
location.join(repo_path)
|
||||
let git_status = if let Some(location) = &job.location_in_repo {
|
||||
statuses.get(&location.join(repo_path))
|
||||
} else {
|
||||
repo_path.to_path_buf()
|
||||
});
|
||||
let git_status = job.containing_repository.statuses.get(&repo_path);
|
||||
if entry.git_status != git_status {
|
||||
let mut entry = entry.clone();
|
||||
statuses.get(&repo_path)
|
||||
};
|
||||
if file.git_status != git_status {
|
||||
let mut entry = file.clone();
|
||||
entry.git_status = git_status;
|
||||
edits.push(Edit::Insert(entry));
|
||||
changes.push((entry.path, git_status));
|
||||
}
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let edits = changes
|
||||
.iter()
|
||||
.filter_map(|(path, git_status)| {
|
||||
let entry = state.snapshot.entry_for_path(path)?.clone();
|
||||
Some(Edit::Insert(Entry {
|
||||
git_status: *git_status,
|
||||
..entry.clone()
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Apply the git status changes.
|
||||
if edits.len() > 0 {
|
||||
let mut state = self.state.lock();
|
||||
let path_changes = edits.iter().map(|edit| {
|
||||
if let Edit::Insert(entry) = edit {
|
||||
entry.path.clone()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
util::extend_sorted(&mut state.changed_paths, path_changes, usize::MAX, Ord::cmp);
|
||||
state.snapshot.entries_by_path.edit(edits, &());
|
||||
}
|
||||
|
||||
util::extend_sorted(
|
||||
&mut state.changed_paths,
|
||||
changes.iter().map(|p| p.0.clone()),
|
||||
usize::MAX,
|
||||
Ord::cmp,
|
||||
);
|
||||
state.snapshot.entries_by_path.edit(edits, &());
|
||||
log::trace!(
|
||||
"refreshed git status of entries starting with {} in {:?}",
|
||||
// entries.len(),
|
||||
job.start_path.display(),
|
||||
t0.elapsed()
|
||||
"applied git status updates for repo {:?} in {:?}",
|
||||
job.work_directory.0,
|
||||
t0.elapsed(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4664,11 +4641,9 @@ struct UpdateIgnoreStatusJob {
|
||||
}
|
||||
|
||||
struct UpdateGitStatusesJob {
|
||||
entries: SumTree<Entry>,
|
||||
start_path: Arc<Path>,
|
||||
end_path: Option<Arc<Path>>,
|
||||
containing_repository: ScanJobContainingRepository,
|
||||
work_directory: RepositoryWorkDirectory,
|
||||
location_in_repo: Option<Arc<Path>>,
|
||||
repository: Arc<dyn GitRepository>,
|
||||
}
|
||||
|
||||
pub trait WorktreeModelHandle {
|
||||
|
||||
@@ -110,14 +110,15 @@ The name of any font family installed on the user's system
|
||||
|
||||
**Options**
|
||||
|
||||
Zed supports a subset of OpenType features that can be enabled or disabled for a given buffer or terminal font. The following [OpenType features](https://en.wikipedia.org/wiki/List_of_typographic_features) can be enabled or disabled too: `calt`, `case`, `cpsp`, `frac`, `liga`, `onum`, `ordn`, `pnum`, `ss01`, `ss02`, `ss03`, `ss04`, `ss05`, `ss06`, `ss07`, `ss08`, `ss09`, `ss10`, `ss11`, `ss12`, `ss13`, `ss14`, `ss15`, `ss16`, `ss17`, `ss18`, `ss19`, `ss20`, `subs`, `sups`, `swsh`, `titl`, `tnum`, `zero`.
|
||||
Zed supports all OpenType features that can be enabled, disabled or set a value to a font feature for a given buffer or terminal font.
|
||||
|
||||
For example, to disable ligatures for a given font you can add the following to your settings:
|
||||
For example, to disable ligatures and set `7` to `cv01` for a given font you can add the following to your settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"buffer_font_features": {
|
||||
"calt": false
|
||||
"calt": false,
|
||||
"cv01": 7
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -11,18 +11,18 @@
|
||||
"args": ["test", "--failed"]
|
||||
},
|
||||
{
|
||||
"label": "mix test $ZED_SYMBOL",
|
||||
"label": "mix test $ZED_RELATIVE_FILE",
|
||||
"command": "mix",
|
||||
"args": ["test", "$ZED_SYMBOL"]
|
||||
"args": ["test", "$ZED_RELATIVE_FILE"]
|
||||
},
|
||||
{
|
||||
"label": "mix test $ZED_FILE:$ZED_ROW",
|
||||
"label": "mix test $ZED_RELATIVE_FILE:$ZED_ROW",
|
||||
"command": "mix",
|
||||
"args": ["test", "$ZED_FILE:$ZED_ROW"]
|
||||
"args": ["test", "$ZED_RELATIVE_FILE:$ZED_ROW"]
|
||||
},
|
||||
{
|
||||
"label": "Elixir: break line",
|
||||
"command": "iex",
|
||||
"args": ["-S", "mix", "test", "-b", "$ZED_FILE:$ZED_ROW"]
|
||||
"args": ["-S", "mix", "test", "-b", "$ZED_RELATIVE_FILE:$ZED_ROW"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
(class "end" @end) @indent
|
||||
(module "end" @end) @indent
|
||||
(begin "end" @end) @indent
|
||||
(singleton_method "end" @end) @indent
|
||||
(do_block "end" @end) @indent
|
||||
|
||||
(then) @indent
|
||||
|
||||
8
extensions/ruby/languages/ruby/injections.scm
Normal file
8
extensions/ruby/languages/ruby/injections.scm
Normal file
@@ -0,0 +1,8 @@
|
||||
(heredoc_body
|
||||
(heredoc_content) @content
|
||||
(heredoc_end) @language
|
||||
(#downcase! @language))
|
||||
|
||||
((regex
|
||||
(string_content) @content)
|
||||
(#set! "language" "regex"))
|
||||
@@ -1,2 +0,0 @@
|
||||
(heredoc_body
|
||||
(heredoc_end) @language) @content
|
||||
Reference in New Issue
Block a user