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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get appropriate URL
|
||||
id: get-appropriate-url
|
||||
- name: Get release URL
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
|
@ -15,14 +15,19 @@ jobs:
|
|||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
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
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: |
|
||||
📣 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 }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
|
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -1467,7 +1467,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.22.1"
|
||||
version = "0.22.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
@ -1563,6 +1563,7 @@ dependencies = [
|
|||
"postage",
|
||||
"project",
|
||||
"recent_projects",
|
||||
"rich_text",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
|
@ -2405,6 +2406,7 @@ dependencies = [
|
|||
"project",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"rich_text",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
@ -6242,6 +6244,24 @@ dependencies = [
|
|||
"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]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
|
@ -10063,7 +10083,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.107.0"
|
||||
version = "0.107.7"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
|
|
@ -64,6 +64,7 @@ members = [
|
|||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/feature_flags",
|
||||
"crates/rich_text",
|
||||
"crates/storybook",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
|
|
|
@ -17,7 +17,7 @@ use editor::{
|
|||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
|
||||
},
|
||||
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
|
@ -278,22 +278,36 @@ impl AssistantPanel {
|
|||
if selection.start.excerpt_id() != selection.end.excerpt_id() {
|
||||
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 snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let provider = Arc::new(OpenAICompletionProvider::new(
|
||||
api_key,
|
||||
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| {
|
||||
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
||||
});
|
||||
|
@ -319,7 +333,7 @@ impl AssistantPanel {
|
|||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
position: selection.head().bias_left(&snapshot),
|
||||
position: snapshot.anchor_before(point_selection.head()),
|
||||
height: 2,
|
||||
render: Arc::new({
|
||||
let inline_assistant = inline_assistant.clone();
|
||||
|
@ -578,10 +592,7 @@ impl AssistantPanel {
|
|||
|
||||
let codegen_kind = codegen.read(cx).kind().clone();
|
||||
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 model = settings::get::<AssistantSettings>(cx)
|
||||
.default_open_ai_model
|
||||
|
@ -597,6 +608,11 @@ impl AssistantPanel {
|
|||
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 {
|
||||
let prompt = prompt.await;
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, OpenAIRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{
|
||||
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{Entity, ModelContext, ModelHandle, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
|
@ -40,26 +38,11 @@ impl Entity for Codegen {
|
|||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
mut kind: CodegenKind,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
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 {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
|
@ -386,7 +369,7 @@ mod tests {
|
|||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, 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 codegen = cx.add_model(|cx| {
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::cmp::{self, Reverse};
|
|||
use std::fmt::Write;
|
||||
use std::ops::Range;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
|
@ -121,6 +122,7 @@ pub fn generate_content_prompt(
|
|||
range: Range<impl ToOffset>,
|
||||
kind: CodegenKind,
|
||||
) -> String {
|
||||
let range = range.to_offset(buffer);
|
||||
let mut prompt = String::new();
|
||||
|
||||
// General Preamble
|
||||
|
@ -130,17 +132,29 @@ pub fn generate_content_prompt(
|
|||
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!(
|
||||
prompt,
|
||||
"The file you are currently working on has the following outline:"
|
||||
"The file you are currently working on has the following content:"
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(language_name) = language_name {
|
||||
let language_name = language_name.to_lowercase();
|
||||
writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap();
|
||||
writeln!(prompt, "```{language_name}\n{content}\n```").unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "```\n{outline}\n```").unwrap();
|
||||
writeln!(prompt, "```\n{content}\n```").unwrap();
|
||||
}
|
||||
|
||||
match kind {
|
||||
|
|
|
@ -600,27 +600,30 @@ impl Room {
|
|||
|
||||
/// Returns the most 'active' projects, defined as most people in the project
|
||||
pub fn most_active_project(&self) -> Option<(u64, u64)> {
|
||||
let mut projects = HashMap::default();
|
||||
let mut hosts = HashMap::default();
|
||||
let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
|
||||
for participant in self.remote_participants.values() {
|
||||
match participant.location {
|
||||
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 => {}
|
||||
}
|
||||
for project in &participant.projects {
|
||||
*projects.entry(project.id).or_insert(0) += 1;
|
||||
hosts.insert(project.id, participant.user.id);
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project.id)
|
||||
.or_default()
|
||||
.0 = Some(participant.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
|
||||
pairs.sort_by_key(|(_, count)| *count as i32);
|
||||
|
||||
pairs
|
||||
.first()
|
||||
.map(|(project_id, _)| (*project_id, hosts[&project_id]))
|
||||
project_hosts_and_guest_counts
|
||||
.into_iter()
|
||||
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
|
||||
.max_by_key(|(_, _, guest_count)| *guest_count)
|
||||
.map(|(id, host, _)| (id, host))
|
||||
}
|
||||
|
||||
async fn handle_room_updated(
|
||||
|
@ -686,6 +689,7 @@ impl Room {
|
|||
let Some(peer_id) = participant.peer_id else {
|
||||
continue;
|
||||
};
|
||||
let participant_index = ParticipantIndex(participant.participant_index);
|
||||
this.participant_user_ids.insert(participant.user_id);
|
||||
|
||||
let old_projects = this
|
||||
|
@ -736,8 +740,9 @@ impl Room {
|
|||
if let Some(remote_participant) =
|
||||
this.remote_participants.get_mut(&participant.user_id)
|
||||
{
|
||||
remote_participant.projects = participant.projects;
|
||||
remote_participant.peer_id = peer_id;
|
||||
remote_participant.projects = participant.projects;
|
||||
remote_participant.participant_index = participant_index;
|
||||
if location != remote_participant.location {
|
||||
remote_participant.location = location;
|
||||
cx.emit(Event::ParticipantLocationChanged {
|
||||
|
@ -749,9 +754,7 @@ impl Room {
|
|||
participant.user_id,
|
||||
RemoteParticipant {
|
||||
user: user.clone(),
|
||||
participant_index: ParticipantIndex(
|
||||
participant.participant_index,
|
||||
),
|
||||
participant_index,
|
||||
peer_id,
|
||||
projects: participant.projects,
|
||||
location,
|
||||
|
|
|
@ -36,7 +36,7 @@ pub struct ChannelMessage {
|
|||
pub nonce: u128,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ChannelMessageId {
|
||||
Saved(u64),
|
||||
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 CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
actions!(client, [SignIn, SignOut]);
|
||||
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
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 {
|
||||
|
@ -1212,6 +1223,11 @@ impl Client {
|
|||
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> {
|
||||
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
|
||||
Ok(connection_id)
|
||||
|
|
|
@ -4,7 +4,9 @@ use lazy_static::lazy_static;
|
|||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
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 util::http::HttpClient;
|
||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||
|
@ -18,7 +20,8 @@ pub struct Telemetry {
|
|||
#[derive(Default)]
|
||||
struct TelemetryState {
|
||||
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>>,
|
||||
release_channel: Option<&'static str>,
|
||||
os_name: &'static str,
|
||||
|
@ -41,6 +44,7 @@ lazy_static! {
|
|||
struct ClickhouseEventRequestBody {
|
||||
token: &'static str,
|
||||
installation_id: Option<Arc<str>>,
|
||||
session_id: Option<Arc<str>>,
|
||||
is_staff: Option<bool>,
|
||||
app_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
|
@ -131,6 +135,7 @@ impl Telemetry {
|
|||
release_channel,
|
||||
installation_id: None,
|
||||
metrics_id: None,
|
||||
session_id: None,
|
||||
clickhouse_events_queue: Default::default(),
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
log_file: None,
|
||||
|
@ -145,9 +150,15 @@ impl Telemetry {
|
|||
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();
|
||||
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();
|
||||
drop(state);
|
||||
|
||||
|
@ -157,8 +168,16 @@ impl Telemetry {
|
|||
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut system = System::new_all();
|
||||
system.refresh_all();
|
||||
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
||||
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 {
|
||||
// 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);
|
||||
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
|
||||
|
||||
system.refresh_memory();
|
||||
system.refresh_processes();
|
||||
system.refresh_specifics(refresh_kind);
|
||||
|
||||
let current_process = Pid::from_u32(std::process::id());
|
||||
let Some(process) = system.processes().get(¤t_process) else {
|
||||
|
@ -279,22 +297,21 @@ impl Telemetry {
|
|||
|
||||
{
|
||||
let state = this.state.lock();
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(
|
||||
&mut json_bytes,
|
||||
&ClickhouseEventRequestBody {
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
installation_id: state.installation_id.clone(),
|
||||
is_staff: state.is_staff.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture,
|
||||
let request_body = ClickhouseEventRequestBody {
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
installation_id: state.installation_id.clone(),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture,
|
||||
|
||||
release_channel: state.release_channel,
|
||||
events,
|
||||
},
|
||||
)?;
|
||||
release_channel: state.release_channel,
|
||||
events,
|
||||
};
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||
}
|
||||
|
||||
this.http_client
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.22.1"
|
||||
version = "0.22.2"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -9,13 +9,3 @@ pub mod projects;
|
|||
pub mod rooms;
|
||||
pub mod servers;
|
||||
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()
|
||||
.filter(condition)
|
||||
.order_by_asc(channel_message::Column::Id)
|
||||
.limit(count as u64)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut max_id = None;
|
||||
let mut messages = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
|
||||
max_assign(&mut max_id, row.id);
|
||||
|
||||
let nonce = row.nonce.as_u64_pair();
|
||||
messages.push(proto::ChannelMessage {
|
||||
id: row.id.to_proto(),
|
||||
|
@ -113,50 +110,6 @@ impl Database {
|
|||
});
|
||||
}
|
||||
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)
|
||||
})
|
||||
.await
|
||||
|
|
|
@ -1906,13 +1906,10 @@ async fn follow(
|
|||
.check_room_participants(room_id, leader_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
let mut response_payload = session
|
||||
let response_payload = session
|
||||
.peer
|
||||
.forward_request(session.connection_id, leader_id, request)
|
||||
.await?;
|
||||
response_payload
|
||||
.views
|
||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
||||
response.send(response_payload)?;
|
||||
|
||||
if let Some(project_id) = project_id {
|
||||
|
@ -1973,14 +1970,17 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
|
|||
.await?
|
||||
};
|
||||
|
||||
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
|
||||
// For now, don't send view update messages back to that view's current leader.
|
||||
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::UpdateActiveView(payload) => payload.leader_id,
|
||||
_ => None,
|
||||
});
|
||||
|
||||
for follower_peer_id in request.follower_ids.iter().copied() {
|
||||
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.connection_id,
|
||||
follower_connection_id,
|
||||
|
|
|
@ -4,6 +4,7 @@ use collab_ui::project_shared_notification::ProjectSharedNotification;
|
|||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use workspace::{
|
||||
|
@ -183,20 +184,12 @@ async fn test_basic_following(
|
|||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
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_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client C unfollows client A.
|
||||
|
@ -206,46 +199,39 @@ async fn test_basic_following(
|
|||
|
||||
// All clients see that clients B is following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
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_a, project_id),
|
||||
&[peer_id_b],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b])],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// Client C re-follows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.follow(peer_id_a, cx);
|
||||
});
|
||||
workspace_c
|
||||
.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.
|
||||
cx_c.foreground().run_until_parked();
|
||||
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_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
|
||||
"followers seen by {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
|
||||
.update(cx_d, |workspace, cx| {
|
||||
workspace.follow(peer_id_c, cx).unwrap()
|
||||
|
@ -255,20 +241,15 @@ async fn test_basic_following(
|
|||
|
||||
// All clients see that D is following C
|
||||
cx_d.foreground().run_until_parked();
|
||||
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),
|
||||
&[peer_id_d],
|
||||
"checking followers for C as {name}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[
|
||||
(peer_id_a, vec![peer_id_b, peer_id_c]),
|
||||
(peer_id_c, vec![peer_id_d])
|
||||
],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
|
||||
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}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 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}"
|
||||
);
|
||||
});
|
||||
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
assert_eq!(
|
||||
followers_by_leader(project_id, cx),
|
||||
&[(peer_id_a, vec![peer_id_b]),],
|
||||
"followers seen by {name}"
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
.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 pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_a1 = workspace_a
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
|
@ -736,10 +696,9 @@ async fn test_peers_following_each_other(
|
|||
.downcast::<Editor>()
|
||||
.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 pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_b1 = workspace_b
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
})
|
||||
|
@ -754,9 +713,7 @@ async fn test_peers_following_each_other(
|
|||
});
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
assert_ne!(*workspace.active_pane(), pane_a1);
|
||||
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
|
||||
workspace.follow(leader_id, cx).unwrap()
|
||||
workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -765,85 +722,443 @@ async fn test_peers_following_each_other(
|
|||
});
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_ne!(*workspace.active_pane(), pane_b1);
|
||||
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
|
||||
workspace.follow(leader_id, cx).unwrap()
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
// Wait for focus effects to be fully flushed
|
||||
workspace_a.update(cx_a, |workspace, _| {
|
||||
assert_eq!(*workspace.active_pane(), pane_a1);
|
||||
});
|
||||
// Clients A and B return focus to the original files they had open
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// 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
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Ensure leader updates don't change the active pane of followers
|
||||
workspace_a.read_with(cx_a, |workspace, _| {
|
||||
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.
|
||||
// Both client's see the other client open the new file, but keep their
|
||||
// focus on their own active pane.
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.project_path(cx)),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
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!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
);
|
||||
workspace.activate_next_pane(cx);
|
||||
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()),
|
||||
(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| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "4.txt").into())
|
||||
);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "4.txt").into())
|
||||
);
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
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()),
|
||||
(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| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
);
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
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)]
|
||||
|
@ -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)]
|
||||
async fn test_following_across_workspaces(
|
||||
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"));
|
||||
});
|
||||
}
|
||||
|
||||
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" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
rich_text = { path = "../rich_text" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
recent_projects = {path = "../recent_projects"}
|
||||
|
|
|
@ -3,6 +3,7 @@ use anyhow::Result;
|
|||
use call::ActiveCall;
|
||||
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
||||
use client::Client;
|
||||
use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::Editor;
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||
|
@ -12,12 +13,13 @@ use gpui::{
|
|||
platform::{CursorStyle, MouseButton},
|
||||
serde_json,
|
||||
views::{ItemType, Select, SelectStyle},
|
||||
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use menu::Confirm;
|
||||
use project::Fs;
|
||||
use rich_text::RichText;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
|
@ -35,6 +37,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
|||
pub struct ChatPanel {
|
||||
client: Arc<Client>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
||||
message_list: ListState<ChatPanel>,
|
||||
input_editor: ViewHandle<Editor>,
|
||||
|
@ -47,6 +50,7 @@ pub struct ChatPanel {
|
|||
subscriptions: Vec<gpui::Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
has_focus: bool,
|
||||
markdown_data: HashMap<ChannelMessageId, RichText>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -78,6 +82,7 @@ impl ChatPanel {
|
|||
let fs = workspace.app_state().fs.clone();
|
||||
let client = workspace.app_state().client.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 mut editor = Editor::auto_height(
|
||||
|
@ -130,6 +135,8 @@ impl ChatPanel {
|
|||
fs,
|
||||
client,
|
||||
channel_store,
|
||||
languages,
|
||||
|
||||
active_chat: Default::default(),
|
||||
pending_serialization: Task::ready(None),
|
||||
message_list,
|
||||
|
@ -141,6 +148,7 @@ impl ChatPanel {
|
|||
workspace: workspace_handle,
|
||||
active: false,
|
||||
width: None,
|
||||
markdown_data: Default::default(),
|
||||
};
|
||||
|
||||
let mut old_dock_position = this.position(cx);
|
||||
|
@ -177,6 +185,25 @@ impl ChatPanel {
|
|||
})
|
||||
.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
|
||||
})
|
||||
}
|
||||
|
@ -327,13 +354,33 @@ impl ChatPanel {
|
|||
messages.flex(1., true).into_any()
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix);
|
||||
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
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 theme = theme::current(cx);
|
||||
let style = if message.is_pending() {
|
||||
let style = if is_pending {
|
||||
&theme.chat_panel.pending_message
|
||||
} else if is_continuation {
|
||||
&theme.chat_panel.continuation_message
|
||||
} else {
|
||||
&theme.chat_panel.message
|
||||
};
|
||||
|
@ -346,52 +393,90 @@ impl ChatPanel {
|
|||
None
|
||||
};
|
||||
|
||||
enum DeleteMessage {}
|
||||
|
||||
let body = message.body.clone();
|
||||
Flex::column()
|
||||
.with_child(
|
||||
enum MessageBackgroundHighlight {}
|
||||
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
|
||||
let container = style.container.style_for(state);
|
||||
if is_continuation {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.sender.github_login.clone(),
|
||||
style.sender.text.clone(),
|
||||
text.element(
|
||||
theme.editor.syntax.clone(),
|
||||
style.body.clone(),
|
||||
theme.editor.document_highlight_read_background,
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.sender.container),
|
||||
.flex(1., true),
|
||||
)
|
||||
.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(
|
||||
Label::new(
|
||||
format_timestamp(message.timestamp, now, self.local_timezone),
|
||||
style.timestamp.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.timestamp.container),
|
||||
Flex::row()
|
||||
.with_child(
|
||||
text.element(
|
||||
theme.editor.syntax.clone(),
|
||||
style.body.clone(),
|
||||
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| {
|
||||
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()
|
||||
})),
|
||||
)
|
||||
.with_child(Text::new(body, style.body.clone()))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.into_any()
|
||||
.contained()
|
||||
.with_style(*container)
|
||||
.with_margin_bottom(if is_last {
|
||||
theme.chat_panel.last_message_bottom_spacing
|
||||
} else {
|
||||
0.
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
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 {
|
||||
let chat = open_chat.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.markdown_data = Default::default();
|
||||
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 {
|
||||
type Event = Event;
|
||||
}
|
||||
|
|
|
@ -1937,6 +1937,8 @@ impl CollabPanel {
|
|||
is_dragged_over = true;
|
||||
}
|
||||
|
||||
let has_messages_notification = channel.unseen_message_id.is_some();
|
||||
|
||||
MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
|
||||
let row_hovered = state.hovered();
|
||||
|
||||
|
@ -1974,11 +1976,7 @@ impl CollabPanel {
|
|||
.left()
|
||||
.with_tooltip::<ChannelTooltip>(
|
||||
ix,
|
||||
if is_active {
|
||||
"Open channel notes"
|
||||
} else {
|
||||
"Join channel"
|
||||
},
|
||||
"Join channel",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
|
@ -2022,24 +2020,33 @@ impl CollabPanel {
|
|||
.flex(1., true)
|
||||
})
|
||||
.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() {
|
||||
Svg::new("icons/conversations.svg")
|
||||
.with_color(collab_theme.channel_note_active_color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.with_uniform_padding(4.)
|
||||
.into_any()
|
||||
} else if row_hovered {
|
||||
Svg::new("icons/conversations.svg")
|
||||
.with_color(collab_theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.with_uniform_padding(4.)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.into_any()
|
||||
Empty::new().into_any()
|
||||
}
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
|
@ -2056,7 +2063,12 @@ impl CollabPanel {
|
|||
.with_margin_right(4.),
|
||||
)
|
||||
.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() {
|
||||
Svg::new("icons/file.svg")
|
||||
.with_color(if channel.unseen_note_version.is_some() {
|
||||
|
@ -2067,6 +2079,8 @@ impl CollabPanel {
|
|||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.with_uniform_padding(4.)
|
||||
.with_margin_right(collab_theme.channel_hash.container.margin.left)
|
||||
.with_tooltip::<NotesTooltip>(
|
||||
ix as usize,
|
||||
|
@ -2076,23 +2090,20 @@ impl CollabPanel {
|
|||
cx,
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
} else if has_messages_notification {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.contained()
|
||||
.with_uniform_padding(4.)
|
||||
.with_margin_right(collab_theme.channel_hash.container.margin.left)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
let participants =
|
||||
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);
|
||||
};
|
||||
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
|
||||
}),
|
||||
)
|
||||
.align_children_center()
|
||||
|
|
|
@ -36,6 +36,7 @@ language = { path = "../language" }
|
|||
lsp = { path = "../lsp" }
|
||||
project = { path = "../project" }
|
||||
rpc = { path = "../rpc" }
|
||||
rich_text = { path = "../rich_text" }
|
||||
settings = { path = "../settings" }
|
||||
snippet = { path = "../snippet" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
|
|
|
@ -8,12 +8,12 @@ use futures::FutureExt;
|
|||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
|
||||
fonts::{HighlightStyle, Underline, Weight},
|
||||
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 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 util::TryFutureExt;
|
||||
|
||||
|
@ -346,158 +346,25 @@ fn show_hover(
|
|||
}
|
||||
|
||||
fn render_blocks(
|
||||
theme_id: usize,
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
style: &EditorStyle,
|
||||
) -> RenderedInfo {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
) -> RichText {
|
||||
let mut data = RichText {
|
||||
text: Default::default(),
|
||||
highlights: Default::default(),
|
||||
region_ranges: Default::default(),
|
||||
regions: Default::default(),
|
||||
};
|
||||
|
||||
for block in blocks {
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text);
|
||||
new_paragraph(&mut data.text, &mut Vec::new());
|
||||
data.text.push_str(&block.text);
|
||||
}
|
||||
HoverBlockKind::Markdown => {
|
||||
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.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(' '),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
render_markdown_mut(&block.text, language_registry, language, &mut data)
|
||||
}
|
||||
HoverBlockKind::Code { language } => {
|
||||
if let Some(language) = language_registry
|
||||
|
@ -505,62 +372,17 @@ fn render_blocks(
|
|||
.now_or_never()
|
||||
.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 {
|
||||
text.push_str(&block.text);
|
||||
data.text.push_str(&block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderedInfo {
|
||||
theme_id,
|
||||
text: text.trim().to_string(),
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
}
|
||||
data.text = data.text.trim().to_string();
|
||||
|
||||
fn render_code(
|
||||
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(" ");
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -623,22 +445,7 @@ pub struct InfoPopover {
|
|||
symbol_range: RangeInEditor,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
language: Option<Arc<Language>>,
|
||||
rendered_content: Option<RenderedInfo>,
|
||||
}
|
||||
|
||||
#[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>,
|
||||
rendered_content: Option<RichText>,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
|
@ -647,63 +454,24 @@ impl InfoPopover {
|
|||
style: &EditorStyle,
|
||||
cx: &mut ViewContext<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(|| {
|
||||
render_blocks(
|
||||
style.theme_id,
|
||||
&self.blocks,
|
||||
self.project.read(cx).languages(),
|
||||
self.language.as_ref(),
|
||||
style,
|
||||
)
|
||||
});
|
||||
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
|
||||
let mut region_id = 0;
|
||||
let view_id = cx.view_id();
|
||||
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
|
||||
let code_span_background_color = style.document_highlight_read_background;
|
||||
let regions = rendered_content.regions.clone();
|
||||
Flex::column()
|
||||
.scrollable::<HoverBlock>(1, None, cx)
|
||||
.with_child(
|
||||
Text::new(rendered_content.text.clone(), style.text.clone())
|
||||
.with_highlights(rendered_content.highlights.clone())
|
||||
.with_custom_runs(
|
||||
rendered_content.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::<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),
|
||||
)
|
||||
.with_child(rendered_content.element(
|
||||
style.syntax.clone(),
|
||||
style.text.clone(),
|
||||
code_span_background_color,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
.with_style(style.hover_popover.container)
|
||||
})
|
||||
|
@ -799,11 +567,12 @@ mod tests {
|
|||
InlayId,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::fonts::Weight;
|
||||
use gpui::fonts::{HighlightStyle, Underline, Weight};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use rich_text::Highlight;
|
||||
use smol::stream::StreamExt;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
|
@ -1014,7 +783,7 @@ mod tests {
|
|||
.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;
|
||||
assert_eq!(
|
||||
blocks,
|
||||
|
@ -1024,8 +793,7 @@ mod tests {
|
|||
}],
|
||||
);
|
||||
|
||||
let style = editor.style(cx);
|
||||
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
|
||||
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
code_str.trim(),
|
||||
|
@ -1217,7 +985,7 @@ mod tests {
|
|||
expected_styles,
|
||||
} 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_highlights = ranges
|
||||
|
@ -1228,8 +996,21 @@ mod tests {
|
|||
rendered.text, expected_text,
|
||||
"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!(
|
||||
rendered.highlights, expected_highlights,
|
||||
rendered_highlights, expected_highlights,
|
||||
"wrong highlights for input {blocks:?}"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -107,13 +107,23 @@ fn matching_history_item_paths(
|
|||
) -> HashMap<Arc<Path>, PathMatch> {
|
||||
let history_items_by_worktrees = history_items
|
||||
.iter()
|
||||
.map(|found_path| {
|
||||
let path = &found_path.project.path;
|
||||
.filter_map(|found_path| {
|
||||
let candidate = PathMatchCandidate {
|
||||
path,
|
||||
char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
|
||||
path: &found_path.project.path,
|
||||
// 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(
|
||||
HashMap::default(),
|
||||
|
@ -212,6 +222,10 @@ fn toggle_or_cycle_file_finder(
|
|||
.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)),
|
||||
)
|
||||
.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 {
|
||||
Selected(ProjectPath),
|
||||
Dismissed,
|
||||
|
@ -505,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| (project.is_local()
|
||||
&& history_item
|
||||
.absolute
|
||||
.as_ref()
|
||||
.filter(|abs_path| abs_path.exists())
|
||||
.is_some())
|
||||
|| (project.is_local() && history_item.absolute.is_some())
|
||||
})
|
||||
.cloned()
|
||||
.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(
|
||||
input: &str,
|
||||
expected_matches: usize,
|
||||
|
|
|
@ -441,7 +441,7 @@ mod tests {
|
|||
score,
|
||||
worktree_id: 0,
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path: Arc::from(candidate.path),
|
||||
path_prefix: "".into(),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
},
|
||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathMatchCandidate<'a> {
|
||||
pub path: &'a Arc<Path>,
|
||||
pub path: &'a Path,
|
||||
pub char_bag: CharBag,
|
||||
}
|
||||
|
||||
|
@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
|
|||
score,
|
||||
worktree_id,
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path: Arc::from(candidate.path),
|
||||
path_prefix: Arc::from(""),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
},
|
||||
|
@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
|||
score,
|
||||
worktree_id,
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path: Arc::from(candidate.path),
|
||||
path_prefix: candidate_set.prefix(),
|
||||
distance_to_relative_ancestor: relative_to.as_ref().map_or(
|
||||
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 channel_select: ChannelSelect,
|
||||
pub input_editor: FieldEditor,
|
||||
pub avatar: AvatarStyle,
|
||||
pub avatar_container: ContainerStyle,
|
||||
pub message: ChatMessage,
|
||||
pub continuation_message: ChatMessage,
|
||||
pub last_message_bottom_spacing: f32,
|
||||
pub pending_message: ChatMessage,
|
||||
pub sign_in_prompt: Interactive<TextStyle>,
|
||||
pub icon_button: Interactive<IconButton>,
|
||||
|
@ -643,7 +647,7 @@ pub struct ChatPanel {
|
|||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ChatMessage {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub container: Interactive<ContainerStyle>,
|
||||
pub body: TextStyle,
|
||||
pub sender: ContainedText,
|
||||
pub timestamp: ContainedText,
|
||||
|
|
|
@ -139,6 +139,12 @@ impl<P> PathLikeWithPosition<P> {
|
|||
column: None,
|
||||
})
|
||||
} 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>() {
|
||||
Ok(col) => Ok(Self {
|
||||
path_like: parse_path_like_str(path_like_str)?,
|
||||
|
@ -241,7 +247,6 @@ mod tests {
|
|||
"test_file.rs:1::",
|
||||
"test_file.rs::1:2",
|
||||
"test_file.rs:1::2",
|
||||
"test_file.rs:1:2:",
|
||||
"test_file.rs:1:2:3",
|
||||
] {
|
||||
let actual = parse_str(input);
|
||||
|
@ -277,6 +282,14 @@ mod tests {
|
|||
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 {
|
||||
|
|
|
@ -78,10 +78,14 @@ fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
|
|||
2 => format!("{:b}", result),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if selection.is_empty() {
|
||||
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
||||
}
|
||||
edits.push((range, replace));
|
||||
edits.push((range.clone(), replace));
|
||||
}
|
||||
if selection.is_empty() {
|
||||
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)")
|
||||
.await;
|
||||
cx.assert_matches_neovim("ˇ-1", ["ctrl-a"], "ˇ0").await;
|
||||
cx.assert_matches_neovim("banˇana", ["ctrl-a"], "banˇana")
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -13,3 +13,6 @@
|
|||
{"Put":{"state":"ˇ-1"}}
|
||||
{"Key":"ctrl-a"}
|
||||
{"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, FollowerStatesByLeader, Pane, Workspace,
|
||||
};
|
||||
use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
|
||||
use anyhow::{anyhow, Result};
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
|
@ -13,6 +10,7 @@ use gpui::{
|
|||
};
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
use theme::Theme;
|
||||
|
||||
const HANDLE_HITBOX_SIZE: f32 = 4.0;
|
||||
|
@ -95,7 +93,7 @@ impl PaneGroup {
|
|||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
|
@ -162,7 +160,7 @@ impl Member {
|
|||
project: &ModelHandle<Project>,
|
||||
basis: usize,
|
||||
theme: &Theme,
|
||||
follower_states: &FollowerStatesByLeader,
|
||||
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
|
@ -179,19 +177,10 @@ impl Member {
|
|||
ChildView::new(pane, cx).into_any()
|
||||
};
|
||||
|
||||
let leader = follower_states
|
||||
.iter()
|
||||
.find_map(|(leader_id, follower_states)| {
|
||||
if follower_states.contains_key(pane) {
|
||||
Some(leader_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|leader_id| {
|
||||
let room = active_call?.read(cx).room()?.read(cx);
|
||||
room.remote_participant_for_peer_id(*leader_id)
|
||||
});
|
||||
let leader = follower_states.get(pane).and_then(|state| {
|
||||
let room = active_call?.read(cx).room()?.read(cx);
|
||||
room.remote_participant_for_peer_id(state.leader_id)
|
||||
});
|
||||
|
||||
let mut leader_border = Border::default();
|
||||
let mut leader_status_box = None;
|
||||
|
@ -486,7 +475,7 @@ impl PaneAxis {
|
|||
project: &ModelHandle<Project>,
|
||||
basis: usize,
|
||||
theme: &Theme,
|
||||
follower_state: &FollowerStatesByLeader,
|
||||
follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
|
||||
active_call: Option<&ModelHandle<ActiveCall>>,
|
||||
active_pane: &ViewHandle<Pane>,
|
||||
zoomed: Option<&AnyViewHandle>,
|
||||
|
@ -515,7 +504,7 @@ impl PaneAxis {
|
|||
project,
|
||||
(basis + ix) * 10,
|
||||
theme,
|
||||
follower_state,
|
||||
follower_states,
|
||||
active_call,
|
||||
active_pane,
|
||||
zoomed,
|
||||
|
|
|
@ -79,7 +79,7 @@ use status_bar::StatusBar;
|
|||
pub use status_bar::StatusItemView;
|
||||
use theme::{Theme, ThemeSettings};
|
||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||
use util::{async_iife, ResultExt};
|
||||
use util::ResultExt;
|
||||
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
|
||||
|
||||
lazy_static! {
|
||||
|
@ -573,11 +573,12 @@ pub struct Workspace {
|
|||
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
|
||||
active_pane: ViewHandle<Pane>,
|
||||
last_active_center_pane: Option<WeakViewHandle<Pane>>,
|
||||
last_active_view_id: Option<proto::ViewId>,
|
||||
status_bar: ViewHandle<StatusBar>,
|
||||
titlebar_item: Option<AnyViewHandle>,
|
||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||
project: ModelHandle<Project>,
|
||||
follower_states_by_leader: FollowerStatesByLeader,
|
||||
follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
||||
window_edited: bool,
|
||||
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
|
||||
|
@ -602,10 +603,9 @@ pub struct ViewId {
|
|||
pub id: u64,
|
||||
}
|
||||
|
||||
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct FollowerState {
|
||||
leader_id: PeerId,
|
||||
active_view_id: Option<ViewId>,
|
||||
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
|
||||
}
|
||||
|
@ -786,6 +786,7 @@ impl Workspace {
|
|||
panes_by_item: Default::default(),
|
||||
active_pane: center_pane.clone(),
|
||||
last_active_center_pane: Some(center_pane.downgrade()),
|
||||
last_active_view_id: None,
|
||||
status_bar,
|
||||
titlebar_item: None,
|
||||
notifications: Default::default(),
|
||||
|
@ -793,7 +794,7 @@ impl Workspace {
|
|||
bottom_dock,
|
||||
right_dock,
|
||||
project: project.clone(),
|
||||
follower_states_by_leader: Default::default(),
|
||||
follower_states: Default::default(),
|
||||
last_leaders_by_pane: Default::default(),
|
||||
window_edited: false,
|
||||
active_call,
|
||||
|
@ -934,7 +935,8 @@ impl Workspace {
|
|||
app_state,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
(workspace, opened_items)
|
||||
})
|
||||
|
@ -2510,13 +2512,16 @@ impl Workspace {
|
|||
}
|
||||
|
||||
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) {
|
||||
for state in states_by_pane.into_values() {
|
||||
for item in state.items_by_leader_view_id.into_values() {
|
||||
self.follower_states.retain(|_, state| {
|
||||
if state.leader_id == peer_id {
|
||||
for item in state.items_by_leader_view_id.values() {
|
||||
item.set_leader_peer_id(None, cx);
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -2529,10 +2534,15 @@ impl Workspace {
|
|||
|
||||
self.last_leaders_by_pane
|
||||
.insert(pane.downgrade(), leader_id);
|
||||
self.follower_states_by_leader
|
||||
.entry(leader_id)
|
||||
.or_default()
|
||||
.insert(pane.clone(), Default::default());
|
||||
self.unfollow(&pane, cx);
|
||||
self.follower_states.insert(
|
||||
pane.clone(),
|
||||
FollowerState {
|
||||
leader_id,
|
||||
active_view_id: None,
|
||||
items_by_leader_view_id: Default::default(),
|
||||
},
|
||||
);
|
||||
cx.notify();
|
||||
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
|
@ -2547,9 +2557,8 @@ impl Workspace {
|
|||
let response = request.await?;
|
||||
this.update(&mut cx, |this, _| {
|
||||
let state = this
|
||||
.follower_states_by_leader
|
||||
.get_mut(&leader_id)
|
||||
.and_then(|states_by_pane| states_by_pane.get_mut(&pane))
|
||||
.follower_states
|
||||
.get_mut(&pane)
|
||||
.ok_or_else(|| anyhow!("following interrupted"))?;
|
||||
state.active_view_id = if let Some(active_view_id) = response.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.
|
||||
for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
|
||||
if leader_id == *existing_leader_id {
|
||||
for (pane, _) in states_by_pane {
|
||||
cx.focus(pane);
|
||||
return None;
|
||||
}
|
||||
for (pane, state) in &self.follower_states {
|
||||
if leader_id == state.leader_id {
|
||||
cx.focus(pane);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2662,36 +2669,37 @@ impl Workspace {
|
|||
pane: &ViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<PeerId> {
|
||||
for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
|
||||
let leader_id = *leader_id;
|
||||
if let Some(state) = states_by_pane.remove(pane) {
|
||||
for (_, item) in state.items_by_leader_view_id {
|
||||
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);
|
||||
}
|
||||
let state = self.follower_states.remove(pane)?;
|
||||
let leader_id = state.leader_id;
|
||||
for (_, item) in state.items_by_leader_view_id {
|
||||
item.set_leader_peer_id(None, cx);
|
||||
}
|
||||
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 {
|
||||
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> {
|
||||
|
@ -2862,6 +2870,7 @@ impl Workspace {
|
|||
|
||||
cx.notify();
|
||||
|
||||
self.last_active_view_id = active_view_id.clone();
|
||||
proto::FollowResponse {
|
||||
active_view_id,
|
||||
views: self
|
||||
|
@ -2873,8 +2882,7 @@ impl Workspace {
|
|||
let cx = &cx;
|
||||
move |item| {
|
||||
let item = item.to_followable_item_handle(cx)?;
|
||||
if project_id.is_some()
|
||||
&& project_id != follower_project_id
|
||||
if (project_id.is_none() || project_id != follower_project_id)
|
||||
&& item.is_project_item(cx)
|
||||
{
|
||||
return None;
|
||||
|
@ -2913,8 +2921,8 @@ impl Workspace {
|
|||
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
|
||||
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
|
||||
this.update(cx, |this, _| {
|
||||
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
|
||||
for state in state.values_mut() {
|
||||
for (_, state) in &mut this.follower_states {
|
||||
if state.leader_id == leader_id {
|
||||
state.active_view_id =
|
||||
if let Some(active_view_id) = update_active_view.id.clone() {
|
||||
Some(ViewId::from_proto(active_view_id)?)
|
||||
|
@ -2936,8 +2944,8 @@ impl Workspace {
|
|||
let mut tasks = Vec::new();
|
||||
this.update(cx, |this, cx| {
|
||||
let project = this.project.clone();
|
||||
if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
|
||||
for state in state.values_mut() {
|
||||
for (_, state) in &mut this.follower_states {
|
||||
if state.leader_id == leader_id {
|
||||
let view_id = ViewId::from_proto(id.clone())?;
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
|
||||
tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
|
||||
|
@ -2950,10 +2958,9 @@ impl Workspace {
|
|||
}
|
||||
proto::update_followers::Variant::CreateView(view) => {
|
||||
let panes = this.read_with(cx, |this, _| {
|
||||
this.follower_states_by_leader
|
||||
.get(&leader_id)
|
||||
.into_iter()
|
||||
.flat_map(|states_by_pane| states_by_pane.keys())
|
||||
this.follower_states
|
||||
.iter()
|
||||
.filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
|
||||
.cloned()
|
||||
.collect()
|
||||
})?;
|
||||
|
@ -3012,11 +3019,7 @@ impl Workspace {
|
|||
for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
|
||||
let items = futures::future::try_join_all(item_tasks).await?;
|
||||
this.update(cx, |this, cx| {
|
||||
let state = this
|
||||
.follower_states_by_leader
|
||||
.get_mut(&leader_id)?
|
||||
.get_mut(&pane)?;
|
||||
|
||||
let state = this.follower_states.get_mut(&pane)?;
|
||||
for (id, item) in leader_view_ids.into_iter().zip(items) {
|
||||
item.set_leader_peer_id(Some(leader_id), cx);
|
||||
state.items_by_leader_view_id.insert(id, item);
|
||||
|
@ -3028,7 +3031,7 @@ impl Workspace {
|
|||
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 update = proto::UpdateActiveView::default();
|
||||
if self.active_pane.read(cx).has_focus() {
|
||||
|
@ -3046,11 +3049,14 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
self.update_followers(
|
||||
is_project_item,
|
||||
proto::update_followers::Variant::UpdateActiveView(update),
|
||||
cx,
|
||||
);
|
||||
if update.id != self.last_active_view_id {
|
||||
self.last_active_view_id = update.id.clone();
|
||||
self.update_followers(
|
||||
is_project_item,
|
||||
proto::update_followers::Variant::UpdateActiveView(update),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_followers(
|
||||
|
@ -3070,15 +3076,7 @@ impl Workspace {
|
|||
}
|
||||
|
||||
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
|
||||
self.follower_states_by_leader
|
||||
.iter()
|
||||
.find_map(|(leader_id, state)| {
|
||||
if state.contains_key(pane) {
|
||||
Some(*leader_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
self.follower_states.get(pane).map(|state| state.leader_id)
|
||||
}
|
||||
|
||||
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)? {
|
||||
if leader_in_this_app {
|
||||
let item = state
|
||||
.active_view_id
|
||||
.and_then(|id| state.items_by_leader_view_id.get(&id));
|
||||
if let Some(item) = item {
|
||||
for (pane, state) in &self.follower_states {
|
||||
if state.leader_id != leader_id {
|
||||
continue;
|
||||
}
|
||||
if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
|
||||
if leader_in_this_project || !item.is_project_item(cx) {
|
||||
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) {
|
||||
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
|
||||
|
@ -3394,140 +3398,124 @@ impl Workspace {
|
|||
serialized_workspace: SerializedWorkspace,
|
||||
paths_to_open: Vec<Option<ProjectPath>>,
|
||||
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 {
|
||||
let result = async_iife! {{
|
||||
let (project, old_center_pane) =
|
||||
workspace.read_with(&cx, |workspace, _| {
|
||||
(
|
||||
workspace.project().clone(),
|
||||
workspace.last_active_center_pane.clone(),
|
||||
)
|
||||
})?;
|
||||
let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
|
||||
(
|
||||
workspace.project().clone(),
|
||||
workspace.last_active_center_pane.clone(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut center_items = None;
|
||||
let mut center_group = None;
|
||||
// Traverse the splits tree and add to things
|
||||
if let Some((group, active_pane, items)) = serialized_workspace
|
||||
.center_group
|
||||
.deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
|
||||
.await {
|
||||
center_items = Some(items);
|
||||
center_group = Some((group, active_pane))
|
||||
let mut center_group = None;
|
||||
let mut center_items = None;
|
||||
// Traverse the splits tree and add to things
|
||||
if let Some((group, active_pane, items)) = serialized_workspace
|
||||
.center_group
|
||||
.deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
|
||||
.await
|
||||
{
|
||||
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 mut opened_items = 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<_, _>>();
|
||||
|
||||
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());
|
||||
let docks = serialized_workspace.docks;
|
||||
workspace.left_dock.update(cx, |dock, 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);
|
||||
}
|
||||
} 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()
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
workspace.left_dock.update(cx, |dock, 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);
|
||||
dock.active_panel()
|
||||
.map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
|
||||
|
||||
}
|
||||
}
|
||||
dock.active_panel()
|
||||
.map(|panel| {
|
||||
panel.set_zoomed(docks.right.zoom, cx)
|
||||
});
|
||||
if docks.bottom.visible && docks.bottom.zoom {
|
||||
cx.focus_self()
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
dock.active_panel()
|
||||
.map(|panel| {
|
||||
panel.set_zoomed(docks.bottom.zoom, cx)
|
||||
});
|
||||
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
|
||||
workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
|
||||
|
||||
if docks.bottom.visible && docks.bottom.zoom {
|
||||
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()
|
||||
Ok(opened_items)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -3601,7 +3589,7 @@ async fn open_items(
|
|||
mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
|
||||
app_state: Arc<AppState>,
|
||||
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());
|
||||
|
||||
if let Some(serialized_workspace) = serialized_workspace {
|
||||
|
@ -3619,16 +3607,19 @@ async fn open_items(
|
|||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
let restored_project_paths = cx.read(|cx| {
|
||||
restored_items
|
||||
.iter()
|
||||
.filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
|
||||
.filter_map(|item| item.as_ref()?.project_path(cx))
|
||||
.collect::<HashSet<_>>()
|
||||
});
|
||||
|
||||
opened_items = restored_items;
|
||||
for restored_item in restored_items {
|
||||
opened_items.push(restored_item.map(Ok));
|
||||
}
|
||||
|
||||
project_paths_to_open
|
||||
.iter_mut()
|
||||
.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) {
|
||||
|
@ -3817,7 +3808,7 @@ impl View for Workspace {
|
|||
self.center.render(
|
||||
&project,
|
||||
&theme,
|
||||
&self.follower_states_by_leader,
|
||||
&self.follower_states,
|
||||
self.active_call(),
|
||||
self.active_pane(),
|
||||
self.zoomed
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.107.0"
|
||||
version = "0.107.7"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
|
|
@ -1 +1 @@
|
|||
dev
|
||||
stable
|
|
@ -74,7 +74,8 @@ fn main() {
|
|||
let mut app = gpui::App::new(Assets).unwrap();
|
||||
|
||||
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);
|
||||
|
||||
|
@ -177,7 +178,7 @@ fn main() {
|
|||
})
|
||||
.detach();
|
||||
|
||||
client.telemetry().start(installation_id, cx);
|
||||
client.telemetry().start(installation_id, session_id, cx);
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
languages,
|
||||
|
@ -402,6 +403,7 @@ struct Panic {
|
|||
panicked_on: u128,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
installation_id: Option<String>,
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -412,7 +414,7 @@ struct PanicRequest {
|
|||
|
||||
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 platform = app.platform();
|
||||
|
||||
|
@ -477,7 +479,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
|
|||
line: location.line(),
|
||||
}),
|
||||
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_version: platform
|
||||
.os_version()
|
||||
|
@ -490,13 +492,14 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
|
|||
.as_millis(),
|
||||
backtrace,
|
||||
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() {
|
||||
eprintln!("{}", panic_data_json);
|
||||
}
|
||||
} else {
|
||||
if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
|
||||
log::error!("{}", panic_data_json);
|
||||
}
|
||||
|
||||
if !is_pty {
|
||||
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 panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
} from "./components"
|
||||
import { icon_button } from "../component/icon_button"
|
||||
import { useTheme } from "../theme"
|
||||
import { interactive } from "../element"
|
||||
|
||||
export default function chat_panel(): any {
|
||||
const theme = useTheme()
|
||||
|
@ -27,11 +28,23 @@ export default function chat_panel(): any {
|
|||
|
||||
return {
|
||||
background: background(layer),
|
||||
list: {
|
||||
margin: {
|
||||
left: SPACING,
|
||||
right: SPACING,
|
||||
avatar: {
|
||||
icon_width: 24,
|
||||
icon_height: 24,
|
||||
corner_radius: 4,
|
||||
outer_width: 24,
|
||||
outer_corner_radius: 16,
|
||||
},
|
||||
avatar_container: {
|
||||
padding: {
|
||||
right: 6,
|
||||
left: 2,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
}
|
||||
},
|
||||
list: {
|
||||
|
||||
},
|
||||
channel_select: {
|
||||
header: {
|
||||
|
@ -79,6 +92,22 @@ export default function chat_panel(): any {
|
|||
},
|
||||
},
|
||||
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"),
|
||||
sender: {
|
||||
margin: {
|
||||
|
@ -87,7 +116,32 @@ export default function chat_panel(): any {
|
|||
...text(layer, "sans", "base", { weight: "bold" }),
|
||||
},
|
||||
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: {
|
||||
body: text(layer, "sans", "base"),
|
||||
|
@ -98,6 +152,21 @@ export default function chat_panel(): any {
|
|||
...text(layer, "sans", "base", "disabled"),
|
||||
},
|
||||
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: {
|
||||
default: text(layer, "sans", "base"),
|
||||
|
|
|
@ -21,6 +21,7 @@ export default function contacts_panel(): any {
|
|||
...text(theme.lowest, "sans", "base"),
|
||||
button: icon_button({ variant: "ghost" }),
|
||||
spacing: 4,
|
||||
padding: 4,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue