Compare commits
31 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
48b8853def | ||
![]() |
c59fd08c65 | ||
![]() |
bfea1f2263 | ||
![]() |
5a2a92c7fe | ||
![]() |
ac96237806 | ||
![]() |
596e2f307b | ||
![]() |
933537e912 | ||
![]() |
c44181d445 | ||
![]() |
ad0e53aa6f | ||
![]() |
0f622417d7 | ||
![]() |
7ef8bd6377 | ||
![]() |
aed317840f | ||
![]() |
a15b9a55d2 | ||
![]() |
91ee6b509f | ||
![]() |
78fe18acbc | ||
![]() |
9d76ba445f | ||
![]() |
b865efe3a3 | ||
![]() |
f92d44ed70 | ||
![]() |
62358b9bce | ||
![]() |
df63290a32 | ||
![]() |
6098f94dc1 | ||
![]() |
6f4dee5b1d | ||
![]() |
4ca2645a54 | ||
![]() |
c41a3ec01b | ||
![]() |
4edd0365a1 | ||
![]() |
cc4fb1c1b5 | ||
![]() |
fc3d754aea | ||
![]() |
643f3db2b2 | ||
![]() |
b90c04009f | ||
![]() |
11f7a2cb0e | ||
![]() |
8bdc59703a |
36 changed files with 1872 additions and 920 deletions
21
.github/workflows/release_actions.yml
vendored
21
.github/workflows/release_actions.yml
vendored
|
@ -6,8 +6,8 @@ jobs:
|
||||||
discord_release:
|
discord_release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get appropriate URL
|
- name: Get release URL
|
||||||
id: get-appropriate-url
|
id: get-release-url
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||||
URL="https://zed.dev/releases/preview/latest"
|
URL="https://zed.dev/releases/preview/latest"
|
||||||
|
@ -15,14 +15,19 @@ jobs:
|
||||||
URL="https://zed.dev/releases/stable/latest"
|
URL="https://zed.dev/releases/stable/latest"
|
||||||
fi
|
fi
|
||||||
echo "::set-output name=URL::$URL"
|
echo "::set-output name=URL::$URL"
|
||||||
|
- name: Get content
|
||||||
|
uses: 2428392/gh-truncate-string-action@v1.2.0
|
||||||
|
id: get-content
|
||||||
|
with:
|
||||||
|
stringToTruncate: |
|
||||||
|
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||||
|
|
||||||
|
Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it.
|
||||||
|
|
||||||
|
${{ github.event.release.body }}
|
||||||
|
maxLength: 2000
|
||||||
- name: Discord Webhook Action
|
- name: Discord Webhook Action
|
||||||
uses: tsickert/discord-webhook@v5.3.0
|
uses: tsickert/discord-webhook@v5.3.0
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
content: |
|
content: ${{ steps.get-content.outputs.string }}
|
||||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
|
||||||
|
|
||||||
Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
|
|
||||||
|
|
||||||
${{ github.event.release.body }}
|
|
||||||
|
|
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -1467,7 +1467,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.22.1"
|
version = "0.22.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -1563,6 +1563,7 @@ dependencies = [
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"recent_projects",
|
"recent_projects",
|
||||||
|
"rich_text",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
|
@ -2405,6 +2406,7 @@ dependencies = [
|
||||||
"project",
|
"project",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"rich_text",
|
||||||
"rpc",
|
"rpc",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -6242,6 +6244,24 @@ dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich_text"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"collections",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"lazy_static",
|
||||||
|
"pulldown-cmark",
|
||||||
|
"smallvec",
|
||||||
|
"smol",
|
||||||
|
"sum_tree",
|
||||||
|
"theme",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.20"
|
version = "0.16.20"
|
||||||
|
@ -10063,7 +10083,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.107.0"
|
version = "0.107.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
|
@ -64,6 +64,7 @@ members = [
|
||||||
"crates/sqlez",
|
"crates/sqlez",
|
||||||
"crates/sqlez_macros",
|
"crates/sqlez_macros",
|
||||||
"crates/feature_flags",
|
"crates/feature_flags",
|
||||||
|
"crates/rich_text",
|
||||||
"crates/storybook",
|
"crates/storybook",
|
||||||
"crates/sum_tree",
|
"crates/sum_tree",
|
||||||
"crates/terminal",
|
"crates/terminal",
|
||||||
|
|
|
@ -17,7 +17,7 @@ use editor::{
|
||||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
|
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
|
||||||
},
|
},
|
||||||
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
|
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
|
||||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
|
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||||
};
|
};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
@ -278,22 +278,36 @@ impl AssistantPanel {
|
||||||
if selection.start.excerpt_id() != selection.end.excerpt_id() {
|
if selection.start.excerpt_id() != selection.end.excerpt_id() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||||
|
|
||||||
|
// Extend the selection to the start and the end of the line.
|
||||||
|
let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
|
||||||
|
if point_selection.end > point_selection.start {
|
||||||
|
point_selection.start.column = 0;
|
||||||
|
// If the selection ends at the start of the line, we don't want to include it.
|
||||||
|
if point_selection.end.column == 0 {
|
||||||
|
point_selection.end.row -= 1;
|
||||||
|
}
|
||||||
|
point_selection.end.column = snapshot.line_len(point_selection.end.row);
|
||||||
|
}
|
||||||
|
|
||||||
|
let codegen_kind = if point_selection.start == point_selection.end {
|
||||||
|
CodegenKind::Generate {
|
||||||
|
position: snapshot.anchor_after(point_selection.start),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CodegenKind::Transform {
|
||||||
|
range: snapshot.anchor_before(point_selection.start)
|
||||||
|
..snapshot.anchor_after(point_selection.end),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
||||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
|
||||||
let provider = Arc::new(OpenAICompletionProvider::new(
|
let provider = Arc::new(OpenAICompletionProvider::new(
|
||||||
api_key,
|
api_key,
|
||||||
cx.background().clone(),
|
cx.background().clone(),
|
||||||
));
|
));
|
||||||
let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
|
|
||||||
CodegenKind::Generate {
|
|
||||||
position: selection.start,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CodegenKind::Transform {
|
|
||||||
range: selection.start..selection.end,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let codegen = cx.add_model(|cx| {
|
let codegen = cx.add_model(|cx| {
|
||||||
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
||||||
});
|
});
|
||||||
|
@ -319,7 +333,7 @@ impl AssistantPanel {
|
||||||
editor.insert_blocks(
|
editor.insert_blocks(
|
||||||
[BlockProperties {
|
[BlockProperties {
|
||||||
style: BlockStyle::Flex,
|
style: BlockStyle::Flex,
|
||||||
position: selection.head().bias_left(&snapshot),
|
position: snapshot.anchor_before(point_selection.head()),
|
||||||
height: 2,
|
height: 2,
|
||||||
render: Arc::new({
|
render: Arc::new({
|
||||||
let inline_assistant = inline_assistant.clone();
|
let inline_assistant = inline_assistant.clone();
|
||||||
|
@ -578,10 +592,7 @@ impl AssistantPanel {
|
||||||
|
|
||||||
let codegen_kind = codegen.read(cx).kind().clone();
|
let codegen_kind = codegen.read(cx).kind().clone();
|
||||||
let user_prompt = user_prompt.to_string();
|
let user_prompt = user_prompt.to_string();
|
||||||
let prompt = cx.background().spawn(async move {
|
|
||||||
let language_name = language_name.as_deref();
|
|
||||||
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
|
|
||||||
});
|
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
let mut model = settings::get::<AssistantSettings>(cx)
|
let mut model = settings::get::<AssistantSettings>(cx)
|
||||||
.default_open_ai_model
|
.default_open_ai_model
|
||||||
|
@ -597,6 +608,11 @@ impl AssistantPanel {
|
||||||
model = conversation.model.clone();
|
model = conversation.model.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let prompt = cx.background().spawn(async move {
|
||||||
|
let language_name = language_name.as_deref();
|
||||||
|
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
|
||||||
|
});
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
let prompt = prompt.await;
|
let prompt = prompt.await;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||||
use ai::completion::{CompletionProvider, OpenAIRequest};
|
use ai::completion::{CompletionProvider, OpenAIRequest};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use editor::{
|
use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||||
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
|
||||||
};
|
|
||||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||||
use gpui::{Entity, ModelContext, ModelHandle, Task};
|
use gpui::{Entity, ModelContext, ModelHandle, Task};
|
||||||
use language::{Rope, TransactionId};
|
use language::{Rope, TransactionId};
|
||||||
|
@ -40,26 +38,11 @@ impl Entity for Codegen {
|
||||||
impl Codegen {
|
impl Codegen {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
buffer: ModelHandle<MultiBuffer>,
|
buffer: ModelHandle<MultiBuffer>,
|
||||||
mut kind: CodegenKind,
|
kind: CodegenKind,
|
||||||
provider: Arc<dyn CompletionProvider>,
|
provider: Arc<dyn CompletionProvider>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let snapshot = buffer.read(cx).snapshot(cx);
|
let snapshot = buffer.read(cx).snapshot(cx);
|
||||||
match &mut kind {
|
|
||||||
CodegenKind::Transform { range } => {
|
|
||||||
let mut point_range = range.to_point(&snapshot);
|
|
||||||
point_range.start.column = 0;
|
|
||||||
if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
|
|
||||||
point_range.end.column = snapshot.line_len(point_range.end.row);
|
|
||||||
}
|
|
||||||
range.start = snapshot.anchor_before(point_range.start);
|
|
||||||
range.end = snapshot.anchor_after(point_range.end);
|
|
||||||
}
|
|
||||||
CodegenKind::Generate { position } => {
|
|
||||||
*position = position.bias_right(&snapshot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
provider,
|
provider,
|
||||||
buffer: buffer.clone(),
|
buffer: buffer.clone(),
|
||||||
|
@ -386,7 +369,7 @@ mod tests {
|
||||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let range = buffer.read_with(cx, |buffer, cx| {
|
let range = buffer.read_with(cx, |buffer, cx| {
|
||||||
let snapshot = buffer.snapshot(cx);
|
let snapshot = buffer.snapshot(cx);
|
||||||
snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4))
|
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||||
});
|
});
|
||||||
let provider = Arc::new(TestCompletionProvider::new());
|
let provider = Arc::new(TestCompletionProvider::new());
|
||||||
let codegen = cx.add_model(|cx| {
|
let codegen = cx.add_model(|cx| {
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::cmp::{self, Reverse};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Match {
|
struct Match {
|
||||||
|
@ -121,6 +122,7 @@ pub fn generate_content_prompt(
|
||||||
range: Range<impl ToOffset>,
|
range: Range<impl ToOffset>,
|
||||||
kind: CodegenKind,
|
kind: CodegenKind,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
let range = range.to_offset(buffer);
|
||||||
let mut prompt = String::new();
|
let mut prompt = String::new();
|
||||||
|
|
||||||
// General Preamble
|
// General Preamble
|
||||||
|
@ -130,17 +132,29 @@ pub fn generate_content_prompt(
|
||||||
writeln!(prompt, "You're an expert engineer.\n").unwrap();
|
writeln!(prompt, "You're an expert engineer.\n").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let outline = summarize(buffer, range);
|
let mut content = String::new();
|
||||||
|
content.extend(buffer.text_for_range(0..range.start));
|
||||||
|
if range.start == range.end {
|
||||||
|
content.push_str("<|START|>");
|
||||||
|
} else {
|
||||||
|
content.push_str("<|START|");
|
||||||
|
}
|
||||||
|
content.extend(buffer.text_for_range(range.clone()));
|
||||||
|
if range.start != range.end {
|
||||||
|
content.push_str("|END|>");
|
||||||
|
}
|
||||||
|
content.extend(buffer.text_for_range(range.end..buffer.len()));
|
||||||
|
|
||||||
writeln!(
|
writeln!(
|
||||||
prompt,
|
prompt,
|
||||||
"The file you are currently working on has the following outline:"
|
"The file you are currently working on has the following content:"
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if let Some(language_name) = language_name {
|
if let Some(language_name) = language_name {
|
||||||
let language_name = language_name.to_lowercase();
|
let language_name = language_name.to_lowercase();
|
||||||
writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap();
|
writeln!(prompt, "```{language_name}\n{content}\n```").unwrap();
|
||||||
} else {
|
} else {
|
||||||
writeln!(prompt, "```\n{outline}\n```").unwrap();
|
writeln!(prompt, "```\n{content}\n```").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
|
|
|
@ -600,27 +600,30 @@ impl Room {
|
||||||
|
|
||||||
/// Returns the most 'active' projects, defined as most people in the project
|
/// Returns the most 'active' projects, defined as most people in the project
|
||||||
pub fn most_active_project(&self) -> Option<(u64, u64)> {
|
pub fn most_active_project(&self) -> Option<(u64, u64)> {
|
||||||
let mut projects = HashMap::default();
|
let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
|
||||||
let mut hosts = HashMap::default();
|
|
||||||
for participant in self.remote_participants.values() {
|
for participant in self.remote_participants.values() {
|
||||||
match participant.location {
|
match participant.location {
|
||||||
ParticipantLocation::SharedProject { project_id } => {
|
ParticipantLocation::SharedProject { project_id } => {
|
||||||
*projects.entry(project_id).or_insert(0) += 1;
|
project_hosts_and_guest_counts
|
||||||
|
.entry(project_id)
|
||||||
|
.or_default()
|
||||||
|
.1 += 1;
|
||||||
}
|
}
|
||||||
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
|
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
|
||||||
}
|
}
|
||||||
for project in &participant.projects {
|
for project in &participant.projects {
|
||||||
*projects.entry(project.id).or_insert(0) += 1;
|
project_hosts_and_guest_counts
|
||||||
hosts.insert(project.id, participant.user.id);
|
.entry(project.id)
|
||||||
|
.or_default()
|
||||||
|
.0 = Some(participant.user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
|
project_hosts_and_guest_counts
|
||||||
pairs.sort_by_key(|(_, count)| *count as i32);
|
.into_iter()
|
||||||
|
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
|
||||||
pairs
|
.max_by_key(|(_, _, guest_count)| *guest_count)
|
||||||
.first()
|
.map(|(id, host, _)| (id, host))
|
||||||
.map(|(project_id, _)| (*project_id, hosts[&project_id]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_room_updated(
|
async fn handle_room_updated(
|
||||||
|
@ -686,6 +689,7 @@ impl Room {
|
||||||
let Some(peer_id) = participant.peer_id else {
|
let Some(peer_id) = participant.peer_id else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let participant_index = ParticipantIndex(participant.participant_index);
|
||||||
this.participant_user_ids.insert(participant.user_id);
|
this.participant_user_ids.insert(participant.user_id);
|
||||||
|
|
||||||
let old_projects = this
|
let old_projects = this
|
||||||
|
@ -736,8 +740,9 @@ impl Room {
|
||||||
if let Some(remote_participant) =
|
if let Some(remote_participant) =
|
||||||
this.remote_participants.get_mut(&participant.user_id)
|
this.remote_participants.get_mut(&participant.user_id)
|
||||||
{
|
{
|
||||||
remote_participant.projects = participant.projects;
|
|
||||||
remote_participant.peer_id = peer_id;
|
remote_participant.peer_id = peer_id;
|
||||||
|
remote_participant.projects = participant.projects;
|
||||||
|
remote_participant.participant_index = participant_index;
|
||||||
if location != remote_participant.location {
|
if location != remote_participant.location {
|
||||||
remote_participant.location = location;
|
remote_participant.location = location;
|
||||||
cx.emit(Event::ParticipantLocationChanged {
|
cx.emit(Event::ParticipantLocationChanged {
|
||||||
|
@ -749,9 +754,7 @@ impl Room {
|
||||||
participant.user_id,
|
participant.user_id,
|
||||||
RemoteParticipant {
|
RemoteParticipant {
|
||||||
user: user.clone(),
|
user: user.clone(),
|
||||||
participant_index: ParticipantIndex(
|
participant_index,
|
||||||
participant.participant_index,
|
|
||||||
),
|
|
||||||
peer_id,
|
peer_id,
|
||||||
projects: participant.projects,
|
projects: participant.projects,
|
||||||
location,
|
location,
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub struct ChannelMessage {
|
||||||
pub nonce: u128,
|
pub nonce: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum ChannelMessageId {
|
pub enum ChannelMessageId {
|
||||||
Saved(u64),
|
Saved(u64),
|
||||||
Pending(usize),
|
Pending(usize),
|
||||||
|
|
|
@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
actions!(client, [SignIn, SignOut]);
|
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||||
|
|
||||||
pub fn init_settings(cx: &mut AppContext) {
|
pub fn init_settings(cx: &mut AppContext) {
|
||||||
settings::register::<TelemetrySettings>(cx);
|
settings::register::<TelemetrySettings>(cx);
|
||||||
|
@ -102,6 +102,17 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
cx.add_global_action({
|
||||||
|
let client = client.clone();
|
||||||
|
move |_: &Reconnect, cx| {
|
||||||
|
if let Some(client) = client.upgrade() {
|
||||||
|
cx.spawn(|cx| async move {
|
||||||
|
client.reconnect(&cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
|
@ -1212,6 +1223,11 @@ impl Client {
|
||||||
self.set_status(Status::SignedOut, cx);
|
self.set_status(Status::SignedOut, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||||
|
self.peer.teardown();
|
||||||
|
self.set_status(Status::ConnectionLost, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn connection_id(&self) -> Result<ConnectionId> {
|
fn connection_id(&self) -> Result<ConnectionId> {
|
||||||
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
|
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
|
||||||
Ok(connection_id)
|
Ok(connection_id)
|
||||||
|
|
|
@ -4,7 +4,9 @@ use lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
|
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||||
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
use sysinfo::{
|
||||||
|
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
|
||||||
|
};
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use util::http::HttpClient;
|
use util::http::HttpClient;
|
||||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||||
|
@ -18,7 +20,8 @@ pub struct Telemetry {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct TelemetryState {
|
struct TelemetryState {
|
||||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||||
installation_id: Option<Arc<str>>, // Per app installation
|
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
|
||||||
|
session_id: Option<Arc<str>>, // Per app launch
|
||||||
app_version: Option<Arc<str>>,
|
app_version: Option<Arc<str>>,
|
||||||
release_channel: Option<&'static str>,
|
release_channel: Option<&'static str>,
|
||||||
os_name: &'static str,
|
os_name: &'static str,
|
||||||
|
@ -41,6 +44,7 @@ lazy_static! {
|
||||||
struct ClickhouseEventRequestBody {
|
struct ClickhouseEventRequestBody {
|
||||||
token: &'static str,
|
token: &'static str,
|
||||||
installation_id: Option<Arc<str>>,
|
installation_id: Option<Arc<str>>,
|
||||||
|
session_id: Option<Arc<str>>,
|
||||||
is_staff: Option<bool>,
|
is_staff: Option<bool>,
|
||||||
app_version: Option<Arc<str>>,
|
app_version: Option<Arc<str>>,
|
||||||
os_name: &'static str,
|
os_name: &'static str,
|
||||||
|
@ -131,6 +135,7 @@ impl Telemetry {
|
||||||
release_channel,
|
release_channel,
|
||||||
installation_id: None,
|
installation_id: None,
|
||||||
metrics_id: None,
|
metrics_id: None,
|
||||||
|
session_id: None,
|
||||||
clickhouse_events_queue: Default::default(),
|
clickhouse_events_queue: Default::default(),
|
||||||
flush_clickhouse_events_task: Default::default(),
|
flush_clickhouse_events_task: Default::default(),
|
||||||
log_file: None,
|
log_file: None,
|
||||||
|
@ -145,9 +150,15 @@ impl Telemetry {
|
||||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(self: &Arc<Self>, installation_id: Option<String>, cx: &mut AppContext) {
|
pub fn start(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
installation_id: Option<String>,
|
||||||
|
session_id: String,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.installation_id = installation_id.map(|id| id.into());
|
state.installation_id = installation_id.map(|id| id.into());
|
||||||
|
state.session_id = Some(session_id.into());
|
||||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
|
@ -157,8 +168,16 @@ impl Telemetry {
|
||||||
|
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let mut system = System::new_all();
|
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
||||||
system.refresh_all();
|
let refresh_kind = RefreshKind::new()
|
||||||
|
.with_memory() // For memory usage
|
||||||
|
.with_processes(ProcessRefreshKind::everything()) // For process usage
|
||||||
|
.with_cpu(CpuRefreshKind::everything()); // For core count
|
||||||
|
|
||||||
|
let mut system = System::new_with_specifics(refresh_kind);
|
||||||
|
|
||||||
|
// Avoiding calling `refresh_all()`, just update what we need
|
||||||
|
system.refresh_specifics(refresh_kind);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Waiting some amount of time before the first query is important to get a reasonable value
|
// Waiting some amount of time before the first query is important to get a reasonable value
|
||||||
|
@ -166,8 +185,7 @@ impl Telemetry {
|
||||||
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
|
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
|
||||||
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
|
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
|
||||||
|
|
||||||
system.refresh_memory();
|
system.refresh_specifics(refresh_kind);
|
||||||
system.refresh_processes();
|
|
||||||
|
|
||||||
let current_process = Pid::from_u32(std::process::id());
|
let current_process = Pid::from_u32(std::process::id());
|
||||||
let Some(process) = system.processes().get(¤t_process) else {
|
let Some(process) = system.processes().get(¤t_process) else {
|
||||||
|
@ -279,22 +297,21 @@ impl Telemetry {
|
||||||
|
|
||||||
{
|
{
|
||||||
let state = this.state.lock();
|
let state = this.state.lock();
|
||||||
json_bytes.clear();
|
let request_body = ClickhouseEventRequestBody {
|
||||||
serde_json::to_writer(
|
token: ZED_SECRET_CLIENT_TOKEN,
|
||||||
&mut json_bytes,
|
installation_id: state.installation_id.clone(),
|
||||||
&ClickhouseEventRequestBody {
|
session_id: state.session_id.clone(),
|
||||||
token: ZED_SECRET_CLIENT_TOKEN,
|
is_staff: state.is_staff.clone(),
|
||||||
installation_id: state.installation_id.clone(),
|
app_version: state.app_version.clone(),
|
||||||
is_staff: state.is_staff.clone(),
|
os_name: state.os_name,
|
||||||
app_version: state.app_version.clone(),
|
os_version: state.os_version.clone(),
|
||||||
os_name: state.os_name,
|
architecture: state.architecture,
|
||||||
os_version: state.os_version.clone(),
|
|
||||||
architecture: state.architecture,
|
|
||||||
|
|
||||||
release_channel: state.release_channel,
|
release_channel: state.release_channel,
|
||||||
events,
|
events,
|
||||||
},
|
};
|
||||||
)?;
|
json_bytes.clear();
|
||||||
|
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http_client
|
this.http_client
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.22.1"
|
version = "0.22.2"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -9,13 +9,3 @@ pub mod projects;
|
||||||
pub mod rooms;
|
pub mod rooms;
|
||||||
pub mod servers;
|
pub mod servers;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
||||||
fn max_assign<T: Ord>(max: &mut Option<T>, val: T) {
|
|
||||||
if let Some(max_val) = max {
|
|
||||||
if val > *max_val {
|
|
||||||
*max = Some(val);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*max = Some(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -89,17 +89,14 @@ impl Database {
|
||||||
|
|
||||||
let mut rows = channel_message::Entity::find()
|
let mut rows = channel_message::Entity::find()
|
||||||
.filter(condition)
|
.filter(condition)
|
||||||
|
.order_by_asc(channel_message::Column::Id)
|
||||||
.limit(count as u64)
|
.limit(count as u64)
|
||||||
.stream(&*tx)
|
.stream(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut max_id = None;
|
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
while let Some(row) = rows.next().await {
|
while let Some(row) = rows.next().await {
|
||||||
let row = row?;
|
let row = row?;
|
||||||
|
|
||||||
max_assign(&mut max_id, row.id);
|
|
||||||
|
|
||||||
let nonce = row.nonce.as_u64_pair();
|
let nonce = row.nonce.as_u64_pair();
|
||||||
messages.push(proto::ChannelMessage {
|
messages.push(proto::ChannelMessage {
|
||||||
id: row.id.to_proto(),
|
id: row.id.to_proto(),
|
||||||
|
@ -113,50 +110,6 @@ impl Database {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
drop(rows);
|
drop(rows);
|
||||||
|
|
||||||
if let Some(max_id) = max_id {
|
|
||||||
let has_older_message = observed_channel_messages::Entity::find()
|
|
||||||
.filter(
|
|
||||||
observed_channel_messages::Column::UserId
|
|
||||||
.eq(user_id)
|
|
||||||
.and(observed_channel_messages::Column::ChannelId.eq(channel_id))
|
|
||||||
.and(observed_channel_messages::Column::ChannelMessageId.lt(max_id)),
|
|
||||||
)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.is_some();
|
|
||||||
|
|
||||||
if has_older_message {
|
|
||||||
observed_channel_messages::Entity::update(
|
|
||||||
observed_channel_messages::ActiveModel {
|
|
||||||
user_id: ActiveValue::Unchanged(user_id),
|
|
||||||
channel_id: ActiveValue::Unchanged(channel_id),
|
|
||||||
channel_message_id: ActiveValue::Set(max_id),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
observed_channel_messages::Entity::insert(
|
|
||||||
observed_channel_messages::ActiveModel {
|
|
||||||
user_id: ActiveValue::Set(user_id),
|
|
||||||
channel_id: ActiveValue::Set(channel_id),
|
|
||||||
channel_message_id: ActiveValue::Set(max_id),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.on_conflict(
|
|
||||||
OnConflict::columns([
|
|
||||||
observed_channel_messages::Column::UserId,
|
|
||||||
observed_channel_messages::Column::ChannelId,
|
|
||||||
])
|
|
||||||
.update_columns([observed_channel_messages::Column::ChannelMessageId])
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(messages)
|
Ok(messages)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -1906,13 +1906,10 @@ async fn follow(
|
||||||
.check_room_participants(room_id, leader_id, session.connection_id)
|
.check_room_participants(room_id, leader_id, session.connection_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut response_payload = session
|
let response_payload = session
|
||||||
.peer
|
.peer
|
||||||
.forward_request(session.connection_id, leader_id, request)
|
.forward_request(session.connection_id, leader_id, request)
|
||||||
.await?;
|
.await?;
|
||||||
response_payload
|
|
||||||
.views
|
|
||||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
|
||||||
response.send(response_payload)?;
|
response.send(response_payload)?;
|
||||||
|
|
||||||
if let Some(project_id) = project_id {
|
if let Some(project_id) = project_id {
|
||||||
|
@ -1973,14 +1970,17 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
|
// For now, don't send view update messages back to that view's current leader.
|
||||||
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
|
let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
|
||||||
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
|
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
|
||||||
proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
for follower_peer_id in request.follower_ids.iter().copied() {
|
for follower_peer_id in request.follower_ids.iter().copied() {
|
||||||
let follower_connection_id = follower_peer_id.into();
|
let follower_connection_id = follower_peer_id.into();
|
||||||
if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
|
if Some(follower_peer_id) != connection_id_to_omit
|
||||||
|
&& connection_ids.contains(&follower_connection_id)
|
||||||
|
{
|
||||||
session.peer.forward_send(
|
session.peer.forward_send(
|
||||||
session.connection_id,
|
session.connection_id,
|
||||||
follower_connection_id,
|
follower_connection_id,
|
||||||
|
|
|
@ -4,6 +4,7 @@ use collab_ui::project_shared_notification::ProjectSharedNotification;
|
||||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||||
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
|
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
|
||||||
use live_kit_client::MacOSDisplay;
|
use live_kit_client::MacOSDisplay;
|
||||||
|
use rpc::proto::PeerId;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -183,20 +184,12 @@ async fn test_basic_following(
|
||||||
|
|
||||||
// All clients see that clients B and C are following client A.
|
// All clients see that clients B and C are following client A.
|
||||||
cx_c.foreground().run_until_parked();
|
cx_c.foreground().run_until_parked();
|
||||||
for (name, active_call, cx) in [
|
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||||
("A", &active_call_a, &cx_a),
|
assert_eq!(
|
||||||
("B", &active_call_b, &cx_b),
|
followers_by_leader(project_id, cx),
|
||||||
("C", &active_call_c, &cx_c),
|
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
|
||||||
("D", &active_call_d, &cx_d),
|
"followers seen by {name}"
|
||||||
] {
|
);
|
||||||
active_call.read_with(*cx, |call, cx| {
|
|
||||||
let room = call.room().unwrap().read(cx);
|
|
||||||
assert_eq!(
|
|
||||||
room.followers_for(peer_id_a, project_id),
|
|
||||||
&[peer_id_b, peer_id_c],
|
|
||||||
"checking followers for A as {name}"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client C unfollows client A.
|
// Client C unfollows client A.
|
||||||
|
@ -206,46 +199,39 @@ async fn test_basic_following(
|
||||||
|
|
||||||
// All clients see that clients B is following client A.
|
// All clients see that clients B is following client A.
|
||||||
cx_c.foreground().run_until_parked();
|
cx_c.foreground().run_until_parked();
|
||||||
for (name, active_call, cx) in [
|
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||||
("A", &active_call_a, &cx_a),
|
assert_eq!(
|
||||||
("B", &active_call_b, &cx_b),
|
followers_by_leader(project_id, cx),
|
||||||
("C", &active_call_c, &cx_c),
|
&[(peer_id_a, vec![peer_id_b])],
|
||||||
("D", &active_call_d, &cx_d),
|
"followers seen by {name}"
|
||||||
] {
|
);
|
||||||
active_call.read_with(*cx, |call, cx| {
|
|
||||||
let room = call.room().unwrap().read(cx);
|
|
||||||
assert_eq!(
|
|
||||||
room.followers_for(peer_id_a, project_id),
|
|
||||||
&[peer_id_b],
|
|
||||||
"checking followers for A as {name}"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client C re-follows client A.
|
// Client C re-follows client A.
|
||||||
workspace_c.update(cx_c, |workspace, cx| {
|
workspace_c
|
||||||
workspace.follow(peer_id_a, cx);
|
.update(cx_c, |workspace, cx| {
|
||||||
});
|
workspace.follow(peer_id_a, cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// All clients see that clients B and C are following client A.
|
// All clients see that clients B and C are following client A.
|
||||||
cx_c.foreground().run_until_parked();
|
cx_c.foreground().run_until_parked();
|
||||||
for (name, active_call, cx) in [
|
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||||
("A", &active_call_a, &cx_a),
|
assert_eq!(
|
||||||
("B", &active_call_b, &cx_b),
|
followers_by_leader(project_id, cx),
|
||||||
("C", &active_call_c, &cx_c),
|
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
|
||||||
("D", &active_call_d, &cx_d),
|
"followers seen by {name}"
|
||||||
] {
|
);
|
||||||
active_call.read_with(*cx, |call, cx| {
|
|
||||||
let room = call.room().unwrap().read(cx);
|
|
||||||
assert_eq!(
|
|
||||||
room.followers_for(peer_id_a, project_id),
|
|
||||||
&[peer_id_b, peer_id_c],
|
|
||||||
"checking followers for A as {name}"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client D follows client C.
|
// Client D follows client B, then switches to following client C.
|
||||||
|
workspace_d
|
||||||
|
.update(cx_d, |workspace, cx| {
|
||||||
|
workspace.follow(peer_id_b, cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
workspace_d
|
workspace_d
|
||||||
.update(cx_d, |workspace, cx| {
|
.update(cx_d, |workspace, cx| {
|
||||||
workspace.follow(peer_id_c, cx).unwrap()
|
workspace.follow(peer_id_c, cx).unwrap()
|
||||||
|
@ -255,20 +241,15 @@ async fn test_basic_following(
|
||||||
|
|
||||||
// All clients see that D is following C
|
// All clients see that D is following C
|
||||||
cx_d.foreground().run_until_parked();
|
cx_d.foreground().run_until_parked();
|
||||||
for (name, active_call, cx) in [
|
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||||
("A", &active_call_a, &cx_a),
|
assert_eq!(
|
||||||
("B", &active_call_b, &cx_b),
|
followers_by_leader(project_id, cx),
|
||||||
("C", &active_call_c, &cx_c),
|
&[
|
||||||
("D", &active_call_d, &cx_d),
|
(peer_id_a, vec![peer_id_b, peer_id_c]),
|
||||||
] {
|
(peer_id_c, vec![peer_id_d])
|
||||||
active_call.read_with(*cx, |call, cx| {
|
],
|
||||||
let room = call.room().unwrap().read(cx);
|
"followers seen by {name}"
|
||||||
assert_eq!(
|
);
|
||||||
room.followers_for(peer_id_c, project_id),
|
|
||||||
&[peer_id_d],
|
|
||||||
"checking followers for C as {name}"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client C closes the project.
|
// Client C closes the project.
|
||||||
|
@ -277,32 +258,12 @@ async fn test_basic_following(
|
||||||
|
|
||||||
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||||
cx_c.foreground().run_until_parked();
|
cx_c.foreground().run_until_parked();
|
||||||
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
|
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||||
active_call.read_with(*cx, |call, cx| {
|
assert_eq!(
|
||||||
let room = call.room().unwrap().read(cx);
|
followers_by_leader(project_id, cx),
|
||||||
assert_eq!(
|
&[(peer_id_a, vec![peer_id_b]),],
|
||||||
room.followers_for(peer_id_a, project_id),
|
"followers seen by {name}"
|
||||||
&[peer_id_b],
|
);
|
||||||
"checking followers for A as {name}"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// All clients see that no-one is following C
|
|
||||||
for (name, active_call, cx) in [
|
|
||||||
("A", &active_call_a, &cx_a),
|
|
||||||
("B", &active_call_b, &cx_b),
|
|
||||||
("C", &active_call_c, &cx_c),
|
|
||||||
("D", &active_call_d, &cx_d),
|
|
||||||
] {
|
|
||||||
active_call.read_with(*cx, |call, cx| {
|
|
||||||
let room = call.room().unwrap().read(cx);
|
|
||||||
assert_eq!(
|
|
||||||
room.followers_for(peer_id_c, project_id),
|
|
||||||
&[],
|
|
||||||
"checking followers for C as {name}"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When client A activates a different editor, client B does so as well.
|
// When client A activates a different editor, client B does so as well.
|
||||||
|
@ -724,10 +685,9 @@ async fn test_peers_following_each_other(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Client A opens some editors.
|
// Client A opens a file.
|
||||||
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||||
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
workspace_a
|
||||||
let _editor_a1 = workspace_a
|
|
||||||
.update(cx_a, |workspace, cx| {
|
.update(cx_a, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||||
})
|
})
|
||||||
|
@ -736,10 +696,9 @@ async fn test_peers_following_each_other(
|
||||||
.downcast::<Editor>()
|
.downcast::<Editor>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Client B opens an editor.
|
// Client B opens a different file.
|
||||||
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||||
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
workspace_b
|
||||||
let _editor_b1 = workspace_b
|
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||||
})
|
})
|
||||||
|
@ -754,9 +713,7 @@ async fn test_peers_following_each_other(
|
||||||
});
|
});
|
||||||
workspace_a
|
workspace_a
|
||||||
.update(cx_a, |workspace, cx| {
|
.update(cx_a, |workspace, cx| {
|
||||||
assert_ne!(*workspace.active_pane(), pane_a1);
|
workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
|
||||||
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
|
|
||||||
workspace.follow(leader_id, cx).unwrap()
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -765,85 +722,443 @@ async fn test_peers_following_each_other(
|
||||||
});
|
});
|
||||||
workspace_b
|
workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
assert_ne!(*workspace.active_pane(), pane_b1);
|
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||||
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
|
|
||||||
workspace.follow(leader_id, cx).unwrap()
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
workspace_a.update(cx_a, |workspace, cx| {
|
// Clients A and B return focus to the original files they had open
|
||||||
workspace.activate_next_pane(cx);
|
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||||
});
|
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||||
// Wait for focus effects to be fully flushed
|
deterministic.run_until_parked();
|
||||||
workspace_a.update(cx_a, |workspace, _| {
|
|
||||||
assert_eq!(*workspace.active_pane(), pane_a1);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Both clients see the other client's focused file in their right pane.
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_a, cx_a),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(true, "1.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: client_b.peer_id(),
|
||||||
|
items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_b, cx_b),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(true, "2.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: client_a.peer_id(),
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clients A and B each open a new file.
|
||||||
workspace_a
|
workspace_a
|
||||||
.update(cx_a, |workspace, cx| {
|
.update(cx_a, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
workspace_b.update(cx_b, |workspace, cx| {
|
|
||||||
workspace.activate_next_pane(cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
workspace_b
|
workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
|
||||||
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
|
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx_a.foreground().run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
// Ensure leader updates don't change the active pane of followers
|
// Both client's see the other client open the new file, but keep their
|
||||||
workspace_a.read_with(cx_a, |workspace, _| {
|
// focus on their own active pane.
|
||||||
assert_eq!(*workspace.active_pane(), pane_a1);
|
|
||||||
});
|
|
||||||
workspace_b.read_with(cx_b, |workspace, _| {
|
|
||||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure peers following each other doesn't cause an infinite loop.
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
pane_summaries(&workspace_a, cx_a),
|
||||||
.active_item(cx)
|
&[
|
||||||
.unwrap()
|
PaneSummary {
|
||||||
.project_path(cx)),
|
active: true,
|
||||||
Some((worktree_id, "3.txt").into())
|
leader: None,
|
||||||
|
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: client_b.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(true, "4.txt".into())
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
);
|
);
|
||||||
workspace_a.update(cx_a, |workspace, cx| {
|
assert_eq!(
|
||||||
assert_eq!(
|
pane_summaries(&workspace_b, cx_b),
|
||||||
workspace.active_item(cx).unwrap().project_path(cx),
|
&[
|
||||||
Some((worktree_id, "3.txt").into())
|
PaneSummary {
|
||||||
);
|
active: true,
|
||||||
workspace.activate_next_pane(cx);
|
leader: None,
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: client_a.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(true, "3.txt".into())
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Client A focuses their right pane, in which they're following client B.
|
||||||
|
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Client B sees that client A is now looking at the same file as them.
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_a, cx_a),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: client_b.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(true, "4.txt".into())
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_b, cx_b),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: client_a.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(false, "3.txt".into()),
|
||||||
|
(true, "4.txt".into())
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Client B focuses their right pane, in which they're following client A,
|
||||||
|
// who is following them.
|
||||||
|
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Client A sees that client B is now looking at the same file as them.
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_b, cx_b),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: client_a.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(false, "3.txt".into()),
|
||||||
|
(true, "4.txt".into())
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_a, cx_a),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: client_b.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(true, "4.txt".into())
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Client B focuses a file that they previously followed A to, breaking
|
||||||
|
// the follow.
|
||||||
|
workspace_b.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
|
pane.activate_prev_item(true, cx);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Both clients see that client B is looking at that previous file.
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_b, cx_b),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: None,
|
||||||
|
items: vec![
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(true, "3.txt".into()),
|
||||||
|
(false, "4.txt".into())
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_a, cx_a),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: client_b.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(false, "4.txt".into()),
|
||||||
|
(true, "3.txt".into()),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Client B closes tabs, some of which were originally opened by client A,
|
||||||
|
// and some of which were originally opened by client B.
|
||||||
|
workspace_b.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
|
pane.close_inactive_items(&Default::default(), cx)
|
||||||
|
.unwrap()
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Both clients see that Client B is looking at the previous tab.
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_b, cx_b),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(true, "3.txt".into()),]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_a, cx_a),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: client_b.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(false, "4.txt".into()),
|
||||||
|
(true, "3.txt".into()),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Client B follows client A again.
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client A cycles through some tabs.
|
||||||
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
|
pane.activate_prev_item(true, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Client B follows client A into those tabs.
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_a, cx_a),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: None,
|
||||||
|
items: vec![
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(true, "4.txt".into()),
|
||||||
|
(false, "3.txt".into()),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_b, cx_b),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: client_a.peer_id(),
|
||||||
|
items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
workspace_a.update(cx_a, |workspace, cx| {
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
assert_eq!(
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
workspace.active_item(cx).unwrap().project_path(cx),
|
pane.activate_prev_item(true, cx);
|
||||||
Some((worktree_id, "4.txt").into())
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
workspace_b.update(cx_b, |workspace, cx| {
|
assert_eq!(
|
||||||
assert_eq!(
|
pane_summaries(&workspace_a, cx_a),
|
||||||
workspace.active_item(cx).unwrap().project_path(cx),
|
&[
|
||||||
Some((worktree_id, "4.txt").into())
|
PaneSummary {
|
||||||
);
|
active: false,
|
||||||
workspace.activate_next_pane(cx);
|
leader: None,
|
||||||
});
|
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: None,
|
||||||
|
items: vec![
|
||||||
|
(false, "1.txt".into()),
|
||||||
|
(true, "2.txt".into()),
|
||||||
|
(false, "4.txt".into()),
|
||||||
|
(false, "3.txt".into()),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_b, cx_b),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: client_a.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "3.txt".into()),
|
||||||
|
(false, "4.txt".into()),
|
||||||
|
(true, "2.txt".into())
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
workspace_b.update(cx_b, |workspace, cx| {
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
assert_eq!(
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
workspace.active_item(cx).unwrap().project_path(cx),
|
pane.activate_prev_item(true, cx);
|
||||||
Some((worktree_id, "3.txt").into())
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_a, cx_a),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: None,
|
||||||
|
items: vec![
|
||||||
|
(true, "1.txt".into()),
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(false, "4.txt".into()),
|
||||||
|
(false, "3.txt".into()),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pane_summaries(&workspace_b, cx_b),
|
||||||
|
&[
|
||||||
|
PaneSummary {
|
||||||
|
active: false,
|
||||||
|
leader: None,
|
||||||
|
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||||
|
},
|
||||||
|
PaneSummary {
|
||||||
|
active: true,
|
||||||
|
leader: client_a.peer_id(),
|
||||||
|
items: vec![
|
||||||
|
(false, "3.txt".into()),
|
||||||
|
(false, "4.txt".into()),
|
||||||
|
(false, "2.txt".into()),
|
||||||
|
(true, "1.txt".into()),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
|
@ -1074,24 +1389,6 @@ async fn test_peers_simultaneously_following_each_other(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visible_push_notifications(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
|
||||||
let mut ret = Vec::new();
|
|
||||||
for window in cx.windows() {
|
|
||||||
window.read_with(cx, |window| {
|
|
||||||
if let Some(handle) = window
|
|
||||||
.root_view()
|
|
||||||
.clone()
|
|
||||||
.downcast::<ProjectSharedNotification>()
|
|
||||||
{
|
|
||||||
ret.push(handle)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_following_across_workspaces(
|
async fn test_following_across_workspaces(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
|
@ -1304,3 +1601,83 @@ async fn test_following_across_workspaces(
|
||||||
assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
|
assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visible_push_notifications(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
for window in cx.windows() {
|
||||||
|
window.read_with(cx, |window| {
|
||||||
|
if let Some(handle) = window
|
||||||
|
.root_view()
|
||||||
|
.clone()
|
||||||
|
.downcast::<ProjectSharedNotification>()
|
||||||
|
{
|
||||||
|
ret.push(handle)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
struct PaneSummary {
|
||||||
|
active: bool,
|
||||||
|
leader: Option<PeerId>,
|
||||||
|
items: Vec<(bool, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
|
||||||
|
cx.read(|cx| {
|
||||||
|
let active_call = ActiveCall::global(cx).read(cx);
|
||||||
|
let peer_id = active_call.client().peer_id();
|
||||||
|
let room = active_call.room().unwrap().read(cx);
|
||||||
|
let mut result = room
|
||||||
|
.remote_participants()
|
||||||
|
.values()
|
||||||
|
.map(|participant| participant.peer_id)
|
||||||
|
.chain(peer_id)
|
||||||
|
.filter_map(|peer_id| {
|
||||||
|
let followers = room.followers_for(peer_id, project_id);
|
||||||
|
if followers.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((peer_id, followers.to_vec()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
result.sort_by_key(|e| e.0);
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
|
||||||
|
workspace.read_with(cx, |workspace, cx| {
|
||||||
|
let active_pane = workspace.active_pane();
|
||||||
|
workspace
|
||||||
|
.panes()
|
||||||
|
.iter()
|
||||||
|
.map(|pane| {
|
||||||
|
let leader = workspace.leader_for_pane(pane);
|
||||||
|
let active = pane == active_pane;
|
||||||
|
let pane = pane.read(cx);
|
||||||
|
let active_ix = pane.active_item_index();
|
||||||
|
PaneSummary {
|
||||||
|
active,
|
||||||
|
leader,
|
||||||
|
items: pane
|
||||||
|
.items()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, item)| {
|
||||||
|
(
|
||||||
|
ix == active_ix,
|
||||||
|
item.tab_description(0, cx)
|
||||||
|
.map_or(String::new(), |s| s.to_string()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
|
rich_text = { path = "../rich_text" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
recent_projects = {path = "../recent_projects"}
|
recent_projects = {path = "../recent_projects"}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use anyhow::Result;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
||||||
use client::Client;
|
use client::Client;
|
||||||
|
use collections::HashMap;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||||
|
@ -12,12 +13,13 @@ use gpui::{
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
serde_json,
|
serde_json,
|
||||||
views::{ItemType, Select, SelectStyle},
|
views::{ItemType, Select, SelectStyle},
|
||||||
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
|
AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task,
|
||||||
ViewContext, ViewHandle, WeakViewHandle,
|
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use language::language_settings::SoftWrap;
|
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||||
use menu::Confirm;
|
use menu::Confirm;
|
||||||
use project::Fs;
|
use project::Fs;
|
||||||
|
use rich_text::RichText;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -35,6 +37,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
||||||
pub struct ChatPanel {
|
pub struct ChatPanel {
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
|
languages: Arc<LanguageRegistry>,
|
||||||
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
||||||
message_list: ListState<ChatPanel>,
|
message_list: ListState<ChatPanel>,
|
||||||
input_editor: ViewHandle<Editor>,
|
input_editor: ViewHandle<Editor>,
|
||||||
|
@ -47,6 +50,7 @@ pub struct ChatPanel {
|
||||||
subscriptions: Vec<gpui::Subscription>,
|
subscriptions: Vec<gpui::Subscription>,
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
has_focus: bool,
|
has_focus: bool,
|
||||||
|
markdown_data: HashMap<ChannelMessageId, RichText>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -78,6 +82,7 @@ impl ChatPanel {
|
||||||
let fs = workspace.app_state().fs.clone();
|
let fs = workspace.app_state().fs.clone();
|
||||||
let client = workspace.app_state().client.clone();
|
let client = workspace.app_state().client.clone();
|
||||||
let channel_store = workspace.app_state().channel_store.clone();
|
let channel_store = workspace.app_state().channel_store.clone();
|
||||||
|
let languages = workspace.app_state().languages.clone();
|
||||||
|
|
||||||
let input_editor = cx.add_view(|cx| {
|
let input_editor = cx.add_view(|cx| {
|
||||||
let mut editor = Editor::auto_height(
|
let mut editor = Editor::auto_height(
|
||||||
|
@ -130,6 +135,8 @@ impl ChatPanel {
|
||||||
fs,
|
fs,
|
||||||
client,
|
client,
|
||||||
channel_store,
|
channel_store,
|
||||||
|
languages,
|
||||||
|
|
||||||
active_chat: Default::default(),
|
active_chat: Default::default(),
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
message_list,
|
message_list,
|
||||||
|
@ -141,6 +148,7 @@ impl ChatPanel {
|
||||||
workspace: workspace_handle,
|
workspace: workspace_handle,
|
||||||
active: false,
|
active: false,
|
||||||
width: None,
|
width: None,
|
||||||
|
markdown_data: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut old_dock_position = this.position(cx);
|
let mut old_dock_position = this.position(cx);
|
||||||
|
@ -177,6 +185,25 @@ impl ChatPanel {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let markdown = this.languages.language_for_name("Markdown");
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let markdown = markdown.await?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.input_editor.update(cx, |editor, cx| {
|
||||||
|
editor.buffer().update(cx, |multi_buffer, cx| {
|
||||||
|
multi_buffer
|
||||||
|
.as_singleton()
|
||||||
|
.unwrap()
|
||||||
|
.update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
this
|
this
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -327,13 +354,33 @@ impl ChatPanel {
|
||||||
messages.flex(1., true).into_any()
|
messages.flex(1., true).into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix);
|
let (message, is_continuation, is_last) = {
|
||||||
|
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
|
||||||
|
let last_message = active_chat.message(ix.saturating_sub(1));
|
||||||
|
let this_message = active_chat.message(ix);
|
||||||
|
let is_continuation = last_message.id != this_message.id
|
||||||
|
&& this_message.sender.id == last_message.sender.id;
|
||||||
|
|
||||||
|
(
|
||||||
|
active_chat.message(ix).clone(),
|
||||||
|
is_continuation,
|
||||||
|
active_chat.message_count() == ix + 1,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_pending = message.is_pending();
|
||||||
|
let text = self
|
||||||
|
.markdown_data
|
||||||
|
.entry(message.id)
|
||||||
|
.or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None));
|
||||||
|
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
let theme = theme::current(cx);
|
let theme = theme::current(cx);
|
||||||
let style = if message.is_pending() {
|
let style = if is_pending {
|
||||||
&theme.chat_panel.pending_message
|
&theme.chat_panel.pending_message
|
||||||
|
} else if is_continuation {
|
||||||
|
&theme.chat_panel.continuation_message
|
||||||
} else {
|
} else {
|
||||||
&theme.chat_panel.message
|
&theme.chat_panel.message
|
||||||
};
|
};
|
||||||
|
@ -346,52 +393,90 @@ impl ChatPanel {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
enum DeleteMessage {}
|
enum MessageBackgroundHighlight {}
|
||||||
|
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
|
||||||
let body = message.body.clone();
|
let container = style.container.style_for(state);
|
||||||
Flex::column()
|
if is_continuation {
|
||||||
.with_child(
|
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Label::new(
|
text.element(
|
||||||
message.sender.github_login.clone(),
|
theme.editor.syntax.clone(),
|
||||||
style.sender.text.clone(),
|
style.body.clone(),
|
||||||
|
theme.editor.document_highlight_read_background,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.contained()
|
.flex(1., true),
|
||||||
.with_style(style.sender.container),
|
)
|
||||||
|
.with_child(render_remove(message_id_to_remove, cx, &theme))
|
||||||
|
.contained()
|
||||||
|
.with_style(*container)
|
||||||
|
.with_margin_bottom(if is_last {
|
||||||
|
theme.chat_panel.last_message_bottom_spacing
|
||||||
|
} else {
|
||||||
|
0.
|
||||||
|
})
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_child(render_avatar(
|
||||||
|
message.sender.avatar.clone(),
|
||||||
|
&theme,
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
message.sender.github_login.clone(),
|
||||||
|
style.sender.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.sender.container),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
format_timestamp(
|
||||||
|
message.timestamp,
|
||||||
|
now,
|
||||||
|
self.local_timezone,
|
||||||
|
),
|
||||||
|
style.timestamp.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.timestamp.container),
|
||||||
|
)
|
||||||
|
.align_children_center()
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.with_child(render_remove(message_id_to_remove, cx, &theme))
|
||||||
|
.align_children_center(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Label::new(
|
Flex::row()
|
||||||
format_timestamp(message.timestamp, now, self.local_timezone),
|
.with_child(
|
||||||
style.timestamp.text.clone(),
|
text.element(
|
||||||
)
|
theme.editor.syntax.clone(),
|
||||||
.contained()
|
style.body.clone(),
|
||||||
.with_style(style.timestamp.container),
|
theme.editor.document_highlight_read_background,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
// Add a spacer to make everything line up
|
||||||
|
.with_child(render_remove(None, cx, &theme)),
|
||||||
)
|
)
|
||||||
.with_children(message_id_to_remove.map(|id| {
|
.contained()
|
||||||
MouseEventHandler::new::<DeleteMessage, _>(
|
.with_style(*container)
|
||||||
id as usize,
|
.with_margin_bottom(if is_last {
|
||||||
cx,
|
theme.chat_panel.last_message_bottom_spacing
|
||||||
|mouse_state, _| {
|
} else {
|
||||||
let button_style =
|
0.
|
||||||
theme.chat_panel.icon_button.style_for(mouse_state);
|
})
|
||||||
render_icon_button(button_style, "icons/x.svg")
|
.into_any()
|
||||||
.aligned()
|
}
|
||||||
.into_any()
|
})
|
||||||
},
|
.into_any()
|
||||||
)
|
|
||||||
.with_padding(Padding::uniform(2.))
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
||||||
this.remove_message(id, cx);
|
|
||||||
})
|
|
||||||
.flex_float()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.with_child(Text::new(body, style.body.clone()))
|
|
||||||
.contained()
|
|
||||||
.with_style(style.container)
|
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
|
fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
|
||||||
|
@ -565,6 +650,7 @@ impl ChatPanel {
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let chat = open_chat.await?;
|
let chat = open_chat.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.markdown_data = Default::default();
|
||||||
this.set_active_chat(chat, cx);
|
this.set_active_chat(chat, cx);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -589,6 +675,72 @@ impl ChatPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_avatar(avatar: Option<Arc<ImageData>>, theme: &Arc<Theme>) -> AnyElement<ChatPanel> {
|
||||||
|
let avatar_style = theme.chat_panel.avatar;
|
||||||
|
|
||||||
|
avatar
|
||||||
|
.map(|avatar| {
|
||||||
|
Image::from_data(avatar)
|
||||||
|
.with_style(avatar_style.image)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_corner_radius(avatar_style.outer_corner_radius)
|
||||||
|
.constrained()
|
||||||
|
.with_width(avatar_style.outer_width)
|
||||||
|
.with_height(avatar_style.outer_width)
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
Empty::new()
|
||||||
|
.constrained()
|
||||||
|
.with_width(avatar_style.outer_width)
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.chat_panel.avatar_container)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_remove(
|
||||||
|
message_id_to_remove: Option<u64>,
|
||||||
|
cx: &mut ViewContext<'_, '_, ChatPanel>,
|
||||||
|
theme: &Arc<Theme>,
|
||||||
|
) -> AnyElement<ChatPanel> {
|
||||||
|
enum DeleteMessage {}
|
||||||
|
|
||||||
|
message_id_to_remove
|
||||||
|
.map(|id| {
|
||||||
|
MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
|
||||||
|
let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
|
||||||
|
render_icon_button(button_style, "icons/x.svg")
|
||||||
|
.aligned()
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
.with_padding(Padding::uniform(2.))
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.remove_message(id, cx);
|
||||||
|
})
|
||||||
|
.flex_float()
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let style = theme.chat_panel.icon_button.default;
|
||||||
|
|
||||||
|
Empty::new()
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.button_width)
|
||||||
|
.with_height(style.button_width)
|
||||||
|
.contained()
|
||||||
|
.with_uniform_padding(2.)
|
||||||
|
.flex_float()
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl Entity for ChatPanel {
|
impl Entity for ChatPanel {
|
||||||
type Event = Event;
|
type Event = Event;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1937,6 +1937,8 @@ impl CollabPanel {
|
||||||
is_dragged_over = true;
|
is_dragged_over = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let has_messages_notification = channel.unseen_message_id.is_some();
|
||||||
|
|
||||||
MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
|
MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
|
||||||
let row_hovered = state.hovered();
|
let row_hovered = state.hovered();
|
||||||
|
|
||||||
|
@ -1974,11 +1976,7 @@ impl CollabPanel {
|
||||||
.left()
|
.left()
|
||||||
.with_tooltip::<ChannelTooltip>(
|
.with_tooltip::<ChannelTooltip>(
|
||||||
ix,
|
ix,
|
||||||
if is_active {
|
"Join channel",
|
||||||
"Open channel notes"
|
|
||||||
} else {
|
|
||||||
"Join channel"
|
|
||||||
},
|
|
||||||
None,
|
None,
|
||||||
theme.tooltip.clone(),
|
theme.tooltip.clone(),
|
||||||
cx,
|
cx,
|
||||||
|
@ -2022,24 +2020,33 @@ impl CollabPanel {
|
||||||
.flex(1., true)
|
.flex(1., true)
|
||||||
})
|
})
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |_, _| {
|
MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
|
||||||
|
let container_style = collab_theme
|
||||||
|
.disclosure
|
||||||
|
.button
|
||||||
|
.style_for(mouse_state)
|
||||||
|
.container;
|
||||||
|
|
||||||
if channel.unseen_message_id.is_some() {
|
if channel.unseen_message_id.is_some() {
|
||||||
Svg::new("icons/conversations.svg")
|
Svg::new("icons/conversations.svg")
|
||||||
.with_color(collab_theme.channel_note_active_color)
|
.with_color(collab_theme.channel_note_active_color)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_width(collab_theme.channel_hash.width)
|
.with_width(collab_theme.channel_hash.width)
|
||||||
|
.contained()
|
||||||
|
.with_style(container_style)
|
||||||
|
.with_uniform_padding(4.)
|
||||||
.into_any()
|
.into_any()
|
||||||
} else if row_hovered {
|
} else if row_hovered {
|
||||||
Svg::new("icons/conversations.svg")
|
Svg::new("icons/conversations.svg")
|
||||||
.with_color(collab_theme.channel_hash.color)
|
.with_color(collab_theme.channel_hash.color)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_width(collab_theme.channel_hash.width)
|
.with_width(collab_theme.channel_hash.width)
|
||||||
|
.contained()
|
||||||
|
.with_style(container_style)
|
||||||
|
.with_uniform_padding(4.)
|
||||||
.into_any()
|
.into_any()
|
||||||
} else {
|
} else {
|
||||||
Empty::new()
|
Empty::new().into_any()
|
||||||
.constrained()
|
|
||||||
.with_width(collab_theme.channel_hash.width)
|
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
@ -2056,7 +2063,12 @@ impl CollabPanel {
|
||||||
.with_margin_right(4.),
|
.with_margin_right(4.),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |_, cx| {
|
MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
|
||||||
|
let container_style = collab_theme
|
||||||
|
.disclosure
|
||||||
|
.button
|
||||||
|
.style_for(mouse_state)
|
||||||
|
.container;
|
||||||
if row_hovered || channel.unseen_note_version.is_some() {
|
if row_hovered || channel.unseen_note_version.is_some() {
|
||||||
Svg::new("icons/file.svg")
|
Svg::new("icons/file.svg")
|
||||||
.with_color(if channel.unseen_note_version.is_some() {
|
.with_color(if channel.unseen_note_version.is_some() {
|
||||||
|
@ -2067,6 +2079,8 @@ impl CollabPanel {
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_width(collab_theme.channel_hash.width)
|
.with_width(collab_theme.channel_hash.width)
|
||||||
.contained()
|
.contained()
|
||||||
|
.with_style(container_style)
|
||||||
|
.with_uniform_padding(4.)
|
||||||
.with_margin_right(collab_theme.channel_hash.container.margin.left)
|
.with_margin_right(collab_theme.channel_hash.container.margin.left)
|
||||||
.with_tooltip::<NotesTooltip>(
|
.with_tooltip::<NotesTooltip>(
|
||||||
ix as usize,
|
ix as usize,
|
||||||
|
@ -2076,23 +2090,20 @@ impl CollabPanel {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.into_any()
|
.into_any()
|
||||||
} else {
|
} else if has_messages_notification {
|
||||||
Empty::new()
|
Empty::new()
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_width(collab_theme.channel_hash.width)
|
.with_width(collab_theme.channel_hash.width)
|
||||||
.contained()
|
.contained()
|
||||||
|
.with_uniform_padding(4.)
|
||||||
.with_margin_right(collab_theme.channel_hash.container.margin.left)
|
.with_margin_right(collab_theme.channel_hash.container.margin.left)
|
||||||
.into_any()
|
.into_any()
|
||||||
|
} else {
|
||||||
|
Empty::new().into_any()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
let participants =
|
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
|
||||||
this.channel_store.read(cx).channel_participants(channel_id);
|
|
||||||
if is_active || participants.is_empty() {
|
|
||||||
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
|
|
||||||
} else {
|
|
||||||
this.join_channel(channel_id, cx);
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.align_children_center()
|
.align_children_center()
|
||||||
|
|
|
@ -36,6 +36,7 @@ language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
|
rich_text = { path = "../rich_text" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
snippet = { path = "../snippet" }
|
snippet = { path = "../snippet" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
|
|
@ -8,12 +8,12 @@ use futures::FutureExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
|
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
|
||||||
fonts::{HighlightStyle, Underline, Weight},
|
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
|
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
|
||||||
};
|
};
|
||||||
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
|
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
|
||||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
||||||
|
use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText};
|
||||||
use std::{ops::Range, sync::Arc, time::Duration};
|
use std::{ops::Range, sync::Arc, time::Duration};
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
|
|
||||||
|
@ -346,158 +346,25 @@ fn show_hover(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_blocks(
|
fn render_blocks(
|
||||||
theme_id: usize,
|
|
||||||
blocks: &[HoverBlock],
|
blocks: &[HoverBlock],
|
||||||
language_registry: &Arc<LanguageRegistry>,
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
language: Option<&Arc<Language>>,
|
language: Option<&Arc<Language>>,
|
||||||
style: &EditorStyle,
|
) -> RichText {
|
||||||
) -> RenderedInfo {
|
let mut data = RichText {
|
||||||
let mut text = String::new();
|
text: Default::default(),
|
||||||
let mut highlights = Vec::new();
|
highlights: Default::default(),
|
||||||
let mut region_ranges = Vec::new();
|
region_ranges: Default::default(),
|
||||||
let mut regions = Vec::new();
|
regions: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
for block in blocks {
|
for block in blocks {
|
||||||
match &block.kind {
|
match &block.kind {
|
||||||
HoverBlockKind::PlainText => {
|
HoverBlockKind::PlainText => {
|
||||||
new_paragraph(&mut text, &mut Vec::new());
|
new_paragraph(&mut data.text, &mut Vec::new());
|
||||||
text.push_str(&block.text);
|
data.text.push_str(&block.text);
|
||||||
}
|
}
|
||||||
HoverBlockKind::Markdown => {
|
HoverBlockKind::Markdown => {
|
||||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
render_markdown_mut(&block.text, language_registry, language, &mut data)
|
||||||
|
|
||||||
let mut bold_depth = 0;
|
|
||||||
let mut italic_depth = 0;
|
|
||||||
let mut link_url = None;
|
|
||||||
let mut current_language = None;
|
|
||||||
let mut list_stack = Vec::new();
|
|
||||||
|
|
||||||
for event in Parser::new_ext(&block.text, Options::all()) {
|
|
||||||
let prev_len = text.len();
|
|
||||||
match event {
|
|
||||||
Event::Text(t) => {
|
|
||||||
if let Some(language) = ¤t_language {
|
|
||||||
render_code(
|
|
||||||
&mut text,
|
|
||||||
&mut highlights,
|
|
||||||
t.as_ref(),
|
|
||||||
language,
|
|
||||||
style,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
text.push_str(t.as_ref());
|
|
||||||
|
|
||||||
let mut style = HighlightStyle::default();
|
|
||||||
if bold_depth > 0 {
|
|
||||||
style.weight = Some(Weight::BOLD);
|
|
||||||
}
|
|
||||||
if italic_depth > 0 {
|
|
||||||
style.italic = Some(true);
|
|
||||||
}
|
|
||||||
if let Some(link_url) = link_url.clone() {
|
|
||||||
region_ranges.push(prev_len..text.len());
|
|
||||||
regions.push(RenderedRegion {
|
|
||||||
link_url: Some(link_url),
|
|
||||||
code: false,
|
|
||||||
});
|
|
||||||
style.underline = Some(Underline {
|
|
||||||
thickness: 1.0.into(),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if style != HighlightStyle::default() {
|
|
||||||
let mut new_highlight = true;
|
|
||||||
if let Some((last_range, last_style)) = highlights.last_mut() {
|
|
||||||
if last_range.end == prev_len && last_style == &style {
|
|
||||||
last_range.end = text.len();
|
|
||||||
new_highlight = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if new_highlight {
|
|
||||||
highlights.push((prev_len..text.len(), style));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::Code(t) => {
|
|
||||||
text.push_str(t.as_ref());
|
|
||||||
region_ranges.push(prev_len..text.len());
|
|
||||||
if link_url.is_some() {
|
|
||||||
highlights.push((
|
|
||||||
prev_len..text.len(),
|
|
||||||
HighlightStyle {
|
|
||||||
underline: Some(Underline {
|
|
||||||
thickness: 1.0.into(),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
regions.push(RenderedRegion {
|
|
||||||
code: true,
|
|
||||||
link_url: link_url.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Event::Start(tag) => match tag {
|
|
||||||
Tag::Paragraph => new_paragraph(&mut text, &mut list_stack),
|
|
||||||
Tag::Heading(_, _, _) => {
|
|
||||||
new_paragraph(&mut text, &mut list_stack);
|
|
||||||
bold_depth += 1;
|
|
||||||
}
|
|
||||||
Tag::CodeBlock(kind) => {
|
|
||||||
new_paragraph(&mut text, &mut list_stack);
|
|
||||||
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
|
||||||
language_registry
|
|
||||||
.language_for_name(language.as_ref())
|
|
||||||
.now_or_never()
|
|
||||||
.and_then(Result::ok)
|
|
||||||
} else {
|
|
||||||
language.cloned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Tag::Emphasis => italic_depth += 1,
|
|
||||||
Tag::Strong => bold_depth += 1,
|
|
||||||
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
|
|
||||||
Tag::List(number) => {
|
|
||||||
list_stack.push((number, false));
|
|
||||||
}
|
|
||||||
Tag::Item => {
|
|
||||||
let len = list_stack.len();
|
|
||||||
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
|
||||||
*has_content = false;
|
|
||||||
if !text.is_empty() && !text.ends_with('\n') {
|
|
||||||
text.push('\n');
|
|
||||||
}
|
|
||||||
for _ in 0..len - 1 {
|
|
||||||
text.push_str(" ");
|
|
||||||
}
|
|
||||||
if let Some(number) = list_number {
|
|
||||||
text.push_str(&format!("{}. ", number));
|
|
||||||
*number += 1;
|
|
||||||
*has_content = false;
|
|
||||||
} else {
|
|
||||||
text.push_str("- ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
Event::End(tag) => match tag {
|
|
||||||
Tag::Heading(_, _, _) => bold_depth -= 1,
|
|
||||||
Tag::CodeBlock(_) => current_language = None,
|
|
||||||
Tag::Emphasis => italic_depth -= 1,
|
|
||||||
Tag::Strong => bold_depth -= 1,
|
|
||||||
Tag::Link(_, _, _) => link_url = None,
|
|
||||||
Tag::List(_) => drop(list_stack.pop()),
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
Event::HardBreak => text.push('\n'),
|
|
||||||
Event::SoftBreak => text.push(' '),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
HoverBlockKind::Code { language } => {
|
HoverBlockKind::Code { language } => {
|
||||||
if let Some(language) = language_registry
|
if let Some(language) = language_registry
|
||||||
|
@ -505,62 +372,17 @@ fn render_blocks(
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(Result::ok)
|
.and_then(Result::ok)
|
||||||
{
|
{
|
||||||
render_code(&mut text, &mut highlights, &block.text, &language, style);
|
render_code(&mut data.text, &mut data.highlights, &block.text, &language);
|
||||||
} else {
|
} else {
|
||||||
text.push_str(&block.text);
|
data.text.push_str(&block.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderedInfo {
|
data.text = data.text.trim().to_string();
|
||||||
theme_id,
|
|
||||||
text: text.trim().to_string(),
|
|
||||||
highlights,
|
|
||||||
region_ranges,
|
|
||||||
regions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_code(
|
data
|
||||||
text: &mut String,
|
|
||||||
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
|
|
||||||
content: &str,
|
|
||||||
language: &Arc<Language>,
|
|
||||||
style: &EditorStyle,
|
|
||||||
) {
|
|
||||||
let prev_len = text.len();
|
|
||||||
text.push_str(content);
|
|
||||||
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
|
|
||||||
if let Some(style) = highlight_id.style(&style.syntax) {
|
|
||||||
highlights.push((prev_len + range.start..prev_len + range.end, style));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
|
|
||||||
let mut is_subsequent_paragraph_of_list = false;
|
|
||||||
if let Some((_, has_content)) = list_stack.last_mut() {
|
|
||||||
if *has_content {
|
|
||||||
is_subsequent_paragraph_of_list = true;
|
|
||||||
} else {
|
|
||||||
*has_content = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !text.is_empty() {
|
|
||||||
if !text.ends_with('\n') {
|
|
||||||
text.push('\n');
|
|
||||||
}
|
|
||||||
text.push('\n');
|
|
||||||
}
|
|
||||||
for _ in 0..list_stack.len().saturating_sub(1) {
|
|
||||||
text.push_str(" ");
|
|
||||||
}
|
|
||||||
if is_subsequent_paragraph_of_list {
|
|
||||||
text.push_str(" ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -623,22 +445,7 @@ pub struct InfoPopover {
|
||||||
symbol_range: RangeInEditor,
|
symbol_range: RangeInEditor,
|
||||||
pub blocks: Vec<HoverBlock>,
|
pub blocks: Vec<HoverBlock>,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
rendered_content: Option<RenderedInfo>,
|
rendered_content: Option<RichText>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct RenderedInfo {
|
|
||||||
theme_id: usize,
|
|
||||||
text: String,
|
|
||||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
|
||||||
region_ranges: Vec<Range<usize>>,
|
|
||||||
regions: Vec<RenderedRegion>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct RenderedRegion {
|
|
||||||
code: bool,
|
|
||||||
link_url: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InfoPopover {
|
impl InfoPopover {
|
||||||
|
@ -647,63 +454,24 @@ impl InfoPopover {
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) -> AnyElement<Editor> {
|
) -> AnyElement<Editor> {
|
||||||
if let Some(rendered) = &self.rendered_content {
|
|
||||||
if rendered.theme_id != style.theme_id {
|
|
||||||
self.rendered_content = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rendered_content = self.rendered_content.get_or_insert_with(|| {
|
let rendered_content = self.rendered_content.get_or_insert_with(|| {
|
||||||
render_blocks(
|
render_blocks(
|
||||||
style.theme_id,
|
|
||||||
&self.blocks,
|
&self.blocks,
|
||||||
self.project.read(cx).languages(),
|
self.project.read(cx).languages(),
|
||||||
self.language.as_ref(),
|
self.language.as_ref(),
|
||||||
style,
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
|
MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
|
||||||
let mut region_id = 0;
|
|
||||||
let view_id = cx.view_id();
|
|
||||||
|
|
||||||
let code_span_background_color = style.document_highlight_read_background;
|
let code_span_background_color = style.document_highlight_read_background;
|
||||||
let regions = rendered_content.regions.clone();
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.scrollable::<HoverBlock>(1, None, cx)
|
.scrollable::<HoverBlock>(1, None, cx)
|
||||||
.with_child(
|
.with_child(rendered_content.element(
|
||||||
Text::new(rendered_content.text.clone(), style.text.clone())
|
style.syntax.clone(),
|
||||||
.with_highlights(rendered_content.highlights.clone())
|
style.text.clone(),
|
||||||
.with_custom_runs(
|
code_span_background_color,
|
||||||
rendered_content.region_ranges.clone(),
|
cx,
|
||||||
move |ix, bounds, cx| {
|
))
|
||||||
region_id += 1;
|
|
||||||
let region = regions[ix].clone();
|
|
||||||
if let Some(url) = region.link_url {
|
|
||||||
cx.scene().push_cursor_region(CursorRegion {
|
|
||||||
bounds,
|
|
||||||
style: CursorStyle::PointingHand,
|
|
||||||
});
|
|
||||||
cx.scene().push_mouse_region(
|
|
||||||
MouseRegion::new::<Self>(view_id, region_id, bounds)
|
|
||||||
.on_click::<Editor, _>(
|
|
||||||
MouseButton::Left,
|
|
||||||
move |_, _, cx| cx.platform().open_url(&url),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if region.code {
|
|
||||||
cx.scene().push_quad(gpui::Quad {
|
|
||||||
bounds,
|
|
||||||
background: Some(code_span_background_color),
|
|
||||||
border: Default::default(),
|
|
||||||
corner_radii: (2.0).into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_soft_wrap(true),
|
|
||||||
)
|
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(style.hover_popover.container)
|
.with_style(style.hover_popover.container)
|
||||||
})
|
})
|
||||||
|
@ -799,11 +567,12 @@ mod tests {
|
||||||
InlayId,
|
InlayId,
|
||||||
};
|
};
|
||||||
use collections::BTreeSet;
|
use collections::BTreeSet;
|
||||||
use gpui::fonts::Weight;
|
use gpui::fonts::{HighlightStyle, Underline, Weight};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use project::{HoverBlock, HoverBlockKind};
|
use project::{HoverBlock, HoverBlockKind};
|
||||||
|
use rich_text::Highlight;
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::test::marked_text_ranges;
|
use util::test::marked_text_ranges;
|
||||||
|
@ -1014,7 +783,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||||
cx.editor(|editor, cx| {
|
cx.editor(|editor, _| {
|
||||||
let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
|
let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
blocks,
|
blocks,
|
||||||
|
@ -1024,8 +793,7 @@ mod tests {
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
|
|
||||||
let style = editor.style(cx);
|
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||||
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rendered.text,
|
rendered.text,
|
||||||
code_str.trim(),
|
code_str.trim(),
|
||||||
|
@ -1217,7 +985,7 @@ mod tests {
|
||||||
expected_styles,
|
expected_styles,
|
||||||
} in &rows[0..]
|
} in &rows[0..]
|
||||||
{
|
{
|
||||||
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
|
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||||
|
|
||||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||||
let expected_highlights = ranges
|
let expected_highlights = ranges
|
||||||
|
@ -1228,8 +996,21 @@ mod tests {
|
||||||
rendered.text, expected_text,
|
rendered.text, expected_text,
|
||||||
"wrong text for input {blocks:?}"
|
"wrong text for input {blocks:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let rendered_highlights: Vec<_> = rendered
|
||||||
|
.highlights
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(range, highlight)| {
|
||||||
|
let style = match highlight {
|
||||||
|
Highlight::Id(id) => id.style(&style.syntax)?,
|
||||||
|
Highlight::Highlight(style) => style.clone(),
|
||||||
|
};
|
||||||
|
Some((range.clone(), style))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rendered.highlights, expected_highlights,
|
rendered_highlights, expected_highlights,
|
||||||
"wrong highlights for input {blocks:?}"
|
"wrong highlights for input {blocks:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,13 +107,23 @@ fn matching_history_item_paths(
|
||||||
) -> HashMap<Arc<Path>, PathMatch> {
|
) -> HashMap<Arc<Path>, PathMatch> {
|
||||||
let history_items_by_worktrees = history_items
|
let history_items_by_worktrees = history_items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|found_path| {
|
.filter_map(|found_path| {
|
||||||
let path = &found_path.project.path;
|
|
||||||
let candidate = PathMatchCandidate {
|
let candidate = PathMatchCandidate {
|
||||||
path,
|
path: &found_path.project.path,
|
||||||
char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
|
// Only match history items names, otherwise their paths may match too many queries, producing false positives.
|
||||||
|
// E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
|
||||||
|
// it would be shown first always, despite the latter being a better match.
|
||||||
|
char_bag: CharBag::from_iter(
|
||||||
|
found_path
|
||||||
|
.project
|
||||||
|
.path
|
||||||
|
.file_name()?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_lowercase()
|
||||||
|
.chars(),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
(found_path.project.worktree_id, candidate)
|
Some((found_path.project.worktree_id, candidate))
|
||||||
})
|
})
|
||||||
.fold(
|
.fold(
|
||||||
HashMap::default(),
|
HashMap::default(),
|
||||||
|
@ -212,6 +222,10 @@ fn toggle_or_cycle_file_finder(
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|found_path| found_path.absolute.as_ref())
|
.and_then(|found_path| found_path.absolute.as_ref())
|
||||||
})
|
})
|
||||||
|
.filter(|(_, history_abs_path)| match history_abs_path {
|
||||||
|
Some(abs_path) => history_file_exists(abs_path),
|
||||||
|
None => true,
|
||||||
|
})
|
||||||
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
|
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
|
||||||
)
|
)
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -236,6 +250,16 @@ fn toggle_or_cycle_file_finder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
fn history_file_exists(abs_path: &PathBuf) -> bool {
|
||||||
|
abs_path.exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn history_file_exists(abs_path: &PathBuf) -> bool {
|
||||||
|
!abs_path.ends_with("nonexistent.rs")
|
||||||
|
}
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Selected(ProjectPath),
|
Selected(ProjectPath),
|
||||||
Dismissed,
|
Dismissed,
|
||||||
|
@ -505,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
project
|
project
|
||||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||||
.is_some()
|
.is_some()
|
||||||
|| (project.is_local()
|
|| (project.is_local() && history_item.absolute.is_some())
|
||||||
&& history_item
|
|
||||||
.absolute
|
|
||||||
.as_ref()
|
|
||||||
.filter(|abs_path| abs_path.exists())
|
|
||||||
.is_some())
|
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|p| (p, None))
|
.map(|p| (p, None))
|
||||||
|
@ -1803,6 +1822,202 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_history_items_vs_very_good_external_match(
|
||||||
|
deterministic: Arc<gpui::executor::Deterministic>,
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/src",
|
||||||
|
json!({
|
||||||
|
"collab_ui": {
|
||||||
|
"first.rs": "// First Rust file",
|
||||||
|
"second.rs": "// Second Rust file",
|
||||||
|
"third.rs": "// Third Rust file",
|
||||||
|
"collab_ui.rs": "// Fourth Rust file",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
|
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
|
let workspace = window.root(cx);
|
||||||
|
// generate some history to select from
|
||||||
|
open_close_queried_buffer(
|
||||||
|
"fir",
|
||||||
|
1,
|
||||||
|
"first.rs",
|
||||||
|
window.into(),
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
open_close_queried_buffer(
|
||||||
|
"sec",
|
||||||
|
1,
|
||||||
|
"second.rs",
|
||||||
|
window.into(),
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
open_close_queried_buffer(
|
||||||
|
"thi",
|
||||||
|
1,
|
||||||
|
"third.rs",
|
||||||
|
window.into(),
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
open_close_queried_buffer(
|
||||||
|
"sec",
|
||||||
|
1,
|
||||||
|
"second.rs",
|
||||||
|
window.into(),
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.dispatch_action(window.into(), Toggle);
|
||||||
|
let query = "collab_ui";
|
||||||
|
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||||
|
finder
|
||||||
|
.update(cx, |finder, cx| {
|
||||||
|
finder.delegate_mut().update_matches(query.to_string(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
finder.read_with(cx, |finder, _| {
|
||||||
|
let delegate = finder.delegate();
|
||||||
|
assert!(
|
||||||
|
delegate.matches.history.is_empty(),
|
||||||
|
"History items should not math query {query}, they should be matched by name only"
|
||||||
|
);
|
||||||
|
|
||||||
|
let search_entries = delegate
|
||||||
|
.matches
|
||||||
|
.search
|
||||||
|
.iter()
|
||||||
|
.map(|path_match| path_match.path.to_path_buf())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
search_entries,
|
||||||
|
vec![
|
||||||
|
PathBuf::from("collab_ui/collab_ui.rs"),
|
||||||
|
PathBuf::from("collab_ui/third.rs"),
|
||||||
|
PathBuf::from("collab_ui/first.rs"),
|
||||||
|
PathBuf::from("collab_ui/second.rs"),
|
||||||
|
],
|
||||||
|
"Despite all search results having the same directory name, the most matching one should be on top"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_nonexistent_history_items_not_shown(
|
||||||
|
deterministic: Arc<gpui::executor::Deterministic>,
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/src",
|
||||||
|
json!({
|
||||||
|
"test": {
|
||||||
|
"first.rs": "// First Rust file",
|
||||||
|
"nonexistent.rs": "// Second Rust file",
|
||||||
|
"third.rs": "// Third Rust file",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
|
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
|
let workspace = window.root(cx);
|
||||||
|
// generate some history to select from
|
||||||
|
open_close_queried_buffer(
|
||||||
|
"fir",
|
||||||
|
1,
|
||||||
|
"first.rs",
|
||||||
|
window.into(),
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
open_close_queried_buffer(
|
||||||
|
"non",
|
||||||
|
1,
|
||||||
|
"nonexistent.rs",
|
||||||
|
window.into(),
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
open_close_queried_buffer(
|
||||||
|
"thi",
|
||||||
|
1,
|
||||||
|
"third.rs",
|
||||||
|
window.into(),
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
open_close_queried_buffer(
|
||||||
|
"fir",
|
||||||
|
1,
|
||||||
|
"first.rs",
|
||||||
|
window.into(),
|
||||||
|
&workspace,
|
||||||
|
&deterministic,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.dispatch_action(window.into(), Toggle);
|
||||||
|
let query = "rs";
|
||||||
|
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||||
|
finder
|
||||||
|
.update(cx, |finder, cx| {
|
||||||
|
finder.delegate_mut().update_matches(query.to_string(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
finder.read_with(cx, |finder, _| {
|
||||||
|
let delegate = finder.delegate();
|
||||||
|
let history_entries = delegate
|
||||||
|
.matches
|
||||||
|
.history
|
||||||
|
.iter()
|
||||||
|
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
history_entries,
|
||||||
|
vec![
|
||||||
|
PathBuf::from("test/first.rs"),
|
||||||
|
PathBuf::from("test/third.rs"),
|
||||||
|
],
|
||||||
|
"Should have all opened files in the history, except the ones that do not exist on disk"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fn open_close_queried_buffer(
|
async fn open_close_queried_buffer(
|
||||||
input: &str,
|
input: &str,
|
||||||
expected_matches: usize,
|
expected_matches: usize,
|
||||||
|
|
|
@ -441,7 +441,7 @@ mod tests {
|
||||||
score,
|
score,
|
||||||
worktree_id: 0,
|
worktree_id: 0,
|
||||||
positions: Vec::new(),
|
positions: Vec::new(),
|
||||||
path: candidate.path.clone(),
|
path: Arc::from(candidate.path),
|
||||||
path_prefix: "".into(),
|
path_prefix: "".into(),
|
||||||
distance_to_relative_ancestor: usize::MAX,
|
distance_to_relative_ancestor: usize::MAX,
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PathMatchCandidate<'a> {
|
pub struct PathMatchCandidate<'a> {
|
||||||
pub path: &'a Arc<Path>,
|
pub path: &'a Path,
|
||||||
pub char_bag: CharBag,
|
pub char_bag: CharBag,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
|
||||||
score,
|
score,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
positions: Vec::new(),
|
positions: Vec::new(),
|
||||||
path: candidate.path.clone(),
|
path: Arc::from(candidate.path),
|
||||||
path_prefix: Arc::from(""),
|
path_prefix: Arc::from(""),
|
||||||
distance_to_relative_ancestor: usize::MAX,
|
distance_to_relative_ancestor: usize::MAX,
|
||||||
},
|
},
|
||||||
|
@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||||
score,
|
score,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
positions: Vec::new(),
|
positions: Vec::new(),
|
||||||
path: candidate.path.clone(),
|
path: Arc::from(candidate.path),
|
||||||
path_prefix: candidate_set.prefix(),
|
path_prefix: candidate_set.prefix(),
|
||||||
distance_to_relative_ancestor: relative_to.as_ref().map_or(
|
distance_to_relative_ancestor: relative_to.as_ref().map_or(
|
||||||
usize::MAX,
|
usize::MAX,
|
||||||
|
|
30
crates/rich_text/Cargo.toml
Normal file
30
crates/rich_text/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "rich_text"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/rich_text.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = [
|
||||||
|
"gpui/test-support",
|
||||||
|
"util/test-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
language = { path = "../language" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
anyhow.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||||
|
smallvec.workspace = true
|
||||||
|
smol.workspace = true
|
287
crates/rich_text/src/rich_text.rs
Normal file
287
crates/rich_text/src/rich_text.rs
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
use futures::FutureExt;
|
||||||
|
use gpui::{
|
||||||
|
color::Color,
|
||||||
|
elements::Text,
|
||||||
|
fonts::{HighlightStyle, TextStyle, Underline, Weight},
|
||||||
|
platform::{CursorStyle, MouseButton},
|
||||||
|
AnyElement, CursorRegion, Element, MouseRegion, ViewContext,
|
||||||
|
};
|
||||||
|
use language::{HighlightId, Language, LanguageRegistry};
|
||||||
|
use theme::SyntaxTheme;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Highlight {
|
||||||
|
Id(HighlightId),
|
||||||
|
Highlight(HighlightStyle),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RichText {
|
||||||
|
pub text: String,
|
||||||
|
pub highlights: Vec<(Range<usize>, Highlight)>,
|
||||||
|
pub region_ranges: Vec<Range<usize>>,
|
||||||
|
pub regions: Vec<RenderedRegion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RenderedRegion {
|
||||||
|
code: bool,
|
||||||
|
link_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RichText {
|
||||||
|
pub fn element<V: 'static>(
|
||||||
|
&self,
|
||||||
|
syntax: Arc<SyntaxTheme>,
|
||||||
|
style: TextStyle,
|
||||||
|
code_span_background_color: Color,
|
||||||
|
cx: &mut ViewContext<V>,
|
||||||
|
) -> AnyElement<V> {
|
||||||
|
let mut region_id = 0;
|
||||||
|
let view_id = cx.view_id();
|
||||||
|
|
||||||
|
let regions = self.regions.clone();
|
||||||
|
|
||||||
|
enum Markdown {}
|
||||||
|
Text::new(self.text.clone(), style.clone())
|
||||||
|
.with_highlights(
|
||||||
|
self.highlights
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(range, highlight)| {
|
||||||
|
let style = match highlight {
|
||||||
|
Highlight::Id(id) => id.style(&syntax)?,
|
||||||
|
Highlight::Highlight(style) => style.clone(),
|
||||||
|
};
|
||||||
|
Some((range.clone(), style))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| {
|
||||||
|
region_id += 1;
|
||||||
|
let region = regions[ix].clone();
|
||||||
|
if let Some(url) = region.link_url {
|
||||||
|
cx.scene().push_cursor_region(CursorRegion {
|
||||||
|
bounds,
|
||||||
|
style: CursorStyle::PointingHand,
|
||||||
|
});
|
||||||
|
cx.scene().push_mouse_region(
|
||||||
|
MouseRegion::new::<Markdown>(view_id, region_id, bounds)
|
||||||
|
.on_click::<V, _>(MouseButton::Left, move |_, _, cx| {
|
||||||
|
cx.platform().open_url(&url)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if region.code {
|
||||||
|
cx.scene().push_quad(gpui::Quad {
|
||||||
|
bounds,
|
||||||
|
background: Some(code_span_background_color),
|
||||||
|
border: Default::default(),
|
||||||
|
corner_radii: (2.0).into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_soft_wrap(true)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_markdown_mut(
|
||||||
|
block: &str,
|
||||||
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
|
language: Option<&Arc<Language>>,
|
||||||
|
data: &mut RichText,
|
||||||
|
) {
|
||||||
|
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
||||||
|
|
||||||
|
let mut bold_depth = 0;
|
||||||
|
let mut italic_depth = 0;
|
||||||
|
let mut link_url = None;
|
||||||
|
let mut current_language = None;
|
||||||
|
let mut list_stack = Vec::new();
|
||||||
|
|
||||||
|
for event in Parser::new_ext(&block, Options::all()) {
|
||||||
|
let prev_len = data.text.len();
|
||||||
|
match event {
|
||||||
|
Event::Text(t) => {
|
||||||
|
if let Some(language) = ¤t_language {
|
||||||
|
render_code(&mut data.text, &mut data.highlights, t.as_ref(), language);
|
||||||
|
} else {
|
||||||
|
data.text.push_str(t.as_ref());
|
||||||
|
|
||||||
|
let mut style = HighlightStyle::default();
|
||||||
|
if bold_depth > 0 {
|
||||||
|
style.weight = Some(Weight::BOLD);
|
||||||
|
}
|
||||||
|
if italic_depth > 0 {
|
||||||
|
style.italic = Some(true);
|
||||||
|
}
|
||||||
|
if let Some(link_url) = link_url.clone() {
|
||||||
|
data.region_ranges.push(prev_len..data.text.len());
|
||||||
|
data.regions.push(RenderedRegion {
|
||||||
|
link_url: Some(link_url),
|
||||||
|
code: false,
|
||||||
|
});
|
||||||
|
style.underline = Some(Underline {
|
||||||
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if style != HighlightStyle::default() {
|
||||||
|
let mut new_highlight = true;
|
||||||
|
if let Some((last_range, last_style)) = data.highlights.last_mut() {
|
||||||
|
if last_range.end == prev_len
|
||||||
|
&& last_style == &Highlight::Highlight(style)
|
||||||
|
{
|
||||||
|
last_range.end = data.text.len();
|
||||||
|
new_highlight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if new_highlight {
|
||||||
|
data.highlights
|
||||||
|
.push((prev_len..data.text.len(), Highlight::Highlight(style)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Code(t) => {
|
||||||
|
data.text.push_str(t.as_ref());
|
||||||
|
data.region_ranges.push(prev_len..data.text.len());
|
||||||
|
if link_url.is_some() {
|
||||||
|
data.highlights.push((
|
||||||
|
prev_len..data.text.len(),
|
||||||
|
Highlight::Highlight(HighlightStyle {
|
||||||
|
underline: Some(Underline {
|
||||||
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
data.regions.push(RenderedRegion {
|
||||||
|
code: true,
|
||||||
|
link_url: link_url.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Event::Start(tag) => match tag {
|
||||||
|
Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack),
|
||||||
|
Tag::Heading(_, _, _) => {
|
||||||
|
new_paragraph(&mut data.text, &mut list_stack);
|
||||||
|
bold_depth += 1;
|
||||||
|
}
|
||||||
|
Tag::CodeBlock(kind) => {
|
||||||
|
new_paragraph(&mut data.text, &mut list_stack);
|
||||||
|
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
||||||
|
language_registry
|
||||||
|
.language_for_name(language.as_ref())
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(Result::ok)
|
||||||
|
} else {
|
||||||
|
language.cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::Emphasis => italic_depth += 1,
|
||||||
|
Tag::Strong => bold_depth += 1,
|
||||||
|
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
|
||||||
|
Tag::List(number) => {
|
||||||
|
list_stack.push((number, false));
|
||||||
|
}
|
||||||
|
Tag::Item => {
|
||||||
|
let len = list_stack.len();
|
||||||
|
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
||||||
|
*has_content = false;
|
||||||
|
if !data.text.is_empty() && !data.text.ends_with('\n') {
|
||||||
|
data.text.push('\n');
|
||||||
|
}
|
||||||
|
for _ in 0..len - 1 {
|
||||||
|
data.text.push_str(" ");
|
||||||
|
}
|
||||||
|
if let Some(number) = list_number {
|
||||||
|
data.text.push_str(&format!("{}. ", number));
|
||||||
|
*number += 1;
|
||||||
|
*has_content = false;
|
||||||
|
} else {
|
||||||
|
data.text.push_str("- ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Event::End(tag) => match tag {
|
||||||
|
Tag::Heading(_, _, _) => bold_depth -= 1,
|
||||||
|
Tag::CodeBlock(_) => current_language = None,
|
||||||
|
Tag::Emphasis => italic_depth -= 1,
|
||||||
|
Tag::Strong => bold_depth -= 1,
|
||||||
|
Tag::Link(_, _, _) => link_url = None,
|
||||||
|
Tag::List(_) => drop(list_stack.pop()),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Event::HardBreak => data.text.push('\n'),
|
||||||
|
Event::SoftBreak => data.text.push(' '),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_markdown(
|
||||||
|
block: String,
|
||||||
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
|
language: Option<&Arc<Language>>,
|
||||||
|
) -> RichText {
|
||||||
|
let mut data = RichText {
|
||||||
|
text: Default::default(),
|
||||||
|
highlights: Default::default(),
|
||||||
|
region_ranges: Default::default(),
|
||||||
|
regions: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render_markdown_mut(&block, language_registry, language, &mut data);
|
||||||
|
|
||||||
|
data.text = data.text.trim().to_string();
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_code(
|
||||||
|
text: &mut String,
|
||||||
|
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
||||||
|
content: &str,
|
||||||
|
language: &Arc<Language>,
|
||||||
|
) {
|
||||||
|
let prev_len = text.len();
|
||||||
|
text.push_str(content);
|
||||||
|
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
|
||||||
|
highlights.push((
|
||||||
|
prev_len + range.start..prev_len + range.end,
|
||||||
|
Highlight::Id(highlight_id),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
|
||||||
|
let mut is_subsequent_paragraph_of_list = false;
|
||||||
|
if let Some((_, has_content)) = list_stack.last_mut() {
|
||||||
|
if *has_content {
|
||||||
|
is_subsequent_paragraph_of_list = true;
|
||||||
|
} else {
|
||||||
|
*has_content = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !text.is_empty() {
|
||||||
|
if !text.ends_with('\n') {
|
||||||
|
text.push('\n');
|
||||||
|
}
|
||||||
|
text.push('\n');
|
||||||
|
}
|
||||||
|
for _ in 0..list_stack.len().saturating_sub(1) {
|
||||||
|
text.push_str(" ");
|
||||||
|
}
|
||||||
|
if is_subsequent_paragraph_of_list {
|
||||||
|
text.push_str(" ");
|
||||||
|
}
|
||||||
|
}
|
|
@ -634,7 +634,11 @@ pub struct ChatPanel {
|
||||||
pub list: ContainerStyle,
|
pub list: ContainerStyle,
|
||||||
pub channel_select: ChannelSelect,
|
pub channel_select: ChannelSelect,
|
||||||
pub input_editor: FieldEditor,
|
pub input_editor: FieldEditor,
|
||||||
|
pub avatar: AvatarStyle,
|
||||||
|
pub avatar_container: ContainerStyle,
|
||||||
pub message: ChatMessage,
|
pub message: ChatMessage,
|
||||||
|
pub continuation_message: ChatMessage,
|
||||||
|
pub last_message_bottom_spacing: f32,
|
||||||
pub pending_message: ChatMessage,
|
pub pending_message: ChatMessage,
|
||||||
pub sign_in_prompt: Interactive<TextStyle>,
|
pub sign_in_prompt: Interactive<TextStyle>,
|
||||||
pub icon_button: Interactive<IconButton>,
|
pub icon_button: Interactive<IconButton>,
|
||||||
|
@ -643,7 +647,7 @@ pub struct ChatPanel {
|
||||||
#[derive(Deserialize, Default, JsonSchema)]
|
#[derive(Deserialize, Default, JsonSchema)]
|
||||||
pub struct ChatMessage {
|
pub struct ChatMessage {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub container: ContainerStyle,
|
pub container: Interactive<ContainerStyle>,
|
||||||
pub body: TextStyle,
|
pub body: TextStyle,
|
||||||
pub sender: ContainedText,
|
pub sender: ContainedText,
|
||||||
pub timestamp: ContainedText,
|
pub timestamp: ContainedText,
|
||||||
|
|
|
@ -139,6 +139,12 @@ impl<P> PathLikeWithPosition<P> {
|
||||||
column: None,
|
column: None,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
let maybe_col_str =
|
||||||
|
if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
|
||||||
|
&maybe_col_str[..maybe_col_str.len() - 1]
|
||||||
|
} else {
|
||||||
|
maybe_col_str
|
||||||
|
};
|
||||||
match maybe_col_str.parse::<u32>() {
|
match maybe_col_str.parse::<u32>() {
|
||||||
Ok(col) => Ok(Self {
|
Ok(col) => Ok(Self {
|
||||||
path_like: parse_path_like_str(path_like_str)?,
|
path_like: parse_path_like_str(path_like_str)?,
|
||||||
|
@ -241,7 +247,6 @@ mod tests {
|
||||||
"test_file.rs:1::",
|
"test_file.rs:1::",
|
||||||
"test_file.rs::1:2",
|
"test_file.rs::1:2",
|
||||||
"test_file.rs:1::2",
|
"test_file.rs:1::2",
|
||||||
"test_file.rs:1:2:",
|
|
||||||
"test_file.rs:1:2:3",
|
"test_file.rs:1:2:3",
|
||||||
] {
|
] {
|
||||||
let actual = parse_str(input);
|
let actual = parse_str(input);
|
||||||
|
@ -277,6 +282,14 @@ mod tests {
|
||||||
column: None,
|
column: None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"crates/file_finder/src/file_finder.rs:1902:13:",
|
||||||
|
PathLikeWithPosition {
|
||||||
|
path_like: "crates/file_finder/src/file_finder.rs".to_string(),
|
||||||
|
row: Some(1902),
|
||||||
|
column: Some(13),
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (input, expected) in input_and_expected {
|
for (input, expected) in input_and_expected {
|
||||||
|
|
|
@ -78,10 +78,14 @@ fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
|
||||||
2 => format!("{:b}", result),
|
2 => format!("{:b}", result),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
if selection.is_empty() {
|
edits.push((range.clone(), replace));
|
||||||
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
}
|
||||||
}
|
if selection.is_empty() {
|
||||||
edits.push((range, replace));
|
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if selection.is_empty() {
|
||||||
|
new_anchors.push((true, snapshot.anchor_after(start)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,6 +230,8 @@ mod test {
|
||||||
cx.assert_matches_neovim("(ˇ0b10f)", ["ctrl-a"], "(0b1ˇ1f)")
|
cx.assert_matches_neovim("(ˇ0b10f)", ["ctrl-a"], "(0b1ˇ1f)")
|
||||||
.await;
|
.await;
|
||||||
cx.assert_matches_neovim("ˇ-1", ["ctrl-a"], "ˇ0").await;
|
cx.assert_matches_neovim("ˇ-1", ["ctrl-a"], "ˇ0").await;
|
||||||
|
cx.assert_matches_neovim("banˇana", ["ctrl-a"], "banˇana")
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -13,3 +13,6 @@
|
||||||
{"Put":{"state":"ˇ-1"}}
|
{"Put":{"state":"ˇ-1"}}
|
||||||
{"Key":"ctrl-a"}
|
{"Key":"ctrl-a"}
|
||||||
{"Get":{"state":"ˇ0","mode":"Normal"}}
|
{"Get":{"state":"ˇ0","mode":"Normal"}}
|
||||||
|
{"Put":{"state":"banˇana"}}
|
||||||
|
{"Key":"ctrl-a"}
|
||||||
|
{"Get":{"state":"banˇana","mode":"Normal"}}
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use call::{ActiveCall, ParticipantLocation};
|
use call::{ActiveCall, ParticipantLocation};
|
||||||
|
use collections::HashMap;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
|
@ -13,6 +10,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
|
|
||||||
const HANDLE_HITBOX_SIZE: f32 = 4.0;
|
const HANDLE_HITBOX_SIZE: f32 = 4.0;
|
||||||
|
@ -95,7 +93,7 @@ impl PaneGroup {
|
||||||
&self,
|
&self,
|
||||||
project: &ModelHandle<Project>,
|
project: &ModelHandle<Project>,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
follower_states: &FollowerStatesByLeader,
|
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||||
active_pane: &ViewHandle<Pane>,
|
active_pane: &ViewHandle<Pane>,
|
||||||
zoomed: Option<&AnyViewHandle>,
|
zoomed: Option<&AnyViewHandle>,
|
||||||
|
@ -162,7 +160,7 @@ impl Member {
|
||||||
project: &ModelHandle<Project>,
|
project: &ModelHandle<Project>,
|
||||||
basis: usize,
|
basis: usize,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
follower_states: &FollowerStatesByLeader,
|
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||||
active_pane: &ViewHandle<Pane>,
|
active_pane: &ViewHandle<Pane>,
|
||||||
zoomed: Option<&AnyViewHandle>,
|
zoomed: Option<&AnyViewHandle>,
|
||||||
|
@ -179,19 +177,10 @@ impl Member {
|
||||||
ChildView::new(pane, cx).into_any()
|
ChildView::new(pane, cx).into_any()
|
||||||
};
|
};
|
||||||
|
|
||||||
let leader = follower_states
|
let leader = follower_states.get(pane).and_then(|state| {
|
||||||
.iter()
|
let room = active_call?.read(cx).room()?.read(cx);
|
||||||
.find_map(|(leader_id, follower_states)| {
|
room.remote_participant_for_peer_id(state.leader_id)
|
||||||
if follower_states.contains_key(pane) {
|
});
|
||||||
Some(leader_id)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.and_then(|leader_id| {
|
|
||||||
let room = active_call?.read(cx).room()?.read(cx);
|
|
||||||
room.remote_participant_for_peer_id(*leader_id)
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut leader_border = Border::default();
|
let mut leader_border = Border::default();
|
||||||
let mut leader_status_box = None;
|
let mut leader_status_box = None;
|
||||||
|
@ -486,7 +475,7 @@ impl PaneAxis {
|
||||||
project: &ModelHandle<Project>,
|
project: &ModelHandle<Project>,
|
||||||
basis: usize,
|
basis: usize,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
follower_state: &FollowerStatesByLeader,
|
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||||
active_pane: &ViewHandle<Pane>,
|
active_pane: &ViewHandle<Pane>,
|
||||||
zoomed: Option<&AnyViewHandle>,
|
zoomed: Option<&AnyViewHandle>,
|
||||||
|
@ -515,7 +504,7 @@ impl PaneAxis {
|
||||||
project,
|
project,
|
||||||
(basis + ix) * 10,
|
(basis + ix) * 10,
|
||||||
theme,
|
theme,
|
||||||
follower_state,
|
follower_states,
|
||||||
active_call,
|
active_call,
|
||||||
active_pane,
|
active_pane,
|
||||||
zoomed,
|
zoomed,
|
||||||
|
|
|
@ -79,7 +79,7 @@ use status_bar::StatusBar;
|
||||||
pub use status_bar::StatusItemView;
|
pub use status_bar::StatusItemView;
|
||||||
use theme::{Theme, ThemeSettings};
|
use theme::{Theme, ThemeSettings};
|
||||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||||
use util::{async_iife, ResultExt};
|
use util::ResultExt;
|
||||||
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
|
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -573,11 +573,12 @@ pub struct Workspace {
|
||||||
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
|
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
|
||||||
active_pane: ViewHandle<Pane>,
|
active_pane: ViewHandle<Pane>,
|
||||||
last_active_center_pane: Option<WeakViewHandle<Pane>>,
|
last_active_center_pane: Option<WeakViewHandle<Pane>>,
|
||||||
|
last_active_view_id: Option<proto::ViewId>,
|
||||||
status_bar: ViewHandle<StatusBar>,
|
status_bar: ViewHandle<StatusBar>,
|
||||||
titlebar_item: Option<AnyViewHandle>,
|
titlebar_item: Option<AnyViewHandle>,
|
||||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
follower_states_by_leader: FollowerStatesByLeader,
|
follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
|
||||||
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
||||||
window_edited: bool,
|
window_edited: bool,
|
||||||
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
|
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
|
||||||
|
@ -602,10 +603,9 @@ pub struct ViewId {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FollowerState {
|
struct FollowerState {
|
||||||
|
leader_id: PeerId,
|
||||||
active_view_id: Option<ViewId>,
|
active_view_id: Option<ViewId>,
|
||||||
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
|
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
|
||||||
}
|
}
|
||||||
|
@ -786,6 +786,7 @@ impl Workspace {
|
||||||
panes_by_item: Default::default(),
|
panes_by_item: Default::default(),
|
||||||
active_pane: center_pane.clone(),
|
active_pane: center_pane.clone(),
|
||||||
last_active_center_pane: Some(center_pane.downgrade()),
|
last_active_center_pane: Some(center_pane.downgrade()),
|
||||||
|
last_active_view_id: None,
|
||||||
status_bar,
|
status_bar,
|
||||||
titlebar_item: None,
|
titlebar_item: None,
|
||||||
notifications: Default::default(),
|
notifications: Default::default(),
|
||||||
|
@ -793,7 +794,7 @@ impl Workspace {
|
||||||
bottom_dock,
|
bottom_dock,
|
||||||
right_dock,
|
right_dock,
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
follower_states_by_leader: Default::default(),
|
follower_states: Default::default(),
|
||||||
last_leaders_by_pane: Default::default(),
|
last_leaders_by_pane: Default::default(),
|
||||||
window_edited: false,
|
window_edited: false,
|
||||||
active_call,
|
active_call,
|
||||||
|
@ -934,7 +935,8 @@ impl Workspace {
|
||||||
app_state,
|
app_state,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
(workspace, opened_items)
|
(workspace, opened_items)
|
||||||
})
|
})
|
||||||
|
@ -2510,13 +2512,16 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
|
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
|
self.follower_states.retain(|_, state| {
|
||||||
for state in states_by_pane.into_values() {
|
if state.leader_id == peer_id {
|
||||||
for item in state.items_by_leader_view_id.into_values() {
|
for item in state.items_by_leader_view_id.values() {
|
||||||
item.set_leader_peer_id(None, cx);
|
item.set_leader_peer_id(None, cx);
|
||||||
}
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2529,10 +2534,15 @@ impl Workspace {
|
||||||
|
|
||||||
self.last_leaders_by_pane
|
self.last_leaders_by_pane
|
||||||
.insert(pane.downgrade(), leader_id);
|
.insert(pane.downgrade(), leader_id);
|
||||||
self.follower_states_by_leader
|
self.unfollow(&pane, cx);
|
||||||
.entry(leader_id)
|
self.follower_states.insert(
|
||||||
.or_default()
|
pane.clone(),
|
||||||
.insert(pane.clone(), Default::default());
|
FollowerState {
|
||||||
|
leader_id,
|
||||||
|
active_view_id: None,
|
||||||
|
items_by_leader_view_id: Default::default(),
|
||||||
|
},
|
||||||
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||||
|
@ -2547,9 +2557,8 @@ impl Workspace {
|
||||||
let response = request.await?;
|
let response = request.await?;
|
||||||
this.update(&mut cx, |this, _| {
|
this.update(&mut cx, |this, _| {
|
||||||
let state = this
|
let state = this
|
||||||
.follower_states_by_leader
|
.follower_states
|
||||||
.get_mut(&leader_id)
|
.get_mut(&pane)
|
||||||
.and_then(|states_by_pane| states_by_pane.get_mut(&pane))
|
|
||||||
.ok_or_else(|| anyhow!("following interrupted"))?;
|
.ok_or_else(|| anyhow!("following interrupted"))?;
|
||||||
state.active_view_id = if let Some(active_view_id) = response.active_view_id {
|
state.active_view_id = if let Some(active_view_id) = response.active_view_id {
|
||||||
Some(ViewId::from_proto(active_view_id)?)
|
Some(ViewId::from_proto(active_view_id)?)
|
||||||
|
@ -2644,12 +2653,10 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if you're already following, find the right pane and focus it.
|
// if you're already following, find the right pane and focus it.
|
||||||
for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
|
for (pane, state) in &self.follower_states {
|
||||||
if leader_id == *existing_leader_id {
|
if leader_id == state.leader_id {
|
||||||
for (pane, _) in states_by_pane {
|
cx.focus(pane);
|
||||||
cx.focus(pane);
|
return None;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2662,36 +2669,37 @@ impl Workspace {
|
||||||
pane: &ViewHandle<Pane>,
|
pane: &ViewHandle<Pane>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<PeerId> {
|
) -> Option<PeerId> {
|
||||||
for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
|
let state = self.follower_states.remove(pane)?;
|
||||||
let leader_id = *leader_id;
|
let leader_id = state.leader_id;
|
||||||
if let Some(state) = states_by_pane.remove(pane) {
|
for (_, item) in state.items_by_leader_view_id {
|
||||||
for (_, item) in state.items_by_leader_view_id {
|
item.set_leader_peer_id(None, cx);
|
||||||
item.set_leader_peer_id(None, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if states_by_pane.is_empty() {
|
|
||||||
self.follower_states_by_leader.remove(&leader_id);
|
|
||||||
let project_id = self.project.read(cx).remote_id();
|
|
||||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
|
||||||
self.app_state
|
|
||||||
.client
|
|
||||||
.send(proto::Unfollow {
|
|
||||||
room_id,
|
|
||||||
project_id,
|
|
||||||
leader_id: Some(leader_id),
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
return Some(leader_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
|
||||||
|
if self
|
||||||
|
.follower_states
|
||||||
|
.values()
|
||||||
|
.all(|state| state.leader_id != state.leader_id)
|
||||||
|
{
|
||||||
|
let project_id = self.project.read(cx).remote_id();
|
||||||
|
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||||
|
self.app_state
|
||||||
|
.client
|
||||||
|
.send(proto::Unfollow {
|
||||||
|
room_id,
|
||||||
|
project_id,
|
||||||
|
leader_id: Some(leader_id),
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
Some(leader_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
|
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
|
||||||
self.follower_states_by_leader.contains_key(&peer_id)
|
self.follower_states
|
||||||
|
.values()
|
||||||
|
.any(|state| state.leader_id == peer_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
@ -2862,6 +2870,7 @@ impl Workspace {
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
|
self.last_active_view_id = active_view_id.clone();
|
||||||
proto::FollowResponse {
|
proto::FollowResponse {
|
||||||
active_view_id,
|
active_view_id,
|
||||||
views: self
|
views: self
|
||||||
|
@ -2873,8 +2882,7 @@ impl Workspace {
|
||||||
let cx = &cx;
|
let cx = &cx;
|
||||||
move |item| {
|
move |item| {
|
||||||
let item = item.to_followable_item_handle(cx)?;
|
let item = item.to_followable_item_handle(cx)?;
|
||||||
if project_id.is_some()
|
if (project_id.is_none() || project_id != follower_project_id)
|
||||||
&& project_id != follower_project_id
|
|
||||||
&& item.is_project_item(cx)
|
&& item.is_project_item(cx)
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
|
@ -2913,8 +2921,8 @@ impl Workspace {
|
||||||
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
|
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
|
||||||
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
|
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
|
||||||
this.update(cx, |this, _| {
|
this.update(cx, |this, _| {
|
||||||
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
|
for (_, state) in &mut this.follower_states {
|
||||||
for state in state.values_mut() {
|
if state.leader_id == leader_id {
|
||||||
state.active_view_id =
|
state.active_view_id =
|
||||||
if let Some(active_view_id) = update_active_view.id.clone() {
|
if let Some(active_view_id) = update_active_view.id.clone() {
|
||||||
Some(ViewId::from_proto(active_view_id)?)
|
Some(ViewId::from_proto(active_view_id)?)
|
||||||
|
@ -2936,8 +2944,8 @@ impl Workspace {
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let project = this.project.clone();
|
let project = this.project.clone();
|
||||||
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
|
for (_, state) in &mut this.follower_states {
|
||||||
for state in state.values_mut() {
|
if state.leader_id == leader_id {
|
||||||
let view_id = ViewId::from_proto(id.clone())?;
|
let view_id = ViewId::from_proto(id.clone())?;
|
||||||
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
|
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
|
||||||
tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
|
tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
|
||||||
|
@ -2950,10 +2958,9 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
proto::update_followers::Variant::CreateView(view) => {
|
proto::update_followers::Variant::CreateView(view) => {
|
||||||
let panes = this.read_with(cx, |this, _| {
|
let panes = this.read_with(cx, |this, _| {
|
||||||
this.follower_states_by_leader
|
this.follower_states
|
||||||
.get(&leader_id)
|
.iter()
|
||||||
.into_iter()
|
.filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
|
||||||
.flat_map(|states_by_pane| states_by_pane.keys())
|
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
})?;
|
})?;
|
||||||
|
@ -3012,11 +3019,7 @@ impl Workspace {
|
||||||
for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
|
for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
|
||||||
let items = futures::future::try_join_all(item_tasks).await?;
|
let items = futures::future::try_join_all(item_tasks).await?;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let state = this
|
let state = this.follower_states.get_mut(&pane)?;
|
||||||
.follower_states_by_leader
|
|
||||||
.get_mut(&leader_id)?
|
|
||||||
.get_mut(&pane)?;
|
|
||||||
|
|
||||||
for (id, item) in leader_view_ids.into_iter().zip(items) {
|
for (id, item) in leader_view_ids.into_iter().zip(items) {
|
||||||
item.set_leader_peer_id(Some(leader_id), cx);
|
item.set_leader_peer_id(Some(leader_id), cx);
|
||||||
state.items_by_leader_view_id.insert(id, item);
|
state.items_by_leader_view_id.insert(id, item);
|
||||||
|
@ -3028,7 +3031,7 @@ impl Workspace {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_active_view_for_followers(&self, cx: &AppContext) {
|
fn update_active_view_for_followers(&mut self, cx: &AppContext) {
|
||||||
let mut is_project_item = true;
|
let mut is_project_item = true;
|
||||||
let mut update = proto::UpdateActiveView::default();
|
let mut update = proto::UpdateActiveView::default();
|
||||||
if self.active_pane.read(cx).has_focus() {
|
if self.active_pane.read(cx).has_focus() {
|
||||||
|
@ -3046,11 +3049,14 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_followers(
|
if update.id != self.last_active_view_id {
|
||||||
is_project_item,
|
self.last_active_view_id = update.id.clone();
|
||||||
proto::update_followers::Variant::UpdateActiveView(update),
|
self.update_followers(
|
||||||
cx,
|
is_project_item,
|
||||||
);
|
proto::update_followers::Variant::UpdateActiveView(update),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_followers(
|
fn update_followers(
|
||||||
|
@ -3070,15 +3076,7 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
|
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
|
||||||
self.follower_states_by_leader
|
self.follower_states.get(pane).map(|state| state.leader_id)
|
||||||
.iter()
|
|
||||||
.find_map(|(leader_id, state)| {
|
|
||||||
if state.contains_key(pane) {
|
|
||||||
Some(*leader_id)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
|
fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||||
|
@ -3106,17 +3104,23 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
|
for (pane, state) in &self.follower_states {
|
||||||
if leader_in_this_app {
|
if state.leader_id != leader_id {
|
||||||
let item = state
|
continue;
|
||||||
.active_view_id
|
}
|
||||||
.and_then(|id| state.items_by_leader_view_id.get(&id));
|
if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
|
||||||
if let Some(item) = item {
|
if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
|
||||||
if leader_in_this_project || !item.is_project_item(cx) {
|
if leader_in_this_project || !item.is_project_item(cx) {
|
||||||
items_to_activate.push((pane.clone(), item.boxed_clone()));
|
items_to_activate.push((pane.clone(), item.boxed_clone()));
|
||||||
}
|
}
|
||||||
continue;
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"unknown view id {:?} for leader {:?}",
|
||||||
|
active_view_id,
|
||||||
|
leader_id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
|
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
|
||||||
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
|
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
|
||||||
|
@ -3394,140 +3398,124 @@ impl Workspace {
|
||||||
serialized_workspace: SerializedWorkspace,
|
serialized_workspace: SerializedWorkspace,
|
||||||
paths_to_open: Vec<Option<ProjectPath>>,
|
paths_to_open: Vec<Option<ProjectPath>>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
|
) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let result = async_iife! {{
|
let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
|
||||||
let (project, old_center_pane) =
|
(
|
||||||
workspace.read_with(&cx, |workspace, _| {
|
workspace.project().clone(),
|
||||||
(
|
workspace.last_active_center_pane.clone(),
|
||||||
workspace.project().clone(),
|
)
|
||||||
workspace.last_active_center_pane.clone(),
|
})?;
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut center_items = None;
|
let mut center_group = None;
|
||||||
let mut center_group = None;
|
let mut center_items = None;
|
||||||
// Traverse the splits tree and add to things
|
// Traverse the splits tree and add to things
|
||||||
if let Some((group, active_pane, items)) = serialized_workspace
|
if let Some((group, active_pane, items)) = serialized_workspace
|
||||||
.center_group
|
.center_group
|
||||||
.deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
|
.deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
|
||||||
.await {
|
.await
|
||||||
center_items = Some(items);
|
{
|
||||||
center_group = Some((group, active_pane))
|
center_items = Some(items);
|
||||||
|
center_group = Some((group, active_pane))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut items_by_project_path = cx.read(|cx| {
|
||||||
|
center_items
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|item| {
|
||||||
|
let item = item?;
|
||||||
|
let project_path = item.project_path(cx)?;
|
||||||
|
Some((project_path, item))
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
let opened_items = paths_to_open
|
||||||
|
.into_iter()
|
||||||
|
.map(|path_to_open| {
|
||||||
|
path_to_open
|
||||||
|
.and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Remove old panes from workspace panes list
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
if let Some((center_group, active_pane)) = center_group {
|
||||||
|
workspace.remove_panes(workspace.center.root.clone(), cx);
|
||||||
|
|
||||||
|
// Swap workspace center group
|
||||||
|
workspace.center = PaneGroup::with_root(center_group);
|
||||||
|
|
||||||
|
// Change the focus to the workspace first so that we retrigger focus in on the pane.
|
||||||
|
cx.focus_self();
|
||||||
|
|
||||||
|
if let Some(active_pane) = active_pane {
|
||||||
|
cx.focus(&active_pane);
|
||||||
|
} else {
|
||||||
|
cx.focus(workspace.panes.last().unwrap());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
|
||||||
|
if let Some(old_center_handle) = old_center_handle {
|
||||||
|
cx.focus(&old_center_handle)
|
||||||
|
} else {
|
||||||
|
cx.focus_self()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let resulting_list = cx.read(|cx| {
|
let docks = serialized_workspace.docks;
|
||||||
let mut opened_items = center_items
|
workspace.left_dock.update(cx, |dock, cx| {
|
||||||
.unwrap_or_default()
|
dock.set_open(docks.left.visible, cx);
|
||||||
.into_iter()
|
if let Some(active_panel) = docks.left.active_panel {
|
||||||
.filter_map(|item| {
|
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||||
let item = item?;
|
dock.activate_panel(ix, cx);
|
||||||
let project_path = item.project_path(cx)?;
|
|
||||||
Some((project_path, item))
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
paths_to_open
|
|
||||||
.into_iter()
|
|
||||||
.map(|path_to_open| {
|
|
||||||
path_to_open.map(|path_to_open| {
|
|
||||||
Ok(opened_items.remove(&path_to_open))
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
.map(|item| item.flatten())
|
|
||||||
.transpose()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove old panes from workspace panes list
|
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
|
||||||
if let Some((center_group, active_pane)) = center_group {
|
|
||||||
workspace.remove_panes(workspace.center.root.clone(), cx);
|
|
||||||
|
|
||||||
// Swap workspace center group
|
|
||||||
workspace.center = PaneGroup::with_root(center_group);
|
|
||||||
|
|
||||||
// Change the focus to the workspace first so that we retrigger focus in on the pane.
|
|
||||||
cx.focus_self();
|
|
||||||
|
|
||||||
if let Some(active_pane) = active_pane {
|
|
||||||
cx.focus(&active_pane);
|
|
||||||
} else {
|
|
||||||
cx.focus(workspace.panes.last().unwrap());
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
|
dock.active_panel()
|
||||||
if let Some(old_center_handle) = old_center_handle {
|
.map(|panel| panel.set_zoomed(docks.left.zoom, cx));
|
||||||
cx.focus(&old_center_handle)
|
if docks.left.visible && docks.left.zoom {
|
||||||
} else {
|
cx.focus_self()
|
||||||
cx.focus_self()
|
}
|
||||||
|
});
|
||||||
|
// TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
|
||||||
|
workspace.right_dock.update(cx, |dock, cx| {
|
||||||
|
dock.set_open(docks.right.visible, cx);
|
||||||
|
if let Some(active_panel) = docks.right.active_panel {
|
||||||
|
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||||
|
dock.activate_panel(ix, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dock.active_panel()
|
||||||
|
.map(|panel| panel.set_zoomed(docks.right.zoom, cx));
|
||||||
|
|
||||||
|
if docks.right.visible && docks.right.zoom {
|
||||||
|
cx.focus_self()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
workspace.bottom_dock.update(cx, |dock, cx| {
|
||||||
|
dock.set_open(docks.bottom.visible, cx);
|
||||||
|
if let Some(active_panel) = docks.bottom.active_panel {
|
||||||
|
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||||
|
dock.activate_panel(ix, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let docks = serialized_workspace.docks;
|
dock.active_panel()
|
||||||
workspace.left_dock.update(cx, |dock, cx| {
|
.map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
|
||||||
dock.set_open(docks.left.visible, cx);
|
|
||||||
if let Some(active_panel) = docks.left.active_panel {
|
|
||||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
|
||||||
dock.activate_panel(ix, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dock.active_panel()
|
|
||||||
.map(|panel| {
|
|
||||||
panel.set_zoomed(docks.left.zoom, cx)
|
|
||||||
});
|
|
||||||
if docks.left.visible && docks.left.zoom {
|
|
||||||
cx.focus_self()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
|
|
||||||
workspace.right_dock.update(cx, |dock, cx| {
|
|
||||||
dock.set_open(docks.right.visible, cx);
|
|
||||||
if let Some(active_panel) = docks.right.active_panel {
|
|
||||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
|
||||||
dock.activate_panel(ix, cx);
|
|
||||||
|
|
||||||
}
|
if docks.bottom.visible && docks.bottom.zoom {
|
||||||
}
|
cx.focus_self()
|
||||||
dock.active_panel()
|
}
|
||||||
.map(|panel| {
|
});
|
||||||
panel.set_zoomed(docks.right.zoom, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
if docks.right.visible && docks.right.zoom {
|
cx.notify();
|
||||||
cx.focus_self()
|
})?;
|
||||||
}
|
|
||||||
});
|
|
||||||
workspace.bottom_dock.update(cx, |dock, cx| {
|
|
||||||
dock.set_open(docks.bottom.visible, cx);
|
|
||||||
if let Some(active_panel) = docks.bottom.active_panel {
|
|
||||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
|
||||||
dock.activate_panel(ix, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dock.active_panel()
|
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
|
||||||
.map(|panel| {
|
workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
|
||||||
panel.set_zoomed(docks.bottom.zoom, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
if docks.bottom.visible && docks.bottom.zoom {
|
Ok(opened_items)
|
||||||
cx.focus_self()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
|
|
||||||
workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
|
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(resulting_list)
|
|
||||||
}};
|
|
||||||
|
|
||||||
result.await.unwrap_or_default()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3601,7 +3589,7 @@ async fn open_items(
|
||||||
mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
|
mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
|
) -> Result<Vec<Option<Result<Box<dyn ItemHandle>>>>> {
|
||||||
let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
|
let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
|
||||||
|
|
||||||
if let Some(serialized_workspace) = serialized_workspace {
|
if let Some(serialized_workspace) = serialized_workspace {
|
||||||
|
@ -3619,16 +3607,19 @@ async fn open_items(
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await?;
|
||||||
|
|
||||||
let restored_project_paths = cx.read(|cx| {
|
let restored_project_paths = cx.read(|cx| {
|
||||||
restored_items
|
restored_items
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
|
.filter_map(|item| item.as_ref()?.project_path(cx))
|
||||||
.collect::<HashSet<_>>()
|
.collect::<HashSet<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
opened_items = restored_items;
|
for restored_item in restored_items {
|
||||||
|
opened_items.push(restored_item.map(Ok));
|
||||||
|
}
|
||||||
|
|
||||||
project_paths_to_open
|
project_paths_to_open
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.for_each(|(_, project_path)| {
|
.for_each(|(_, project_path)| {
|
||||||
|
@ -3681,7 +3672,7 @@ async fn open_items(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opened_items
|
Ok(opened_items)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
||||||
|
@ -3817,7 +3808,7 @@ impl View for Workspace {
|
||||||
self.center.render(
|
self.center.render(
|
||||||
&project,
|
&project,
|
||||||
&theme,
|
&theme,
|
||||||
&self.follower_states_by_leader,
|
&self.follower_states,
|
||||||
self.active_call(),
|
self.active_call(),
|
||||||
self.active_pane(),
|
self.active_pane(),
|
||||||
self.zoomed
|
self.zoomed
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.107.0"
|
version = "0.107.7"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
dev
|
stable
|
|
@ -74,7 +74,8 @@ fn main() {
|
||||||
let mut app = gpui::App::new(Assets).unwrap();
|
let mut app = gpui::App::new(Assets).unwrap();
|
||||||
|
|
||||||
let installation_id = app.background().block(installation_id()).ok();
|
let installation_id = app.background().block(installation_id()).ok();
|
||||||
init_panic_hook(&app, installation_id.clone());
|
let session_id = Uuid::new_v4().to_string();
|
||||||
|
init_panic_hook(&app, installation_id.clone(), session_id.clone());
|
||||||
|
|
||||||
load_embedded_fonts(&app);
|
load_embedded_fonts(&app);
|
||||||
|
|
||||||
|
@ -177,7 +178,7 @@ fn main() {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
client.telemetry().start(installation_id, cx);
|
client.telemetry().start(installation_id, session_id, cx);
|
||||||
|
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
languages,
|
languages,
|
||||||
|
@ -402,6 +403,7 @@ struct Panic {
|
||||||
panicked_on: u128,
|
panicked_on: u128,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
installation_id: Option<String>,
|
installation_id: Option<String>,
|
||||||
|
session_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -412,7 +414,7 @@ struct PanicRequest {
|
||||||
|
|
||||||
static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
|
static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
fn init_panic_hook(app: &App, installation_id: Option<String>) {
|
fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: String) {
|
||||||
let is_pty = stdout_is_a_pty();
|
let is_pty = stdout_is_a_pty();
|
||||||
let platform = app.platform();
|
let platform = app.platform();
|
||||||
|
|
||||||
|
@ -477,7 +479,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
|
||||||
line: location.line(),
|
line: location.line(),
|
||||||
}),
|
}),
|
||||||
app_version: app_version.clone(),
|
app_version: app_version.clone(),
|
||||||
release_channel: RELEASE_CHANNEL.dev_name().into(),
|
release_channel: RELEASE_CHANNEL.display_name().into(),
|
||||||
os_name: platform.os_name().into(),
|
os_name: platform.os_name().into(),
|
||||||
os_version: platform
|
os_version: platform
|
||||||
.os_version()
|
.os_version()
|
||||||
|
@ -490,13 +492,14 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
|
||||||
.as_millis(),
|
.as_millis(),
|
||||||
backtrace,
|
backtrace,
|
||||||
installation_id: installation_id.clone(),
|
installation_id: installation_id.clone(),
|
||||||
|
session_id: session_id.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_pty {
|
if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
|
||||||
if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
|
log::error!("{}", panic_data_json);
|
||||||
eprintln!("{}", panic_data_json);
|
}
|
||||||
}
|
|
||||||
} else {
|
if !is_pty {
|
||||||
if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
|
if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
|
||||||
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
|
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
|
||||||
let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
|
let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
} from "./components"
|
} from "./components"
|
||||||
import { icon_button } from "../component/icon_button"
|
import { icon_button } from "../component/icon_button"
|
||||||
import { useTheme } from "../theme"
|
import { useTheme } from "../theme"
|
||||||
|
import { interactive } from "../element"
|
||||||
|
|
||||||
export default function chat_panel(): any {
|
export default function chat_panel(): any {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
@ -27,11 +28,23 @@ export default function chat_panel(): any {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
background: background(layer),
|
background: background(layer),
|
||||||
list: {
|
avatar: {
|
||||||
margin: {
|
icon_width: 24,
|
||||||
left: SPACING,
|
icon_height: 24,
|
||||||
right: SPACING,
|
corner_radius: 4,
|
||||||
|
outer_width: 24,
|
||||||
|
outer_corner_radius: 16,
|
||||||
|
},
|
||||||
|
avatar_container: {
|
||||||
|
padding: {
|
||||||
|
right: 6,
|
||||||
|
left: 2,
|
||||||
|
top: 2,
|
||||||
|
bottom: 2,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
|
||||||
},
|
},
|
||||||
channel_select: {
|
channel_select: {
|
||||||
header: {
|
header: {
|
||||||
|
@ -79,6 +92,22 @@ export default function chat_panel(): any {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
|
...interactive({
|
||||||
|
base: {
|
||||||
|
margin: { top: SPACING },
|
||||||
|
padding: {
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
left: SPACING / 2,
|
||||||
|
right: SPACING / 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
hovered: {
|
||||||
|
background: background(layer, "hovered"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
body: text(layer, "sans", "base"),
|
body: text(layer, "sans", "base"),
|
||||||
sender: {
|
sender: {
|
||||||
margin: {
|
margin: {
|
||||||
|
@ -87,7 +116,32 @@ export default function chat_panel(): any {
|
||||||
...text(layer, "sans", "base", { weight: "bold" }),
|
...text(layer, "sans", "base", { weight: "bold" }),
|
||||||
},
|
},
|
||||||
timestamp: text(layer, "sans", "base", "disabled"),
|
timestamp: text(layer, "sans", "base", "disabled"),
|
||||||
margin: { bottom: SPACING }
|
},
|
||||||
|
last_message_bottom_spacing: SPACING,
|
||||||
|
continuation_message: {
|
||||||
|
body: text(layer, "sans", "base"),
|
||||||
|
sender: {
|
||||||
|
margin: {
|
||||||
|
right: 8,
|
||||||
|
},
|
||||||
|
...text(layer, "sans", "base", { weight: "bold" }),
|
||||||
|
},
|
||||||
|
timestamp: text(layer, "sans", "base", "disabled"),
|
||||||
|
...interactive({
|
||||||
|
base: {
|
||||||
|
padding: {
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
left: SPACING / 2,
|
||||||
|
right: SPACING / 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
hovered: {
|
||||||
|
background: background(layer, "hovered"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
pending_message: {
|
pending_message: {
|
||||||
body: text(layer, "sans", "base"),
|
body: text(layer, "sans", "base"),
|
||||||
|
@ -98,6 +152,21 @@ export default function chat_panel(): any {
|
||||||
...text(layer, "sans", "base", "disabled"),
|
...text(layer, "sans", "base", "disabled"),
|
||||||
},
|
},
|
||||||
timestamp: text(layer, "sans", "base"),
|
timestamp: text(layer, "sans", "base"),
|
||||||
|
...interactive({
|
||||||
|
base: {
|
||||||
|
padding: {
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
left: SPACING / 2,
|
||||||
|
right: SPACING / 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
hovered: {
|
||||||
|
background: background(layer, "hovered"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
sign_in_prompt: {
|
sign_in_prompt: {
|
||||||
default: text(layer, "sans", "base"),
|
default: text(layer, "sans", "base"),
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default function contacts_panel(): any {
|
||||||
...text(theme.lowest, "sans", "base"),
|
...text(theme.lowest, "sans", "base"),
|
||||||
button: icon_button({ variant: "ghost" }),
|
button: icon_button({ variant: "ghost" }),
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
|
padding: 4,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue