Merge branch 'main' into settings-for-journal

This commit is contained in:
Joseph T Lyons 2022-10-16 12:42:18 -04:00
commit 9a381c1803
259 changed files with 22647 additions and 14445 deletions

33
.github/workflows/release_actions.yml vendored Normal file
View File

@ -0,0 +1,33 @@
on:
release:
types: [published]
jobs:
discord_release:
runs-on: ubuntu-latest
steps:
- 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 https://zed.dev/releases to grab it.
```md
### Changelog
${{ github.event.release.body }}
```
amplitude_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10.5"
architecture: "x64"
cache: "pip"
- run: pip install -r script/amplitude_release/requirements.txt
- run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}

3
.gitignore vendored
View File

@ -8,4 +8,5 @@
/vendor/bin
/assets/themes/*.json
/assets/themes/internal/*.json
/assets/themes/experiments/*.json
/assets/themes/experiments/*.json
**/venv

1436
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,11 @@ members = ["crates/*"]
default-members = ["crates/zed"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
@ -21,3 +26,4 @@ split-debuginfo = "unpacked"
[profile.release]
debug = true

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.62-bullseye as builder
FROM rust:1.64-bullseye as builder
WORKDIR app
COPY . .

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.62-bullseye as builder
FROM rust:1.64-bullseye as builder
WORKDIR app
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \

View File

@ -1,4 +0,0 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5C7.68629 5 5 7.68629 5 11ZM11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09092 8.09088H14.6364L10.5511 12.4545H12.4546L13.9091 13.9091H7.36365L11.7273 9.54543H9.54547L8.09092 8.09088Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 571 B

View File

@ -3,8 +3,12 @@
{
"bindings": {
"up": "menu::SelectPrev",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
@ -60,13 +64,18 @@
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"up": "editor::MoveUp",
"pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp",
"down": "editor::MoveDown",
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
"ctrl-p": "editor::MoveUp",
"ctrl-n": "editor::MoveDown",
"ctrl-b": "editor::MoveLeft",
"ctrl-f": "editor::MoveRight",
"ctrl-l": "editor::CenterScreen",
"alt-left": "editor::MoveToPreviousWordStart",
"alt-b": "editor::MoveToPreviousWordStart",
"alt-right": "editor::MoveToNextWordEnd",
@ -118,8 +127,18 @@
"stop_at_soft_wraps": true
}
],
"pageup": "editor::PageUp",
"pagedown": "editor::PageDown",
"ctrl-v": [
"editor::MovePageDown",
{
"center_cursor": true
}
],
"alt-v": [
"editor::MovePageUp",
{
"center_cursor": true
}
],
"ctrl-cmd-space": "editor::ShowCharacterPalette"
}
},
@ -376,6 +395,7 @@
{
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
"cmd-shift-c": "collab::ToggleCollaborationMenu",
"cmd-alt-i": "zed::DebugElements"
}
},
@ -395,7 +415,6 @@
"context": "Workspace",
"bindings": {
"shift-escape": "dock::FocusDock",
"cmd-shift-c": "contacts_panel::ToggleFocus",
"cmd-shift-b": "workspace::ToggleRightSidebar"
}
},
@ -451,10 +470,18 @@
"terminal::SendKeystroke",
"up"
],
"pageup": [
"terminal::SendKeystroke",
"pageup"
],
"down": [
"terminal::SendKeystroke",
"down"
],
"pagedown": [
"terminal::SendKeystroke",
"pagedown"
],
"escape": [
"terminal::SendKeystroke",
"escape"

View File

@ -9,11 +9,10 @@
}
],
"h": "vim::Left",
"backspace": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right",
"0": "vim::StartOfLine",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
@ -38,7 +37,60 @@
}
],
"%": "vim::Matching",
"escape": "editor::Cancel"
"escape": "editor::Cancel",
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
],
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
}
},
{
@ -98,6 +150,15 @@
]
}
},
{
"context": "Editor && vim_operator == n",
"bindings": {
"0": [
"vim::Number",
0
]
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
@ -112,13 +173,6 @@
{
"context": "Editor && vim_operator == c",
"bindings": {
"w": "vim::ChangeWord",
"shift-w": [
"vim::ChangeWord",
{
"ignorePunctuation": true
}
],
"c": "vim::CurrentLine"
}
},
@ -134,9 +188,34 @@
"y": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"s": "vim::Sentence",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets"
}
},
{
"context": "Editor && vim_mode == visual",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",

View File

@ -74,6 +74,15 @@
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
@ -86,7 +95,7 @@
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration (e.g. $TERM).
// "shell": "system"
// 2. A program:
@ -103,7 +112,7 @@
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
@ -113,7 +122,7 @@
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
@ -125,7 +134,7 @@
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
@ -133,7 +142,7 @@
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
@ -149,6 +158,9 @@
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {

View File

@ -285,7 +285,7 @@ impl View for ActivityIndicator {
.workspace
.status_bar
.lsp_status;
let style = if state.hovered && action.is_some() {
let style = if state.hovered() && action.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default)
} else {
&theme.default

35
crates/call/Cargo.toml Normal file
View File

@ -0,0 +1,35 @@
[package]
name = "call"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/call.rs"
doctest = false
[features]
test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
"project/test-support",
"util/test-support"
]
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
project = { path = "../project" }
util = { path = "../util" }
anyhow = "1.0.38"
futures = "0.3"
postage = { version = "0.4.1", features = ["futures-traits"] }
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

261
crates/call/src/call.rs Normal file
View File

@ -0,0 +1,261 @@
mod participant;
pub mod room;
use anyhow::{anyhow, Result};
use client::{proto, Client, TypedEnvelope, User, UserStore};
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Subscription, Task,
};
pub use participant::ParticipantLocation;
use postage::watch;
use project::Project;
pub use room::Room;
use std::sync::Arc;
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(active_call);
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub caller: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
pub struct ActiveCall {
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl Entity for ActiveCall {
type Event = room::Event;
}
impl ActiveCall {
fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
Self {
room: None,
incoming_call: watch::channel(),
_subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
],
client,
user_store,
}
}
async fn handle_incoming_call(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})
.await?,
caller: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.caller_user_id, cx)
})
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
});
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::CallCanceled>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = None;
});
Ok(())
}
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
cx.global::<ModelHandle<Self>>().clone()
}
pub fn invite(
&mut self,
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.spawn(|this, mut cx| async move {
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
.await?,
)
} else {
None
};
room.update(&mut cx, |room, cx| {
room.call(recipient_user_id, initial_project_id, cx)
})
.await?;
} else {
let room = cx
.update(|cx| {
Room::create(recipient_user_id, initial_project, client, user_store, cx)
})
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx));
};
Ok(())
})
}
pub fn cancel_invite(
&mut self,
recipient_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.foreground().spawn(async move {
client
.request(proto::CancelCall {
room_id,
recipient_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.1.borrow().clone() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx));
Ok(())
})
}
pub fn decline_incoming(&mut self) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))?;
cx.notify();
}
Ok(())
}
pub fn share_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.set_location(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
fn set_room(&mut self, room: Option<ModelHandle<Room>>, cx: &mut ModelContext<Self>) {
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room, subscriptions));
}
} else {
self.room = None;
}
cx.notify();
}
}
pub fn room(&self) -> Option<&ModelHandle<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
}

View File

@ -0,0 +1,42 @@
use anyhow::{anyhow, Result};
use client::{proto, User};
use gpui::WeakModelHandle;
use project::Project;
use std::sync::Arc;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
SharedProject { project_id: u64 },
UnsharedProject,
External,
}
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}
#[derive(Clone, Default)]
pub struct LocalParticipant {
pub projects: Vec<proto::ParticipantProject>,
pub active_project: Option<WeakModelHandle<Project>>,
}
#[derive(Clone, Debug)]
pub struct RemoteParticipant {
pub user: Arc<User>,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
}

472
crates/call/src/room.rs Normal file
View File

@ -0,0 +1,472 @@
use crate::{
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
IncomingCall,
};
use anyhow::{anyhow, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use collections::{BTreeMap, HashSet};
use futures::StreamExt;
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
use project::Project;
use std::sync::Arc;
use util::ResultExt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
RemoteProjectShared {
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
},
RemoteProjectUnshared {
project_id: u64,
},
Left,
}
pub struct Room {
id: u64,
status: RoomStatus,
local_participant: LocalParticipant,
remote_participants: BTreeMap<PeerId, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
pending_call_count: usize,
leave_when_empty: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
}
impl Entity for Room {
type Event = Event;
fn release(&mut self, _: &mut MutableAppContext) {
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
}
}
impl Room {
fn new(
id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let mut client_status = client.status();
cx.spawn_weak(|this, mut cx| async move {
let is_connected = client_status
.next()
.await
.map_or(false, |s| s.is_connected());
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
if !is_connected || client_status.next().await.is_some() {
if let Some(this) = this.upgrade(&cx) {
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
}
})
.detach();
Self {
id,
status: RoomStatus::Online,
participant_user_ids: Default::default(),
local_participant: Default::default(),
remote_participants: Default::default(),
pending_participants: Default::default(),
pending_call_count: 0,
subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
leave_when_empty: false,
pending_room_update: None,
client,
user_store,
}
}
pub(crate) fn create(
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
let room = cx.add_model(|cx| Self::new(response.id, client, user_store, cx));
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
.update(&mut cx, |room, cx| {
room.share_project(initial_project.clone(), cx)
})
.await?;
Some(initial_project_id)
} else {
None
};
match room
.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.call(recipient_user_id, initial_project_id, cx)
})
.await
{
Ok(()) => Ok(room),
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
}
})
}
pub(crate) fn join(
call: &IncomingCall,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
let room_id = call.room_id;
cx.spawn(|mut cx| async move {
let response = client.request(proto::JoinRoom { id: room_id }).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
room.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.apply_room_update(room_proto, cx)?;
anyhow::Ok(())
})?;
Ok(room)
})
}
fn should_leave(&self) -> bool {
self.leave_when_empty
&& self.pending_room_update.is_none()
&& self.pending_participants.is_empty()
&& self.remote_participants.is_empty()
&& self.pending_call_count == 0
}
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
cx.notify();
cx.emit(Event::Left);
self.status = RoomStatus::Offline;
self.remote_participants.clear();
self.pending_participants.clear();
self.participant_user_ids.clear();
self.subscriptions.clear();
self.client.send(proto::LeaveRoom { id: self.id })?;
Ok(())
}
pub fn id(&self) -> u64 {
self.id
}
pub fn status(&self) -> RoomStatus {
self.status
}
pub fn local_participant(&self) -> &LocalParticipant {
&self.local_participant
}
pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
&self.remote_participants
}
pub fn pending_participants(&self) -> &[Arc<User>] {
&self.pending_participants
}
pub fn contains_participant(&self, user_id: u64) -> bool {
self.participant_user_ids.contains(&user_id)
}
async fn handle_room_updated(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let room = envelope
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))
}
fn apply_room_update(
&mut self,
mut room: proto::Room,
cx: &mut ModelContext<Self>,
) -> Result<()> {
// Filter ourselves out from the room's participants.
let local_participant_ix = room
.participants
.iter()
.position(|participant| Some(participant.user_id) == self.client.user_id());
let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix));
let remote_participant_user_ids = room
.participants
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, cx| {
(
user_store.get_users(remote_participant_user_ids, cx),
user_store.get_users(room.pending_participant_user_ids, cx),
)
});
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
this.update(&mut cx, |this, cx| {
this.participant_user_ids.clear();
if let Some(participant) = local_participant {
this.local_participant.projects = participant.projects;
} else {
this.local_participant.projects.clear();
}
if let Some(participants) = remote_participants.log_err() {
for (participant, user) in room.participants.into_iter().zip(participants) {
let peer_id = PeerId(participant.peer_id);
this.participant_user_ids.insert(participant.user_id);
let old_projects = this
.remote_participants
.get(&peer_id)
.into_iter()
.flat_map(|existing| &existing.projects)
.map(|project| project.id)
.collect::<HashSet<_>>();
let new_projects = participant
.projects
.iter()
.map(|project| project.id)
.collect::<HashSet<_>>();
for project in &participant.projects {
if !old_projects.contains(&project.id) {
cx.emit(Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
});
}
}
for unshared_project_id in old_projects.difference(&new_projects) {
cx.emit(Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
});
}
this.remote_participants.insert(
peer_id,
RemoteParticipant {
user: user.clone(),
projects: participant.projects,
location: ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External),
},
);
}
this.remote_participants.retain(|_, participant| {
if this.participant_user_ids.contains(&participant.user.id) {
true
} else {
for project in &participant.projects {
cx.emit(Event::RemoteProjectUnshared {
project_id: project.id,
});
}
false
}
});
}
if let Some(pending_participants) = pending_participants.log_err() {
this.pending_participants = pending_participants;
for participant in &this.pending_participants {
this.participant_user_ids.insert(participant.id);
}
}
this.pending_room_update.take();
if this.should_leave() {
let _ = this.leave(cx);
}
this.check_invariants();
cx.notify();
});
}));
cx.notify();
Ok(())
}
fn check_invariants(&self) {
#[cfg(any(test, feature = "test-support"))]
{
for participant in self.remote_participants.values() {
assert!(self.participant_user_ids.contains(&participant.user.id));
}
for participant in &self.pending_participants {
assert!(self.participant_user_ids.contains(&participant.id));
}
assert_eq!(
self.participant_user_ids.len(),
self.remote_participants.len() + self.pending_participants.len()
);
}
}
pub(crate) fn call(
&mut self,
recipient_user_id: u64,
initial_project_id: Option<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
cx.notify();
let client = self.client.clone();
let room_id = self.id;
self.pending_call_count += 1;
cx.spawn(|this, mut cx| async move {
let result = client
.request(proto::Call {
room_id,
recipient_user_id,
initial_project_id,
})
.await;
this.update(&mut cx, |this, cx| {
this.pending_call_count -= 1;
if this.should_leave() {
this.leave(cx)?;
}
result
})?;
Ok(())
})
}
pub(crate) fn share_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
}
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project
.read(cx)
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
proto::WorktreeMetadata {
id: worktree.id().to_proto(),
root_name: worktree.root_name().into(),
visible: worktree.is_visible(),
}
})
.collect(),
});
cx.spawn(|this, mut cx| async move {
let response = request.await?;
project.update(&mut cx, |project, cx| {
project
.shared(response.project_id, cx)
.detach_and_log_err(cx)
});
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, cx| {
let active_project = this.local_participant.active_project.as_ref();
if active_project.map_or(false, |location| *location == project) {
this.set_location(Some(&project), cx)
} else {
Task::ready(Ok(()))
}
})
.await?;
Ok(response.project_id)
})
}
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
let client = self.client.clone();
let room_id = self.id;
let location = if let Some(project) = project {
self.local_participant.active_project = Some(project.downgrade());
if let Some(project_id) = project.read(cx).remote_id() {
proto::participant_location::Variant::SharedProject(
proto::participant_location::SharedProject { id: project_id },
)
} else {
proto::participant_location::Variant::UnsharedProject(
proto::participant_location::UnsharedProject {},
)
}
} else {
self.local_participant.active_project = None;
proto::participant_location::Variant::External(proto::participant_location::External {})
};
cx.notify();
cx.foreground().spawn(async move {
client
.request(proto::UpdateParticipantLocation {
room_id,
location: Some(proto::ParticipantLocation {
variant: Some(location),
}),
})
.await?;
Ok(())
})
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum RoomStatus {
Online,
Offline,
}
impl RoomStatus {
pub fn is_offline(&self) -> bool {
matches!(self, RoomStatus::Offline)
}
}

View File

@ -200,7 +200,7 @@ impl ChatPanel {
let theme = &cx.global::<Settings>().theme;
Flex::column()
.with_child(
Container::new(ChildView::new(&self.channel_select).boxed())
Container::new(ChildView::new(&self.channel_select, cx).boxed())
.with_style(theme.chat_panel.channel_select.container)
.boxed(),
)
@ -265,7 +265,7 @@ impl ChatPanel {
fn render_input_box(&self, cx: &AppContext) -> ElementBox {
let theme = &cx.global::<Settings>().theme;
Container::new(ChildView::new(&self.input_editor).boxed())
Container::new(ChildView::new(&self.input_editor, cx).boxed())
.with_style(theme.chat_panel.input_editor.container)
.boxed()
}
@ -311,7 +311,7 @@ impl ChatPanel {
MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
Label::new(
"Sign in to use chat".to_string(),
if mouse_state.hovered {
if mouse_state.hovered() {
theme.chat_panel.hovered_sign_in_prompt.clone()
} else {
theme.chat_panel.sign_in_prompt.clone()

View File

@ -530,7 +530,7 @@ impl ChannelMessage {
) -> Result<Self> {
let sender = user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(message.sender_id, cx)
user_store.get_user(message.sender_id, cx)
})
.await?;
Ok(ChannelMessage {

View File

@ -15,11 +15,9 @@ use async_tungstenite::tungstenite::{
use db::Db;
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
use gpui::{
actions,
serde_json::{json, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
ViewHandle,
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, View, ViewContext, ViewHandle,
};
use http::HttpClient;
use lazy_static::lazy_static;
@ -55,8 +53,10 @@ lazy_static! {
}
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, [Authenticate, TestTelemetry]);
actions!(client, [Authenticate]);
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action({
@ -69,17 +69,6 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
.detach();
}
});
cx.add_global_action({
let client = client.clone();
move |_: &TestTelemetry, _| {
client.report_event(
"test_telemetry",
json!({
"test_property": "test_value"
}),
)
}
});
}
pub struct Client {
@ -333,11 +322,9 @@ impl Client {
log::info!("set status on client {}: {:?}", self.id, status);
let mut state = self.state.write();
*state.status.0.borrow_mut() = status;
let user_id = state.credentials.as_ref().map(|c| c.user_id);
match status {
Status::Connected { .. } => {
self.telemetry.set_user_id(user_id);
state._reconnect_task = None;
}
Status::ConnectionLost => {
@ -345,7 +332,7 @@ impl Client {
let reconnect_interval = state.reconnect_interval;
state._reconnect_task = Some(cx.spawn(|cx| async move {
let mut rng = StdRng::from_entropy();
let mut delay = Duration::from_millis(100);
let mut delay = INITIAL_RECONNECTION_DELAY;
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
log::error!("failed to connect {}", error);
if matches!(*this.status().borrow(), Status::ConnectionError) {
@ -366,7 +353,7 @@ impl Client {
}));
}
Status::SignedOut | Status::UpgradeRequired => {
self.telemetry.set_user_id(user_id);
self.telemetry.set_authenticated_user_info(None, false);
state._reconnect_task.take();
}
_ => {}
@ -449,6 +436,29 @@ impl Client {
}
}
pub fn add_request_handler<M, E, H, F>(
self: &Arc<Self>,
model: ModelHandle<E>,
handler: H,
) -> Subscription
where
M: RequestMessage,
E: Entity,
H: 'static
+ Send
+ Sync
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_message_handler(model, move |handle, envelope, this, cx| {
Self::respond_to_request(
envelope.receipt(),
handler(handle, envelope, this.clone(), cx),
this,
)
})
}
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
@ -653,44 +663,51 @@ impl Client {
self.set_status(Status::Reconnecting, cx);
}
match self.establish_connection(&credentials, cx).await {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
write_credentials_to_keychain(&credentials, cx).log_err();
}
self.set_connection(conn, cx).await;
Ok(())
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if read_from_keychain {
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(false, cx).await
} else {
self.set_status(Status::ConnectionError, cx);
Err(EstablishConnectionError::Unauthorized)?
futures::select_biased! {
connection = self.establish_connection(&credentials, cx).fuse() => {
match connection {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
write_credentials_to_keychain(&credentials, cx).log_err();
}
self.set_connection(conn, cx);
Ok(())
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if read_from_keychain {
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(false, cx).await
} else {
self.set_status(Status::ConnectionError, cx);
Err(EstablishConnectionError::Unauthorized)?
}
}
Err(EstablishConnectionError::UpgradeRequired) => {
self.set_status(Status::UpgradeRequired, cx);
Err(EstablishConnectionError::UpgradeRequired)?
}
Err(error) => {
self.set_status(Status::ConnectionError, cx);
Err(error)?
}
}
}
Err(EstablishConnectionError::UpgradeRequired) => {
self.set_status(Status::UpgradeRequired, cx);
Err(EstablishConnectionError::UpgradeRequired)?
}
Err(error) => {
_ = cx.background().timer(CONNECTION_TIMEOUT).fuse() => {
self.set_status(Status::ConnectionError, cx);
Err(error)?
Err(anyhow!("timed out trying to establish connection"))
}
}
}
async fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
let executor = cx.background();
log::info!("add connection to peer");
let (connection_id, handle_io, mut incoming) = self
.peer
.add_connection(conn, move |duration| executor.timer(duration))
.await;
.add_connection(conn, move |duration| executor.timer(duration));
log::info!("set status to connected {}", connection_id);
self.set_status(Status::Connected { connection_id }, cx);
cx.foreground()
@ -1161,6 +1178,76 @@ mod tests {
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
}
#[gpui::test(iterations = 10)]
async fn test_connection_timeout(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
deterministic.forbid_parking();
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let mut status = client.status();
// Time out when client tries to connect.
client.override_authenticate(move |cx| {
cx.foreground().spawn(async move {
Ok(Credentials {
user_id,
access_token: "token".into(),
})
})
});
client.override_establish_connection(|_, cx| {
cx.foreground().spawn(async move {
future::pending::<()>().await;
unreachable!()
})
});
let auth_and_connect = cx.spawn({
let client = client.clone();
|cx| async move { client.authenticate_and_connect(false, &cx).await }
});
deterministic.run_until_parked();
assert!(matches!(status.next().await, Some(Status::Connecting)));
deterministic.advance_clock(CONNECTION_TIMEOUT);
assert!(matches!(
status.next().await,
Some(Status::ConnectionError { .. })
));
auth_and_connect.await.unwrap_err();
// Allow the connection to be established.
let server = FakeServer::for_client(user_id, &client, cx).await;
assert!(matches!(
status.next().await,
Some(Status::Connected { .. })
));
// Disconnect client.
server.forbid_connections();
server.disconnect();
while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
// Time out when re-establishing the connection.
server.allow_connections();
client.override_establish_connection(|_, cx| {
cx.foreground().spawn(async move {
future::pending::<()>().await;
unreachable!()
})
});
deterministic.advance_clock(2 * INITIAL_RECONNECTION_DELAY);
assert!(matches!(
status.next().await,
Some(Status::Reconnecting { .. })
));
deterministic.advance_clock(CONNECTION_TIMEOUT);
assert!(matches!(
status.next().await,
Some(Status::ReconnectionError { .. })
));
}
#[gpui::test(iterations = 10)]
async fn test_authenticating_more_than_once(
cx: &mut TestAppContext,

View File

@ -9,6 +9,7 @@ use isahc::Request;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use std::{
io::Write,
mem,
@ -29,7 +30,7 @@ pub struct Telemetry {
#[derive(Default)]
struct TelemetryState {
user_id: Option<Arc<str>>,
metrics_id: Option<Arc<str>>,
device_id: Option<Arc<str>>,
app_version: Option<Arc<str>>,
os_version: Option<Arc<str>>,
@ -67,6 +68,7 @@ struct AmplitudeEvent {
os_name: &'static str,
os_version: Option<Arc<str>>,
app_version: Option<Arc<str>>,
platform: &'static str,
event_id: usize,
session_id: u128,
time: u128,
@ -109,7 +111,7 @@ impl Telemetry {
flush_task: Default::default(),
next_event_id: 0,
log_file: None,
user_id: None,
metrics_id: None,
}),
});
@ -170,11 +172,32 @@ impl Telemetry {
.detach();
}
pub fn set_user_id(&self, user_id: Option<u64>) {
self.state.lock().user_id = user_id.map(|id| id.to_string().into());
pub fn set_authenticated_user_info(
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
) {
let is_signed_in = metrics_id.is_some();
self.state.lock().metrics_id = metrics_id.map(|s| s.into());
if is_signed_in {
self.report_event_with_user_properties(
"$identify",
Default::default(),
json!({ "$set": { "staff": is_staff } }),
)
}
}
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
self.report_event_with_user_properties(kind, properties, Default::default());
}
fn report_event_with_user_properties(
self: &Arc<Self>,
kind: &str,
properties: Value,
user_properties: Value,
) {
if AMPLITUDE_API_KEY.is_none() {
return;
}
@ -192,10 +215,15 @@ impl Telemetry {
} else {
None
},
user_properties: None,
user_id: state.user_id.clone(),
user_properties: if let Value::Object(user_properties) = user_properties {
Some(user_properties)
} else {
None
},
user_id: state.metrics_id.clone(),
device_id: state.device_id.clone(),
os_name: state.os_name,
platform: "Zed",
os_version: state.os_version.clone(),
app_version: state.app_version.clone(),
event_id: post_inc(&mut state.next_event_id),

View File

@ -6,7 +6,10 @@ use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
use gpui::{executor, ModelHandle, TestAppContext};
use parking_lot::Mutex;
use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
use rpc::{
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
ConnectionId, Peer, Receipt, TypedEnvelope,
};
use std::{fmt, rc::Rc, sync::Arc};
pub struct FakeServer {
@ -79,7 +82,7 @@ impl FakeServer {
let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
let (connection_id, io, incoming) =
peer.add_test_connection(server_conn, cx.background()).await;
peer.add_test_connection(server_conn, cx.background());
cx.background().spawn(io).detach();
let mut state = state.lock();
state.connection_id = Some(connection_id);
@ -93,14 +96,17 @@ impl FakeServer {
.authenticate_and_connect(false, &cx.to_async())
.await
.unwrap();
server
}
pub fn disconnect(&self) {
self.peer.disconnect(self.connection_id());
let mut state = self.state.lock();
state.connection_id.take();
state.incoming.take();
if self.state.lock().connection_id.is_some() {
self.peer.disconnect(self.connection_id());
let mut state = self.state.lock();
state.connection_id.take();
state.incoming.take();
}
}
pub fn auth_count(&self) -> usize {
@ -126,26 +132,45 @@ impl FakeServer {
#[allow(clippy::await_holding_lock)]
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
self.executor.start_waiting();
let message = self
.state
.lock()
.incoming
.as_mut()
.expect("not connected")
.next()
.await
.ok_or_else(|| anyhow!("other half hung up"))?;
self.executor.finish_waiting();
let type_name = message.payload_type_name();
Ok(*message
.into_any()
.downcast::<TypedEnvelope<M>>()
.unwrap_or_else(|_| {
panic!(
"fake server received unexpected message type: {:?}",
type_name
);
}))
loop {
let message = self
.state
.lock()
.incoming
.as_mut()
.expect("not connected")
.next()
.await
.ok_or_else(|| anyhow!("other half hung up"))?;
self.executor.finish_waiting();
let type_name = message.payload_type_name();
let message = message.into_any();
if message.is::<TypedEnvelope<M>>() {
return Ok(*message.downcast().unwrap());
}
if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
self.respond(
message
.downcast::<TypedEnvelope<GetPrivateUserInfo>>()
.unwrap()
.receipt(),
GetPrivateUserInfoResponse {
metrics_id: "the-metrics-id".into(),
staff: false,
},
)
.await;
continue;
}
panic!(
"fake server received unexpected message type: {:?}",
type_name
);
}
}
pub async fn respond<T: proto::RequestMessage>(

View File

@ -1,14 +1,14 @@
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
use collections::{hash_map::Entry, HashMap, HashSet};
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{prelude::Stream, sink::Sink, watch};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
use util::TryFutureExt as _;
#[derive(Debug)]
#[derive(Default, Debug)]
pub struct User {
pub id: u64,
pub github_login: String,
@ -39,14 +39,7 @@ impl Eq for User {}
pub struct Contact {
pub user: Arc<User>,
pub online: bool,
pub projects: Vec<ProjectMetadata>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProjectMetadata {
pub id: u64,
pub visible_worktree_root_names: Vec<String>,
pub guests: BTreeSet<Arc<User>>,
pub busy: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -138,14 +131,25 @@ impl UserStore {
}),
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
let mut status = client.status();
while let Some(status) = status.recv().await {
while let Some(status) = status.next().await {
match status {
Status::Connected { .. } => {
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
let user = this
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
.log_err()
.await;
let fetch_user = this
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
.log_err();
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
if let Some(info) = info {
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id),
info.staff,
);
} else {
client.telemetry.set_authenticated_user_info(None, false);
}
client.telemetry.report_event("sign in", Default::default());
current_user_tx.send(user).await.ok();
}
}
@ -233,7 +237,6 @@ impl UserStore {
let mut user_ids = HashSet::default();
for contact in &message.contacts {
user_ids.insert(contact.user_id);
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
}
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
user_ids.extend(message.outgoing_requests.iter());
@ -257,9 +260,7 @@ impl UserStore {
for request in message.incoming_requests {
incoming_requests.push({
let user = this
.update(&mut cx, |this, cx| {
this.fetch_user(request.requester_id, cx)
})
.update(&mut cx, |this, cx| this.get_user(request.requester_id, cx))
.await?;
(user, request.should_notify)
});
@ -268,7 +269,7 @@ impl UserStore {
let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests {
outgoing_requests.push(
this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx))
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))
.await?,
);
}
@ -493,7 +494,7 @@ impl UserStore {
.unbounded_send(UpdateContacts::Clear(tx))
.unwrap();
async move {
rx.recv().await;
rx.next().await;
}
}
@ -503,25 +504,43 @@ impl UserStore {
.unbounded_send(UpdateContacts::Wait(tx))
.unwrap();
async move {
rx.recv().await;
rx.next().await;
}
}
pub fn get_users(
&mut self,
mut user_ids: Vec<u64>,
user_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
user_ids.retain(|id| !self.users.contains_key(id));
if user_ids.is_empty() {
Task::ready(Ok(()))
} else {
let load = self.load_users(proto::GetUsers { user_ids }, cx);
cx.foreground().spawn(async move {
load.await?;
Ok(())
) -> Task<Result<Vec<Arc<User>>>> {
let mut user_ids_to_fetch = user_ids.clone();
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
cx.spawn(|this, mut cx| async move {
if !user_ids_to_fetch.is_empty() {
this.update(&mut cx, |this, cx| {
this.load_users(
proto::GetUsers {
user_ids: user_ids_to_fetch,
},
cx,
)
})
.await?;
}
this.read_with(&cx, |this, _| {
user_ids
.iter()
.map(|user_id| {
this.users
.get(user_id)
.cloned()
.ok_or_else(|| anyhow!("user {} not found", user_id))
})
.collect()
})
}
})
}
pub fn fuzzy_search_users(
@ -532,7 +551,7 @@ impl UserStore {
self.load_users(proto::FuzzySearchUsers { query }, cx)
}
pub fn fetch_user(
pub fn get_user(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
@ -612,39 +631,15 @@ impl Contact {
) -> Result<Self> {
let user = user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(contact.user_id, cx)
user_store.get_user(contact.user_id, cx)
})
.await?;
let mut projects = Vec::new();
for project in contact.projects {
let mut guests = BTreeSet::new();
for participant_id in project.guests {
guests.insert(
user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(participant_id, cx)
})
.await?,
);
}
projects.push(ProjectMetadata {
id: project.id,
visible_worktree_root_names: project.visible_worktree_root_names.clone(),
guests,
});
}
Ok(Self {
user,
online: contact.online,
projects,
busy: contact.busy,
})
}
pub fn non_empty_projects(&self) -> impl Iterator<Item = &ProjectMetadata> {
self.projects
.iter()
.filter(|project| !project.visible_worktree_root_names.is_empty())
}
}
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {

View File

@ -1,5 +1,5 @@
[package]
authors = ["Nathan Sobo <nathan@warp.dev>"]
authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
@ -16,7 +16,6 @@ required-features = ["seed-support"]
collections = { path = "../collections" }
rpc = { path = "../rpc" }
util = { path = "../util" }
anyhow = "1.0.40"
async-trait = "0.1.50"
async-tungstenite = "0.16"
@ -55,13 +54,16 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
@ -70,6 +72,7 @@ env_logger = "0.9"
util = { path = "../util" }
lazy_static = "1.4"
serde_json = { version = "1.0", features = ["preserve_order"] }
unindent = "0.1"
[features]
seed-support = ["clap", "lipsum", "reqwest"]

View File

@ -1,6 +0,0 @@
DROP TABLE signups;
ALTER TABLE users
DROP COLUMN github_user_id;
DROP INDEX index_users_on_email_address;

View File

@ -0,0 +1,2 @@
ALTER TABLE "users"
ADD "metrics_id" uuid NOT NULL DEFAULT gen_random_uuid();

View File

@ -24,6 +24,7 @@ use tracing::instrument;
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
Router::new()
.route("/user", get(get_authenticated_user))
.route("/users", get(get_users).post(create_user))
.route("/users/:id", put(update_user).delete(destroy_user))
.route("/users/:id/access_tokens", post(create_access_token))
@ -85,10 +86,33 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
Ok::<_, Error>(next.run(req).await)
}
#[derive(Debug, Deserialize)]
struct AuthenticatedUserParams {
github_user_id: i32,
github_login: String,
}
#[derive(Debug, Serialize)]
struct AuthenticatedUserResponse {
user: User,
metrics_id: String,
}
async fn get_authenticated_user(
Query(params): Query<AuthenticatedUserParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<AuthenticatedUserResponse>> {
let user = app
.db
.get_user_by_github_account(&params.github_login, Some(params.github_user_id))
.await?
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
}
#[derive(Debug, Deserialize)]
struct GetUsersQueryParams {
github_user_id: Option<i32>,
github_login: Option<String>,
query: Option<String>,
page: Option<u32>,
limit: Option<u32>,
@ -98,14 +122,6 @@ async fn get_users(
Query(params): Query<GetUsersQueryParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<Vec<User>>> {
if let Some(github_login) = &params.github_login {
let user = app
.db
.get_user_by_github_account(github_login, params.github_user_id)
.await?;
return Ok(Json(Vec::from_iter(user)));
}
let limit = params.limit.unwrap_or(100);
let users = if let Some(query) = params.query {
app.db.fuzzy_search_users(&query, limit).await?
@ -124,6 +140,8 @@ struct CreateUserParams {
email_address: String,
email_confirmation_code: Option<String>,
#[serde(default)]
admin: bool,
#[serde(default)]
invite_count: i32,
}
@ -131,6 +149,7 @@ struct CreateUserParams {
struct CreateUserResponse {
user: User,
signup_device_id: Option<String>,
metrics_id: String,
}
async fn create_user(
@ -143,12 +162,10 @@ async fn create_user(
github_user_id: params.github_user_id,
invite_count: params.invite_count,
};
let user_id;
let signup_device_id;
// Creating a user via the normal signup process
if let Some(email_confirmation_code) = params.email_confirmation_code {
let result = app
.db
let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
app.db
.create_user_from_invite(
&Invite {
email_address: params.email_address,
@ -156,34 +173,37 @@ async fn create_user(
},
user,
)
.await?;
user_id = result.user_id;
signup_device_id = result.signup_device_id;
if let Some(inviter_id) = result.inviting_user_id {
rpc_server
.invite_code_redeemed(inviter_id, user_id)
.await
.trace_err();
}
.await?
}
// Creating a user as an admin
else {
user_id = app
.db
else if params.admin {
app.db
.create_user(&params.email_address, false, user)
.await?;
signup_device_id = None;
.await?
} else {
Err(Error::Http(
StatusCode::UNPROCESSABLE_ENTITY,
"email confirmation code is required".into(),
))?
};
if let Some(inviter_id) = result.inviting_user_id {
rpc_server
.invite_code_redeemed(inviter_id, result.user_id)
.await
.trace_err();
}
let user = app
.db
.get_user_by_id(user_id)
.get_user_by_id(result.user_id)
.await?
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
Ok(Json(CreateUserResponse {
user,
signup_device_id,
metrics_id: result.metrics_id,
signup_device_id: result.signup_device_id,
}))
}

View File

@ -84,7 +84,23 @@ async fn main() {
},
)
.await
.expect("failed to insert user"),
.expect("failed to insert user")
.user_id,
);
} else if admin {
zed_user_ids.push(
db.create_user(
&format!("{}@zed.dev", github_user.login),
admin,
db::NewUserParams {
github_login: github_user.login,
github_user_id: github_user.id,
invite_count: 5,
},
)
.await
.expect("failed to insert user")
.user_id,
);
}
}

View File

@ -17,10 +17,11 @@ pub trait Db: Send + Sync {
email_address: &str,
admin: bool,
params: NewUserParams,
) -> Result<UserId>;
) -> Result<NewUserResult>;
async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>>;
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>>;
async fn get_user_by_id(&self, id: UserId) -> Result<Option<User>>;
async fn get_user_metrics_id(&self, id: UserId) -> Result<String>;
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>>;
async fn get_users_with_no_invites(&self, invited_by_another_user: bool) -> Result<Vec<User>>;
async fn get_user_by_github_account(
@ -208,21 +209,26 @@ impl Db for PostgresDb {
email_address: &str,
admin: bool,
params: NewUserParams,
) -> Result<UserId> {
) -> Result<NewUserResult> {
let query = "
INSERT INTO users (email_address, github_login, github_user_id, admin)
VALUES ($1, $2, $3, $4)
ON CONFLICT (github_login) DO UPDATE SET github_login = excluded.github_login
RETURNING id
RETURNING id, metrics_id::text
";
Ok(sqlx::query_scalar(query)
let (user_id, metrics_id): (UserId, String) = sqlx::query_as(query)
.bind(email_address)
.bind(params.github_login)
.bind(params.github_user_id)
.bind(admin)
.fetch_one(&self.pool)
.await
.map(UserId)?)
.await?;
Ok(NewUserResult {
user_id,
metrics_id,
signup_device_id: None,
inviting_user_id: None,
})
}
async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
@ -256,6 +262,18 @@ impl Db for PostgresDb {
Ok(users.into_iter().next())
}
async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
let query = "
SELECT metrics_id::text
FROM users
WHERE id = $1
";
Ok(sqlx::query_scalar(query)
.bind(id)
.fetch_one(&self.pool)
.await?)
}
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>> {
let ids = ids.into_iter().map(|id| id.0).collect::<Vec<_>>();
let query = "
@ -410,7 +428,8 @@ impl Db for PostgresDb {
COUNT(*) as count,
COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count
COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
FROM (
SELECT *
FROM signups
@ -431,7 +450,7 @@ impl Db for PostgresDb {
FROM signups
WHERE
NOT email_confirmation_sent AND
platform_mac
(platform_mac OR platform_unknown)
LIMIT $1
",
)
@ -493,13 +512,13 @@ impl Db for PostgresDb {
))?;
}
let user_id: UserId = sqlx::query_scalar(
let (user_id, metrics_id): (UserId, String) = sqlx::query_as(
"
INSERT INTO users
(email_address, github_login, github_user_id, admin, invite_count, invite_code)
VALUES
($1, $2, $3, 'f', $4, $5)
RETURNING id
RETURNING id, metrics_id::text
",
)
.bind(&invite.email_address)
@ -559,6 +578,7 @@ impl Db for PostgresDb {
tx.commit().await?;
Ok(NewUserResult {
user_id,
metrics_id,
inviting_user_id,
signup_device_id,
})
@ -1079,10 +1099,7 @@ impl Db for PostgresDb {
.bind(user_id)
.fetch(&self.pool);
let mut contacts = vec![Contact::Accepted {
user_id,
should_notify: false,
}];
let mut contacts = Vec::new();
while let Some(row) = rows.next().await {
let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?;
@ -1704,6 +1721,8 @@ pub struct WaitlistSummary {
pub mac_count: i64,
#[sqlx(default)]
pub windows_count: i64,
#[sqlx(default)]
pub unknown_count: i64,
}
#[derive(FromRow, PartialEq, Debug, Serialize, Deserialize)]
@ -1722,6 +1741,7 @@ pub struct NewUserParams {
#[derive(Debug)]
pub struct NewUserResult {
pub user_id: UserId,
pub metrics_id: String,
pub inviting_user_id: Option<UserId>,
pub signup_device_id: Option<String>,
}
@ -1808,15 +1828,15 @@ mod test {
email_address: &str,
admin: bool,
params: NewUserParams,
) -> Result<UserId> {
) -> Result<NewUserResult> {
self.background.simulate_random_delay().await;
let mut users = self.users.lock();
if let Some(user) = users
let user_id = if let Some(user) = users
.values()
.find(|user| user.github_login == params.github_login)
{
Ok(user.id)
user.id
} else {
let id = post_inc(&mut *self.next_user_id.lock());
let user_id = UserId(id);
@ -1833,8 +1853,14 @@ mod test {
connected_once: false,
},
);
Ok(user_id)
}
user_id
};
Ok(NewUserResult {
user_id,
metrics_id: "the-metrics-id".to_string(),
inviting_user_id: None,
signup_device_id: None,
})
}
async fn get_all_users(&self, _page: u32, _limit: u32) -> Result<Vec<User>> {
@ -1850,6 +1876,10 @@ mod test {
Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next())
}
async fn get_user_metrics_id(&self, _id: UserId) -> Result<String> {
Ok("the-metrics-id".to_string())
}
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>> {
self.background.simulate_random_delay().await;
let users = self.users.lock();
@ -2050,10 +2080,7 @@ mod test {
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> {
self.background.simulate_random_delay().await;
let mut contacts = vec![Contact::Accepted {
user_id: id,
should_notify: false,
}];
let mut contacts = Vec::new();
for contact in self.contacts.lock().iter() {
if contact.requester_id == id {

View File

@ -12,89 +12,56 @@ async fn test_get_users_by_ids() {
] {
let db = test_db.db();
let user1 = db
.create_user(
"u1@example.com",
false,
NewUserParams {
github_login: "u1".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
.unwrap();
let user2 = db
.create_user(
"u2@example.com",
false,
NewUserParams {
github_login: "u2".into(),
github_user_id: 2,
invite_count: 0,
},
)
.await
.unwrap();
let user3 = db
.create_user(
"u3@example.com",
false,
NewUserParams {
github_login: "u3".into(),
github_user_id: 3,
invite_count: 0,
},
)
.await
.unwrap();
let user4 = db
.create_user(
"u4@example.com",
false,
NewUserParams {
github_login: "u4".into(),
github_user_id: 4,
invite_count: 0,
},
)
.await
.unwrap();
let mut user_ids = Vec::new();
for i in 1..=4 {
user_ids.push(
db.create_user(
&format!("user{i}@example.com"),
false,
NewUserParams {
github_login: format!("user{i}"),
github_user_id: i,
invite_count: 0,
},
)
.await
.unwrap()
.user_id,
);
}
assert_eq!(
db.get_users_by_ids(vec![user1, user2, user3, user4])
.await
.unwrap(),
db.get_users_by_ids(user_ids.clone()).await.unwrap(),
vec![
User {
id: user1,
github_login: "u1".to_string(),
id: user_ids[0],
github_login: "user1".to_string(),
github_user_id: Some(1),
email_address: Some("u1@example.com".to_string()),
email_address: Some("user1@example.com".to_string()),
admin: false,
..Default::default()
},
User {
id: user2,
github_login: "u2".to_string(),
id: user_ids[1],
github_login: "user2".to_string(),
github_user_id: Some(2),
email_address: Some("u2@example.com".to_string()),
email_address: Some("user2@example.com".to_string()),
admin: false,
..Default::default()
},
User {
id: user3,
github_login: "u3".to_string(),
id: user_ids[2],
github_login: "user3".to_string(),
github_user_id: Some(3),
email_address: Some("u3@example.com".to_string()),
email_address: Some("user3@example.com".to_string()),
admin: false,
..Default::default()
},
User {
id: user4,
github_login: "u4".to_string(),
id: user_ids[3],
github_login: "user4".to_string(),
github_user_id: Some(4),
email_address: Some("u4@example.com".to_string()),
email_address: Some("user4@example.com".to_string()),
admin: false,
..Default::default()
}
@ -121,7 +88,8 @@ async fn test_get_user_by_github_account() {
},
)
.await
.unwrap();
.unwrap()
.user_id;
let user_id2 = db
.create_user(
"user2@example.com",
@ -133,7 +101,8 @@ async fn test_get_user_by_github_account() {
},
)
.await
.unwrap();
.unwrap()
.user_id;
let user = db
.get_user_by_github_account("login1", None)
@ -177,7 +146,8 @@ async fn test_worktree_extensions() {
},
)
.await
.unwrap();
.unwrap()
.user_id;
let project = db.register_project(user).await.unwrap();
db.update_worktree_extensions(project, 100, Default::default())
@ -237,43 +207,25 @@ async fn test_user_activity() {
let test_db = TestDb::postgres().await;
let db = test_db.db();
let user_1 = db
.create_user(
"u1@example.com",
false,
NewUserParams {
github_login: "u1".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
.unwrap();
let user_2 = db
.create_user(
"u2@example.com",
false,
NewUserParams {
github_login: "u2".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
.unwrap();
let user_3 = db
.create_user(
"u3@example.com",
false,
NewUserParams {
github_login: "u3".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
.unwrap();
let project_1 = db.register_project(user_1).await.unwrap();
let mut user_ids = Vec::new();
for i in 0..=2 {
user_ids.push(
db.create_user(
&format!("user{i}@example.com"),
false,
NewUserParams {
github_login: format!("user{i}"),
github_user_id: i,
invite_count: 0,
},
)
.await
.unwrap()
.user_id,
);
}
let project_1 = db.register_project(user_ids[0]).await.unwrap();
db.update_worktree_extensions(
project_1,
1,
@ -281,34 +233,37 @@ async fn test_user_activity() {
)
.await
.unwrap();
let project_2 = db.register_project(user_2).await.unwrap();
let project_2 = db.register_project(user_ids[1]).await.unwrap();
let t0 = OffsetDateTime::now_utc() - Duration::from_secs(60 * 60);
// User 2 opens a project
let t1 = t0 + Duration::from_secs(10);
db.record_user_activity(t0..t1, &[(user_2, project_2)])
db.record_user_activity(t0..t1, &[(user_ids[1], project_2)])
.await
.unwrap();
let t2 = t1 + Duration::from_secs(10);
db.record_user_activity(t1..t2, &[(user_2, project_2)])
db.record_user_activity(t1..t2, &[(user_ids[1], project_2)])
.await
.unwrap();
// User 1 joins the project
let t3 = t2 + Duration::from_secs(10);
db.record_user_activity(t2..t3, &[(user_2, project_2), (user_1, project_2)])
.await
.unwrap();
db.record_user_activity(
t2..t3,
&[(user_ids[1], project_2), (user_ids[0], project_2)],
)
.await
.unwrap();
// User 1 opens another project
let t4 = t3 + Duration::from_secs(10);
db.record_user_activity(
t3..t4,
&[
(user_2, project_2),
(user_1, project_2),
(user_1, project_1),
(user_ids[1], project_2),
(user_ids[0], project_2),
(user_ids[0], project_1),
],
)
.await
@ -319,10 +274,10 @@ async fn test_user_activity() {
db.record_user_activity(
t4..t5,
&[
(user_2, project_2),
(user_1, project_2),
(user_1, project_1),
(user_3, project_1),
(user_ids[1], project_2),
(user_ids[0], project_2),
(user_ids[0], project_1),
(user_ids[2], project_1),
],
)
.await
@ -330,13 +285,16 @@ async fn test_user_activity() {
// User 2 leaves
let t6 = t5 + Duration::from_secs(5);
db.record_user_activity(t5..t6, &[(user_1, project_1), (user_3, project_1)])
.await
.unwrap();
db.record_user_activity(
t5..t6,
&[(user_ids[0], project_1), (user_ids[2], project_1)],
)
.await
.unwrap();
let t7 = t6 + Duration::from_secs(60);
let t8 = t7 + Duration::from_secs(10);
db.record_user_activity(t7..t8, &[(user_1, project_1)])
db.record_user_activity(t7..t8, &[(user_ids[0], project_1)])
.await
.unwrap();
@ -344,8 +302,8 @@ async fn test_user_activity() {
db.get_top_users_activity_summary(t0..t6, 10).await.unwrap(),
&[
UserActivitySummary {
id: user_1,
github_login: "u1".to_string(),
id: user_ids[0],
github_login: "user0".to_string(),
project_activity: vec![
ProjectActivitySummary {
id: project_1,
@ -360,8 +318,8 @@ async fn test_user_activity() {
]
},
UserActivitySummary {
id: user_2,
github_login: "u2".to_string(),
id: user_ids[1],
github_login: "user1".to_string(),
project_activity: vec![ProjectActivitySummary {
id: project_2,
duration: Duration::from_secs(50),
@ -369,8 +327,8 @@ async fn test_user_activity() {
}]
},
UserActivitySummary {
id: user_3,
github_login: "u3".to_string(),
id: user_ids[2],
github_login: "user2".to_string(),
project_activity: vec![ProjectActivitySummary {
id: project_1,
duration: Duration::from_secs(15),
@ -442,7 +400,9 @@ async fn test_user_activity() {
);
assert_eq!(
db.get_user_activity_timeline(t3..t6, user_1).await.unwrap(),
db.get_user_activity_timeline(t3..t6, user_ids[0])
.await
.unwrap(),
&[
UserActivityPeriod {
project_id: project_1,
@ -459,7 +419,9 @@ async fn test_user_activity() {
]
);
assert_eq!(
db.get_user_activity_timeline(t0..t8, user_1).await.unwrap(),
db.get_user_activity_timeline(t0..t8, user_ids[0])
.await
.unwrap(),
&[
UserActivityPeriod {
project_id: project_2,
@ -501,7 +463,8 @@ async fn test_recent_channel_messages() {
},
)
.await
.unwrap();
.unwrap()
.user_id;
let org = db.create_org("org", "org").await.unwrap();
let channel = db.create_org_channel(org, "channel").await.unwrap();
for i in 0..10 {
@ -545,7 +508,8 @@ async fn test_channel_message_nonces() {
},
)
.await
.unwrap();
.unwrap()
.user_id;
let org = db.create_org("org", "org").await.unwrap();
let channel = db.create_org_channel(org, "channel").await.unwrap();
@ -587,7 +551,8 @@ async fn test_create_access_tokens() {
},
)
.await
.unwrap();
.unwrap()
.user_id;
db.create_access_token_hash(user, "h1", 3).await.unwrap();
db.create_access_token_hash(user, "h2", 3).await.unwrap();
@ -678,51 +643,30 @@ async fn test_add_contacts() {
] {
let db = test_db.db();
let user_1 = db
.create_user(
"u1@example.com",
false,
NewUserParams {
github_login: "u1".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
.unwrap();
let user_2 = db
.create_user(
"u2@example.com",
false,
NewUserParams {
github_login: "u2".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
.unwrap();
let user_3 = db
.create_user(
"u3@example.com",
false,
NewUserParams {
github_login: "u3".into(),
github_user_id: 2,
invite_count: 0,
},
)
.await
.unwrap();
let mut user_ids = Vec::new();
for i in 0..3 {
user_ids.push(
db.create_user(
&format!("user{i}@example.com"),
false,
NewUserParams {
github_login: format!("user{i}"),
github_user_id: i,
invite_count: 0,
},
)
.await
.unwrap()
.user_id,
);
}
let user_1 = user_ids[0];
let user_2 = user_ids[1];
let user_3 = user_ids[2];
// User starts with no contacts
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
vec![Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
// User requests a contact. Both users see the pending request.
db.send_contact_request(user_1, user_2).await.unwrap();
@ -730,26 +674,14 @@ async fn test_add_contacts() {
assert!(!db.has_contact(user_2, user_1).await.unwrap());
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Outgoing { user_id: user_2 }
],
&[Contact::Outgoing { user_id: user_2 }],
);
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Incoming {
user_id: user_1,
should_notify: true
},
Contact::Accepted {
user_id: user_2,
should_notify: false
},
]
&[Contact::Incoming {
user_id: user_1,
should_notify: true
}]
);
// User 2 dismisses the contact request notification without accepting or rejecting.
@ -762,16 +694,10 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Incoming {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false
},
]
&[Contact::Incoming {
user_id: user_1,
should_notify: false
}]
);
// User can't accept their own contact request
@ -785,31 +711,19 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: true
}
],
&[Contact::Accepted {
user_id: user_2,
should_notify: true
}],
);
assert!(db.has_contact(user_1, user_2).await.unwrap());
assert!(db.has_contact(user_2, user_1).await.unwrap());
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false,
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
},
]
&[Contact::Accepted {
user_id: user_1,
should_notify: false,
}]
);
// Users cannot re-request existing contacts.
@ -822,16 +736,10 @@ async fn test_add_contacts() {
.unwrap_err();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: true,
},
]
&[Contact::Accepted {
user_id: user_2,
should_notify: true,
}]
);
// Users can dismiss notifications of other users accepting their requests.
@ -840,16 +748,10 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
},
]
&[Contact::Accepted {
user_id: user_2,
should_notify: false,
}]
);
// Users send each other concurrent contact requests and
@ -859,10 +761,6 @@ async fn test_add_contacts() {
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
@ -870,21 +768,15 @@ async fn test_add_contacts() {
Contact::Accepted {
user_id: user_3,
should_notify: false
},
}
]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_3,
should_notify: false
}
],
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
// User declines a contact request. Both users see that it is gone.
@ -896,29 +788,17 @@ async fn test_add_contacts() {
assert!(!db.has_contact(user_3, user_2).await.unwrap());
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false
}
]
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_3,
should_notify: false
}
],
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
}
}
@ -927,12 +807,12 @@ async fn test_add_contacts() {
async fn test_invite_codes() {
let postgres = TestDb::postgres().await;
let db = postgres.db();
let user1 = db
let NewUserResult { user_id: user1, .. } = db
.create_user(
"u1@example.com",
"user1@example.com",
false,
NewUserParams {
github_login: "u1".into(),
github_login: "user1".into(),
github_user_id: 0,
invite_count: 0,
},
@ -954,13 +834,14 @@ async fn test_invite_codes() {
// User 2 redeems the invite code and becomes a contact of user 1.
let user2_invite = db
.create_invite_from_code(&invite_code, "u2@example.com", Some("user-2-device-id"))
.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
.await
.unwrap();
let NewUserResult {
user_id: user2,
inviting_user_id,
signup_device_id,
metrics_id,
} = db
.create_user_from_invite(
&user2_invite,
@ -976,31 +857,20 @@ async fn test_invite_codes() {
assert_eq!(invite_count, 1);
assert_eq!(inviting_user_id, Some(user1));
assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
}
]
[Contact::Accepted {
user_id: user2,
should_notify: true
}]
);
assert_eq!(
db.get_contacts(user2).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: false
}
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
@ -1009,13 +879,14 @@ async fn test_invite_codes() {
// User 3 redeems the invite code and becomes a contact of user 1.
let user3_invite = db
.create_invite_from_code(&invite_code, "u3@example.com", None)
.create_invite_from_code(&invite_code, "user3@example.com", None)
.await
.unwrap();
let NewUserResult {
user_id: user3,
inviting_user_id,
signup_device_id,
..
} = db
.create_user_from_invite(
&user3_invite,
@ -1034,10 +905,6 @@ async fn test_invite_codes() {
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
@ -1050,16 +917,10 @@ async fn test_invite_codes() {
);
assert_eq!(
db.get_contacts(user3).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user3,
should_notify: false
},
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
@ -1067,7 +928,7 @@ async fn test_invite_codes() {
);
// Trying to reedem the code for the third time results in an error.
db.create_invite_from_code(&invite_code, "u4@example.com", Some("user-4-device-id"))
db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
.await
.unwrap_err();
@ -1079,7 +940,7 @@ async fn test_invite_codes() {
// User 4 can now redeem the invite code and becomes a contact of user 1.
let user4_invite = db
.create_invite_from_code(&invite_code, "u4@example.com", Some("user-4-device-id"))
.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
.await
.unwrap();
let user4 = db
@ -1100,10 +961,6 @@ async fn test_invite_codes() {
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
@ -1120,16 +977,10 @@ async fn test_invite_codes() {
);
assert_eq!(
db.get_contacts(user4).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user4,
should_notify: false
},
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
@ -1137,7 +988,7 @@ async fn test_invite_codes() {
);
// An existing user cannot redeem invite codes.
db.create_invite_from_code(&invite_code, "u2@example.com", Some("user-2-device-id"))
db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
.await
.unwrap_err();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
@ -1171,6 +1022,7 @@ async fn test_signups() {
mac_count: 8,
linux_count: 4,
windows_count: 2,
unknown_count: 0,
}
);
@ -1223,6 +1075,7 @@ async fn test_signups() {
mac_count: 5,
linux_count: 2,
windows_count: 1,
unknown_count: 0,
}
);
@ -1232,6 +1085,7 @@ async fn test_signups() {
user_id,
inviting_user_id,
signup_device_id,
..
} = db
.create_user_from_invite(
&Invite {
@ -1284,6 +1138,51 @@ async fn test_signups() {
.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_metrics_id() {
let postgres = TestDb::postgres().await;
let db = postgres.db();
let NewUserResult {
user_id: user1,
metrics_id: metrics_id1,
..
} = db
.create_user(
"person1@example.com",
false,
NewUserParams {
github_login: "person1".into(),
github_user_id: 101,
invite_count: 5,
},
)
.await
.unwrap();
let NewUserResult {
user_id: user2,
metrics_id: metrics_id2,
..
} = db
.create_user(
"person2@example.com",
false,
NewUserParams {
github_login: "person2".into(),
github_user_id: 102,
invite_count: 5,
},
)
.await
.unwrap();
assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
assert_eq!(metrics_id1.len(), 36);
assert_eq!(metrics_id2.len(), 36);
assert_ne!(metrics_id1, metrics_id2);
}
fn build_background_executor() -> Arc<Background> {
Deterministic::new(0).build_background()
}

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ use axum::{
routing::get,
Extension, Router, TypedHeader,
};
use collections::HashMap;
use collections::{HashMap, HashSet};
use futures::{
channel::mpsc,
future::{self, BoxFuture},
@ -88,11 +88,6 @@ impl<R: RequestMessage> Response<R> {
self.server.peer.respond(self.receipt, payload)?;
Ok(())
}
fn into_receipt(self) -> Receipt<R> {
self.responded.store(true, SeqCst);
self.receipt
}
}
pub struct Server {
@ -151,11 +146,17 @@ impl Server {
server
.add_request_handler(Server::ping)
.add_request_handler(Server::register_project)
.add_request_handler(Server::unregister_project)
.add_request_handler(Server::create_room)
.add_request_handler(Server::join_room)
.add_message_handler(Server::leave_room)
.add_request_handler(Server::call)
.add_request_handler(Server::cancel_call)
.add_message_handler(Server::decline_call)
.add_request_handler(Server::update_participant_location)
.add_request_handler(Server::share_project)
.add_message_handler(Server::unshare_project)
.add_request_handler(Server::join_project)
.add_message_handler(Server::leave_project)
.add_message_handler(Server::respond_to_join_project_request)
.add_message_handler(Server::update_project)
.add_message_handler(Server::register_project_activity)
.add_request_handler(Server::update_worktree)
@ -205,7 +206,9 @@ impl Server {
.add_request_handler(Server::follow)
.add_message_handler(Server::unfollow)
.add_message_handler(Server::update_followers)
.add_request_handler(Server::get_channel_messages);
.add_request_handler(Server::get_channel_messages)
.add_message_handler(Server::update_diff_base)
.add_request_handler(Server::get_private_user_info);
Arc::new(server)
}
@ -362,8 +365,7 @@ impl Server {
timer.await;
}
}
})
.await;
});
tracing::info!(%user_id, %login, %connection_id, %address, "connection opened");
@ -383,7 +385,11 @@ impl Server {
{
let mut store = this.store().await;
store.add_connection(connection_id, user_id, user.admin);
let incoming_call = store.add_connection(connection_id, user_id, user.admin);
if let Some(incoming_call) = incoming_call {
this.peer.send(connection_id, incoming_call)?;
}
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
if let Some((code, count)) = invite_code {
@ -466,69 +472,58 @@ impl Server {
async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> Result<()> {
self.peer.disconnect(connection_id);
let mut projects_to_unregister = Vec::new();
let removed_user_id;
let mut projects_to_unshare = Vec::new();
let mut contacts_to_update = HashSet::default();
{
let mut store = self.store().await;
let removed_connection = store.remove_connection(connection_id)?;
for (project_id, project) in removed_connection.hosted_projects {
projects_to_unregister.push(project_id);
for project in removed_connection.hosted_projects {
projects_to_unshare.push(project.id);
broadcast(connection_id, project.guests.keys().copied(), |conn_id| {
self.peer.send(
conn_id,
proto::UnregisterProject {
project_id: project_id.to_proto(),
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)
});
for (_, receipts) in project.join_requests {
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::WentOffline as i32
},
)),
},
)?;
}
}
}
for project_id in removed_connection.guest_project_ids {
if let Some(project) = store.project(project_id).trace_err() {
broadcast(connection_id, project.connection_ids(), |conn_id| {
self.peer.send(
conn_id,
proto::RemoveProjectCollaborator {
project_id: project_id.to_proto(),
peer_id: connection_id.0,
},
)
});
if project.guests.is_empty() {
self.peer
.send(
project.host_connection_id,
proto::ProjectUnshared {
project_id: project_id.to_proto(),
},
)
.trace_err();
}
}
for project in removed_connection.guest_projects {
broadcast(connection_id, project.connection_ids, |conn_id| {
self.peer.send(
conn_id,
proto::RemoveProjectCollaborator {
project_id: project.id.to_proto(),
peer_id: connection_id.0,
},
)
});
}
removed_user_id = removed_connection.user_id;
for connection_id in removed_connection.canceled_call_connection_ids {
self.peer
.send(connection_id, proto::CallCanceled {})
.trace_err();
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
}
if let Some(room) = removed_connection
.room_id
.and_then(|room_id| store.room(room_id))
{
self.room_updated(room);
}
contacts_to_update.insert(removed_connection.user_id);
};
self.update_user_contacts(removed_user_id).await.trace_err();
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await.trace_err();
}
for project_id in projects_to_unregister {
for project_id in projects_to_unshare {
self.app_state
.db
.unregister_project(project_id)
@ -596,76 +591,286 @@ impl Server {
Ok(())
}
async fn register_project(
async fn create_room(
self: Arc<Server>,
request: TypedEnvelope<proto::RegisterProject>,
response: Response<proto::RegisterProject>,
request: TypedEnvelope<proto::CreateRoom>,
response: Response<proto::CreateRoom>,
) -> Result<()> {
let user_id;
let room_id;
{
let mut store = self.store().await;
user_id = store.user_id_for_connection(request.sender_id)?;
room_id = store.create_room(request.sender_id)?;
}
response.send(proto::CreateRoomResponse { id: room_id })?;
self.update_user_contacts(user_id).await?;
Ok(())
}
async fn join_room(
self: Arc<Server>,
request: TypedEnvelope<proto::JoinRoom>,
response: Response<proto::JoinRoom>,
) -> Result<()> {
let user_id;
{
let mut store = self.store().await;
user_id = store.user_id_for_connection(request.sender_id)?;
let (room, recipient_connection_ids) =
store.join_room(request.payload.id, request.sender_id)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
response.send(proto::JoinRoomResponse {
room: Some(room.clone()),
})?;
self.room_updated(room);
}
self.update_user_contacts(user_id).await?;
Ok(())
}
async fn leave_room(self: Arc<Server>, message: TypedEnvelope<proto::LeaveRoom>) -> Result<()> {
let mut contacts_to_update = HashSet::default();
{
let mut store = self.store().await;
let user_id = store.user_id_for_connection(message.sender_id)?;
let left_room = store.leave_room(message.payload.id, message.sender_id)?;
contacts_to_update.insert(user_id);
for project in left_room.unshared_projects {
for connection_id in project.connection_ids() {
self.peer.send(
connection_id,
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)?;
}
}
for project in left_room.left_projects {
if project.remove_collaborator {
for connection_id in project.connection_ids {
self.peer.send(
connection_id,
proto::RemoveProjectCollaborator {
project_id: project.id.to_proto(),
peer_id: message.sender_id.0,
},
)?;
}
self.peer.send(
message.sender_id,
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)?;
}
}
if let Some(room) = left_room.room {
self.room_updated(room);
}
for connection_id in left_room.canceled_call_connection_ids {
self.peer
.send(connection_id, proto::CallCanceled {})
.trace_err();
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
}
}
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await?;
}
Ok(())
}
async fn call(
self: Arc<Server>,
request: TypedEnvelope<proto::Call>,
response: Response<proto::Call>,
) -> Result<()> {
let caller_user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
let initial_project_id = request
.payload
.initial_project_id
.map(ProjectId::from_proto);
if !self
.app_state
.db
.has_contact(caller_user_id, recipient_user_id)
.await?
{
return Err(anyhow!("cannot call a user who isn't a contact"))?;
}
let room_id = request.payload.room_id;
let mut calls = {
let mut store = self.store().await;
let (room, recipient_connection_ids, incoming_call) = store.call(
room_id,
recipient_user_id,
initial_project_id,
request.sender_id,
)?;
self.room_updated(room);
recipient_connection_ids
.into_iter()
.map(|recipient_connection_id| {
self.peer
.request(recipient_connection_id, incoming_call.clone())
})
.collect::<FuturesUnordered<_>>()
};
self.update_user_contacts(recipient_user_id).await?;
while let Some(call_response) = calls.next().await {
match call_response.as_ref() {
Ok(_) => {
response.send(proto::Ack {})?;
return Ok(());
}
Err(_) => {
call_response.trace_err();
}
}
}
{
let mut store = self.store().await;
let room = store.call_failed(room_id, recipient_user_id)?;
self.room_updated(&room);
}
self.update_user_contacts(recipient_user_id).await?;
Err(anyhow!("failed to ring call recipient"))?
}
async fn cancel_call(
self: Arc<Server>,
request: TypedEnvelope<proto::CancelCall>,
response: Response<proto::CancelCall>,
) -> Result<()> {
let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id);
{
let mut store = self.store().await;
let (room, recipient_connection_ids) = store.cancel_call(
request.payload.room_id,
recipient_user_id,
request.sender_id,
)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
self.room_updated(room);
response.send(proto::Ack {})?;
}
self.update_user_contacts(recipient_user_id).await?;
Ok(())
}
async fn decline_call(
self: Arc<Server>,
message: TypedEnvelope<proto::DeclineCall>,
) -> Result<()> {
let recipient_user_id;
{
let mut store = self.store().await;
recipient_user_id = store.user_id_for_connection(message.sender_id)?;
let (room, recipient_connection_ids) =
store.decline_call(message.payload.room_id, message.sender_id)?;
for recipient_id in recipient_connection_ids {
self.peer
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
self.room_updated(room);
}
self.update_user_contacts(recipient_user_id).await?;
Ok(())
}
async fn update_participant_location(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateParticipantLocation>,
response: Response<proto::UpdateParticipantLocation>,
) -> Result<()> {
let room_id = request.payload.room_id;
let location = request
.payload
.location
.ok_or_else(|| anyhow!("invalid location"))?;
let mut store = self.store().await;
let room = store.update_participant_location(room_id, location, request.sender_id)?;
self.room_updated(room);
response.send(proto::Ack {})?;
Ok(())
}
fn room_updated(&self, room: &proto::Room) {
for participant in &room.participants {
self.peer
.send(
ConnectionId(participant.peer_id),
proto::RoomUpdated {
room: Some(room.clone()),
},
)
.trace_err();
}
}
async fn share_project(
self: Arc<Server>,
request: TypedEnvelope<proto::ShareProject>,
response: Response<proto::ShareProject>,
) -> Result<()> {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let project_id = self.app_state.db.register_project(user_id).await?;
self.store().await.register_project(
request.sender_id,
let mut store = self.store().await;
let room = store.share_project(
request.payload.room_id,
project_id,
request.payload.online,
request.payload.worktrees,
request.sender_id,
)?;
response.send(proto::RegisterProjectResponse {
response.send(proto::ShareProjectResponse {
project_id: project_id.to_proto(),
})?;
self.room_updated(room);
Ok(())
}
async fn unregister_project(
async fn unshare_project(
self: Arc<Server>,
request: TypedEnvelope<proto::UnregisterProject>,
response: Response<proto::UnregisterProject>,
message: TypedEnvelope<proto::UnshareProject>,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let (user_id, project) = {
let mut state = self.store().await;
let project = state.unregister_project(project_id, request.sender_id)?;
(state.user_id_for_connection(request.sender_id)?, project)
};
self.app_state.db.unregister_project(project_id).await?;
let project_id = ProjectId::from_proto(message.payload.project_id);
let mut store = self.store().await;
let (room, project) = store.unshare_project(project_id, message.sender_id)?;
broadcast(
request.sender_id,
project.guests.keys().copied(),
|conn_id| {
self.peer.send(
conn_id,
proto::UnregisterProject {
project_id: project_id.to_proto(),
},
)
},
message.sender_id,
project.guest_connection_ids(),
|conn_id| self.peer.send(conn_id, message.payload.clone()),
);
for (_, receipts) in project.join_requests {
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::Closed
as i32,
},
)),
},
)?;
}
}
// Send out the `UpdateContacts` message before responding to the unregister
// request. This way, when the project's host can keep track of the project's
// remote id until after they've received the `UpdateContacts` message for
// themself.
self.update_user_contacts(user_id).await?;
response.send(proto::Ack {})?;
self.room_updated(room);
Ok(())
}
@ -719,176 +924,109 @@ impl Server {
};
tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project");
let has_contact = self
.app_state
.db
.has_contact(guest_user_id, host_user_id)
.await?;
if !has_contact {
return Err(anyhow!("no such project"))?;
let mut store = self.store().await;
let (project, replica_id) = store.join_project(request.sender_id, project_id)?;
let peer_count = project.guests.len();
let mut collaborators = Vec::with_capacity(peer_count);
collaborators.push(proto::Collaborator {
peer_id: project.host_connection_id.0,
replica_id: 0,
user_id: project.host.user_id.to_proto(),
});
let worktrees = project
.worktrees
.iter()
.map(|(id, worktree)| proto::WorktreeMetadata {
id: *id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
})
.collect::<Vec<_>>();
// Add all guests other than the requesting user's own connections as collaborators
for (guest_conn_id, guest) in &project.guests {
if request.sender_id != *guest_conn_id {
collaborators.push(proto::Collaborator {
peer_id: guest_conn_id.0,
replica_id: guest.replica_id as u32,
user_id: guest.user_id.to_proto(),
});
}
}
self.store().await.request_join_project(
guest_user_id,
project_id,
response.into_receipt(),
)?;
self.peer.send(
host_connection_id,
proto::RequestJoinProject {
project_id: project_id.to_proto(),
requester_id: guest_user_id.to_proto(),
},
)?;
Ok(())
}
async fn respond_to_join_project_request(
self: Arc<Server>,
request: TypedEnvelope<proto::RespondToJoinProjectRequest>,
) -> Result<()> {
let host_user_id;
{
let mut state = self.store().await;
let project_id = ProjectId::from_proto(request.payload.project_id);
let project = state.project(project_id)?;
if project.host_connection_id != request.sender_id {
Err(anyhow!("no such connection"))?;
}
host_user_id = project.host.user_id;
let guest_user_id = UserId::from_proto(request.payload.requester_id);
if !request.payload.allow {
let receipts = state
.deny_join_project_request(request.sender_id, guest_user_id, project_id)
.ok_or_else(|| anyhow!("no such request"))?;
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::Declined
as i32,
},
)),
},
)?;
}
return Ok(());
}
let (receipts_with_replica_ids, project) = state
.accept_join_project_request(request.sender_id, guest_user_id, project_id)
.ok_or_else(|| anyhow!("no such request"))?;
let peer_count = project.guests.len();
let mut collaborators = Vec::with_capacity(peer_count);
collaborators.push(proto::Collaborator {
peer_id: project.host_connection_id.0,
replica_id: 0,
user_id: project.host.user_id.to_proto(),
});
let worktrees = project
.worktrees
.iter()
.map(|(id, worktree)| proto::WorktreeMetadata {
id: *id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
})
.collect::<Vec<_>>();
// Add all guests other than the requesting user's own connections as collaborators
for (guest_conn_id, guest) in &project.guests {
if receipts_with_replica_ids
.iter()
.all(|(receipt, _)| receipt.sender_id != *guest_conn_id)
{
collaborators.push(proto::Collaborator {
peer_id: guest_conn_id.0,
replica_id: guest.replica_id as u32,
user_id: guest.user_id.to_proto(),
});
}
}
for conn_id in project.connection_ids() {
for (receipt, replica_id) in &receipts_with_replica_ids {
if conn_id != receipt.sender_id {
self.peer.send(
conn_id,
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: receipt.sender_id.0,
replica_id: *replica_id as u32,
user_id: guest_user_id.to_proto(),
}),
},
)?;
}
}
}
// First, we send the metadata associated with each worktree.
for (receipt, replica_id) in &receipts_with_replica_ids {
self.peer.respond(
*receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Accept(
proto::join_project_response::Accept {
worktrees: worktrees.clone(),
replica_id: *replica_id as u32,
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
},
)),
for conn_id in project.connection_ids() {
if conn_id != request.sender_id {
self.peer.send(
conn_id,
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: request.sender_id.0,
replica_id: replica_id as u32,
user_id: guest_user_id.to_proto(),
}),
},
)?;
}
}
for (worktree_id, worktree) in &project.worktrees {
#[cfg(any(test, feature = "test-support"))]
const MAX_CHUNK_SIZE: usize = 2;
#[cfg(not(any(test, feature = "test-support")))]
const MAX_CHUNK_SIZE: usize = 256;
// First, we send the metadata associated with each worktree.
response.send(proto::JoinProjectResponse {
worktrees: worktrees.clone(),
replica_id: replica_id as u32,
collaborators: collaborators.clone(),
language_servers: project.language_servers.clone(),
})?;
// Stream this worktree's entries.
let message = proto::UpdateWorktree {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
root_name: worktree.root_name.clone(),
updated_entries: worktree.entries.values().cloned().collect(),
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.is_complete,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
for (receipt, _) in &receipts_with_replica_ids {
self.peer.send(receipt.sender_id, update.clone())?;
}
}
for (worktree_id, worktree) in &project.worktrees {
#[cfg(any(test, feature = "test-support"))]
const MAX_CHUNK_SIZE: usize = 2;
#[cfg(not(any(test, feature = "test-support")))]
const MAX_CHUNK_SIZE: usize = 256;
// Stream this worktree's diagnostics.
for summary in worktree.diagnostic_summaries.values() {
for (receipt, _) in &receipts_with_replica_ids {
self.peer.send(
receipt.sender_id,
proto::UpdateDiagnosticSummary {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
summary: Some(summary.clone()),
},
)?;
}
}
// Stream this worktree's entries.
let message = proto::UpdateWorktree {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
root_name: worktree.root_name.clone(),
updated_entries: worktree.entries.values().cloned().collect(),
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.is_complete,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
self.peer.send(request.sender_id, update.clone())?;
}
// Stream this worktree's diagnostics.
for summary in worktree.diagnostic_summaries.values() {
self.peer.send(
request.sender_id,
proto::UpdateDiagnosticSummary {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
summary: Some(summary.clone()),
},
)?;
}
}
self.update_user_contacts(host_user_id).await?;
for language_server in &project.language_servers {
self.peer.send(
request.sender_id,
proto::UpdateLanguageServer {
project_id: project_id.to_proto(),
language_server_id: language_server.id,
variant: Some(
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
),
},
)?;
}
Ok(())
}
@ -901,7 +1039,7 @@ impl Server {
let project;
{
let mut store = self.store().await;
project = store.leave_project(sender_id, project_id)?;
project = store.leave_project(project_id, sender_id)?;
tracing::info!(
%project_id,
host_user_id = %project.host_user_id,
@ -920,27 +1058,8 @@ impl Server {
)
});
}
if let Some(requester_id) = project.cancel_request {
self.peer.send(
project.host_connection_id,
proto::JoinProjectRequestCancelled {
project_id: project_id.to_proto(),
requester_id: requester_id.to_proto(),
},
)?;
}
if project.unshare {
self.peer.send(
project.host_connection_id,
proto::ProjectUnshared {
project_id: project_id.to_proto(),
},
)?;
}
}
self.update_user_contacts(project.host_user_id).await?;
Ok(())
}
@ -949,61 +1068,20 @@ impl Server {
request: TypedEnvelope<proto::UpdateProject>,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let user_id;
{
let mut state = self.store().await;
user_id = state.user_id_for_connection(request.sender_id)?;
let guest_connection_ids = state
.read_project(project_id, request.sender_id)?
.guest_connection_ids();
let unshared_project = state.update_project(
project_id,
&request.payload.worktrees,
request.payload.online,
request.sender_id,
)?;
if let Some(unshared_project) = unshared_project {
broadcast(
request.sender_id,
unshared_project.guests.keys().copied(),
|conn_id| {
self.peer.send(
conn_id,
proto::UnregisterProject {
project_id: project_id.to_proto(),
},
)
},
);
for (_, receipts) in unshared_project.pending_join_requests {
for receipt in receipts {
self.peer.respond(
receipt,
proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {
reason:
proto::join_project_response::decline::Reason::Closed
as i32,
},
)),
},
)?;
}
}
} else {
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
self.peer.forward_send(
request.sender_id,
connection_id,
request.payload.clone(),
)
});
}
let room =
state.update_project(project_id, &request.payload.worktrees, request.sender_id)?;
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
});
self.room_updated(room);
};
self.update_user_contacts(user_id).await?;
Ok(())
}
@ -1025,32 +1103,21 @@ impl Server {
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let worktree_id = request.payload.worktree_id;
let (connection_ids, metadata_changed) = {
let mut store = self.store().await;
let (connection_ids, metadata_changed) = store.update_worktree(
request.sender_id,
project_id,
worktree_id,
&request.payload.root_name,
&request.payload.removed_entries,
&request.payload.updated_entries,
request.payload.scan_id,
request.payload.is_last_update,
)?;
(connection_ids, metadata_changed)
};
let connection_ids = self.store().await.update_worktree(
request.sender_id,
project_id,
worktree_id,
&request.payload.root_name,
&request.payload.removed_entries,
&request.payload.updated_entries,
request.payload.scan_id,
request.payload.is_last_update,
)?;
broadcast(request.sender_id, connection_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
});
if metadata_changed {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
self.update_user_contacts(user_id).await?;
}
response.send(proto::Ack {})?;
Ok(())
}
@ -1727,6 +1794,44 @@ impl Server {
Ok(())
}
async fn update_diff_base(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateDiffBase>,
) -> Result<()> {
let receiver_ids = self.store().await.project_connection_ids(
ProjectId::from_proto(request.payload.project_id),
request.sender_id,
)?;
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
});
Ok(())
}
async fn get_private_user_info(
self: Arc<Self>,
request: TypedEnvelope<proto::GetPrivateUserInfo>,
response: Response<proto::GetPrivateUserInfo>,
) -> Result<()> {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let metrics_id = self.app_state.db.get_user_metrics_id(user_id).await?;
let user = self
.app_state
.db
.get_user_by_id(user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
response.send(proto::GetPrivateUserInfoResponse {
metrics_id,
staff: user.admin,
})?;
Ok(())
}
pub(crate) async fn store(&self) -> StoreGuard<'_> {
#[cfg(test)]
tokio::task::yield_now().await;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
[package]
name = "collab_ui"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/collab_ui.rs"
doctest = false
[features]
test-support = [
"call/test-support",
"client/test-support",
"collections/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
"settings/test-support",
"util/test-support",
"workspace/test-support",
]
[dependencies]
call = { path = "../call" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@ -0,0 +1,566 @@
use crate::{contact_notification::ContactNotification, contacts_popover};
use call::{ActiveCall, ParticipantLocation};
use client::{Authenticate, ContactEventKind, PeerId, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use gpui::{
actions,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
json::{self, ToJson},
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
use std::ops::Range;
use theme::Theme;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
actions!(collab, [ToggleCollaborationMenu, ShareProject]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::share_project);
}
pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
_subscriptions: Vec<Subscription>,
}
impl Entity for CollabTitlebarItem {
type Event = ();
}
impl View for CollabTitlebarItem {
fn ui_name() -> &'static str {
"CollabTitlebarItem"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
workspace
} else {
return Empty::new().boxed();
};
let theme = cx.global::<Settings>().theme.clone();
let project = workspace.read(cx).project().read(cx);
let mut container = Flex::row();
if workspace.read(cx).client().status().borrow().is_connected() {
if project.is_shared()
|| project.is_remote()
|| ActiveCall::global(cx).read(cx).room().is_none()
{
container.add_child(self.render_toggle_contacts_button(&theme, cx));
} else {
container.add_child(self.render_share_button(&theme, cx));
}
}
container.add_children(self.render_collaborators(&workspace, &theme, cx));
container.add_children(self.render_current_user(&workspace, &theme, cx));
container.add_children(self.render_connection_status(&workspace, cx));
container.boxed()
}
}
impl CollabTitlebarItem {
pub fn new(
workspace: &ViewHandle<Workspace>,
user_store: &ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
this.window_activation_changed(active, cx)
}));
subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
subscriptions.push(
cx.subscribe(user_store, move |this, user_store, event, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let client::Event::Contact { user, kind } = event {
if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
workspace.show_notification(user.id as usize, cx, |cx| {
cx.add_view(|cx| {
ContactNotification::new(
user.clone(),
*kind,
user_store,
cx,
)
})
})
}
}
});
}
}),
);
Self {
workspace: workspace.downgrade(),
user_store: user_store.clone(),
contacts_popover: None,
_subscriptions: subscriptions,
}
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.upgrade(cx);
let room = ActiveCall::global(cx).read(cx).room().cloned();
if let Some((workspace, room)) = workspace.zip(room) {
let workspace = workspace.read(cx);
let project = if active {
Some(workspace.project().clone())
} else {
None
};
room.update(cx, |room, cx| {
room.set_location(project.as_ref(), cx)
.detach_and_log_err(cx);
});
}
}
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let active_call = ActiveCall::global(cx);
let project = workspace.read(cx).project().clone();
active_call
.update(cx, |call, cx| call.share_project(project, cx))
.detach_and_log_err(cx);
}
}
pub fn toggle_contacts_popover(
&mut self,
_: &ToggleCollaborationMenu,
cx: &mut ViewContext<Self>,
) {
match self.contacts_popover.take() {
Some(_) => {}
None => {
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = workspace.read(cx).project().clone();
let user_store = workspace.read(cx).user_store().clone();
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
contacts_popover::Event::Dismissed => {
this.contacts_popover = None;
}
}
cx.notify();
})
.detach();
self.contacts_popover = Some(view);
}
}
}
cx.notify();
}
fn render_toggle_contacts_button(
&self,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
let badge = if self
.user_store
.read(cx)
.incoming_contact_requests()
.is_empty()
{
None
} else {
Some(
Empty::new()
.collapsed()
.contained()
.with_style(titlebar.toggle_contacts_badge)
.contained()
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
.aligned()
.boxed(),
)
};
Stack::new()
.with_child(
MouseEventHandler::<ToggleCollaborationMenu>::new(0, cx, |state, _| {
let style = titlebar
.toggle_contacts_button
.style_for(state, self.contacts_popover.is_some());
Svg::new("icons/plus_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleCollaborationMenu);
})
.aligned()
.boxed(),
)
.with_children(badge)
.with_children(self.contacts_popover.as_ref().map(|popover| {
Overlay::new(
ChildView::new(popover, cx)
.contained()
.with_margin_top(titlebar.height)
.with_margin_left(titlebar.toggle_contacts_button.default.button_width)
.with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
.boxed(),
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::BottomLeft)
.boxed()
}))
.boxed()
}
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
enum Share {}
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<Share>::new(0, cx, |state, _| {
let style = titlebar.share_button.style_for(state, false);
Label::new("Share".into(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
.with_tooltip::<Share, _>(
0,
"Share project with call participants".into(),
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.boxed()
}
fn render_collaborators(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Vec<ElementBox> {
let active_call = ActiveCall::global(cx);
if let Some(room) = active_call.read(cx).room().cloned() {
let project = workspace.read(cx).project().read(cx);
let mut participants = room
.read(cx)
.remote_participants()
.iter()
.map(|(peer_id, collaborator)| (*peer_id, collaborator.clone()))
.collect::<Vec<_>>();
participants
.sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id));
participants
.into_iter()
.filter_map(|(peer_id, participant)| {
let project = workspace.read(cx).project().read(cx);
let replica_id = project
.collaborators()
.get(&peer_id)
.map(|collaborator| collaborator.replica_id);
let user = participant.user.clone();
Some(self.render_avatar(
&user,
replica_id,
Some((peer_id, &user.github_login, participant.location)),
workspace,
theme,
cx,
))
})
.collect()
} else {
Default::default()
}
}
fn render_current_user(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let user = workspace.read(cx).user_store().read(cx).current_user();
let replica_id = workspace.read(cx).project().read(cx).replica_id();
let status = *workspace.read(cx).client().status().borrow();
if let Some(user) = user {
Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
} else if matches!(status, client::Status::UpgradeRequired) {
None
} else {
Some(
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
let style = theme
.workspace
.titlebar
.sign_in_prompt
.style_for(state, false);
Label::new("Sign in".to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.boxed(),
)
}
}
fn render_avatar(
&self,
user: &User,
replica_id: Option<ReplicaId>,
peer: Option<(PeerId, &str, ParticipantLocation)>,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let is_followed = peer.map_or(false, |(peer_id, _, _)| {
workspace.read(cx).is_following(peer_id)
});
let mut avatar_style;
if let Some((_, _, location)) = peer.as_ref() {
if let ParticipantLocation::SharedProject { project_id } = *location {
if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
avatar_style = theme.workspace.titlebar.avatar;
} else {
avatar_style = theme.workspace.titlebar.inactive_avatar;
}
} else {
avatar_style = theme.workspace.titlebar.inactive_avatar;
}
} else {
avatar_style = theme.workspace.titlebar.avatar;
}
let mut replica_color = None;
if let Some(replica_id) = replica_id {
let color = theme.editor.replica_selection_style(replica_id).cursor;
replica_color = Some(color);
if is_followed {
avatar_style.border = Border::all(1.0, color);
}
}
let content = Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| {
Image::new(avatar.clone())
.with_style(avatar_style)
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.aligned()
.boxed()
}))
.with_children(replica_color.map(|replica_color| {
AvatarRibbon::new(replica_color)
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed()
}))
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.contained()
.with_margin_left(theme.workspace.titlebar.avatar_margin)
.boxed();
if let Some((peer_id, peer_github_login, location)) = peer {
if let Some(replica_id) = replica_id {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleFollow(peer_id))
})
.with_tooltip::<ToggleFollow, _>(
peer_id.0 as usize,
if is_followed {
format!("Unfollow {}", peer_github_login)
} else {
format!("Follow {}", peer_github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else if let ParticipantLocation::SharedProject { project_id } = location {
let user_id = user.id;
MouseEventHandler::<JoinProject>::new(peer_id.0 as usize, cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
})
.with_tooltip::<JoinProject, _>(
peer_id.0 as usize,
format!("Follow {} into external project", peer_github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else {
content
}
} else {
content
}
}
fn render_connection_status(
&self,
workspace: &ViewHandle<Workspace>,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let theme = &cx.global::<Settings>().theme;
match &*workspace.read(cx).client().status().borrow() {
client::Status::ConnectionError
| client::Status::ConnectionLost
| client::Status::Reauthenticating { .. }
| client::Status::Reconnecting { .. }
| client::Status::ReconnectionError { .. } => Some(
Container::new(
Align::new(
ConstrainedBox::new(
Svg::new("icons/cloud_slash_12.svg")
.with_color(theme.workspace.titlebar.offline_icon.color)
.boxed(),
)
.with_width(theme.workspace.titlebar.offline_icon.width)
.boxed(),
)
.boxed(),
)
.with_style(theme.workspace.titlebar.offline_icon.container)
.boxed(),
),
client::Status::UpgradeRequired => Some(
Label::new(
"Please update Zed to collaborate".to_string(),
theme.workspace.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.workspace.titlebar.outdated_warning.container)
.aligned()
.boxed(),
),
_ => None,
}
}
}
pub struct AvatarRibbon {
color: Color,
}
impl AvatarRibbon {
pub fn new(color: Color) -> AvatarRibbon {
AvatarRibbon { color }
}
}
impl Element for AvatarRibbon {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
_: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
(constraint.max, ())
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
_: gpui::geometry::rect::RectF,
_: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let mut path = PathBuilder::new();
path.reset(bounds.lower_left());
path.curve_to(
bounds.origin() + vec2f(bounds.height(), 0.),
bounds.origin(),
);
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
path.curve_to(bounds.lower_right(), bounds.upper_right());
path.line_to(bounds.lower_left());
cx.scene.push_path(path.build(self.color, None));
}
fn dispatch_event(
&mut self,
_: &gpui::Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::MeasurementContext,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: gpui::geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::DebugContext,
) -> gpui::json::Value {
json::json!({
"type": "AvatarRibbon",
"bounds": bounds.to_json(),
"color": self.color.to_json(),
})
}
}

View File

@ -0,0 +1,97 @@
mod collab_titlebar_item;
mod contact_finder;
mod contact_list;
mod contact_notification;
mod contacts_popover;
mod incoming_call_notification;
mod notifications;
mod project_shared_notification;
use call::ActiveCall;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
use gpui::MutableAppContext;
use project::Project;
use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
collab_titlebar_item::init(cx);
contact_notification::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
contacts_popover::init(cx);
incoming_call_notification::init(cx);
project_shared_notification::init(cx);
cx.add_global_action(move |action: &JoinProject, cx| {
let project_id = action.project_id;
let follow_user_id = action.follow_user_id;
let app_state = app_state.clone();
cx.spawn(|mut cx| async move {
let existing_workspace = cx.update(|cx| {
cx.window_ids()
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace
} else {
let project = Project::remote(
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.project_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx.clone(),
)
.await?;
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
});
workspace
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(peer_id, _)| *peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_following(follow_peer_id) {
workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
});
}

View File

@ -1,21 +1,15 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext,
RenderContext, Task, View, ViewContext, ViewHandle,
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Workspace;
use crate::render_icon_button;
actions!(contact_finder, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
Picker::<ContactFinder>::init(cx);
cx.add_action(ContactFinder::toggle);
}
pub struct ContactFinder {
@ -38,8 +32,8 @@ impl View for ContactFinder {
"ContactFinder"
}
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.picker.clone()).boxed()
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@ -107,7 +101,7 @@ impl PickerDelegate for ContactFinder {
fn render_match(
&self,
ix: usize,
mouse_state: MouseState,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> ElementBox {
@ -117,18 +111,21 @@ impl PickerDelegate for ContactFinder {
let icon_path = match request_status {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
"icons/check_8.svg"
}
ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
"icons/x_mark_8.svg"
Some("icons/check_8.svg")
}
ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
ContactRequestStatus::RequestAccepted => None,
};
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.contact_finder.disabled_contact_button
} else {
&theme.contact_finder.contact_button
};
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme
.contact_finder
.picker
.item
.style_for(mouse_state, selected);
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar)
@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder {
.left()
.boxed(),
)
.with_child(
render_icon_button(button_style, icon_path)
.with_children(icon_path.map(|icon_path| {
Svg::new(icon_path)
.with_color(button_style.color)
.constrained()
.with_width(button_style.icon_width)
.aligned()
.contained()
.with_style(button_style.container)
.constrained()
.with_width(button_style.button_width)
.with_height(button_style.button_width)
.aligned()
.flex_float()
.boxed(),
)
.boxed()
}))
.contained()
.with_style(style.container)
.constrained()
@ -160,34 +166,16 @@ impl PickerDelegate for ContactFinder {
}
impl ContactFinder {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
}
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
Self {
picker: cx.add_view(|cx| Picker::new(this, cx)),
picker: cx.add_view(|cx| {
Picker::new(this, cx)
.with_theme(|cx| &cx.global::<Settings>().theme.contact_finder.picker)
}),
potential_contacts: Arc::from([]),
user_store,
selected_index: 0,
}
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => {
workspace.dismiss_modal(cx);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -49,10 +49,7 @@ impl View for ContactNotification {
self.user.clone(),
"wants to add you as a contact",
Some("They won't know if you decline."),
RespondToContactRequest {
user_id: self.user.id,
accept: false,
},
Dismiss(self.user.id),
vec![
(
"Decline",

View File

@ -0,0 +1,171 @@
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
use client::UserStore;
use gpui::{
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
};
use project::Project;
use settings::Settings;
actions!(contacts_popover, [ToggleContactFinder]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPopover::toggle_contact_finder);
}
pub enum Event {
Dismissed,
}
enum Child {
ContactList(ViewHandle<ContactList>),
ContactFinder(ViewHandle<ContactFinder>),
}
pub struct ContactsPopover {
child: Child,
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
_subscription: Option<gpui::Subscription>,
}
impl ContactsPopover {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
child: Child::ContactList(
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
),
project,
user_store,
_subscription: None,
};
this.show_contact_list(cx);
this
}
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child {
Child::ContactList(_) => self.show_contact_finder(cx),
Child::ContactFinder(_) => self.show_contact_list(cx),
}
}
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactFinder(child);
cx.notify();
}
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
let child =
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactList(child);
cx.notify();
}
}
impl Entity for ContactsPopover {
type Event = Event;
}
impl View for ContactsPopover {
fn ui_name() -> &'static str {
"ContactsPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let child = match &self.child {
Child::ContactList(child) => ChildView::new(child, cx),
Child::ContactFinder(child) => ChildView::new(child, cx),
};
MouseEventHandler::<ContactsPopover>::new(0, cx, |_, cx| {
Flex::column()
.with_child(child.flex(1., true).boxed())
.with_children(
self.user_store
.read(cx)
.invite_info()
.cloned()
.and_then(|info| {
enum InviteLink {}
if info.count > 0 {
Some(
MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
let style = theme
.contacts_popover
.invite_row
.style_for(state, false)
.clone();
let copied =
cx.read_from_clipboard().map_or(false, |item| {
item.text().as_str() == info.url.as_ref()
});
Label::new(
format!(
"{} invite link ({} left)",
if copied { "Copied" } else { "Copy" },
info.count
),
style.label.clone(),
)
.aligned()
.left()
.constrained()
.with_height(theme.contacts_popover.invite_row_height)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new(
info.url.to_string(),
));
cx.notify();
})
.boxed(),
)
} else {
None
}
}),
)
.contained()
.with_style(theme.contacts_popover.container)
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
.boxed()
})
.on_down_out(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleCollaborationMenu);
})
.boxed()
}
fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
match &self.child {
Child::ContactList(child) => cx.focus(child),
Child::ContactFinder(child) => cx.focus(child),
}
}
}
}

View File

@ -0,0 +1,232 @@
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_internal_actions, CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext,
View, ViewContext, WindowBounds, WindowKind, WindowOptions,
};
use settings::Settings;
use util::ResultExt;
use workspace::JoinProject;
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(IncomingCallNotification::respond_to_call);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_window = None;
while let Some(incoming_call) = incoming_call.next().await {
if let Some(window_id) = notification_window.take() {
cx.remove_window(window_id);
}
if let Some(incoming_call) = incoming_call {
const PADDING: f32 = 16.;
let screen_size = cx.platform().screen_size();
let window_size = cx.read(|cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
window_size,
)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|_| IncomingCallNotification::new(incoming_call),
);
notification_window = Some(window_id);
}
}
})
.detach();
}
#[derive(Clone, PartialEq)]
struct RespondToCall {
accept: bool,
}
pub struct IncomingCallNotification {
call: IncomingCall,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall) -> Self {
Self { call }
}
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if action.accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.caller.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
cx.spawn_weak(|_, mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: caller_user_id,
})
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, _| {
active_call.decline_incoming().log_err();
});
}
}
fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
.call
.initial_project
.as_ref()
.unwrap_or(&default_project);
Flex::row()
.with_children(self.call.caller.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.caller_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.call.caller.github_login.clone(),
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container)
.boxed(),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if initial_project.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container)
.boxed(),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
initial_project.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container)
.boxed(),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.boxed()
}
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Accept {}
enum Decline {}
Flex::column()
.with_child(
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Accept".to_string(), theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: true });
})
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Decline".to_string(), theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: false });
})
.flex(1., true)
.boxed(),
)
.constrained()
.with_width(
cx.global::<Settings>()
.theme
.incoming_call_notification
.button_width,
)
.boxed()
}
}
impl Entity for IncomingCallNotification {
type Event = ();
}
impl View for IncomingCallNotification {
fn ui_name() -> &'static str {
"IncomingCallNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let background = cx
.global::<Settings>()
.theme
.incoming_call_notification
.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.boxed()
}
}

View File

@ -1,9 +1,7 @@
use crate::render_icon_button;
use client::User;
use gpui::{
elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text},
platform::CursorStyle,
Action, Element, ElementBox, MouseButton, RenderContext, View,
elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
View,
};
use settings::Settings;
use std::sync::Arc;
@ -53,11 +51,18 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
)
.with_child(
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
render_icon_button(
theme.dismiss_button.style_for(state, false),
"icons/x_mark_thin_8.svg",
)
.boxed()
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_thin_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(5.))

View File

@ -0,0 +1,232 @@
use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
WindowBounds, WindowKind, WindowOptions,
};
use settings::Settings;
use std::sync::Arc;
use workspace::JoinProject;
actions!(project_shared_notification, [DismissProject]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectSharedNotification::join);
cx.add_action(ProjectSharedNotification::dismiss);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
room::Event::RemoteProjectShared {
owner,
project_id,
worktree_root_names,
} => {
const PADDING: f32 = 16.;
let screen_size = cx.platform().screen_size();
let theme = &cx.global::<Settings>().theme.project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
window_size,
)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
)
},
);
notification_windows.insert(*project_id, window_id);
}
room::Event::RemoteProjectUnshared { project_id } => {
if let Some(window_id) = notification_windows.remove(&project_id) {
cx.remove_window(window_id);
}
}
room::Event::Left => {
for (_, window_id) in notification_windows.drain() {
cx.remove_window(window_id);
}
}
})
.detach();
}
pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
}
impl ProjectSharedNotification {
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
Self {
project_id,
worktree_root_names,
owner,
}
}
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
cx.propagate_action();
}
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
}
fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.owner_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.owner.github_login.clone(),
theme.owner_username.text.clone(),
)
.contained()
.with_style(theme.owner_username.container)
.boxed(),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container)
.boxed(),
)
.with_children(if self.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
self.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container)
.boxed(),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.boxed()
}
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Open {}
enum Dismiss {}
let project_id = self.project_id;
let owner_user_id = self.owner.id;
Flex::column()
.with_child(
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Open".to_string(), theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: owner_user_id,
});
})
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(DismissProject);
})
.flex(1., true)
.boxed(),
)
.constrained()
.with_width(
cx.global::<Settings>()
.theme
.project_shared_notification
.button_width,
)
.boxed()
}
}
impl Entity for ProjectSharedNotification {
type Event = ();
}
impl View for ProjectSharedNotification {
fn ui_name() -> &'static str {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let background = cx
.global::<Settings>()
.theme
.project_shared_notification
.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.boxed()
}
}

View File

@ -4,8 +4,8 @@ use gpui::{
actions,
elements::{ChildView, Flex, Label, ParentElement},
keymap::Keystroke,
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, View, ViewContext,
ViewHandle,
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
@ -131,8 +131,8 @@ impl View for CommandPalette {
"CommandPalette"
}
fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
ChildView::new(self.picker.clone()).boxed()
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@ -224,7 +224,7 @@ impl PickerDelegate for CommandPalette {
fn render_match(
&self,
ix: usize,
mouse_state: MouseState,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> gpui::ElementBox {

View File

@ -1,32 +0,0 @@
[package]
name = "contacts_panel"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/contacts_panel.rs"
doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@ -1,80 +0,0 @@
use client::User;
use gpui::{
actions, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext,
};
use project::Project;
use std::sync::Arc;
use workspace::Notification;
use crate::notifications::render_user_notification;
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(JoinProjectNotification::decline);
cx.add_action(JoinProjectNotification::accept);
}
pub enum Event {
Dismiss,
}
actions!(contacts_panel, [Accept, Decline]);
pub struct JoinProjectNotification {
project: ModelHandle<Project>,
user: Arc<User>,
}
impl JoinProjectNotification {
pub fn new(project: ModelHandle<Project>, user: Arc<User>, cx: &mut ViewContext<Self>) -> Self {
cx.subscribe(&project, |this, _, event, cx| {
if let project::Event::ContactCancelledJoinRequest(user) = event {
if *user == this.user {
cx.emit(Event::Dismiss);
}
}
})
.detach();
Self { project, user }
}
fn decline(&mut self, _: &Decline, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
project.respond_to_join_request(self.user.id, false, cx)
});
cx.emit(Event::Dismiss)
}
fn accept(&mut self, _: &Accept, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
project.respond_to_join_request(self.user.id, true, cx)
});
cx.emit(Event::Dismiss)
}
}
impl Entity for JoinProjectNotification {
type Event = Event;
}
impl View for JoinProjectNotification {
fn ui_name() -> &'static str {
"JoinProjectNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
render_user_notification(
self.user.clone(),
"wants to join your project",
None,
Decline,
vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))],
cx,
)
}
}
impl Notification for JoinProjectNotification {
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
matches!(event, Event::Dismiss)
}
}

View File

@ -1,32 +0,0 @@
[package]
name = "contacts_status_item"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/contacts_status_item.rs"
doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@ -1,94 +0,0 @@
use editor::Editor;
use gpui::{elements::*, Entity, RenderContext, View, ViewContext, ViewHandle};
use settings::Settings;
pub enum Event {
Deactivated,
}
pub struct ContactsPopover {
filter_editor: ViewHandle<Editor>,
}
impl Entity for ContactsPopover {
type Event = Event;
}
impl View for ContactsPopover {
fn ui_name() -> &'static str {
"ContactsPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.contacts_popover;
Flex::row()
.with_child(
ChildView::new(self.filter_editor.clone())
.contained()
.with_style(
cx.global::<Settings>()
.theme
.contacts_panel
.user_query_editor
.container,
)
.flex(1., true)
.boxed(),
)
// .with_child(
// MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
// Svg::new("icons/user_plus_16.svg")
// .with_color(theme.add_contact_button.color)
// .constrained()
// .with_height(16.)
// .contained()
// .with_style(theme.add_contact_button.container)
// .aligned()
// .boxed()
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, |_, cx| {
// cx.dispatch_action(contact_finder::Toggle)
// })
// .boxed(),
// )
.constrained()
.with_height(
cx.global::<Settings>()
.theme
.contacts_panel
.user_query_editor_height,
)
.aligned()
.top()
.contained()
.with_background_color(theme.background)
.with_uniform_padding(4.)
.boxed()
}
}
impl ContactsPopover {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
cx.observe_window_activation(Self::window_activation_changed)
.detach();
let filter_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
cx,
);
editor.set_placeholder_text("Filter contacts", cx);
editor
});
Self { filter_editor }
}
fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
if !is_active {
cx.emit(Event::Deactivated);
}
}
}

View File

@ -1,94 +0,0 @@
mod contacts_popover;
use contacts_popover::ContactsPopover;
use gpui::{
actions,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f},
Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
ViewHandle, WindowKind,
};
actions!(contacts_status_item, [ToggleContactsPopover]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsStatusItem::toggle_contacts_popover);
}
pub struct ContactsStatusItem {
popover: Option<ViewHandle<ContactsPopover>>,
}
impl Entity for ContactsStatusItem {
type Event = ();
}
impl View for ContactsStatusItem {
fn ui_name() -> &'static str {
"ContactsStatusItem"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let color = match cx.appearance {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
MouseEventHandler::<Self>::new(0, cx, |_, _| {
Svg::new("icons/zed_22.svg")
.with_color(color)
.aligned()
.boxed()
})
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(ToggleContactsPopover);
})
.boxed()
}
}
impl ContactsStatusItem {
pub fn new() -> Self {
Self { popover: None }
}
fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
match self.popover.take() {
Some(popover) => {
cx.remove_window(popover.window_id());
}
None => {
let window_bounds = cx.window_bounds();
let size = vec2f(360., 460.);
let origin = window_bounds.lower_left()
+ vec2f(window_bounds.width() / 2. - size.x() / 2., 0.);
let (_, popover) = cx.add_window(
gpui::WindowOptions {
bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|cx| ContactsPopover::new(cx),
);
cx.subscribe(&popover, Self::on_popover_event).detach();
self.popover = Some(popover);
}
}
}
fn on_popover_event(
&mut self,
popover: ViewHandle<ContactsPopover>,
event: &contacts_popover::Event,
cx: &mut ViewContext<Self>,
) {
match event {
contacts_popover::Event::Deactivated => {
self.popover.take();
cx.remove_window(popover.window_id());
}
}
}
}

View File

@ -258,9 +258,10 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, .. } => {
let style = style
.item
.style_for(Default::default(), Some(ix) == self.selected_index);
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
Label::new(label.to_string(), style.label.clone())
.contained()
@ -283,9 +284,10 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { action, .. } => {
let style = style
.item
.style_for(Default::default(), Some(ix) == self.selected_index);
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
KeystrokeLabel::new(
action.boxed_clone(),
style.keystroke.container,

View File

@ -95,7 +95,7 @@ impl View for ProjectDiagnosticsEditor {
.with_style(theme.container)
.boxed()
} else {
ChildView::new(&self.editor).boxed()
ChildView::new(&self.editor, cx).boxed()
}
}

View File

@ -25,6 +25,7 @@ clock = { path = "../clock" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
@ -47,10 +48,12 @@ ordered-float = "2.1.1"
parking_lot = "0.11"
postage = { version = "0.4", features = ["futures-traits"] }
rand = { version = "0.8.3", optional = true }
serde = { version = "1.0", features = ["derive", "rc"] }
serde = { workspace = true }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { version = "*", optional = true }
[dev-dependencies]
text = { path = "../text", features = ["test-support"] }
@ -67,3 +70,5 @@ rand = "0.8"
unindent = "0.1.7"
tree-sitter = "0.20"
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"
tree-sitter-javascript = "0.20"

View File

@ -330,34 +330,91 @@ impl DisplaySnapshot {
DisplayPoint(self.blocks_snapshot.max_point())
}
/// Returns text chunks starting at the given display row until the end of the file
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
self.blocks_snapshot
.chunks(display_row..self.max_point().row() + 1, false, None)
.map(|h| h.text)
}
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.blocks_snapshot
.chunks(row..row + 1, false, None)
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
.rev()
})
}
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
self.blocks_snapshot
.chunks(display_rows, language_aware, Some(&self.text_highlights))
}
pub fn chars_at(&self, point: DisplayPoint) -> impl Iterator<Item = char> + '_ {
let mut column = 0;
let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
while column < point.column() {
if let Some(c) = chars.next() {
column += c.len_utf8() as u32;
} else {
break;
}
}
chars
pub fn chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
self.text_chunks(point.row())
.flat_map(str::chars)
.skip_while({
let mut column = 0;
move |char| {
let at_point = column >= point.column();
column += char.len_utf8() as u32;
!at_point
}
})
.map(move |ch| {
let result = (ch, point);
if ch == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += ch.len_utf8() as u32;
}
result
})
}
pub fn reverse_chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
self.reverse_text_chunks(point.row())
.flat_map(|chunk| chunk.chars().rev())
.skip_while({
let mut column = self.line_len(point.row());
if self.max_point().row() > point.row() {
column += 1;
}
move |char| {
let at_point = column <= point.column();
column = column.saturating_sub(char.len_utf8() as u32);
!at_point
}
})
.map(move |ch| {
if ch == '\n' {
*point.row_mut() -= 1;
*point.column_mut() = self.line_len(point.row());
} else {
*point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
}
(ch, point)
})
}
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0;
let mut column = 0;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
if column >= target {
break;
}
@ -370,7 +427,7 @@ impl DisplaySnapshot {
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
let mut column = 0;
for (count, c) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
if c == '\n' || count >= char_count as usize {
break;
}
@ -454,7 +511,7 @@ impl DisplaySnapshot {
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
let mut indent = 0;
let mut is_blank = true;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
if c == ' ' {
indent += 1;
} else {
@ -565,7 +622,7 @@ pub mod tests {
use super::*;
use crate::{movement, test::marked_display_snapshot};
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
use language::{Buffer, Language, LanguageConfig, SelectionGoal};
use rand::{prelude::*, Rng};
use smol::stream::StreamExt;
use std::{env, sync::Arc};
@ -609,7 +666,9 @@ pub mod tests {
let buffer = cx.update(|cx| {
if rng.gen() {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)

View File

@ -5,7 +5,7 @@ use super::{
use crate::{Anchor, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
use gpui::{ElementBox, RenderContext};
use language::{BufferSnapshot, Chunk, Patch};
use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex;
use std::{
cell::RefCell,
@ -18,7 +18,7 @@ use std::{
},
};
use sum_tree::{Bias, SumTree};
use text::{Edit, Point};
use text::Edit;
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
@ -42,7 +42,7 @@ pub struct BlockSnapshot {
pub struct BlockId(usize);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct BlockPoint(pub super::Point);
pub struct BlockPoint(pub Point);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct BlockRow(u32);
@ -994,7 +994,7 @@ mod tests {
use rand::prelude::*;
use settings::Settings;
use std::env;
use text::RandomCharIter;
use util::RandomCharIter;
#[gpui::test]
fn test_offset_for_row() {

View File

@ -18,11 +18,11 @@ use std::{
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct FoldPoint(pub super::Point);
pub struct FoldPoint(pub Point);
impl FoldPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(super::Point::new(row, column))
Self(Point::new(row, column))
}
pub fn row(self) -> u32 {
@ -274,6 +274,7 @@ impl FoldMap {
if buffer.edit_count() != new_buffer.edit_count()
|| buffer.parse_count() != new_buffer.parse_count()
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|| buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
|| buffer.trailing_excerpt_update_count()
!= new_buffer.trailing_excerpt_update_count()
{
@ -1195,8 +1196,8 @@ mod tests {
use settings::Settings;
use std::{cmp::Reverse, env, mem, sync::Arc};
use sum_tree::TreeMap;
use text::RandomCharIter;
use util::test::sample_text;
use util::RandomCharIter;
use Bias::{Left, Right};
#[gpui::test]

View File

@ -3,11 +3,10 @@ use super::{
TextHighlights,
};
use crate::MultiBufferSnapshot;
use language::{rope, Chunk};
use language::{Chunk, Point};
use parking_lot::Mutex;
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;
use text::Point;
pub struct TabMap(Mutex<TabSnapshot>);
@ -332,11 +331,11 @@ impl TabSnapshot {
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct TabPoint(pub super::Point);
pub struct TabPoint(pub Point);
impl TabPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(super::Point::new(row, column))
Self(Point::new(row, column))
}
pub fn zero() -> Self {
@ -352,8 +351,8 @@ impl TabPoint {
}
}
impl From<super::Point> for TabPoint {
fn from(point: super::Point) -> Self {
impl From<Point> for TabPoint {
fn from(point: Point) -> Self {
Self(point)
}
}
@ -362,7 +361,7 @@ pub type TabEdit = text::Edit<TabPoint>;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
pub lines: super::Point,
pub lines: Point,
pub first_line_chars: u32,
pub last_line_chars: u32,
pub longest_row: u32,
@ -371,7 +370,7 @@ pub struct TextSummary {
impl<'a> From<&'a str> for TextSummary {
fn from(text: &'a str) -> Self {
let sum = rope::TextSummary::from(text);
let sum = text::TextSummary::from(text);
TextSummary {
lines: sum.lines,
@ -485,7 +484,6 @@ mod tests {
use super::*;
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
use rand::{prelude::StdRng, Rng};
use text::{RandomCharIter, Rope};
#[test]
fn test_expand_tabs() {
@ -508,7 +506,9 @@ mod tests {
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
let len = rng.gen_range(0..30);
let buffer = if rng.gen() {
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
@ -522,7 +522,7 @@ mod tests {
log::info!("FoldMap text: {:?}", folds_snapshot.text());
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
let text = Rope::from(tabs_snapshot.text().as_str());
let text = text::Rope::from(tabs_snapshot.text().as_str());
log::info!(
"TabMap text (tab size: {}): {:?}",
tab_size,

View File

@ -3,12 +3,12 @@ use super::{
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
TextHighlights,
};
use crate::{MultiBufferSnapshot, Point};
use crate::MultiBufferSnapshot;
use gpui::{
fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
Task,
};
use language::Chunk;
use language::{Chunk, Point};
use lazy_static::lazy_static;
use smol::future::yield_now;
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
@ -52,7 +52,7 @@ struct TransformSummary {
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct WrapPoint(pub super::Point);
pub struct WrapPoint(pub Point);
pub struct WrapChunks<'a> {
input_chunks: tab_map::TabChunks<'a>,
@ -959,7 +959,7 @@ impl SumTreeExt for SumTree<Transform> {
impl WrapPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(super::Point::new(row, column))
Self(Point::new(row, column))
}
pub fn row(self) -> u32 {
@ -1029,7 +1029,6 @@ mod tests {
MultiBuffer,
};
use gpui::test::observe;
use language::RandomCharIter;
use rand::prelude::*;
use settings::Settings;
use smol::stream::StreamExt;
@ -1067,7 +1066,9 @@ mod tests {
MultiBuffer::build_random(&mut rng, cx)
} else {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@ use crate::{
};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
use git::diff::{DiffHunk, DiffHunkStatus};
use gpui::{
color::Color,
elements::*,
@ -36,15 +37,16 @@ use gpui::{
use json::json;
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
use project::ProjectPath;
use settings::Settings;
use settings::{GitGutter, Settings};
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
fmt::Write,
iter,
ops::Range,
ops::{DerefMut, Range},
sync::Arc,
};
use theme::DiffStyle;
struct SelectionLayout {
head: DisplayPoint,
@ -452,7 +454,6 @@ impl EditorElement {
let bounds = gutter_bounds.union_rect(text_bounds);
let scroll_top =
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
let editor = self.view(cx.app);
cx.scene.push_quad(Quad {
bounds: gutter_bounds,
background: Some(self.style.gutter_background),
@ -466,7 +467,7 @@ impl EditorElement {
corner_radius: 0.,
});
if let EditorMode::Full = editor.mode {
if let EditorMode::Full = layout.mode {
let mut active_rows = layout.active_rows.iter().peekable();
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
let mut end_row = *start_row;
@ -524,30 +525,141 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut PaintContext,
) {
let scroll_top =
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
struct GutterLayout {
line_height: f32,
// scroll_position: Vector2F,
scroll_top: f32,
bounds: RectF,
}
struct DiffLayout<'a> {
buffer_row: u32,
last_diff: Option<&'a DiffHunk<u32>>,
}
fn diff_quad(
hunk: &DiffHunk<u32>,
gutter_layout: &GutterLayout,
diff_style: &DiffStyle,
) -> Quad {
let color = match hunk.status() {
DiffHunkStatus::Added => diff_style.inserted,
DiffHunkStatus::Modified => diff_style.modified,
//TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => {
let row = hunk.buffer_range.start;
let offset = gutter_layout.line_height / 2.;
let start_y =
row as f32 * gutter_layout.line_height + offset - gutter_layout.scroll_top;
let end_y = start_y + gutter_layout.line_height;
let width = diff_style.removed_width_em * gutter_layout.line_height;
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
return Quad {
bounds: highlight_bounds,
background: Some(diff_style.deleted),
border: Border::new(0., Color::transparent_black()),
corner_radius: 1. * gutter_layout.line_height,
};
}
};
let start_row = hunk.buffer_range.start;
let end_row = hunk.buffer_range.end;
let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
let width = diff_style.width_em * gutter_layout.line_height;
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
Quad {
bounds: highlight_bounds,
background: Some(color),
border: Border::new(0., Color::transparent_black()),
corner_radius: diff_style.corner_radius * gutter_layout.line_height,
}
}
let scroll_position = layout.position_map.snapshot.scroll_position();
let gutter_layout = {
let line_height = layout.position_map.line_height;
GutterLayout {
scroll_top: scroll_position.y() * line_height,
line_height,
bounds,
}
};
let mut diff_layout = DiffLayout {
buffer_row: scroll_position.y() as u32,
last_diff: None,
};
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
let show_gutter = matches!(
&cx.global::<Settings>()
.git_overrides
.git_gutter
.unwrap_or_default(),
GitGutter::TrackedFiles
);
// line is `None` when there's a line wrap
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
if let Some(line) = line {
let line_origin = bounds.origin()
+ vec2f(
bounds.width() - line.width() - layout.gutter_padding,
ix as f32 * layout.position_map.line_height
- (scroll_top % layout.position_map.line_height),
ix as f32 * gutter_layout.line_height
- (gutter_layout.scroll_top % gutter_layout.line_height),
);
line.paint(
line_origin,
visible_bounds,
layout.position_map.line_height,
cx,
);
line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx);
if show_gutter {
//This line starts a buffer line, so let's do the diff calculation
let new_hunk = get_hunk(diff_layout.buffer_row, &layout.diff_hunks);
let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) {
(Some(old_hunk), Some(new_hunk)) if new_hunk == old_hunk => (false, false),
(a, b) => (a.is_some(), b.is_some()),
};
if is_ending {
let last_hunk = diff_layout.last_diff.take().unwrap();
cx.scene
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style));
}
if is_starting {
let new_hunk = new_hunk.unwrap();
diff_layout.last_diff = Some(new_hunk);
};
diff_layout.buffer_row += 1;
}
}
}
// If we ran out with a diff hunk still being prepped, paint it now
if let Some(last_hunk) = diff_layout.last_diff {
cx.scene
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style))
}
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut y = *row as f32 * layout.position_map.line_height - scroll_top;
let mut y = *row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
y += (layout.position_map.line_height - indicator.size().y()) / 2.;
y += (gutter_layout.line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
}
@ -563,10 +675,8 @@ impl EditorElement {
let style = &self.style;
let local_replica_id = view.replica_id(cx);
let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = scroll_position.y() as u32;
let start_row = layout.visible_display_row_range.start;
let scroll_top = scroll_position.y() * layout.position_map.line_height;
let end_row =
((scroll_top + bounds.height()) / layout.position_map.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
let max_glyph_width = layout.position_map.em_width;
let scroll_left = scroll_position.x() * max_glyph_width;
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
@ -585,8 +695,6 @@ impl EditorElement {
for (range, color) in &layout.highlighted_ranges {
self.paint_highlighted_range(
range.clone(),
start_row,
end_row,
*color,
0.,
0.15 * layout.position_map.line_height,
@ -607,8 +715,6 @@ impl EditorElement {
for selection in selections {
self.paint_highlighted_range(
selection.range.clone(),
start_row,
end_row,
selection_style.selection,
corner_radius,
corner_radius * 2.,
@ -622,7 +728,10 @@ impl EditorElement {
if view.show_local_cursors() || *replica_id != local_replica_id {
let cursor_position = selection.head;
if (start_row..end_row).contains(&cursor_position.row()) {
if layout
.visible_display_row_range
.contains(&cursor_position.row())
{
let cursor_row_layout = &layout.position_map.line_layouts
[(cursor_position.row() - start_row) as usize];
let cursor_column = cursor_position.column() as usize;
@ -639,7 +748,7 @@ impl EditorElement {
.snapshot
.chars_at(cursor_position)
.next()
.and_then(|character| {
.and_then(|(character, _)| {
let font_id =
cursor_row_layout.font_for_index(cursor_column)?;
let text = character.to_string();
@ -796,12 +905,123 @@ impl EditorElement {
cx.scene.pop_layer();
}
fn paint_scrollbar(&mut self, bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
enum ScrollbarMouseHandlers {}
if layout.mode != EditorMode::Full {
return;
}
let view = self.view.clone();
let style = &self.style.theme.scrollbar;
let top = bounds.min_y();
let bottom = bounds.max_y();
let right = bounds.max_x();
let left = right - style.width;
let row_range = &layout.scrollbar_row_range;
let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
let mut height = bounds.height();
let mut first_row_y_offset = 0.0;
// Impose a minimum height on the scrollbar thumb
let min_thumb_height =
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
let thumb_height = (row_range.end - row_range.start) * height / max_row;
if thumb_height < min_thumb_height {
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
height -= min_thumb_height - thumb_height;
}
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row };
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom));
let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom));
if layout.show_scrollbars {
cx.scene.push_quad(Quad {
bounds: track_bounds,
border: style.track.border,
background: style.track.background_color,
..Default::default()
});
cx.scene.push_quad(Quad {
bounds: thumb_bounds,
border: style.thumb.border,
background: style.thumb.background_color,
corner_radius: style.thumb.corner_radius,
});
}
cx.scene.push_cursor_region(CursorRegion {
bounds: track_bounds,
style: CursorStyle::Arrow,
});
cx.scene.push_mouse_region(
MouseRegion::new::<ScrollbarMouseHandlers>(view.id(), view.id(), track_bounds)
.on_move({
let view = view.clone();
move |_, cx| {
if let Some(view) = view.upgrade(cx.deref_mut()) {
view.update(cx.deref_mut(), |view, cx| {
view.make_scrollbar_visible(cx);
});
}
}
})
.on_down(MouseButton::Left, {
let view = view.clone();
let row_range = row_range.clone();
move |e, cx| {
let y = e.position.y();
if let Some(view) = view.upgrade(cx.deref_mut()) {
view.update(cx.deref_mut(), |view, cx| {
if y < thumb_top || thumb_bottom < y {
let center_row =
((y - top) * max_row as f32 / height).round() as u32;
let top_row = center_row.saturating_sub(
(row_range.end - row_range.start) as u32 / 2,
);
let mut position = view.scroll_position(cx);
position.set_y(top_row as f32);
view.set_scroll_position(position, cx);
} else {
view.make_scrollbar_visible(cx);
}
});
}
}
})
.on_drag(MouseButton::Left, {
let view = view.clone();
move |e, cx| {
let y = e.prev_mouse_position.y();
let new_y = e.position.y();
if thumb_top < y && y < thumb_bottom {
if let Some(view) = view.upgrade(cx.deref_mut()) {
view.update(cx.deref_mut(), |view, cx| {
let mut position = view.scroll_position(cx);
position.set_y(
position.y() + (new_y - y) * (max_row as f32) / height,
);
if position.y() < 0.0 {
position.set_y(0.);
}
view.set_scroll_position(position, cx);
});
}
}
}
}),
);
}
#[allow(clippy::too_many_arguments)]
fn paint_highlighted_range(
&self,
range: Range<DisplayPoint>,
start_row: u32,
end_row: u32,
color: Color,
corner_radius: f32,
line_end_overshoot: f32,
@ -812,6 +1032,8 @@ impl EditorElement {
bounds: RectF,
cx: &mut PaintContext,
) {
let start_row = layout.visible_display_row_range.start;
let end_row = layout.visible_display_row_range.end;
if range.start != range.end {
let row_range = if range.end.column() == 0 {
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
@ -1252,6 +1474,27 @@ impl EditorElement {
}
}
/// Get the hunk that contains buffer_line, starting from start_idx
/// Returns none if there is none found, and
fn get_hunk(buffer_line: u32, hunks: &[DiffHunk<u32>]) -> Option<&DiffHunk<u32>> {
for i in 0..hunks.len() {
// Safety: Index out of bounds is handled by the check above
let hunk = hunks.get(i).unwrap();
if hunk.buffer_range.contains(&(buffer_line as u32)) {
return Some(hunk);
} else if hunk.status() == DiffHunkStatus::Removed && buffer_line == hunk.buffer_range.start
{
return Some(hunk);
} else if hunk.buffer_range.start > buffer_line as u32 {
// If we've passed the buffer_line, just stop
return None;
}
}
// We reached the end of the array without finding a hunk, just return none.
return None;
}
impl Element for EditorElement {
type LayoutState = LayoutState;
type PaintState = ();
@ -1288,6 +1531,8 @@ impl Element for EditorElement {
let em_advance = style.text.em_advance(cx.font_cache);
let overscroll = vec2f(em_width, 0.);
let snapshot = self.update_view(cx.app, |view, cx| {
view.set_visible_line_count(size.y() / line_height);
let wrap_width = match view.soft_wrap_mode(cx) {
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
SoftWrap::EditorWidth => {
@ -1333,12 +1578,13 @@ impl Element for EditorElement {
// The scroll position is a fractional point, the whole number of which represents
// the top of the window in terms of display rows.
let start_row = scroll_position.y() as u32;
let scroll_top = scroll_position.y() * line_height;
let height_in_lines = size.y() / line_height;
let max_row = snapshot.max_point().row();
// Add 1 to ensure selections bleed off screen
let end_row = 1 + cmp::min(
((scroll_top + size.y()) / line_height).ceil() as u32,
snapshot.max_point().row(),
(scroll_position.y() + height_in_lines).ceil() as u32,
max_row,
);
let start_anchor = if start_row == 0 {
@ -1348,7 +1594,7 @@ impl Element for EditorElement {
.buffer_snapshot
.anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
};
let end_anchor = if end_row > snapshot.max_point().row() {
let end_anchor = if end_row > max_row {
Anchor::max()
} else {
snapshot
@ -1360,6 +1606,7 @@ impl Element for EditorElement {
let mut active_rows = BTreeMap::new();
let mut highlighted_rows = None;
let mut highlighted_ranges = Vec::new();
let mut show_scrollbars = false;
self.update_view(cx.app, |view, cx| {
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
@ -1420,11 +1667,20 @@ impl Element for EditorElement {
.collect(),
));
}
show_scrollbars = view.show_scrollbars();
});
let line_number_layouts =
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
let diff_hunks = snapshot
.buffer_snapshot
.git_diff_hunks_in_range(start_row..end_row)
.collect();
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
let mut max_visible_line_width = 0.0;
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
for line in &line_layouts {
@ -1458,10 +1714,9 @@ impl Element for EditorElement {
cx,
);
let max_row = snapshot.max_point().row();
let scroll_max = vec2f(
((scroll_width - text_size.x()) / em_width).max(0.0),
max_row.saturating_sub(1) as f32,
max_row as f32,
);
self.update_view(cx.app, |view, cx| {
@ -1488,6 +1743,7 @@ impl Element for EditorElement {
let mut context_menu = None;
let mut code_actions_indicator = None;
let mut hover = None;
let mut mode = EditorMode::Full;
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
let newest_selection_head = view
.selections
@ -1509,6 +1765,7 @@ impl Element for EditorElement {
let visible_rows = start_row..start_row + line_layouts.len() as u32;
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
mode = view.mode;
});
if let Some((_, context_menu)) = context_menu.as_mut() {
@ -1556,6 +1813,7 @@ impl Element for EditorElement {
(
size,
LayoutState {
mode,
position_map: Arc::new(PositionMap {
size,
scroll_max,
@ -1565,14 +1823,19 @@ impl Element for EditorElement {
em_advance,
snapshot,
}),
visible_display_row_range: start_row..end_row,
gutter_size,
gutter_padding,
text_size,
scrollbar_row_range,
show_scrollbars,
max_row,
gutter_margin,
active_rows,
highlighted_rows,
highlighted_ranges,
line_number_layouts,
diff_hunks,
blocks,
selections,
context_menu,
@ -1589,7 +1852,8 @@ impl Element for EditorElement {
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_layer(Some(bounds));
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
let text_bounds = RectF::new(
@ -1613,11 +1877,12 @@ impl Element for EditorElement {
}
self.paint_text(text_bounds, visible_bounds, layout, cx);
cx.scene.push_layer(Some(bounds));
if !layout.blocks.is_empty() {
cx.scene.push_layer(Some(bounds));
self.paint_blocks(bounds, visible_bounds, layout, cx);
cx.scene.pop_layer();
}
self.paint_scrollbar(bounds, layout, cx);
cx.scene.pop_layer();
cx.scene.pop_layer();
}
@ -1703,13 +1968,19 @@ pub struct LayoutState {
gutter_padding: f32,
gutter_margin: f32,
text_size: Vector2F,
mode: EditorMode,
visible_display_row_range: Range<u32>,
active_rows: BTreeMap<u32, bool>,
highlighted_rows: Option<Range<u32>>,
line_number_layouts: Vec<Option<text_layout::Line>>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
scrollbar_row_range: Range<f32>,
show_scrollbars: bool,
max_row: u32,
context_menu: Option<(DisplayPoint, ElementBox)>,
diff_hunks: Vec<DiffHunk<u32>>,
code_actions_indicator: Option<(u32, ElementBox)>,
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
}

View File

@ -32,8 +32,9 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
#[cfg(test)]
mod tests {
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
use crate::test::EditorLspTestContext;
use indoc::indoc;
use language::{BracketPair, Language, LanguageConfig};

View File

@ -354,7 +354,7 @@ impl InfoPopover {
.with_style(style.hover_popover.container)
.boxed()
})
.on_move(|_, _| {})
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
.with_cursor_style(CursorStyle::Arrow)
.with_padding(Padding {
bottom: HOVER_POPOVER_GAP,
@ -400,7 +400,7 @@ impl DiagnosticPopover {
bottom: HOVER_POPOVER_GAP,
..Default::default()
})
.on_move(|_, _| {})
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(GoToDiagnostic)
})
@ -427,13 +427,13 @@ impl DiagnosticPopover {
#[cfg(test)]
mod tests {
use futures::StreamExt;
use indoc::indoc;
use language::{Diagnostic, DiagnosticSet};
use project::HoverBlock;
use smol::stream::StreamExt;
use crate::test::EditorLspTestContext;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;

View File

@ -9,7 +9,7 @@ use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
};
use language::{Bias, Buffer, File as _, OffsetRangeExt, SelectionGoal};
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view};
use settings::Settings;
@ -21,7 +21,7 @@ use std::{
ops::Range,
path::{Path, PathBuf},
};
use text::{Point, Selection};
use text::Selection;
use util::TryFutureExt;
use workspace::{
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@ -478,6 +478,17 @@ impl Item for Editor {
})
}
fn git_diff_recalc(
&mut self,
_project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.buffer().update(cx, |multibuffer, cx| {
multibuffer.git_diff_recalc(cx);
});
Task::ready(Ok(()))
}
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
let mut result = Vec::new();
match event {

View File

@ -400,7 +400,7 @@ mod tests {
use indoc::indoc;
use lsp::request::{GotoDefinition, GotoTypeDefinition};
use crate::test::EditorLspTestContext;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;

View File

@ -70,8 +70,9 @@ pub fn deploy_context_menu(
#[cfg(test)]
mod tests {
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
use crate::test::EditorLspTestContext;
use indoc::indoc;
#[gpui::test]

View File

@ -29,6 +29,25 @@ pub fn up(
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_start: bool,
) -> (DisplayPoint, SelectionGoal) {
up_by_rows(map, start, 1, goal, preserve_column_at_start)
}
pub fn down(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_end: bool,
) -> (DisplayPoint, SelectionGoal) {
down_by_rows(map, start, 1, goal, preserve_column_at_end)
}
pub fn up_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_start: bool,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_column = if let SelectionGoal::Column(column) = goal {
column
@ -36,7 +55,7 @@ pub fn up(
map.column_to_chars(start.row(), start.column())
};
let prev_row = start.row().saturating_sub(1);
let prev_row = start.row().saturating_sub(row_count);
let mut point = map.clip_point(
DisplayPoint::new(prev_row, map.line_len(prev_row)),
Bias::Left,
@ -62,9 +81,10 @@ pub fn up(
)
}
pub fn down(
pub fn down_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_end: bool,
) -> (DisplayPoint, SelectionGoal) {
@ -74,8 +94,8 @@ pub fn down(
map.column_to_chars(start.row(), start.column())
};
let next_row = start.row() + 1;
let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
let new_row = start.row() + row_count;
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
} else if preserve_column_at_end {
@ -101,6 +121,22 @@ pub fn line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
let line_start = map.prev_line_boundary(point).1;
if stop_at_soft_boundaries && display_point != soft_line_start {
soft_line_start
} else {
line_start
}
}
pub fn indented_line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
@ -167,54 +203,79 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
})
}
/// Scans for a boundary from the start of each line preceding the given end point until a boundary
/// is found, indicated by the given predicate returning true. The predicate is called with the
/// character to the left and right of the candidate boundary location, and will be called with `\n`
/// characters indicating the start or end of a line. If the predicate returns true multiple times
/// on a line, the *rightmost* boundary is returned.
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line.
pub fn find_preceding_boundary(
map: &DisplaySnapshot,
end: DisplayPoint,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut point = end;
loop {
*point.column_mut() = 0;
if point.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
*point.column_mut() = indent;
let mut start_column = 0;
let mut soft_wrap_row = from.row() + 1;
let mut prev = None;
for (ch, point) in map.reverse_chars_at(from) {
// Recompute soft_wrap_indent if the row has changed
if point.row() != soft_wrap_row {
soft_wrap_row = point.row();
if point.row() == 0 {
start_column = 0;
} else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
start_column = indent;
}
}
let mut boundary = None;
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
for ch in map.chars_at(point) {
if point >= end {
break;
}
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
boundary = Some(point);
}
}
if ch == '\n' {
break;
}
prev_ch = Some(ch);
*point.column_mut() += ch.len_utf8() as u32;
// If the current point is in the soft_wrap, skip comparing it
if point.column() < start_column {
continue;
}
if let Some(boundary) = boundary {
return boundary;
} else if point.row() == 0 {
return DisplayPoint::zero();
} else {
*point.row_mut() -= 1;
if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) {
return prev_point;
}
}
prev = Some((ch, point));
}
DisplayPoint::zero()
}
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line. If no boundary is found, the start of the line is returned.
pub fn find_preceding_boundary_in_line(
map: &DisplaySnapshot,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut start_column = 0;
if from.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
start_column = indent;
}
}
let mut prev = None;
for (ch, point) in map.reverse_chars_at(from) {
if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) {
return prev_point;
}
}
if ch == '\n' || point.column() < start_column {
break;
}
prev = Some((ch, point));
}
prev.map(|(_, point)| point).unwrap_or(from)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
@ -223,26 +284,48 @@ pub fn find_preceding_boundary(
/// or end of a line.
pub fn find_boundary(
map: &DisplaySnapshot,
mut point: DisplayPoint,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut prev_ch = None;
for ch in map.chars_at(point) {
for (ch, point) in map.chars_at(from) {
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
break;
return map.clip_point(point, Bias::Right);
}
}
if ch == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += ch.len_utf8() as u32;
}
prev_ch = Some(ch);
}
map.clip_point(point, Bias::Right)
map.clip_point(map.max_point(), Bias::Right)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line. If no boundary is found, the end of the line is returned
pub fn find_boundary_in_line(
map: &DisplaySnapshot,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut prev = None;
for (ch, point) in map.chars_at(from) {
if let Some((prev_ch, _)) = prev {
if is_boundary(prev_ch, ch) {
return map.clip_point(point, Bias::Right);
}
}
prev = Some((ch, point));
if ch == '\n' {
break;
}
}
// Return the last position checked so that we give a point right before the newline or eof.
map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
}
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
@ -273,7 +356,6 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
mod tests {
use super::*;
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
use language::Point;
use settings::Settings;
#[gpui::test]

View File

@ -4,12 +4,14 @@ pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result;
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use git::diff::DiffHunk;
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk,
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem,
Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId,
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
ToPoint as _, ToPointUtf16 as _, TransactionId,
};
use smallvec::SmallVec;
use std::{
@ -26,9 +28,8 @@ use std::{
use sum_tree::{Bias, Cursor, SumTree};
use text::{
locator::Locator,
rope::TextDimension,
subscription::{Subscription, Topic},
Edit, OffsetUtf16, Point, PointUtf16, TextSummary,
Edit, TextSummary,
};
use theme::SyntaxTheme;
use util::post_inc;
@ -90,6 +91,7 @@ struct BufferState {
last_selections_update_count: usize,
last_diagnostics_update_count: usize,
last_file_update_count: usize,
last_git_diff_update_count: usize,
excerpts: Vec<ExcerptId>,
_subscriptions: [gpui::Subscription; 2],
}
@ -101,6 +103,7 @@ pub struct MultiBufferSnapshot {
parse_count: usize,
diagnostics_update_count: usize,
trailing_excerpt_update_count: usize,
git_diff_update_count: usize,
edit_count: usize,
is_dirty: bool,
has_conflict: bool,
@ -165,7 +168,7 @@ struct ExcerptChunks<'a> {
}
struct ExcerptBytes<'a> {
content_bytes: language::rope::Bytes<'a>,
content_bytes: text::Bytes<'a>,
footer_height: usize,
}
@ -202,6 +205,7 @@ impl MultiBuffer {
last_selections_update_count: buffer_state.last_selections_update_count,
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
last_file_update_count: buffer_state.last_file_update_count,
last_git_diff_update_count: buffer_state.last_git_diff_update_count,
excerpts: buffer_state.excerpts.clone(),
_subscriptions: [
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
@ -308,6 +312,17 @@ impl MultiBuffer {
self.read(cx).symbols_containing(offset, theme)
}
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
let buffers = self.buffers.borrow();
for buffer_state in buffers.values() {
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
buffer_state
.buffer
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
}
}
}
pub fn edit<I, S, T>(
&mut self,
edits: I,
@ -827,6 +842,7 @@ impl MultiBuffer {
last_selections_update_count: buffer_snapshot.selections_update_count(),
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
last_file_update_count: buffer_snapshot.file_update_count(),
last_git_diff_update_count: buffer_snapshot.git_diff_update_count(),
excerpts: Default::default(),
_subscriptions: [
cx.observe(&buffer, |_, _, cx| cx.notify()),
@ -1212,9 +1228,9 @@ impl MultiBuffer {
&self,
point: T,
cx: &'a AppContext,
) -> Option<&'a Arc<Language>> {
) -> Option<Arc<Language>> {
self.point_to_buffer_offset(point, cx)
.and_then(|(buffer, _)| buffer.read(cx).language())
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
}
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
@ -1249,6 +1265,7 @@ impl MultiBuffer {
let mut excerpts_to_edit = Vec::new();
let mut reparsed = false;
let mut diagnostics_updated = false;
let mut git_diff_updated = false;
let mut is_dirty = false;
let mut has_conflict = false;
let mut edited = false;
@ -1260,6 +1277,7 @@ impl MultiBuffer {
let selections_update_count = buffer.selections_update_count();
let diagnostics_update_count = buffer.diagnostics_update_count();
let file_update_count = buffer.file_update_count();
let git_diff_update_count = buffer.git_diff_update_count();
let buffer_edited = version.changed_since(&buffer_state.last_version);
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
@ -1268,17 +1286,21 @@ impl MultiBuffer {
let buffer_diagnostics_updated =
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
let buffer_file_updated = file_update_count > buffer_state.last_file_update_count;
let buffer_git_diff_updated =
git_diff_update_count > buffer_state.last_git_diff_update_count;
if buffer_edited
|| buffer_reparsed
|| buffer_selections_updated
|| buffer_diagnostics_updated
|| buffer_file_updated
|| buffer_git_diff_updated
{
buffer_state.last_version = version;
buffer_state.last_parse_count = parse_count;
buffer_state.last_selections_update_count = selections_update_count;
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
buffer_state.last_file_update_count = file_update_count;
buffer_state.last_git_diff_update_count = git_diff_update_count;
excerpts_to_edit.extend(
buffer_state
.excerpts
@ -1290,6 +1312,7 @@ impl MultiBuffer {
edited |= buffer_edited;
reparsed |= buffer_reparsed;
diagnostics_updated |= buffer_diagnostics_updated;
git_diff_updated |= buffer_git_diff_updated;
is_dirty |= buffer.is_dirty();
has_conflict |= buffer.has_conflict();
}
@ -1302,6 +1325,9 @@ impl MultiBuffer {
if diagnostics_updated {
snapshot.diagnostics_update_count += 1;
}
if git_diff_updated {
snapshot.git_diff_update_count += 1;
}
snapshot.is_dirty = is_dirty;
snapshot.has_conflict = has_conflict;
@ -1386,7 +1412,7 @@ impl MultiBuffer {
edit_count: usize,
cx: &mut ModelContext<Self>,
) {
use text::RandomCharIter;
use util::RandomCharIter;
let snapshot = self.read(cx);
let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new();
@ -1425,7 +1451,7 @@ impl MultiBuffer {
) {
use rand::prelude::*;
use std::env;
use text::RandomCharIter;
use util::RandomCharIter;
let max_excerpts = env::var("MAX_EXCERPTS")
.map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
@ -1940,6 +1966,24 @@ impl MultiBufferSnapshot {
}
}
pub fn point_to_buffer_offset<T: ToOffset>(
&self,
point: T,
) -> Option<(&BufferSnapshot, usize)> {
let offset = point.to_offset(&self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&offset, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
}
cursor.item().map(|excerpt| {
let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let buffer_point = excerpt_start + offset - *cursor.start();
(&excerpt.buffer, buffer_point)
})
}
pub fn suggested_indents(
&self,
rows: impl IntoIterator<Item = u32>,
@ -1949,8 +1993,10 @@ impl MultiBufferSnapshot {
let mut rows_for_excerpt = Vec::new();
let mut cursor = self.excerpts.cursor::<Point>();
let mut rows = rows.into_iter().peekable();
let mut prev_row = u32::MAX;
let mut prev_language_indent_size = IndentSize::default();
while let Some(row) = rows.next() {
cursor.seek(&Point::new(row, 0), Bias::Right, &());
let excerpt = match cursor.item() {
@ -1958,7 +2004,17 @@ impl MultiBufferSnapshot {
_ => continue,
};
let single_indent_size = excerpt.buffer.single_indent_size(cx);
// Retrieve the language and indent size once for each disjoint region being indented.
let single_indent_size = if row.saturating_sub(1) == prev_row {
prev_language_indent_size
} else {
excerpt
.buffer
.language_indent_size_at(Point::new(row, 0), cx)
};
prev_language_indent_size = single_indent_size;
prev_row = row;
let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
let start_multibuffer_row = cursor.start().row;
@ -2479,15 +2535,17 @@ impl MultiBufferSnapshot {
self.diagnostics_update_count
}
pub fn git_diff_update_count(&self) -> usize {
self.git_diff_update_count
}
pub fn trailing_excerpt_update_count(&self) -> usize {
self.trailing_excerpt_update_count
}
pub fn language(&self) -> Option<&Arc<Language>> {
self.excerpts
.iter()
.next()
.and_then(|excerpt| excerpt.buffer.language())
pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
self.point_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_at(offset))
}
pub fn is_dirty(&self) -> bool {
@ -2529,6 +2587,15 @@ impl MultiBufferSnapshot {
})
}
pub fn git_diff_hunks_in_range<'a>(
&'a self,
row_range: Range<u32>,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| buffer.git_diff_hunks_in_range(row_range.clone()))
}
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
@ -3270,7 +3337,7 @@ mod tests {
use rand::prelude::*;
use settings::Settings;
use std::{env, rc::Rc};
use text::{Point, RandomCharIter};
use util::test::sample_text;
#[gpui::test]
@ -3888,7 +3955,9 @@ mod tests {
}
_ => {
let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
let base_text = RandomCharIter::new(&mut rng).take(10).collect::<String>();
let base_text = util::RandomCharIter::new(&mut rng)
.take(10)
.collect::<String>();
buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx)));
buffers.last().unwrap()
} else {

View File

@ -1,10 +1,10 @@
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
use language::{OffsetUtf16, Point, TextDimension};
use std::{
cmp::Ordering,
ops::{Range, Sub},
};
use sum_tree::Bias;
use text::{rope::TextDimension, OffsetUtf16, Point};
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {

View File

@ -8,7 +8,7 @@ use std::{
use collections::HashMap;
use gpui::{AppContext, ModelHandle, MutableAppContext};
use itertools::Itertools;
use language::{rope::TextDimension, Bias, Point, Selection, SelectionGoal, ToPoint};
use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
use util::post_inc;
use crate::{

View File

@ -1,28 +1,14 @@
pub mod editor_lsp_test_context;
pub mod editor_test_context;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
multi_buffer::ToPointUtf16,
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
DisplayPoint, Editor, EditorMode, MultiBuffer,
};
use anyhow::Result;
use futures::{Future, StreamExt};
use gpui::{
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
};
use indoc::indoc;
use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig};
use lsp::{notification, request};
use project::Project;
use settings::Settings;
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
sync::Arc,
};
use util::{
assert_set_eq, set_eq,
test::{generate_marked_text, marked_text_offsets, marked_text_ranges},
};
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
use gpui::{ModelHandle, ViewContext};
use util::test::{marked_text_offsets, marked_text_ranges};
#[cfg(test)]
#[ctor::ctor]
@ -80,430 +66,3 @@ pub(crate) fn build_editor(
) -> Editor {
Editor::new(EditorMode::Full, buffer, None, None, cx)
}
pub struct EditorTestContext<'a> {
pub cx: &'a mut gpui::TestAppContext,
pub window_id: usize,
pub editor: ViewHandle<Editor>,
}
impl<'a> EditorTestContext<'a> {
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let (window_id, editor) = cx.update(|cx| {
cx.set_global(Settings::test(cx));
crate::init(cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
build_editor(MultiBuffer::build_simple("", cx), cx)
});
editor.update(cx, |_, cx| cx.focus_self());
(window_id, editor)
});
Self {
cx,
window_id,
editor,
}
}
pub fn condition(
&self,
predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> {
self.editor.condition(self.cx, predicate)
}
pub fn editor<F, T>(&self, read: F) -> T
where
F: FnOnce(&Editor, &AppContext) -> T,
{
self.editor.read_with(self.cx, read)
}
pub fn update_editor<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
{
self.editor.update(self.cx, update)
}
pub fn multibuffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&MultiBuffer, &AppContext) -> T,
{
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
}
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
{
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
}
pub fn buffer_text(&self) -> String {
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
}
pub fn buffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&Buffer, &AppContext) -> T,
{
self.multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap().read(cx);
read(buffer, cx)
})
}
pub fn update_buffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
{
self.update_multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap();
buffer.update(cx, update)
})
}
pub fn buffer_snapshot(&self) -> BufferSnapshot {
self.buffer(|buffer, _| buffer.snapshot())
}
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
}
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
}
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);
ranges
}
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
let ranges = self.ranges(marked_text);
let snapshot = self
.editor
.update(self.cx, |editor, cx| editor.snapshot(cx));
ranges[0].start.to_display_point(&snapshot)
}
// Returns anchors for the current buffer using `«` and `»`
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
let ranges = self.ranges(marked_text);
let snapshot = self.buffer_snapshot();
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
/// Change the editor's text and selections using a string containing
/// embedded range markers that represent the ranges and directions of
/// each selection.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) {
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
editor.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges(selection_ranges)
})
})
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
assert_eq!(
buffer_text, unmarked_text,
"Unmarked text doesn't match buffer text"
);
self.assert_selections(expected_selections, marked_text.to_string())
}
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
editor
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()
});
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let actual_ranges: Vec<Range<usize>> = snapshot
.highlight_ranges::<Tag>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect();
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text)
}
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self
.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter()
.map(|s| {
if s.reversed {
s.end..s.start
} else {
s.start..s.end
}
})
.collect::<Vec<_>>();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
if expected_selections != actual_selections {
panic!(
indoc! {"
Editor has unexpected selections.
Expected selections:
{}
Actual selections:
{}
"},
expected_marked_text, actual_marked_text,
);
}
}
}
impl<'a> Deref for EditorTestContext<'a> {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
self.cx
}
}
impl<'a> DerefMut for EditorTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
pub buffer_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
pub async fn new(
mut language: Language,
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
use json::json;
cx.update(|cx| {
crate::init(cx);
pane::init(cx);
});
let params = cx.update(AppState::test);
let file_name = format!(
"file.{}",
language
.path_suffixes()
.first()
.unwrap_or(&"txt".to_string())
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities,
..Default::default()
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
params
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(cx, |_, cx| cx.focus_self());
let lsp = fake_servers.next().await.unwrap();
Self {
cx: EditorTestContext {
cx,
window_id,
editor,
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
}
}
pub async fn new_rust(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
Self::new(language, capabilities, cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())
}
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
buffer
.point_to_buffer_offset(start_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
let end = point_to_lsp(
buffer
.point_to_buffer_offset(end_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
lsp::Range { start, end }
})
}
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
}
impl<'a> Deref for EditorLspTestContext<'a> {
type Target = EditorTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for EditorLspTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}

View File

@ -0,0 +1,208 @@
use std::{
ops::{Deref, DerefMut, Range},
sync::Arc,
};
use anyhow::Result;
use futures::Future;
use gpui::{json, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
use lsp::{notification, request};
use project::Project;
use smol::stream::StreamExt;
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint};
use super::editor_test_context::EditorTestContext;
pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
pub buffer_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
pub async fn new(
mut language: Language,
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
use json::json;
cx.update(|cx| {
crate::init(cx);
pane::init(cx);
});
let params = cx.update(AppState::test);
let file_name = format!(
"file.{}",
language
.path_suffixes()
.first()
.unwrap_or(&"txt".to_string())
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities,
..Default::default()
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
params
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(cx, |_, cx| cx.focus_self());
let lsp = fake_servers.next().await.unwrap();
Self {
cx: EditorTestContext {
cx,
window_id,
editor,
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
}
}
pub async fn new_rust(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
Self::new(language, capabilities, cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())
}
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
buffer
.point_to_buffer_offset(start_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
let end = point_to_lsp(
buffer
.point_to_buffer_offset(end_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
lsp::Range { start, end }
})
}
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
}
impl<'a> Deref for EditorLspTestContext<'a> {
type Target = EditorTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for EditorLspTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}

View File

@ -0,0 +1,273 @@
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
};
use futures::Future;
use indoc::indoc;
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
use language::{Buffer, BufferSnapshot};
use settings::Settings;
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
};
use super::build_editor;
pub struct EditorTestContext<'a> {
pub cx: &'a mut gpui::TestAppContext,
pub window_id: usize,
pub editor: ViewHandle<Editor>,
}
impl<'a> EditorTestContext<'a> {
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let (window_id, editor) = cx.update(|cx| {
cx.set_global(Settings::test(cx));
crate::init(cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
build_editor(MultiBuffer::build_simple("", cx), cx)
});
editor.update(cx, |_, cx| cx.focus_self());
(window_id, editor)
});
Self {
cx,
window_id,
editor,
}
}
pub fn condition(
&self,
predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> {
self.editor.condition(self.cx, predicate)
}
pub fn editor<F, T>(&self, read: F) -> T
where
F: FnOnce(&Editor, &AppContext) -> T,
{
self.editor.read_with(self.cx, read)
}
pub fn update_editor<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
{
self.editor.update(self.cx, update)
}
pub fn multibuffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&MultiBuffer, &AppContext) -> T,
{
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
}
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
{
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
}
pub fn buffer_text(&self) -> String {
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
}
pub fn buffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&Buffer, &AppContext) -> T,
{
self.multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap().read(cx);
read(buffer, cx)
})
}
pub fn update_buffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
{
self.update_multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap();
buffer.update(cx, update)
})
}
pub fn buffer_snapshot(&self) -> BufferSnapshot {
self.buffer(|buffer, _| buffer.snapshot())
}
pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
let keystroke_under_test_handle =
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
keystroke_under_test_handle
}
pub fn simulate_keystrokes<const COUNT: usize>(
&mut self,
keystroke_texts: [&str; COUNT],
) -> ContextHandle {
let keystrokes_under_test_handle =
self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
keystrokes_under_test_handle
}
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);
ranges
}
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
let ranges = self.ranges(marked_text);
let snapshot = self
.editor
.update(self.cx, |editor, cx| editor.snapshot(cx));
ranges[0].start.to_display_point(&snapshot)
}
// Returns anchors for the current buffer using `«` and `»`
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
let ranges = self.ranges(marked_text);
let snapshot = self.buffer_snapshot();
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
/// Change the editor's text and selections using a string containing
/// embedded range markers that represent the ranges and directions of
/// each selection.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let _state_context = self.add_assertion_context(format!(
"Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
editor.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges(selection_ranges)
})
});
_state_context
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
assert_eq!(
buffer_text, unmarked_text,
"Unmarked text doesn't match buffer text"
);
self.assert_selections(expected_selections, marked_text.to_string())
}
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
editor
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()
});
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let actual_ranges: Vec<Range<usize>> = snapshot
.highlight_ranges::<Tag>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect();
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text)
}
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self
.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter()
.map(|s| {
if s.reversed {
s.end..s.start
} else {
s.start..s.end
}
})
.collect::<Vec<_>>();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
if expected_selections != actual_selections {
panic!(
indoc! {"
{}Editor has unexpected selections.
Expected selections:
{}
Actual selections:
{}
"},
self.assertion_context(),
expected_marked_text,
actual_marked_text,
);
}
}
}
impl<'a> Deref for EditorTestContext<'a> {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
self.cx
}
}
impl<'a> DerefMut for EditorTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}

View File

@ -49,8 +49,8 @@ impl View for FileFinder {
"FileFinder"
}
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.picker.clone()).boxed()
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@ -251,7 +251,7 @@ impl PickerDelegate for FileFinder {
fn render_match(
&self,
ix: usize,
mouse_state: MouseState,
mouse_state: &mut MouseState,
selected: bool,
cx: &AppContext,
) -> ElementBox {

31
crates/fs/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "fs"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/fs.rs"
[dependencies]
collections = { path = "../collections" }
gpui = { path = "../gpui" }
lsp = { path = "../lsp" }
rope = { path = "../rope" }
util = { path = "../util" }
anyhow = "1.0.57"
async-trait = "0.1"
futures = "0.3"
tempfile = "3"
fsevent = { path = "../fsevent" }
lazy_static = "1.4.0"
parking_lot = "0.11.1"
smol = "1.2.5"
regex = "1.5"
git2 = { version = "0.15", default-features = false }
serde = { workspace = true }
serde_json = { workspace = true }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
libc = "0.2"
[features]
test-support = []

View File

@ -1,8 +1,19 @@
pub mod repository;
use anyhow::{anyhow, Result};
use fsevent::EventStream;
use futures::{future::BoxFuture, Stream, StreamExt};
use language::LineEnding;
use git2::Repository as LibGitRepository;
use lazy_static::lazy_static;
use parking_lot::Mutex as SyncMutex;
use regex::Regex;
use repository::GitRepository;
use rope::Rope;
use smol::io::{AsyncReadExt, AsyncWriteExt};
use std::borrow::Cow;
use std::cmp;
use std::io::Write;
use std::sync::Arc;
use std::{
io,
os::unix::fs::MetadataExt,
@ -10,15 +21,77 @@ use std::{
pin::Pin,
time::{Duration, SystemTime},
};
use text::Rope;
use tempfile::NamedTempFile;
use util::ResultExt;
#[cfg(any(test, feature = "test-support"))]
use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))]
use futures::lock::Mutex;
#[cfg(any(test, feature = "test-support"))]
use std::sync::{Arc, Weak};
use repository::FakeGitRepositoryState;
#[cfg(any(test, feature = "test-support"))]
use std::sync::Weak;
lazy_static! {
static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap();
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LineEnding {
Unix,
Windows,
}
impl Default for LineEnding {
fn default() -> Self {
#[cfg(unix)]
return Self::Unix;
#[cfg(not(unix))]
return Self::CRLF;
}
}
impl LineEnding {
pub fn as_str(&self) -> &'static str {
match self {
LineEnding::Unix => "\n",
LineEnding::Windows => "\r\n",
}
}
pub fn detect(text: &str) -> Self {
let mut max_ix = cmp::min(text.len(), 1000);
while !text.is_char_boundary(max_ix) {
max_ix -= 1;
}
if let Some(ix) = text[..max_ix].find(&['\n']) {
if ix > 0 && text.as_bytes()[ix - 1] == b'\r' {
Self::Windows
} else {
Self::Unix
}
} else {
Self::default()
}
}
pub fn normalize(text: &mut String) {
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
*text = replaced;
}
}
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
replaced.into()
} else {
text
}
}
}
#[async_trait::async_trait]
pub trait Fs: Send + Sync {
async fn create_dir(&self, path: &Path) -> Result<()>;
@ -29,6 +102,7 @@ pub trait Fs: Send + Sync {
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
async fn load(&self, path: &Path) -> Result<String>;
async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
async fn is_file(&self, path: &Path) -> bool;
@ -42,6 +116,7 @@ pub trait Fs: Send + Sync {
path: &Path,
latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>>;
fn is_fake(&self) -> bool;
#[cfg(any(test, feature = "test-support"))]
fn as_fake(&self) -> &FakeFs;
@ -79,6 +154,33 @@ pub struct Metadata {
pub is_dir: bool,
}
impl From<lsp::CreateFileOptions> for CreateOptions {
fn from(options: lsp::CreateFileOptions) -> Self {
Self {
overwrite: options.overwrite.unwrap_or(false),
ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
}
}
}
impl From<lsp::RenameFileOptions> for RenameOptions {
fn from(options: lsp::RenameFileOptions) -> Self {
Self {
overwrite: options.overwrite.unwrap_or(false),
ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
}
}
}
impl From<lsp::DeleteFileOptions> for RemoveOptions {
fn from(options: lsp::DeleteFileOptions) -> Self {
Self {
recursive: options.recursive.unwrap_or(false),
ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
}
}
}
pub struct RealFs;
#[async_trait::async_trait]
@ -161,6 +263,18 @@ impl Fs for RealFs {
Ok(text)
}
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
smol::unblock(move || {
let mut tmp_file = NamedTempFile::new()?;
tmp_file.write_all(data.as_bytes())?;
tmp_file.persist(path)?;
Ok::<(), anyhow::Error>(())
})
.await?;
Ok(())
}
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
let buffer_size = text.summary().len.min(10 * 1024);
let file = smol::fs::File::create(path).await?;
@ -235,6 +349,14 @@ impl Fs for RealFs {
})))
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
LibGitRepository::open(&dotgit_path)
.log_err()
.and_then::<Arc<SyncMutex<dyn GitRepository>>, _>(|libgit_repository| {
Some(Arc::new(SyncMutex::new(libgit_repository)))
})
}
fn is_fake(&self) -> bool {
false
}
@ -270,6 +392,7 @@ enum FakeFsEntry {
inode: u64,
mtime: SystemTime,
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
git_repo_state: Option<Arc<SyncMutex<repository::FakeGitRepositoryState>>>,
},
Symlink {
target: PathBuf,
@ -384,6 +507,7 @@ impl FakeFs {
inode: 0,
mtime: SystemTime::now(),
entries: Default::default(),
git_repo_state: None,
})),
next_inode: 1,
event_txs: Default::default(),
@ -473,6 +597,28 @@ impl FakeFs {
.boxed()
}
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
let mut state = self.state.lock().await;
let entry = state.read_path(dot_git).await.unwrap();
let mut entry = entry.lock().await;
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let repo_state = git_repo_state.get_or_insert_with(Default::default);
let mut repo_state = repo_state.lock();
repo_state.index_contents.clear();
repo_state.index_contents.extend(
head_state
.iter()
.map(|(path, content)| (path.to_path_buf(), content.clone())),
);
state.emit_event([dot_git]);
} else {
panic!("not a directory");
}
}
pub async fn files(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
@ -562,6 +708,7 @@ impl Fs for FakeFs {
inode,
mtime: SystemTime::now(),
entries: Default::default(),
git_repo_state: None,
}))
});
Ok(())
@ -748,6 +895,14 @@ impl Fs for FakeFs {
entry.file_content(&path).cloned()
}
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
self.simulate_random_delay().await;
let path = normalize_path(path.as_path());
self.insert_file(path, data.to_string()).await;
Ok(())
}
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
self.simulate_random_delay().await;
let path = normalize_path(path);
@ -846,6 +1001,24 @@ impl Fs for FakeFs {
}))
}
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
smol::block_on(async move {
let state = self.state.lock().await;
let entry = state.read_path(abs_dot_git).await.unwrap();
let mut entry = entry.lock().await;
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let state = git_repo_state
.get_or_insert_with(|| {
Arc::new(SyncMutex::new(FakeGitRepositoryState::default()))
})
.clone();
Some(repository::FakeGitRepository::open(state))
} else {
None
}
})
}
fn is_fake(&self) -> bool {
true
}

View File

@ -0,0 +1,71 @@
use anyhow::Result;
use collections::HashMap;
use parking_lot::Mutex;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
pub use git2::Repository as LibGitRepository;
#[async_trait::async_trait]
pub trait GitRepository: Send {
fn reload_index(&self);
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
}
#[async_trait::async_trait]
impl GitRepository for LibGitRepository {
fn reload_index(&self) {
if let Ok(mut index) = self.index() {
_ = index.read(false);
}
}
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
const STAGE_NORMAL: i32 = 0;
let index = repo.index()?;
let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
Some(entry) => entry.id,
None => return Ok(None),
};
let content = repo.find_blob(oid)?.content().to_owned();
Ok(Some(String::from_utf8(content)?))
}
match logic(&self, relative_file_path) {
Ok(value) => return value,
Err(err) => log::error!("Error loading head text: {:?}", err),
}
None
}
}
#[derive(Debug, Clone, Default)]
pub struct FakeGitRepository {
state: Arc<Mutex<FakeGitRepositoryState>>,
}
#[derive(Debug, Clone, Default)]
pub struct FakeGitRepositoryState {
pub index_contents: HashMap<PathBuf, String>,
}
impl FakeGitRepository {
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
Arc::new(Mutex::new(FakeGitRepository { state }))
}
}
#[async_trait::async_trait]
impl GitRepository for FakeGitRepository {
fn reload_index(&self) {}
fn load_index_text(&self, path: &Path) -> Option<String> {
let state = self.state.lock();
state.index_contents.get(path).cloned()
}
}

28
crates/git/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "git"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/git.rs"
[dependencies]
anyhow = "1.0.38"
clock = { path = "../clock" }
lazy_static = "1.4.0"
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
collections = { path = "../collections" }
util = { path = "../util" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
smol = "1.2"
parking_lot = "0.11.1"
async-trait = "0.1"
futures = "0.3"
git2 = { version = "0.15", default-features = false }
[dev-dependencies]
unindent = "0.1.7"
[features]
test-support = []

361
crates/git/src/diff.rs Normal file
View File

@ -0,0 +1,361 @@
use std::ops::Range;
use sum_tree::SumTree;
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
pub use git2 as libgit;
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffHunkStatus {
Added,
Modified,
Removed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffHunk<T> {
pub buffer_range: Range<T>,
pub head_byte_range: Range<usize>,
}
impl DiffHunk<u32> {
pub fn status(&self) -> DiffHunkStatus {
if self.head_byte_range.is_empty() {
DiffHunkStatus::Added
} else if self.buffer_range.is_empty() {
DiffHunkStatus::Removed
} else {
DiffHunkStatus::Modified
}
}
}
impl sum_tree::Item for DiffHunk<Anchor> {
type Summary = DiffHunkSummary;
fn summary(&self) -> Self::Summary {
DiffHunkSummary {
buffer_range: self.buffer_range.clone(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct DiffHunkSummary {
buffer_range: Range<Anchor>,
}
impl sum_tree::Summary for DiffHunkSummary {
type Context = text::BufferSnapshot;
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
self.buffer_range.start = self
.buffer_range
.start
.min(&other.buffer_range.start, buffer);
self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
}
}
#[derive(Clone)]
pub struct BufferDiff {
last_buffer_version: Option<clock::Global>,
tree: SumTree<DiffHunk<Anchor>>,
}
impl BufferDiff {
pub fn new() -> BufferDiff {
BufferDiff {
last_buffer_version: None,
tree: SumTree::new(),
}
}
pub fn hunks_in_range<'a>(
&'a self,
query_row_range: Range<u32>,
buffer: &'a BufferSnapshot,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = buffer.anchor_before(Point::new(query_row_range.start, 0));
let end = buffer.anchor_after(Point::new(query_row_range.end, 0));
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt();
let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt();
!before_start && !after_end
});
std::iter::from_fn(move || {
cursor.next(buffer);
let hunk = cursor.item()?;
let range = hunk.buffer_range.to_point(buffer);
let end_row = if range.end.column > 0 {
range.end.row + 1
} else {
range.end.row
};
Some(DiffHunk {
buffer_range: range.start.row..end_row,
head_byte_range: hunk.head_byte_range.clone(),
})
})
}
pub fn clear(&mut self, buffer: &text::BufferSnapshot) {
self.last_buffer_version = Some(buffer.version().clone());
self.tree = SumTree::new();
}
pub fn needs_update(&self, buffer: &text::BufferSnapshot) -> bool {
match &self.last_buffer_version {
Some(last) => buffer.version().changed_since(last),
None => true,
}
}
pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) {
let mut tree = SumTree::new();
let buffer_text = buffer.as_rope().to_string();
let patch = Self::diff(&diff_base, &buffer_text);
if let Some(patch) = patch {
let mut divergence = 0;
for hunk_index in 0..patch.num_hunks() {
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
tree.push(hunk, buffer);
}
}
self.tree = tree;
self.last_buffer_version = Some(buffer.version().clone());
}
#[cfg(test)]
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.hunks_in_range(0..u32::MAX, text)
}
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
let mut options = GitOptions::default();
options.context_lines(0);
let patch = GitPatch::from_buffers(
head.as_bytes(),
None,
current.as_bytes(),
None,
Some(&mut options),
);
match patch {
Ok(patch) => Some(patch),
Err(err) => {
log::error!("`GitPatch::from_buffers` failed: {}", err);
None
}
}
}
fn process_patch_hunk<'a>(
patch: &GitPatch<'a>,
hunk_index: usize,
buffer: &text::BufferSnapshot,
buffer_row_divergence: &mut i64,
) -> DiffHunk<Anchor> {
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
assert!(line_item_count > 0);
let mut first_deletion_buffer_row: Option<u32> = None;
let mut buffer_row_range: Option<Range<u32>> = None;
let mut head_byte_range: Option<Range<usize>> = None;
for line_index in 0..line_item_count {
let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
let kind = line.origin_value();
let content_offset = line.content_offset() as isize;
let content_len = line.content().len() as isize;
if kind == GitDiffLineType::Addition {
*buffer_row_divergence += 1;
let row = line.new_lineno().unwrap().saturating_sub(1);
match &mut buffer_row_range {
Some(buffer_row_range) => buffer_row_range.end = row + 1,
None => buffer_row_range = Some(row..row + 1),
}
}
if kind == GitDiffLineType::Deletion {
*buffer_row_divergence -= 1;
let end = content_offset + content_len;
match &mut head_byte_range {
Some(head_byte_range) => head_byte_range.end = end as usize,
None => head_byte_range = Some(content_offset as usize..end as usize),
}
if first_deletion_buffer_row.is_none() {
let old_row = line.old_lineno().unwrap().saturating_sub(1);
let row = old_row as i64 + *buffer_row_divergence;
first_deletion_buffer_row = Some(row as u32);
}
}
}
//unwrap_or deletion without addition
let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
//we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
let row = first_deletion_buffer_row.unwrap();
row..row
});
//unwrap_or addition without deletion
let head_byte_range = head_byte_range.unwrap_or(0..0);
let start = Point::new(buffer_row_range.start, 0);
let end = Point::new(buffer_row_range.end, 0);
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
DiffHunk {
buffer_range,
head_byte_range,
}
}
}
/// Range (crossing new lines), old, new
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn assert_hunks<Iter>(
diff_hunks: Iter,
buffer: &BufferSnapshot,
diff_base: &str,
expected_hunks: &[(Range<u32>, &str, &str)],
) where
Iter: Iterator<Item = DiffHunk<u32>>,
{
let actual_hunks = diff_hunks
.map(|hunk| {
(
hunk.buffer_range.clone(),
&diff_base[hunk.head_byte_range],
buffer
.text_for_range(
Point::new(hunk.buffer_range.start, 0)
..Point::new(hunk.buffer_range.end, 0),
)
.collect::<String>(),
)
})
.collect::<Vec<_>>();
let expected_hunks: Vec<_> = expected_hunks
.iter()
.map(|(r, s, h)| (r.clone(), *s, h.to_string()))
.collect();
assert_eq!(actual_hunks, expected_hunks);
}
#[cfg(test)]
mod tests {
use super::*;
use text::Buffer;
use unindent::Unindent as _;
#[test]
fn test_buffer_diff_simple() {
let diff_base = "
one
two
three
"
.unindent();
let buffer_text = "
one
HELLO
three
"
.unindent();
let mut buffer = Buffer::new(0, 0, buffer_text);
let mut diff = BufferDiff::new();
smol::block_on(diff.update(&diff_base, &buffer));
assert_hunks(
diff.hunks(&buffer),
&buffer,
&diff_base,
&[(1..2, "two\n", "HELLO\n")],
);
buffer.edit([(0..0, "point five\n")]);
smol::block_on(diff.update(&diff_base, &buffer));
assert_hunks(
diff.hunks(&buffer),
&buffer,
&diff_base,
&[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")],
);
diff.clear(&buffer);
assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]);
}
#[test]
fn test_buffer_diff_range() {
let diff_base = "
one
two
three
four
five
six
seven
eight
nine
ten
"
.unindent();
let buffer_text = "
A
one
B
two
C
three
HELLO
four
five
SIXTEEN
seven
eight
WORLD
nine
ten
"
.unindent();
let buffer = Buffer::new(0, 0, buffer_text);
let mut diff = BufferDiff::new();
smol::block_on(diff.update(&diff_base, &buffer));
assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks(
diff.hunks_in_range(7..12, &buffer),
&buffer,
&diff_base,
&[
(6..7, "", "HELLO\n"),
(9..10, "six\n", "SIXTEEN\n"),
(12..13, "", "WORLD\n"),
],
);
}
}

11
crates/git/src/git.rs Normal file
View File

@ -0,0 +1,11 @@
use std::ffi::OsStr;
pub use git2 as libgit;
pub use lazy_static::lazy_static;
pub mod diff;
lazy_static! {
pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git");
pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
}

View File

@ -165,7 +165,7 @@ impl View for GoToLine {
Container::new(
Flex::new(Axis::Vertical)
.with_child(
Container::new(ChildView::new(&self.line_editor).boxed())
Container::new(ChildView::new(&self.line_editor, cx).boxed())
.with_style(theme.input_editor.container)
.boxed(),
)

View File

@ -25,6 +25,7 @@ env_logger = { version = "0.9", optional = true }
etagere = "0.2"
futures = "0.3"
image = "0.23"
itertools = "0.10"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
num_cpus = "1.13"

View File

@ -1,28 +1,8 @@
pub mod action;
mod callback_collection;
#[cfg(any(test, feature = "test-support"))]
pub mod test_app_context;
use crate::{
elements::ElementBox,
executor::{self, Task},
geometry::rect::RectF,
keymap::{self, Binding, Keystroke},
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::post_inc,
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
MouseRegionId, PathPromptOptions, TextLayoutCache,
};
pub use action::*;
use anyhow::{anyhow, Context, Result};
use callback_collection::CallbackCollection;
use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
use keymap::MatchResult;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use platform::Event;
use postage::oneshot;
use smallvec::SmallVec;
use smol::prelude::*;
use std::{
any::{type_name, Any, TypeId},
cell::RefCell,
@ -38,7 +18,32 @@ use std::{
time::Duration,
};
use self::callback_collection::Mapping;
use anyhow::{anyhow, Context, Result};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use postage::oneshot;
use smallvec::SmallVec;
use smol::prelude::*;
pub use action::*;
use callback_collection::{CallbackCollection, Mapping};
use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
use keymap::MatchResult;
use platform::Event;
#[cfg(any(test, feature = "test-support"))]
pub use test_app_context::{ContextHandle, TestAppContext};
use crate::{
elements::ElementBox,
executor::{self, Task},
geometry::rect::RectF,
keymap::{self, Binding, Keystroke},
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::post_inc,
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
MouseRegionId, PathPromptOptions, TextLayoutCache,
};
pub trait Entity: 'static {
type Event;
@ -177,13 +182,6 @@ pub struct App(Rc<RefCell<MutableAppContext>>);
#[derive(Clone)]
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
#[cfg(any(test, feature = "test-support"))]
pub struct TestAppContext {
cx: Rc<RefCell<MutableAppContext>>,
foreground_platform: Rc<platform::test::ForegroundPlatform>,
condition_duration: Option<Duration>,
}
pub struct WindowInputHandler {
app: Rc<RefCell<MutableAppContext>>,
window_id: usize,
@ -427,327 +425,6 @@ impl InputHandler for WindowInputHandler {
}
}
#[cfg(any(test, feature = "test-support"))]
impl TestAppContext {
pub fn new(
foreground_platform: Rc<platform::test::ForegroundPlatform>,
platform: Arc<dyn Platform>,
foreground: Rc<executor::Foreground>,
background: Arc<executor::Background>,
font_cache: Arc<FontCache>,
leak_detector: Arc<Mutex<LeakDetector>>,
first_entity_id: usize,
) -> Self {
let mut cx = MutableAppContext::new(
foreground,
background,
platform,
foreground_platform.clone(),
font_cache,
RefCounts {
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
},
(),
);
cx.next_entity_id = first_entity_id;
let cx = TestAppContext {
cx: Rc::new(RefCell::new(cx)),
foreground_platform,
condition_duration: None,
};
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
cx
}
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
let mut cx = self.cx.borrow_mut();
if let Some(view_id) = cx.focused_view_id(window_id) {
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
}
}
pub fn dispatch_global_action<A: Action>(&self, action: A) {
self.cx.borrow_mut().dispatch_global_action(action);
}
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
let handled = self.cx.borrow_mut().update(|cx| {
let presenter = cx
.presenters_and_platform_windows
.get(&window_id)
.unwrap()
.0
.clone();
if cx.dispatch_keystroke(window_id, &keystroke) {
return true;
}
if presenter.borrow_mut().dispatch_event(
Event::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held,
}),
false,
cx,
) {
return true;
}
false
});
if !handled && !keystroke.cmd && !keystroke.ctrl {
WindowInputHandler {
app: self.cx.clone(),
window_id,
}
.replace_text_in_range(None, &keystroke.key)
}
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
F: FnOnce(&mut ModelContext<T>) -> T,
{
self.cx.borrow_mut().add_model(build_model)
}
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
let (window_id, view) = self
.cx
.borrow_mut()
.add_window(Default::default(), build_root_view);
self.simulate_window_activation(Some(window_id));
(window_id, view)
}
pub fn add_view<T, F>(
&mut self,
parent_handle: impl Into<AnyViewHandle>,
build_view: F,
) -> ViewHandle<T>
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
self.cx.borrow_mut().add_view(parent_handle, build_view)
}
pub fn window_ids(&self) -> Vec<usize> {
self.cx.borrow().window_ids().collect()
}
pub fn root_view<T: View>(&self, window_id: usize) -> Option<ViewHandle<T>> {
self.cx.borrow().root_view(window_id)
}
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
callback(self.cx.borrow().as_ref())
}
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
let mut state = self.cx.borrow_mut();
// Don't increment pending flushes in order for effects to be flushed before the callback
// completes, which is helpful in tests.
let result = callback(&mut *state);
// Flush effects after the callback just in case there are any. This can happen in edge
// cases such as the closure dropping handles.
state.flush_effects();
result
}
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
where
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
V: View,
{
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
let mut render_cx = RenderContext {
app: cx,
window_id: handle.window_id(),
view_id: handle.id(),
view_type: PhantomData,
titlebar_height: 0.,
hovered_region_ids: Default::default(),
clicked_region_ids: None,
refreshing: false,
appearance: Appearance::Light,
};
f(view, &mut render_cx)
})
}
pub fn to_async(&self) -> AsyncAppContext {
AsyncAppContext(self.cx.clone())
}
pub fn font_cache(&self) -> Arc<FontCache> {
self.cx.borrow().cx.font_cache.clone()
}
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
self.foreground_platform.clone()
}
pub fn platform(&self) -> Arc<dyn platform::Platform> {
self.cx.borrow().cx.platform.clone()
}
pub fn foreground(&self) -> Rc<executor::Foreground> {
self.cx.borrow().foreground().clone()
}
pub fn background(&self) -> Arc<executor::Background> {
self.cx.borrow().background().clone()
}
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where
F: FnOnce(AsyncAppContext) -> Fut,
Fut: 'static + Future<Output = T>,
T: 'static,
{
let foreground = self.foreground();
let future = f(self.to_async());
let cx = self.to_async();
foreground.spawn(async move {
let result = future.await;
cx.0.borrow_mut().flush_effects();
result
})
}
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
self.foreground_platform.simulate_new_path_selection(result);
}
pub fn did_prompt_for_new_path(&self) -> bool {
self.foreground_platform.as_ref().did_prompt_for_new_path()
}
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
use postage::prelude::Sink as _;
let mut done_tx = self
.window_mut(window_id)
.pending_prompts
.borrow_mut()
.pop_front()
.expect("prompt was not called");
let _ = done_tx.try_send(answer);
}
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
let window = self.window_mut(window_id);
let prompts = window.pending_prompts.borrow_mut();
!prompts.is_empty()
}
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
self.window_mut(window_id).title.clone()
}
pub fn simulate_window_close(&self, window_id: usize) -> bool {
let handler = self.window_mut(window_id).should_close_handler.take();
if let Some(mut handler) = handler {
let should_close = handler();
self.window_mut(window_id).should_close_handler = Some(handler);
should_close
} else {
false
}
}
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
let mut handlers = BTreeMap::new();
{
let mut cx = self.cx.borrow_mut();
for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows {
let window = window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
handlers.insert(
*window_id,
mem::take(&mut window.active_status_change_handlers),
);
}
};
let mut handlers = handlers.into_iter().collect::<Vec<_>>();
handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate);
for (window_id, mut window_handlers) in handlers {
for window_handler in &mut window_handlers {
window_handler(Some(window_id) == to_activate);
}
self.window_mut(window_id)
.active_status_change_handlers
.extend(window_handlers);
}
}
pub fn is_window_edited(&self, window_id: usize) -> bool {
self.window_mut(window_id).edited
}
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
self.cx.borrow().leak_detector()
}
pub fn assert_dropped(&self, handle: impl WeakHandle) {
self.cx
.borrow()
.leak_detector()
.lock()
.assert_dropped(handle.id())
}
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
let (_, window) = state
.presenters_and_platform_windows
.get_mut(&window_id)
.unwrap();
let test_window = window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
test_window
})
}
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
self.condition_duration = duration;
}
pub fn condition_duration(&self) -> Duration {
self.condition_duration.unwrap_or_else(|| {
if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
}
})
}
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
self.update(|cx| {
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
let expected_content = expected_content.map(|content| content.to_owned());
assert_eq!(actual_content, expected_content);
})
}
}
impl AsyncAppContext {
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where
@ -786,6 +463,24 @@ impl AsyncAppContext {
self.update(|cx| cx.add_window(window_options, build_root_view))
}
pub fn remove_window(&mut self, window_id: usize) {
self.update(|cx| cx.remove_window(window_id))
}
pub fn activate_window(&mut self, window_id: usize) {
self.update(|cx| cx.activate_window(window_id))
}
pub fn prompt(
&mut self,
window_id: usize,
level: PromptLevel,
msg: &str,
answers: &[&str],
) -> oneshot::Receiver<usize> {
self.update(|cx| cx.prompt(window_id, level, msg, answers))
}
pub fn platform(&self) -> Arc<dyn Platform> {
self.0.borrow().platform()
}
@ -876,60 +571,6 @@ impl ReadViewWith for AsyncAppContext {
}
}
#[cfg(any(test, feature = "test-support"))]
impl UpdateModel for TestAppContext {
fn update_model<T: Entity, O>(
&mut self,
handle: &ModelHandle<T>,
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
) -> O {
self.cx.borrow_mut().update_model(handle, update)
}
}
#[cfg(any(test, feature = "test-support"))]
impl ReadModelWith for TestAppContext {
fn read_model_with<E: Entity, T>(
&self,
handle: &ModelHandle<E>,
read: &mut dyn FnMut(&E, &AppContext) -> T,
) -> T {
let cx = self.cx.borrow();
let cx = cx.as_ref();
read(handle.read(cx), cx)
}
}
#[cfg(any(test, feature = "test-support"))]
impl UpdateView for TestAppContext {
fn update_view<T, S>(
&mut self,
handle: &ViewHandle<T>,
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
) -> S
where
T: View,
{
self.cx.borrow_mut().update_view(handle, update)
}
}
#[cfg(any(test, feature = "test-support"))]
impl ReadViewWith for TestAppContext {
fn read_view_with<V, T>(
&self,
handle: &ViewHandle<V>,
read: &mut dyn FnMut(&V, &AppContext) -> T,
) -> T
where
V: View,
{
let cx = self.cx.borrow();
let cx = cx.as_ref();
read(handle.read(cx), cx)
}
}
type ActionCallback =
dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
@ -977,7 +618,6 @@ pub struct MutableAppContext {
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
foreground: Rc<executor::Foreground>,
pending_effects: VecDeque<Effect>,
pending_focus_index: Option<usize>,
pending_notifications: HashSet<usize>,
pending_global_notifications: HashSet<TypeId>,
pending_flushes: usize,
@ -1032,7 +672,6 @@ impl MutableAppContext {
presenters_and_platform_windows: Default::default(),
foreground,
pending_effects: VecDeque::new(),
pending_focus_index: None,
pending_notifications: Default::default(),
pending_global_notifications: Default::default(),
pending_flushes: 0,
@ -1519,6 +1158,17 @@ impl MutableAppContext {
}
}
pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
where
G: Any + Default,
F: 'static + FnMut(&mut MutableAppContext),
{
if !self.has_global::<G>() {
self.set_global(G::default());
}
self.observe_global::<G, F>(observe)
}
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
where
E: Entity,
@ -1887,6 +1537,10 @@ impl MutableAppContext {
})
}
pub fn clear_globals(&mut self) {
self.cx.globals.clear();
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
@ -1967,6 +1621,10 @@ impl MutableAppContext {
})
}
pub fn remove_status_bar_item(&mut self, id: usize) {
self.remove_window(id);
}
fn register_platform_window(
&mut self,
window_id: usize,
@ -2216,9 +1874,6 @@ impl MutableAppContext {
let mut refreshing = false;
loop {
if let Some(effect) = self.pending_effects.pop_front() {
if let Some(pending_focus_index) = self.pending_focus_index.as_mut() {
*pending_focus_index = pending_focus_index.saturating_sub(1);
}
match effect {
Effect::Subscription {
entity_id,
@ -2599,8 +2254,6 @@ impl MutableAppContext {
}
fn handle_focus_effect(&mut self, window_id: usize, focused_id: Option<usize>) {
self.pending_focus_index.take();
if self
.cx
.windows
@ -2723,10 +2376,6 @@ impl MutableAppContext {
}
pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
if let Some(pending_focus_index) = self.pending_focus_index {
self.pending_effects.remove(pending_focus_index);
}
self.pending_focus_index = Some(self.pending_effects.len());
self.pending_effects
.push_back(Effect::Focus { window_id, view_id });
}
@ -2922,6 +2571,10 @@ impl AppContext {
.and_then(|window| window.focused_view_id)
}
pub fn view_ui_name(&self, window_id: usize, view_id: usize) -> Option<&'static str> {
Some(self.views.get(&(window_id, view_id))?.ui_name())
}
pub fn background(&self) -> &Arc<executor::Background> {
&self.background
}
@ -3805,6 +3458,15 @@ impl<'a, T: View> ViewContext<'a, T> {
self.app.focused_view_id(self.window_id) == Some(self.view_id)
}
pub fn is_child(&self, view: impl Into<AnyViewHandle>) -> bool {
let view = view.into();
if self.window_id != view.window_id {
return false;
}
self.parents(view.window_id, view.view_id)
.any(|parent| parent == self.view_id)
}
pub fn blur(&mut self) {
self.app.focus(self.window_id, None);
}
@ -4112,10 +3774,32 @@ pub struct RenderContext<'a, T: View> {
pub refreshing: bool,
}
#[derive(Clone, Copy, Default)]
#[derive(Clone, Default)]
pub struct MouseState {
pub hovered: bool,
pub clicked: Option<MouseButton>,
hovered: bool,
clicked: Option<MouseButton>,
accessed_hovered: bool,
accessed_clicked: bool,
}
impl MouseState {
pub fn hovered(&mut self) -> bool {
self.accessed_hovered = true;
self.hovered
}
pub fn clicked(&mut self) -> Option<MouseButton> {
self.accessed_clicked = true;
self.clicked
}
pub fn accessed_hovered(&self) -> bool {
self.accessed_hovered
}
pub fn accessed_clicked(&self) -> bool {
self.accessed_clicked
}
}
impl<'a, V: View> RenderContext<'a, V> {
@ -4156,6 +3840,8 @@ impl<'a, V: View> RenderContext<'a, V> {
None
}
}),
accessed_hovered: false,
accessed_clicked: false,
}
}
@ -4409,117 +4095,6 @@ impl<T: Entity> ModelHandle<T> {
update(model, cx)
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.unbounded_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.next())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
where
T::Event: Clone,
{
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.subscribe(self, move |_, event, _| {
tx.unbounded_send(event.clone()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
cx.foreground.start_waiting();
async move {
let event = crate::util::timeout(duration, rx.next())
.await
.expect("next event timed out");
drop(subscription);
event.expect("model dropped while test was waiting for its next event")
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscriptions = (
cx.observe(self, {
let tx = tx.clone();
move |_, _| {
tx.unbounded_send(()).ok();
}
}),
cx.subscribe(self, {
move |_, _, _| {
tx.unbounded_send(()).ok();
}
}),
);
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
crate::util::timeout(duration, async move {
loop {
{
let cx = cx.borrow();
let cx = cx.as_ref();
if predicate(
handle
.upgrade(cx)
.expect("model dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.next()
.await
.expect("model dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
impl<T: Entity> Clone for ModelHandle<T> {
@ -4650,6 +4225,12 @@ impl<T> PartialEq for WeakModelHandle<T> {
impl<T> Eq for WeakModelHandle<T> {}
impl<T: Entity> PartialEq<ModelHandle<T>> for WeakModelHandle<T> {
fn eq(&self, other: &ModelHandle<T>) -> bool {
self.model_id == other.model_id
}
}
impl<T> Clone for WeakModelHandle<T> {
fn clone(&self) -> Self {
Self {
@ -4746,93 +4327,6 @@ impl<T: View> ViewHandle<T> {
cx.focused_view_id(self.window_id)
.map_or(false, |focused_id| focused_id == self.view_id)
}
#[cfg(any(test, feature = "test-support"))]
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (mut tx, mut rx) = postage::mpsc::channel(1);
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.try_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
let timeout_duration = cx.condition_duration();
let mut cx = cx.cx.borrow_mut();
let subscriptions = self.update(&mut *cx, |_, cx| {
(
cx.observe(self, {
let mut tx = tx.clone();
move |_, _, _| {
tx.blocking_send(()).ok();
}
}),
cx.subscribe(self, {
let mut tx = tx.clone();
move |_, _, _, _| {
tx.blocking_send(()).ok();
}
}),
)
});
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
async move {
crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
let cx = cx.as_ref();
if predicate(
handle
.upgrade(cx)
.expect("view dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.recv()
.await
.expect("view dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
impl<T: View> Clone for ViewHandle<T> {
@ -4950,6 +4444,10 @@ impl AnyViewHandle {
}
}
pub fn window_id(&self) -> usize {
self.window_id
}
pub fn id(&self) -> usize {
self.view_id
}
@ -5266,6 +4764,10 @@ pub struct AnyWeakViewHandle {
}
impl AnyWeakViewHandle {
pub fn id(&self) -> usize {
self.view_id
}
pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<AnyViewHandle> {
cx.upgrade_any_view_handle(self)
}
@ -6910,18 +6412,29 @@ mod tests {
assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
view_1.update(cx, |_, cx| {
// Ensure only the latest focus is honored.
// Ensure focus events are sent for all intermediate focuses
cx.focus(&view_2);
cx.focus(&view_1);
cx.focus(&view_2);
});
assert_eq!(
mem::take(&mut *view_events.lock()),
["view 1 blurred", "view 2 focused"],
[
"view 1 blurred",
"view 2 focused",
"view 2 blurred",
"view 1 focused",
"view 1 blurred",
"view 2 focused"
],
);
assert_eq!(
mem::take(&mut *observed_events.lock()),
[
"view 2 observed view 1's blur",
"view 1 observed view 2's focus",
"view 1 observed view 2's blur",
"view 2 observed view 1's focus",
"view 2 observed view 1's blur",
"view 1 observed view 2's focus"
]
@ -7555,4 +7068,73 @@ mod tests {
cx.simulate_window_activation(Some(window_3));
assert_eq!(mem::take(&mut *events.borrow_mut()), []);
}
#[crate::test(self)]
fn test_child_view(cx: &mut MutableAppContext) {
struct Child {
rendered: Rc<Cell<bool>>,
dropped: Rc<Cell<bool>>,
}
impl super::Entity for Child {
type Event = ();
}
impl super::View for Child {
fn ui_name() -> &'static str {
"child view"
}
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
self.rendered.set(true);
Empty::new().boxed()
}
}
impl Drop for Child {
fn drop(&mut self) {
self.dropped.set(true);
}
}
struct Parent {
child: Option<ViewHandle<Child>>,
}
impl super::Entity for Parent {
type Event = ();
}
impl super::View for Parent {
fn ui_name() -> &'static str {
"parent view"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(child) = self.child.as_ref() {
ChildView::new(child, cx).boxed()
} else {
Empty::new().boxed()
}
}
}
let child_rendered = Rc::new(Cell::new(false));
let child_dropped = Rc::new(Cell::new(false));
let (_, root_view) = cx.add_window(Default::default(), |cx| Parent {
child: Some(cx.add_view(|_| Child {
rendered: child_rendered.clone(),
dropped: child_dropped.clone(),
})),
});
assert!(child_rendered.take());
assert!(!child_dropped.take());
root_view.update(cx, |view, cx| {
view.child.take();
cx.notify();
});
assert!(!child_rendered.take());
assert!(child_dropped.take());
}
}

View File

@ -0,0 +1,667 @@
use std::{
cell::RefCell,
marker::PhantomData,
mem,
path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};
use futures::Future;
use itertools::Itertools;
use parking_lot::{Mutex, RwLock};
use smol::stream::StreamExt;
use crate::{
executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle,
AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector,
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
WindowInputHandler,
};
use collections::BTreeMap;
use super::{AsyncAppContext, RefCounts};
pub struct TestAppContext {
cx: Rc<RefCell<MutableAppContext>>,
foreground_platform: Rc<platform::test::ForegroundPlatform>,
condition_duration: Option<Duration>,
pub function_name: String,
assertion_context: AssertionContextManager,
}
impl TestAppContext {
pub fn new(
foreground_platform: Rc<platform::test::ForegroundPlatform>,
platform: Arc<dyn Platform>,
foreground: Rc<executor::Foreground>,
background: Arc<executor::Background>,
font_cache: Arc<FontCache>,
leak_detector: Arc<Mutex<LeakDetector>>,
first_entity_id: usize,
function_name: String,
) -> Self {
let mut cx = MutableAppContext::new(
foreground,
background,
platform,
foreground_platform.clone(),
font_cache,
RefCounts {
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
},
(),
);
cx.next_entity_id = first_entity_id;
let cx = TestAppContext {
cx: Rc::new(RefCell::new(cx)),
foreground_platform,
condition_duration: None,
function_name,
assertion_context: AssertionContextManager::new(),
};
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
cx
}
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
let mut cx = self.cx.borrow_mut();
if let Some(view_id) = cx.focused_view_id(window_id) {
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
}
}
pub fn dispatch_global_action<A: Action>(&self, action: A) {
self.cx.borrow_mut().dispatch_global_action(action);
}
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
let handled = self.cx.borrow_mut().update(|cx| {
let presenter = cx
.presenters_and_platform_windows
.get(&window_id)
.unwrap()
.0
.clone();
if cx.dispatch_keystroke(window_id, &keystroke) {
return true;
}
if presenter.borrow_mut().dispatch_event(
Event::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held,
}),
false,
cx,
) {
return true;
}
false
});
if !handled && !keystroke.cmd && !keystroke.ctrl {
WindowInputHandler {
app: self.cx.clone(),
window_id,
}
.replace_text_in_range(None, &keystroke.key)
}
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
F: FnOnce(&mut ModelContext<T>) -> T,
{
self.cx.borrow_mut().add_model(build_model)
}
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
let (window_id, view) = self
.cx
.borrow_mut()
.add_window(Default::default(), build_root_view);
self.simulate_window_activation(Some(window_id));
(window_id, view)
}
pub fn add_view<T, F>(
&mut self,
parent_handle: impl Into<AnyViewHandle>,
build_view: F,
) -> ViewHandle<T>
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
self.cx.borrow_mut().add_view(parent_handle, build_view)
}
pub fn window_ids(&self) -> Vec<usize> {
self.cx.borrow().window_ids().collect()
}
pub fn root_view<T: View>(&self, window_id: usize) -> Option<ViewHandle<T>> {
self.cx.borrow().root_view(window_id)
}
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
callback(self.cx.borrow().as_ref())
}
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
let mut state = self.cx.borrow_mut();
// Don't increment pending flushes in order for effects to be flushed before the callback
// completes, which is helpful in tests.
let result = callback(&mut *state);
// Flush effects after the callback just in case there are any. This can happen in edge
// cases such as the closure dropping handles.
state.flush_effects();
result
}
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
where
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
V: View,
{
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
let mut render_cx = RenderContext {
app: cx,
window_id: handle.window_id(),
view_id: handle.id(),
view_type: PhantomData,
titlebar_height: 0.,
hovered_region_ids: Default::default(),
clicked_region_ids: None,
refreshing: false,
appearance: Appearance::Light,
};
f(view, &mut render_cx)
})
}
pub fn to_async(&self) -> AsyncAppContext {
AsyncAppContext(self.cx.clone())
}
pub fn font_cache(&self) -> Arc<FontCache> {
self.cx.borrow().cx.font_cache.clone()
}
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
self.foreground_platform.clone()
}
pub fn platform(&self) -> Arc<dyn platform::Platform> {
self.cx.borrow().cx.platform.clone()
}
pub fn foreground(&self) -> Rc<executor::Foreground> {
self.cx.borrow().foreground().clone()
}
pub fn background(&self) -> Arc<executor::Background> {
self.cx.borrow().background().clone()
}
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where
F: FnOnce(AsyncAppContext) -> Fut,
Fut: 'static + Future<Output = T>,
T: 'static,
{
let foreground = self.foreground();
let future = f(self.to_async());
let cx = self.to_async();
foreground.spawn(async move {
let result = future.await;
cx.0.borrow_mut().flush_effects();
result
})
}
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
self.foreground_platform.simulate_new_path_selection(result);
}
pub fn did_prompt_for_new_path(&self) -> bool {
self.foreground_platform.as_ref().did_prompt_for_new_path()
}
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
use postage::prelude::Sink as _;
let mut done_tx = self
.window_mut(window_id)
.pending_prompts
.borrow_mut()
.pop_front()
.expect("prompt was not called");
let _ = done_tx.try_send(answer);
}
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
let window = self.window_mut(window_id);
let prompts = window.pending_prompts.borrow_mut();
!prompts.is_empty()
}
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
self.window_mut(window_id).title.clone()
}
pub fn simulate_window_close(&self, window_id: usize) -> bool {
let handler = self.window_mut(window_id).should_close_handler.take();
if let Some(mut handler) = handler {
let should_close = handler();
self.window_mut(window_id).should_close_handler = Some(handler);
should_close
} else {
false
}
}
pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) {
let mut window = self.window_mut(window_id);
window.size = size;
let mut handlers = mem::take(&mut window.resize_handlers);
drop(window);
for handler in &mut handlers {
handler();
}
self.window_mut(window_id).resize_handlers = handlers;
}
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
let mut handlers = BTreeMap::new();
{
let mut cx = self.cx.borrow_mut();
for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows {
let window = window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
handlers.insert(
*window_id,
mem::take(&mut window.active_status_change_handlers),
);
}
};
let mut handlers = handlers.into_iter().collect::<Vec<_>>();
handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate);
for (window_id, mut window_handlers) in handlers {
for window_handler in &mut window_handlers {
window_handler(Some(window_id) == to_activate);
}
self.window_mut(window_id)
.active_status_change_handlers
.extend(window_handlers);
}
}
pub fn is_window_edited(&self, window_id: usize) -> bool {
self.window_mut(window_id).edited
}
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
self.cx.borrow().leak_detector()
}
pub fn assert_dropped(&self, handle: impl WeakHandle) {
self.cx
.borrow()
.leak_detector()
.lock()
.assert_dropped(handle.id())
}
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
let (_, window) = state
.presenters_and_platform_windows
.get_mut(&window_id)
.unwrap();
let test_window = window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
test_window
})
}
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
self.condition_duration = duration;
}
pub fn condition_duration(&self) -> Duration {
self.condition_duration.unwrap_or_else(|| {
if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
}
})
}
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
self.update(|cx| {
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
let expected_content = expected_content.map(|content| content.to_owned());
assert_eq!(actual_content, expected_content);
})
}
pub fn add_assertion_context(&self, context: String) -> ContextHandle {
self.assertion_context.add_context(context)
}
pub fn assertion_context(&self) -> String {
self.assertion_context.context()
}
}
impl UpdateModel for TestAppContext {
fn update_model<T: Entity, O>(
&mut self,
handle: &ModelHandle<T>,
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
) -> O {
self.cx.borrow_mut().update_model(handle, update)
}
}
impl ReadModelWith for TestAppContext {
fn read_model_with<E: Entity, T>(
&self,
handle: &ModelHandle<E>,
read: &mut dyn FnMut(&E, &AppContext) -> T,
) -> T {
let cx = self.cx.borrow();
let cx = cx.as_ref();
read(handle.read(cx), cx)
}
}
impl UpdateView for TestAppContext {
fn update_view<T, S>(
&mut self,
handle: &ViewHandle<T>,
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
) -> S
where
T: View,
{
self.cx.borrow_mut().update_view(handle, update)
}
}
impl ReadViewWith for TestAppContext {
fn read_view_with<V, T>(
&self,
handle: &ViewHandle<V>,
read: &mut dyn FnMut(&V, &AppContext) -> T,
) -> T
where
V: View,
{
let cx = self.cx.borrow();
let cx = cx.as_ref();
read(handle.read(cx), cx)
}
}
impl<T: Entity> ModelHandle<T> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.unbounded_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.next())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
where
T::Event: Clone,
{
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.subscribe(self, move |_, event, _| {
tx.unbounded_send(event.clone()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
cx.foreground.start_waiting();
async move {
let event = crate::util::timeout(duration, rx.next())
.await
.expect("next event timed out");
drop(subscription);
event.expect("model dropped while test was waiting for its next event")
}
}
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscriptions = (
cx.observe(self, {
let tx = tx.clone();
move |_, _| {
tx.unbounded_send(()).ok();
}
}),
cx.subscribe(self, {
move |_, _, _| {
tx.unbounded_send(()).ok();
}
}),
);
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
crate::util::timeout(duration, async move {
loop {
{
let cx = cx.borrow();
let cx = cx.as_ref();
if predicate(
handle
.upgrade(cx)
.expect("model dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.next()
.await
.expect("model dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
impl<T: View> ViewHandle<T> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (mut tx, mut rx) = postage::mpsc::channel(1);
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.try_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
let timeout_duration = cx.condition_duration();
let mut cx = cx.cx.borrow_mut();
let subscriptions = self.update(&mut *cx, |_, cx| {
(
cx.observe(self, {
let mut tx = tx.clone();
move |_, _, _| {
tx.blocking_send(()).ok();
}
}),
cx.subscribe(self, {
let mut tx = tx.clone();
move |_, _, _, _| {
tx.blocking_send(()).ok();
}
}),
)
});
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
async move {
crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
let cx = cx.as_ref();
if predicate(
handle
.upgrade(cx)
.expect("view dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.recv()
.await
.expect("view dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
#[derive(Clone)]
pub struct AssertionContextManager {
id: Arc<AtomicUsize>,
contexts: Arc<RwLock<BTreeMap<usize, String>>>,
}
impl AssertionContextManager {
pub fn new() -> Self {
Self {
id: Arc::new(AtomicUsize::new(0)),
contexts: Arc::new(RwLock::new(BTreeMap::new())),
}
}
pub fn add_context(&self, context: String) -> ContextHandle {
let id = self.id.fetch_add(1, Ordering::Relaxed);
let mut contexts = self.contexts.write();
contexts.insert(id, context);
ContextHandle {
id,
manager: self.clone(),
}
}
pub fn context(&self) -> String {
let contexts = self.contexts.read();
format!("\n{}\n", contexts.values().join("\n"))
}
}
pub struct ContextHandle {
id: usize,
manager: AssertionContextManager,
}
impl Drop for ContextHandle {
fn drop(&mut self) {
let mut contexts = self.manager.contexts.write();
contexts.remove(&self.id);
}
}

View File

@ -271,9 +271,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
mut layout,
} => {
let bounds = RectF::new(origin, size);
let visible_bounds = visible_bounds
.intersection(bounds)
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
Lifecycle::PostPaint {
element,
@ -292,9 +289,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
..
} => {
let bounds = RectF::new(origin, bounds.size());
let visible_bounds = visible_bounds
.intersection(bounds)
.unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default()));
let paint = element.paint(bounds, visible_bounds, &mut layout, cx);
Lifecycle::PostPaint {
element,

View File

@ -241,11 +241,12 @@ impl Element for Flex {
remaining_space: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let mut remaining_space = *remaining_space;
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
let mut remaining_space = *remaining_space;
let overflowing = remaining_space < 0.;
if overflowing {
cx.scene.push_layer(Some(bounds));
cx.scene.push_layer(Some(visible_bounds));
}
if let Some(scroll_state) = &self.scroll_state {

View File

@ -27,6 +27,8 @@ pub struct ImageStyle {
pub height: Option<f32>,
#[serde(default)]
pub width: Option<f32>,
#[serde(default)]
pub grayscale: bool,
}
impl Image {
@ -74,6 +76,7 @@ impl Element for Image {
bounds,
border: self.style.border,
corner_radius: self.style.corner_radius,
grayscale: self.style.grayscale,
data: self.data.clone(),
});
}

View File

@ -261,10 +261,11 @@ impl Element for List {
scroll_top: &mut ListOffset,
cx: &mut PaintContext,
) {
cx.scene.push_layer(Some(bounds));
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
cx.scene
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
cx.scene.push_mouse_region(
MouseRegion::new::<Self>(cx.current_view_id(), 0, bounds).on_scroll({
let state = self.state.clone();
let height = bounds.height();
let scroll_top = scroll_top.clone();
@ -277,7 +278,8 @@ impl Element for List {
cx,
)
}
}));
}),
);
let state = &mut *self.state.0.borrow_mut();
for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
@ -556,6 +558,8 @@ impl StateInner {
let visible_range = self.visible_range(height, scroll_top);
self.scroll_handler.as_mut().unwrap()(visible_range, cx);
}
cx.notify();
}
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {

View File

@ -22,6 +22,8 @@ pub struct MouseEventHandler<Tag: 'static> {
cursor_style: Option<CursorStyle>,
handlers: HandlerSet,
hoverable: bool,
notify_on_hover: bool,
notify_on_click: bool,
padding: Padding,
_tag: PhantomData<Tag>,
}
@ -30,13 +32,19 @@ impl<Tag> MouseEventHandler<Tag> {
pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
where
V: View,
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
{
let mut mouse_state = cx.mouse_state::<Tag>(region_id);
let child = render_child(&mut mouse_state, cx);
let notify_on_hover = mouse_state.accessed_hovered();
let notify_on_click = mouse_state.accessed_clicked();
Self {
child: render_child(cx.mouse_state::<Tag>(region_id), cx),
child,
region_id,
cursor_style: None,
handlers: Default::default(),
notify_on_hover,
notify_on_click,
hoverable: true,
padding: Default::default(),
_tag: PhantomData,
@ -169,6 +177,7 @@ impl<Tag> Element for MouseEventHandler<Tag> {
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
let hit_bounds = self.hit_bounds(visible_bounds);
if let Some(style) = self.cursor_style {
cx.scene.push_cursor_region(CursorRegion {
@ -184,7 +193,9 @@ impl<Tag> Element for MouseEventHandler<Tag> {
hit_bounds,
self.handlers.clone(),
)
.with_hoverable(self.hoverable),
.with_hoverable(self.hoverable)
.with_notify_on_hover(self.notify_on_hover)
.with_notify_on_click(self.notify_on_click),
);
self.child.paint(bounds.origin(), visible_bounds, cx);

View File

@ -217,7 +217,11 @@ impl Element for Overlay {
));
}
self.child.paint(bounds.origin(), bounds, cx);
self.child.paint(
bounds.origin(),
RectF::new(Vector2F::zero(), cx.window_size),
cx,
);
cx.scene.pop_stacking_context();
}

View File

@ -284,7 +284,9 @@ impl Element for UniformList {
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_layer(Some(bounds));
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
cx.scene.push_mouse_region(
MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({

View File

@ -325,7 +325,12 @@ impl Deterministic {
let mut state = self.state.lock();
let wakeup_at = state.now + duration;
let id = util::post_inc(&mut state.next_timer_id);
state.pending_timers.push((id, wakeup_at, tx));
match state
.pending_timers
.binary_search_by_key(&wakeup_at, |e| e.1)
{
Ok(ix) | Err(ix) => state.pending_timers.insert(ix, (id, wakeup_at, tx)),
}
let state = self.state.clone();
Timer::Deterministic(DeterministicTimer { rx, id, state })
}

View File

@ -44,6 +44,8 @@ pub trait Platform: Send + Sync {
fn unhide_other_apps(&self);
fn quit(&self);
fn screen_size(&self) -> Vector2F;
fn open_window(
&self,
id: usize,
@ -63,6 +65,7 @@ pub trait Platform: Send + Sync {
fn delete_credentials(&self, url: &str) -> Result<()>;
fn set_cursor_style(&self, style: CursorStyle);
fn should_auto_hide_scrollbars(&self) -> bool;
fn local_timezone(&self) -> UtcOffset;

View File

@ -14,8 +14,10 @@ use core_graphics::{
event::{CGEvent, CGEventFlags, CGKeyCode},
event_source::{CGEventSource, CGEventSourceStateID},
};
use ctor::ctor;
use foreign_types::ForeignType;
use objc::{class, msg_send, sel, sel_impl};
use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
@ -25,6 +27,15 @@ const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
#[ctor]
unsafe fn build_event_source() {
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
EVENT_SOURCE = source.as_ptr();
mem::forget(source);
}
pub fn key_to_native(key: &str) -> Cow<str> {
use cocoa::appkit::*;
let code = match key {
@ -228,7 +239,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
let mut chars_ignoring_modifiers =
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
.to_str()
.unwrap();
.unwrap()
.to_string();
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags();
@ -243,31 +255,31 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
#[allow(non_upper_case_globals)]
let key = match first_char {
Some(SPACE_KEY) => "space",
Some(BACKSPACE_KEY) => "backspace",
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter",
Some(ESCAPE_KEY) => "escape",
Some(TAB_KEY) => "tab",
Some(SHIFT_TAB_KEY) => "tab",
Some(NSUpArrowFunctionKey) => "up",
Some(NSDownArrowFunctionKey) => "down",
Some(NSLeftArrowFunctionKey) => "left",
Some(NSRightArrowFunctionKey) => "right",
Some(NSPageUpFunctionKey) => "pageup",
Some(NSPageDownFunctionKey) => "pagedown",
Some(NSDeleteFunctionKey) => "delete",
Some(NSF1FunctionKey) => "f1",
Some(NSF2FunctionKey) => "f2",
Some(NSF3FunctionKey) => "f3",
Some(NSF4FunctionKey) => "f4",
Some(NSF5FunctionKey) => "f5",
Some(NSF6FunctionKey) => "f6",
Some(NSF7FunctionKey) => "f7",
Some(NSF8FunctionKey) => "f8",
Some(NSF9FunctionKey) => "f9",
Some(NSF10FunctionKey) => "f10",
Some(NSF11FunctionKey) => "f11",
Some(NSF12FunctionKey) => "f12",
Some(SPACE_KEY) => "space".to_string(),
Some(BACKSPACE_KEY) => "backspace".to_string(),
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
Some(ESCAPE_KEY) => "escape".to_string(),
Some(TAB_KEY) => "tab".to_string(),
Some(SHIFT_TAB_KEY) => "tab".to_string(),
Some(NSUpArrowFunctionKey) => "up".to_string(),
Some(NSDownArrowFunctionKey) => "down".to_string(),
Some(NSLeftArrowFunctionKey) => "left".to_string(),
Some(NSRightArrowFunctionKey) => "right".to_string(),
Some(NSPageUpFunctionKey) => "pageup".to_string(),
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
Some(NSDeleteFunctionKey) => "delete".to_string(),
Some(NSF1FunctionKey) => "f1".to_string(),
Some(NSF2FunctionKey) => "f2".to_string(),
Some(NSF3FunctionKey) => "f3".to_string(),
Some(NSF4FunctionKey) => "f4".to_string(),
Some(NSF5FunctionKey) => "f5".to_string(),
Some(NSF6FunctionKey) => "f6".to_string(),
Some(NSF7FunctionKey) => "f7".to_string(),
Some(NSF8FunctionKey) => "f8".to_string(),
Some(NSF9FunctionKey) => "f9".to_string(),
Some(NSF10FunctionKey) => "f10".to_string(),
Some(NSF11FunctionKey) => "f11".to_string(),
Some(NSF12FunctionKey) => "f12".to_string(),
_ => {
let mut chars_ignoring_modifiers_and_shift =
chars_for_modified_key(native_event.keyCode(), false, false);
@ -303,21 +315,19 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
shift,
cmd,
function,
key: key.into(),
key,
}
}
fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str {
fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
// an event with the given flags instead lets us access `characters`, which always
// returns a valid string.
let event = CGEvent::new_keyboard_event(
CGEventSource::new(CGEventSourceStateID::Private).unwrap(),
code,
true,
)
.unwrap();
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
mem::forget(source);
let mut flags = CGEventFlags::empty();
if cmd {
flags |= CGEventFlags::CGEventFlagCommand;
@ -327,10 +337,11 @@ fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a st
}
event.set_flags(flags);
let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] };
unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
CStr::from_ptr(event.characters().UTF8String())
.to_str()
.unwrap()
.to_string()
}
}

View File

@ -2,7 +2,9 @@ use super::{
event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
};
use crate::{
executor, keymap,
executor,
geometry::vector::{vec2f, Vector2F},
keymap,
platform::{self, CursorStyle},
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
};
@ -12,7 +14,7 @@ use cocoa::{
appkit::{
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
NSPasteboardTypeString, NSSavePanel, NSWindow,
NSPasteboardTypeString, NSSavePanel, NSScreen, NSWindow,
},
base::{id, nil, selector, YES},
foundation::{
@ -486,6 +488,14 @@ impl platform::Platform for MacPlatform {
}
}
fn screen_size(&self) -> Vector2F {
unsafe {
let screen = NSScreen::mainScreen(nil);
let frame = NSScreen::frame(screen);
vec2f(frame.size.width as f32, frame.size.height as f32)
}
}
fn open_window(
&self,
id: usize,
@ -699,6 +709,16 @@ impl platform::Platform for MacPlatform {
}
}
fn should_auto_hide_scrollbars(&self) -> bool {
#[allow(non_upper_case_globals)]
const NSScrollerStyleOverlay: NSInteger = 1;
unsafe {
let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle];
style == NSScrollerStyleOverlay
}
}
fn local_timezone(&self) -> UtcOffset {
unsafe {
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];

View File

@ -747,6 +747,7 @@ impl Renderer {
border_left: border_width * (image.border.left as usize as f32),
border_color: image.border.color.to_uchar4(),
corner_radius,
grayscale: image.grayscale as u8,
});
}
@ -769,6 +770,7 @@ impl Renderer {
border_left: 0.,
border_color: Default::default(),
corner_radius: 0.,
grayscale: false as u8,
});
} else {
log::warn!("could not render glyph with id {}", image_glyph.id);

View File

@ -90,6 +90,7 @@ typedef struct {
float border_left;
vector_uchar4 border_color;
float corner_radius;
uint8_t grayscale;
} GPUIImage;
typedef enum {

View File

@ -44,6 +44,7 @@ struct QuadFragmentInput {
float border_left;
float4 border_color;
float corner_radius;
uchar grayscale; // only used in image shader
};
float4 quad_sdf(QuadFragmentInput input) {
@ -110,6 +111,7 @@ vertex QuadFragmentInput quad_vertex(
quad.border_left,
coloru_to_colorf(quad.border_color),
quad.corner_radius,
0,
};
}
@ -251,6 +253,7 @@ vertex QuadFragmentInput image_vertex(
image.border_left,
coloru_to_colorf(image.border_color),
image.corner_radius,
image.grayscale,
};
}
@ -260,6 +263,13 @@ fragment float4 image_fragment(
) {
constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
input.background_color = atlas.sample(atlas_sampler, input.atlas_position);
if (input.grayscale) {
float grayscale =
0.2126 * input.background_color.r +
0.7152 * input.background_color.g +
0.0722 * input.background_color.b;
input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a);
}
return quad_sdf(input);
}
@ -289,6 +299,7 @@ vertex QuadFragmentInput surface_vertex(
0.,
float4(0.),
0.,
0,
};
}

View File

@ -34,11 +34,11 @@ pub struct ForegroundPlatform {
struct Dispatcher;
pub struct Window {
size: Vector2F,
pub(crate) size: Vector2F,
scale_factor: f32,
current_scene: Option<crate::Scene>,
event_handlers: Vec<Box<dyn FnMut(super::Event) -> bool>>,
resize_handlers: Vec<Box<dyn FnMut()>>,
pub(crate) resize_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
fullscreen_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) active_status_change_handlers: Vec<Box<dyn FnMut(bool)>>,
@ -131,6 +131,10 @@ impl super::Platform for Platform {
fn quit(&self) {}
fn screen_size(&self) -> Vector2F {
vec2f(1024., 768.)
}
fn open_window(
&self,
_: usize,
@ -177,6 +181,10 @@ impl super::Platform for Platform {
*self.cursor.lock() = style;
}
fn should_auto_hide_scrollbars(&self) -> bool {
false
}
fn local_timezone(&self) -> UtcOffset {
UtcOffset::UTC
}

View File

@ -12,10 +12,10 @@ use crate::{
UpOutRegionEvent, UpRegionEvent,
},
text_layout::TextLayoutCache,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, Appearance, AssetCache, ElementBox,
Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId,
ParentId, ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle,
UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, Appearance,
AssetCache, ElementBox, Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent,
MouseRegion, MouseRegionId, ParentId, ReadModel, ReadView, RenderContext, RenderParams, Scene,
UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
};
use collections::{HashMap, HashSet};
use pathfinder_geometry::vector::{vec2f, Vector2F};
@ -231,7 +231,7 @@ impl Presenter {
) -> bool {
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
let mut events_to_send = Vec::new();
let mut invalidated_views: HashSet<usize> = Default::default();
let mut notified_views: HashSet<usize> = Default::default();
// 1. Allocate the correct set of GPUI events generated from the platform events
// -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
@ -257,11 +257,6 @@ impl Presenter {
})
.collect();
// Clicked status is used when rendering views via the RenderContext.
// So when it changes, these views need to be rerendered
for clicked_region_id in self.clicked_region_ids.iter() {
invalidated_views.insert(clicked_region_id.view_id());
}
self.clicked_button = Some(e.button);
}
@ -392,17 +387,31 @@ impl Presenter {
//Ensure that hover entrance events aren't sent twice
if self.hovered_region_ids.insert(region.id()) {
valid_regions.push(region.clone());
invalidated_views.insert(region.id().view_id());
if region.notify_on_hover {
notified_views.insert(region.id().view_id());
}
}
} else {
// Ensure that hover exit events aren't sent twice
if self.hovered_region_ids.remove(&region.id()) {
valid_regions.push(region.clone());
invalidated_views.insert(region.id().view_id());
if region.notify_on_hover {
notified_views.insert(region.id().view_id());
}
}
}
}
}
MouseRegionEvent::Down(_) | MouseRegionEvent::Up(_) => {
for (region, _) in self.mouse_regions.iter().rev() {
if region.bounds.contains_point(self.mouse_position) {
if region.notify_on_click {
notified_views.insert(region.id().view_id());
}
valid_regions.push(region.clone());
}
}
}
MouseRegionEvent::Click(e) => {
// Only raise click events if the released button is the same as the one stored
if self
@ -413,11 +422,6 @@ impl Presenter {
// Clear clicked regions and clicked button
let clicked_region_ids =
std::mem::replace(&mut self.clicked_region_ids, Default::default());
// Clicked status is used when rendering views via the RenderContext.
// So when it changes, these views need to be rerendered
for clicked_region_id in clicked_region_ids.iter() {
invalidated_views.insert(clicked_region_id.view_id());
}
self.clicked_button = None;
// Find regions which still overlap with the mouse since the last MouseDown happened
@ -459,7 +463,7 @@ impl Presenter {
//3. Fire region events
let hovered_region_ids = self.hovered_region_ids.clone();
for valid_region in valid_regions.into_iter() {
let mut event_cx = self.build_event_context(&mut invalidated_views, cx);
let mut event_cx = self.build_event_context(&mut notified_views, cx);
region_event.set_region(valid_region.bounds);
if let MouseRegionEvent::Hover(e) = &mut region_event {
@ -482,9 +486,6 @@ impl Presenter {
if let Some(callback) = valid_region.handlers.get(&region_event.handler_key()) {
event_cx.handled = true;
event_cx
.invalidated_views
.insert(valid_region.id().view_id());
event_cx.with_current_view(valid_region.id().view_id(), {
let region_event = region_event.clone();
|cx| {
@ -503,11 +504,11 @@ impl Presenter {
}
if !any_event_handled && !event_reused {
let mut event_cx = self.build_event_context(&mut invalidated_views, cx);
let mut event_cx = self.build_event_context(&mut notified_views, cx);
any_event_handled = event_cx.dispatch_event(root_view_id, &event);
}
for view_id in invalidated_views {
for view_id in notified_views {
cx.notify_view(self.window_id, view_id);
}
@ -519,7 +520,7 @@ impl Presenter {
pub fn build_event_context<'a>(
&'a mut self,
invalidated_views: &'a mut HashSet<usize>,
notified_views: &'a mut HashSet<usize>,
cx: &'a mut MutableAppContext,
) -> EventContext<'a> {
EventContext {
@ -527,7 +528,7 @@ impl Presenter {
font_cache: &self.font_cache,
text_layout_cache: &self.text_layout_cache,
view_stack: Default::default(),
invalidated_views,
notified_views,
notify_count: 0,
handled: false,
window_id: self.window_id,
@ -750,7 +751,7 @@ pub struct EventContext<'a> {
pub notify_count: usize,
view_stack: Vec<usize>,
handled: bool,
invalidated_views: &'a mut HashSet<usize>,
notified_views: &'a mut HashSet<usize>,
}
impl<'a> EventContext<'a> {
@ -809,7 +810,7 @@ impl<'a> EventContext<'a> {
pub fn notify(&mut self) {
self.notify_count += 1;
if let Some(view_id) = self.view_stack.last() {
self.invalidated_views.insert(*view_id);
self.notified_views.insert(*view_id);
}
}
@ -972,17 +973,23 @@ impl ToJson for SizeConstraint {
}
pub struct ChildView {
view: AnyViewHandle,
view: AnyWeakViewHandle,
view_name: &'static str,
}
impl ChildView {
pub fn new(view: impl Into<AnyViewHandle>) -> Self {
Self { view: view.into() }
pub fn new(view: impl Into<AnyViewHandle>, cx: &AppContext) -> Self {
let view = view.into();
let view_name = cx.view_ui_name(view.window_id(), view.id()).unwrap();
Self {
view: view.downgrade(),
view_name,
}
}
}
impl Element for ChildView {
type LayoutState = ();
type LayoutState = bool;
type PaintState = ();
fn layout(
@ -990,18 +997,35 @@ impl Element for ChildView {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = cx.layout(self.view.id(), constraint);
(size, ())
if cx.rendered_views.contains_key(&self.view.id()) {
let size = cx.layout(self.view.id(), constraint);
(size, true)
} else {
log::error!(
"layout called on a ChildView element whose underlying view was dropped (view_id: {}, name: {:?})",
self.view.id(),
self.view_name
);
(Vector2F::zero(), false)
}
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
view_is_valid: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.paint(self.view.id(), bounds.origin(), visible_bounds);
) {
if *view_is_valid {
cx.paint(self.view.id(), bounds.origin(), visible_bounds);
} else {
log::error!(
"paint called on a ChildView element whose underlying view was dropped (view_id: {}, name: {:?})",
self.view.id(),
self.view_name
);
}
}
fn dispatch_event(
@ -1009,11 +1033,20 @@ impl Element for ChildView {
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
view_is_valid: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
cx.dispatch_event(self.view.id(), event)
if *view_is_valid {
cx.dispatch_event(self.view.id(), event)
} else {
log::error!(
"dispatch_event called on a ChildView element whose underlying view was dropped (view_id: {}, name: {:?})",
self.view.id(),
self.view_name
);
false
}
}
fn rect_for_text_range(
@ -1021,11 +1054,20 @@ impl Element for ChildView {
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
view_is_valid: &Self::LayoutState,
_: &Self::PaintState,
cx: &MeasurementContext,
) -> Option<RectF> {
cx.rect_for_text_range(self.view.id(), range_utf16)
if *view_is_valid {
cx.rect_for_text_range(self.view.id(), range_utf16)
} else {
log::error!(
"rect_for_text_range called on a ChildView element whose underlying view was dropped (view_id: {}, name: {:?})",
self.view.id(),
self.view_name
);
None
}
}
fn debug(
@ -1039,7 +1081,11 @@ impl Element for ChildView {
"type": "ChildView",
"view_id": self.view.id(),
"bounds": bounds.to_json(),
"view": self.view.debug_json(cx.app),
"view": if let Some(view) = self.view.upgrade(cx.app) {
view.debug_json(cx.app)
} else {
json!(null)
},
"child": if let Some(view) = cx.rendered_views.get(&self.view.id()) {
view.debug(cx)
} else {

View File

@ -172,6 +172,7 @@ pub struct Image {
pub bounds: RectF,
pub border: Border,
pub corner_radius: f32,
pub grayscale: bool,
pub data: Arc<ImageData>,
}

Some files were not shown because too many files have changed in this diff Show More