Merge branch 'main' into fix-typos
This commit is contained in:
commit
0711476fd5
60 changed files with 1325 additions and 526 deletions
57
CONTRIBUTING.md
Normal file
57
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# CONTRIBUTING
|
||||
|
||||
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
|
||||
|
||||
We want to ensure that no one ends up spending time on a pull request that may not be accepted, so we ask that you discuss your ideas with the team and community before starting on a contribution.
|
||||
|
||||
All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Contributors to Zed must sign our Contributor License Agreement (link coming soon) before their contributions can be merged.
|
||||
|
||||
## Contribution ideas
|
||||
|
||||
If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on:
|
||||
|
||||
- Our public roadmap (link coming soon!) details what features we plan to add to Zed.
|
||||
- Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community.
|
||||
|
||||
At the moment, we are generally not looking to extend Zed's language or theme support by directly adding these features to Zed - we really want to build a plugin system to handle making the editor extensible going forward.
|
||||
|
||||
If you are passionate about shipping new languages or themes we suggest contributing to the extension system to help us get there faster.
|
||||
|
||||
## Resources
|
||||
|
||||
### Bird-eye's view of Zed
|
||||
|
||||
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
|
||||
|
||||
- [gpui](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation**
|
||||
- [editor](/crates/editor) contains the core `Editor` type that drives both the code editor and all various input fields within Zed. It also handles a display layer for LSP features such as Inlay Hints or code completions.
|
||||
- [project](/crates/project) manages files and navigation within the filetree. It is also Zed's side of communication with LSP.
|
||||
- [workspace](/crates/workspace) handles local state serialization and groups projects together.
|
||||
- [vim](/crates/vim) is a thin implementation of Vim workflow over `editor`.
|
||||
- [lsp](/crates/lsp) handles communication with external LSP server.
|
||||
- [language](/crates/language) drives `editor`'s understanding of language - from providing a list of symbols to the syntax map.
|
||||
- [collab](/crates/collab) is the collaboration server itself, driving the collaboration features such as project sharing.
|
||||
- [rpc](/crates/rpc) defines messages to be exchanged with collaboration server.
|
||||
- [theme](/crates/theme) defines the theme system and provides a default theme.
|
||||
- [ui](/crates/ui) is a collection of UI components and common patterns used throughout Zed.
|
||||
|
||||
### Proposal & Discussion
|
||||
|
||||
Before starting on a contribution, we ask that you look to see if there is any existing PRs, or in-Zed discussions about the thing you want to implement. If there is no existing work, find a public channel that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can start a discussion, ask a team member or another contributor.
|
||||
|
||||
*Please remember contributions not discussed with the team ahead of time likely have a lower chance of being merged or looked at in a timely manner.*
|
||||
|
||||
## Implementation & Help
|
||||
|
||||
When you start working on your contribution if you find you are struggling with something specific feel free to reach out to the team for help.
|
||||
|
||||
Remember the team is more likely to be available to help if you have already discussed your contribution or are working on something that is higher priority, like something on the roadmap or a top-ranking issue.
|
||||
|
||||
We're happy to pair with you to help you learn the codebase and get your contribution merged.
|
||||
|
||||
**Zed makes heavy use of unit and integration testing, it is highly likely that contributions without any unit tests will be rejected**
|
||||
|
||||
Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming.
|
||||
We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed.
|
||||
|
||||
Remeber that smaller, incremental PRs are easier to review and merge than large PRs.
|
109
README.md
109
README.md
|
@ -1,110 +1,27 @@
|
|||
# 🚧 TODO 🚧
|
||||
|
||||
- [ ] Add intro
|
||||
- [ ] Add link to contributing guide
|
||||
- [ ] Add barebones running zed from source instructions
|
||||
- [ ] Link out to further dev docs
|
||||
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Development tips
|
||||
## Developing Zed
|
||||
|
||||
### Dependencies
|
||||
|
||||
* Install Xcode from https://apps.apple.com/us/app/xcode/id497799835?mt=12, and accept the license:
|
||||
```
|
||||
sudo xcodebuild -license
|
||||
```
|
||||
|
||||
* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.)
|
||||
```
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
brew install node rustup-init
|
||||
rustup-init # follow the installation steps
|
||||
```
|
||||
|
||||
* Install postgres and configure the database
|
||||
```
|
||||
brew install postgresql@15
|
||||
brew services start postgresql@15
|
||||
psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres
|
||||
psql -U postgres -c "CREATE DATABASE zed"
|
||||
```
|
||||
|
||||
* Install the `LiveKit` server, the `PostgREST` API server, and the `foreman` process supervisor:
|
||||
|
||||
```
|
||||
brew install livekit
|
||||
brew install postgrest
|
||||
brew install foreman
|
||||
```
|
||||
|
||||
* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies:
|
||||
|
||||
```
|
||||
cd ..
|
||||
git clone https://github.com/zed-industries/zed.dev
|
||||
cd zed.dev && npm install
|
||||
npm install -g vercel
|
||||
```
|
||||
|
||||
* Return to Zed project directory and Initialize submodules
|
||||
|
||||
```
|
||||
cd zed
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
* Set up a local `zed` database and seed it with some initial users:
|
||||
|
||||
[Create a personal GitHub token](https://github.com/settings/tokens/new) to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope).
|
||||
Then delete that token.
|
||||
|
||||
```
|
||||
GITHUB_TOKEN=<$token> script/bootstrap
|
||||
```
|
||||
|
||||
* Now try running zed with collaboration disabled:
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Common errors
|
||||
|
||||
* `xcrun: error: unable to find utility "metal", not a developer tool or in PATH`
|
||||
* You need to install Xcode and then run: `xcode-select --switch /Applications/Xcode.app/Contents/Developer`
|
||||
* (see https://github.com/gfx-rs/gfx/issues/2309)
|
||||
|
||||
### Testing against locally-running servers
|
||||
|
||||
Start the web and collab servers:
|
||||
|
||||
```
|
||||
foreman start
|
||||
```
|
||||
|
||||
If you want to run Zed pointed at the local servers, you can run:
|
||||
|
||||
```
|
||||
script/zed-local
|
||||
```
|
||||
|
||||
### Dump element JSON
|
||||
|
||||
If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way.
|
||||
- [Building Zed](./docs/src/developing_zed__building_zed.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
### Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
|
||||
|
||||
### Wasm Plugins
|
||||
|
||||
Zed has a Wasm-based plugin runtime which it currently uses to embed plugins. To compile Zed, you'll need to have the `wasm32-wasi` toolchain installed on your system. To install this toolchain, run:
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-wasi
|
||||
```
|
||||
|
||||
Plugins can be found in the `plugins` folder in the root. For more information about how plugins work, check the [Plugin Guide](./crates/plugin_runtime/README.md) in `crates/plugin_runtime/README.md`.
|
||||
|
|
BIN
assets/screenshots/staff_usage_of_channels.png
Normal file
BIN
assets/screenshots/staff_usage_of_channels.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 131 KiB |
|
@ -1148,7 +1148,7 @@ impl Render for AssistantPanel {
|
|||
|panel, cx| panel.toolbar.read(cx).item_of_type::<BufferSearchBar>(),
|
||||
cx,
|
||||
);
|
||||
BufferSearchBar::register_inner(&mut registrar);
|
||||
BufferSearchBar::register(&mut registrar);
|
||||
registrar.into_div()
|
||||
} else {
|
||||
div()
|
||||
|
@ -2311,8 +2311,7 @@ impl ConversationEditor {
|
|||
}
|
||||
});
|
||||
|
||||
div()
|
||||
.h_flex()
|
||||
h_flex()
|
||||
.id(("message_header", message_id.0))
|
||||
.h_11()
|
||||
.relative()
|
||||
|
@ -2328,6 +2327,7 @@ impl ConversationEditor {
|
|||
.add_suffix(true)
|
||||
.to_string(),
|
||||
)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(
|
||||
|
|
|
@ -27,6 +27,8 @@ lazy_static! {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
/// Validates the authorization header. This has two mechanisms, one for the ADMIN_TOKEN
|
||||
/// and one for the access tokens that we issue.
|
||||
pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
|
||||
let mut auth_header = req
|
||||
.headers()
|
||||
|
@ -88,6 +90,8 @@ struct AccessTokenJson {
|
|||
token: String,
|
||||
}
|
||||
|
||||
/// Creates a new access token to identify the given user. before returning it, you should
|
||||
/// encrypt it with the user's public key.
|
||||
pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result<String> {
|
||||
const VERSION: usize = 1;
|
||||
let access_token = rpc::auth::random_token();
|
||||
|
@ -122,6 +126,8 @@ fn hash_access_token(token: &str) -> Result<String> {
|
|||
.to_string())
|
||||
}
|
||||
|
||||
/// Encrypts the given access token with the given public key to avoid leaking it on the way
|
||||
/// to the client.
|
||||
pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<String> {
|
||||
let native_app_public_key =
|
||||
rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?;
|
||||
|
@ -131,6 +137,7 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<St
|
|||
Ok(encrypted_access_token)
|
||||
}
|
||||
|
||||
/// verify access token returns true if the given token is valid for the given user.
|
||||
pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc<Database>) -> Result<bool> {
|
||||
let token: AccessTokenJson = serde_json::from_str(&token)?;
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@ pub use ids::*;
|
|||
pub use sea_orm::ConnectOptions;
|
||||
pub use tables::user::Model as User;
|
||||
|
||||
/// Database gives you a handle that lets you access the database.
|
||||
/// It handles pooling internally.
|
||||
pub struct Database {
|
||||
options: ConnectOptions,
|
||||
pool: DatabaseConnection,
|
||||
|
@ -62,6 +64,7 @@ pub struct Database {
|
|||
// The `Database` type has so many methods that its impl blocks are split into
|
||||
// separate files in the `queries` folder.
|
||||
impl Database {
|
||||
/// Connects to the database with the given options
|
||||
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
|
||||
sqlx::any::install_default_drivers();
|
||||
Ok(Self {
|
||||
|
@ -82,6 +85,7 @@ impl Database {
|
|||
self.rooms.clear();
|
||||
}
|
||||
|
||||
/// Runs the database migrations.
|
||||
pub async fn migrate(
|
||||
&self,
|
||||
migrations_path: &Path,
|
||||
|
@ -123,11 +127,15 @@ impl Database {
|
|||
Ok(new_migrations)
|
||||
}
|
||||
|
||||
/// Initializes static data that resides in the database by upserting it.
|
||||
pub async fn initialize_static_data(&mut self) -> Result<()> {
|
||||
self.initialize_notification_kinds().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transaction runs things in a transaction. If you want to call other methods
|
||||
/// and pass the transaction around you need to reborrow the transaction at each
|
||||
/// call site with: `&*tx`.
|
||||
pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
|
@ -160,6 +168,7 @@ impl Database {
|
|||
self.run(body).await
|
||||
}
|
||||
|
||||
/// The same as room_transaction, but if you need to only optionally return a Room.
|
||||
async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
|
@ -210,6 +219,9 @@ impl Database {
|
|||
self.run(body).await
|
||||
}
|
||||
|
||||
/// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps
|
||||
/// the database locked until it is dropped. This ensures that updates sent to clients are
|
||||
/// properly serialized with respect to database changes.
|
||||
async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
|
@ -330,6 +342,7 @@ fn is_serialization_error(error: &Error) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// A handle to a [`DatabaseTransaction`].
|
||||
pub struct TransactionHandle(Arc<Option<DatabaseTransaction>>);
|
||||
|
||||
impl Deref for TransactionHandle {
|
||||
|
@ -340,6 +353,8 @@ impl Deref for TransactionHandle {
|
|||
}
|
||||
}
|
||||
|
||||
/// [`RoomGuard`] keeps a database transaction alive until it is dropped.
|
||||
/// so that updates to rooms are serialized.
|
||||
pub struct RoomGuard<T> {
|
||||
data: T,
|
||||
_guard: OwnedMutexGuard<()>,
|
||||
|
@ -361,6 +376,7 @@ impl<T> DerefMut for RoomGuard<T> {
|
|||
}
|
||||
|
||||
impl<T> RoomGuard<T> {
|
||||
/// Returns the inner value of the guard.
|
||||
pub fn into_inner(self) -> T {
|
||||
self.data
|
||||
}
|
||||
|
@ -420,12 +436,14 @@ pub struct WaitlistSummary {
|
|||
pub unknown_count: i64,
|
||||
}
|
||||
|
||||
/// The parameters to create a new user.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NewUserParams {
|
||||
pub github_login: String,
|
||||
pub github_user_id: i32,
|
||||
}
|
||||
|
||||
/// The result of creating a new user.
|
||||
#[derive(Debug)]
|
||||
pub struct NewUserResult {
|
||||
pub user_id: UserId,
|
||||
|
@ -434,6 +452,7 @@ pub struct NewUserResult {
|
|||
pub signup_device_id: Option<String>,
|
||||
}
|
||||
|
||||
/// The result of moving a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct MoveChannelResult {
|
||||
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
|
||||
|
@ -441,18 +460,21 @@ pub struct MoveChannelResult {
|
|||
pub moved_channels: HashSet<ChannelId>,
|
||||
}
|
||||
|
||||
/// The result of renaming a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct RenameChannelResult {
|
||||
pub channel: Channel,
|
||||
pub participants_to_update: HashMap<UserId, Channel>,
|
||||
}
|
||||
|
||||
/// The result of creating a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct CreateChannelResult {
|
||||
pub channel: Channel,
|
||||
pub participants_to_update: Vec<(UserId, ChannelsForUser)>,
|
||||
}
|
||||
|
||||
/// The result of setting a channel's visibility.
|
||||
#[derive(Debug)]
|
||||
pub struct SetChannelVisibilityResult {
|
||||
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
|
||||
|
@ -460,6 +482,7 @@ pub struct SetChannelVisibilityResult {
|
|||
pub channels_to_remove: Vec<ChannelId>,
|
||||
}
|
||||
|
||||
/// The result of updating a channel membership.
|
||||
#[derive(Debug)]
|
||||
pub struct MembershipUpdated {
|
||||
pub channel_id: ChannelId,
|
||||
|
@ -467,12 +490,14 @@ pub struct MembershipUpdated {
|
|||
pub removed_channels: Vec<ChannelId>,
|
||||
}
|
||||
|
||||
/// The result of setting a member's role.
|
||||
#[derive(Debug)]
|
||||
pub enum SetMemberRoleResult {
|
||||
InviteUpdated(Channel),
|
||||
MembershipUpdated(MembershipUpdated),
|
||||
}
|
||||
|
||||
/// The result of inviting a member to a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct InviteMemberResult {
|
||||
pub channel: Channel,
|
||||
|
@ -497,6 +522,7 @@ pub struct Channel {
|
|||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
pub role: ChannelRole,
|
||||
/// parent_path is the channel ids from the root to this one (not including this one)
|
||||
pub parent_path: Vec<ChannelId>,
|
||||
}
|
||||
|
||||
|
|
|
@ -19,19 +19,23 @@ macro_rules! id_type {
|
|||
Deserialize,
|
||||
DeriveValueType,
|
||||
)]
|
||||
#[allow(missing_docs)]
|
||||
#[serde(transparent)]
|
||||
pub struct $name(pub i32);
|
||||
|
||||
impl $name {
|
||||
#[allow(unused)]
|
||||
#[allow(missing_docs)]
|
||||
pub const MAX: Self = Self(i32::MAX);
|
||||
|
||||
#[allow(unused)]
|
||||
#[allow(missing_docs)]
|
||||
pub fn from_proto(value: u64) -> Self {
|
||||
Self(value as i32)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[allow(missing_docs)]
|
||||
pub fn to_proto(self) -> u64 {
|
||||
self.0 as u64
|
||||
}
|
||||
|
@ -84,21 +88,28 @@ id_type!(FlagId);
|
|||
id_type!(NotificationId);
|
||||
id_type!(NotificationKindId);
|
||||
|
||||
/// ChannelRole gives you permissions for both channels and calls.
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||
pub enum ChannelRole {
|
||||
/// Admin can read/write and change permissions.
|
||||
#[sea_orm(string_value = "admin")]
|
||||
Admin,
|
||||
/// Member can read/write, but not change pemissions.
|
||||
#[sea_orm(string_value = "member")]
|
||||
#[default]
|
||||
Member,
|
||||
/// Guest can read, but not write.
|
||||
/// (thought they can use the channel chat)
|
||||
#[sea_orm(string_value = "guest")]
|
||||
Guest,
|
||||
/// Banned may not read.
|
||||
#[sea_orm(string_value = "banned")]
|
||||
Banned,
|
||||
}
|
||||
|
||||
impl ChannelRole {
|
||||
/// Returns true if this role is more powerful than the other role.
|
||||
pub fn should_override(&self, other: Self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
|
@ -109,6 +120,7 @@ impl ChannelRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the maximal role between the two
|
||||
pub fn max(&self, other: Self) -> Self {
|
||||
if self.should_override(other) {
|
||||
*self
|
||||
|
@ -117,6 +129,7 @@ impl ChannelRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// True if the role allows access to all descendant channels
|
||||
pub fn can_see_all_descendants(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
|
@ -125,6 +138,7 @@ impl ChannelRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// True if the role only allows access to public descendant channels
|
||||
pub fn can_only_see_public_descendants(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
|
@ -133,6 +147,7 @@ impl ChannelRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// True if the role can share screen/microphone/projects into rooms.
|
||||
pub fn can_publish_to_rooms(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
|
@ -141,6 +156,7 @@ impl ChannelRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// True if the role can edit shared projects.
|
||||
pub fn can_edit_projects(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
|
@ -149,6 +165,7 @@ impl ChannelRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// True if the role can read shared projects.
|
||||
pub fn can_read_projects(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
|
@ -187,11 +204,14 @@ impl Into<i32> for ChannelRole {
|
|||
}
|
||||
}
|
||||
|
||||
/// ChannelVisibility controls whether channels are public or private.
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||
pub enum ChannelVisibility {
|
||||
/// Public channels are visible to anyone with the link. People join with the Guest role by default.
|
||||
#[sea_orm(string_value = "public")]
|
||||
Public,
|
||||
/// Members channels are only visible to members of this channel or its parents.
|
||||
#[sea_orm(string_value = "members")]
|
||||
#[default]
|
||||
Members,
|
||||
|
|
|
@ -2,6 +2,7 @@ use super::*;
|
|||
use sea_orm::sea_query::Query;
|
||||
|
||||
impl Database {
|
||||
/// Creates a new access token for the given user.
|
||||
pub async fn create_access_token(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
@ -39,6 +40,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Retrieves the access token with the given ID.
|
||||
pub async fn get_access_token(
|
||||
&self,
|
||||
access_token_id: AccessTokenId,
|
||||
|
|
|
@ -9,6 +9,8 @@ pub struct LeftChannelBuffer {
|
|||
}
|
||||
|
||||
impl Database {
|
||||
/// Open a channel buffer. Returns the current contents, and adds you to the list of people
|
||||
/// to notify on changes.
|
||||
pub async fn join_channel_buffer(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -121,6 +123,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Rejoin a channel buffer (after a connection interruption)
|
||||
pub async fn rejoin_channel_buffers(
|
||||
&self,
|
||||
buffers: &[proto::ChannelBufferVersion],
|
||||
|
@ -232,6 +235,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Clear out any buffer collaborators who are no longer collaborating.
|
||||
pub async fn clear_stale_channel_buffer_collaborators(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -274,6 +278,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Close the channel buffer, and stop receiving updates for it.
|
||||
pub async fn leave_channel_buffer(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -286,6 +291,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Close the channel buffer, and stop receiving updates for it.
|
||||
pub async fn channel_buffer_connection_lost(
|
||||
&self,
|
||||
connection: ConnectionId,
|
||||
|
@ -309,6 +315,7 @@ impl Database {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Close all open channel buffers
|
||||
pub async fn leave_channel_buffers(
|
||||
&self,
|
||||
connection: ConnectionId,
|
||||
|
@ -342,7 +349,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn leave_channel_buffer_internal(
|
||||
async fn leave_channel_buffer_internal(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
connection: ConnectionId,
|
||||
|
@ -798,6 +805,7 @@ impl Database {
|
|||
Ok(changes)
|
||||
}
|
||||
|
||||
/// Returns the latest operations for the buffers with the specified IDs.
|
||||
pub async fn get_latest_operations_for_buffers(
|
||||
&self,
|
||||
buffer_ids: impl IntoIterator<Item = BufferId>,
|
||||
|
|
|
@ -40,6 +40,7 @@ impl Database {
|
|||
.id)
|
||||
}
|
||||
|
||||
/// Creates a new channel.
|
||||
pub async fn create_channel(
|
||||
&self,
|
||||
name: &str,
|
||||
|
@ -97,6 +98,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Adds a user to the specified channel.
|
||||
pub async fn join_channel(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -179,6 +181,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Sets the visibiltity of the given channel.
|
||||
pub async fn set_channel_visibility(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -258,6 +261,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Deletes the channel with the specified ID.
|
||||
pub async fn delete_channel(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -294,6 +298,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Invites a user to a channel as a member.
|
||||
pub async fn invite_channel_member(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -349,6 +354,7 @@ impl Database {
|
|||
Ok(new_name)
|
||||
}
|
||||
|
||||
/// Renames the specified channel.
|
||||
pub async fn rename_channel(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -387,6 +393,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// accept or decline an invite to join a channel
|
||||
pub async fn respond_to_channel_invite(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -486,6 +493,7 @@ impl Database {
|
|||
})
|
||||
}
|
||||
|
||||
/// Removes a channel member.
|
||||
pub async fn remove_channel_member(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -530,6 +538,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns all channel invites for the user with the given ID.
|
||||
pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut role_for_channel: HashMap<ChannelId, ChannelRole> = HashMap::default();
|
||||
|
@ -565,6 +574,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = tx;
|
||||
|
@ -574,6 +584,8 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID that are descendants
|
||||
/// of the specified ancestor channel.
|
||||
pub async fn get_user_channels(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
@ -743,6 +755,7 @@ impl Database {
|
|||
Ok(results)
|
||||
}
|
||||
|
||||
/// Sets the role for the specified channel member.
|
||||
pub async fn set_channel_member_role(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -786,6 +799,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns the details for the specified channel member.
|
||||
pub async fn get_channel_participant_details(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -911,6 +925,7 @@ impl Database {
|
|||
.collect())
|
||||
}
|
||||
|
||||
/// Returns the participants in the given channel.
|
||||
pub async fn get_channel_participants(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
|
@ -925,6 +940,7 @@ impl Database {
|
|||
.collect())
|
||||
}
|
||||
|
||||
/// Returns whether the given user is an admin in the specified channel.
|
||||
pub async fn check_user_is_channel_admin(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
|
@ -943,6 +959,7 @@ impl Database {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns whether the given user is a member of the specified channel.
|
||||
pub async fn check_user_is_channel_member(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
|
@ -958,6 +975,7 @@ impl Database {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns whether the given user is a participant in the specified channel.
|
||||
pub async fn check_user_is_channel_participant(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
|
@ -975,6 +993,7 @@ impl Database {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a user's pending invite for the given channel, if one exists.
|
||||
pub async fn pending_invite_for_channel(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
|
@ -991,7 +1010,7 @@ impl Database {
|
|||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn public_parent_channel(
|
||||
async fn public_parent_channel(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
|
@ -1003,7 +1022,7 @@ impl Database {
|
|||
Ok(path.pop())
|
||||
}
|
||||
|
||||
pub async fn public_ancestors_including_self(
|
||||
pub(crate) async fn public_ancestors_including_self(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
|
@ -1018,6 +1037,7 @@ impl Database {
|
|||
Ok(visible_channels)
|
||||
}
|
||||
|
||||
/// Returns the role for a user in the given channel.
|
||||
pub async fn channel_role_for_user(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
|
@ -1143,7 +1163,7 @@ impl Database {
|
|||
.await?)
|
||||
}
|
||||
|
||||
/// Returns the channel with the given ID
|
||||
/// Returns the channel with the given ID.
|
||||
pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
|
@ -1156,7 +1176,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn get_channel_internal(
|
||||
pub(crate) async fn get_channel_internal(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
tx: &DatabaseTransaction,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::*;
|
||||
|
||||
impl Database {
|
||||
/// Retrieves the contacts for the user with the given ID.
|
||||
pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct ContactWithUserBusyStatuses {
|
||||
|
@ -86,6 +87,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns whether the given user is a busy (on a call).
|
||||
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
|
||||
self.transaction(|tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
|
@ -97,6 +99,9 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns whether the user with `user_id_1` has the user with `user_id_2` as a contact.
|
||||
///
|
||||
/// In order for this to return `true`, `user_id_2` must have an accepted invite from `user_id_1`.
|
||||
pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
|
||||
self.transaction(|tx| async move {
|
||||
let (id_a, id_b) = if user_id_1 < user_id_2 {
|
||||
|
@ -119,6 +124,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Invite the user with `receiver_id` to be a contact of the user with `sender_id`.
|
||||
pub async fn send_contact_request(
|
||||
&self,
|
||||
sender_id: UserId,
|
||||
|
@ -231,6 +237,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Dismisses a contact notification for the given user.
|
||||
pub async fn dismiss_contact_notification(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
@ -272,6 +279,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Accept or decline a contact request
|
||||
pub async fn respond_to_contact_request(
|
||||
&self,
|
||||
responder_id: UserId,
|
||||
|
|
|
@ -4,6 +4,7 @@ use sea_orm::TryInsertResult;
|
|||
use time::OffsetDateTime;
|
||||
|
||||
impl Database {
|
||||
/// Inserts a record representing a user joining the chat for a given channel.
|
||||
pub async fn join_channel_chat(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -28,6 +29,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Removes `channel_chat_participant` records associated with the given connection ID.
|
||||
pub async fn channel_chat_connection_lost(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
|
@ -47,6 +49,8 @@ impl Database {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes `channel_chat_participant` records associated with the given user ID so they
|
||||
/// will no longer get chat notifications.
|
||||
pub async fn leave_channel_chat(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -72,6 +76,9 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Retrieves the messages in the specified channel.
|
||||
///
|
||||
/// Use `before_message_id` to paginate through the channel's messages.
|
||||
pub async fn get_channel_messages(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -103,6 +110,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns the channel messages with the given IDs.
|
||||
pub async fn get_channel_messages_by_id(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
@ -190,6 +198,7 @@ impl Database {
|
|||
Ok(messages)
|
||||
}
|
||||
|
||||
/// Creates a new channel message.
|
||||
pub async fn create_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -376,6 +385,7 @@ impl Database {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the unseen messages for the given user in the specified channels.
|
||||
pub async fn unseen_channel_messages(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
@ -449,6 +459,7 @@ impl Database {
|
|||
Ok(changes)
|
||||
}
|
||||
|
||||
/// Removes the channel message with the given ID.
|
||||
pub async fn remove_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
|
|
@ -2,6 +2,7 @@ use super::*;
|
|||
use rpc::Notification;
|
||||
|
||||
impl Database {
|
||||
/// Initializes the different kinds of notifications by upserting records for them.
|
||||
pub async fn initialize_notification_kinds(&mut self) -> Result<()> {
|
||||
notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map(
|
||||
|kind| notification_kind::ActiveModel {
|
||||
|
@ -28,6 +29,7 @@ impl Database {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the notifications for the given recipient.
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
|
@ -140,6 +142,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Marks the given notification as read.
|
||||
pub async fn mark_notification_as_read(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
|
@ -150,6 +153,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Marks the notification with the given ID as read.
|
||||
pub async fn mark_notification_as_read_by_id(
|
||||
&self,
|
||||
recipient_id: UserId,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::*;
|
||||
|
||||
impl Database {
|
||||
/// Returns the count of all projects, excluding ones marked as admin.
|
||||
pub async fn project_count_excluding_admins(&self) -> Result<usize> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryAs {
|
||||
|
@ -21,6 +22,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Shares a project with the given room.
|
||||
pub async fn share_project(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
|
@ -100,6 +102,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Unshares the given project.
|
||||
pub async fn unshare_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
@ -126,6 +129,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Updates the worktrees associated with the given project.
|
||||
pub async fn update_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
@ -346,6 +350,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Updates the diagnostic summary for the given connection.
|
||||
pub async fn update_diagnostic_summary(
|
||||
&self,
|
||||
update: &proto::UpdateDiagnosticSummary,
|
||||
|
@ -401,6 +406,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Starts the language server for the given connection.
|
||||
pub async fn start_language_server(
|
||||
&self,
|
||||
update: &proto::StartLanguageServer,
|
||||
|
@ -447,6 +453,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Updates the worktree settings for the given connection.
|
||||
pub async fn update_worktree_settings(
|
||||
&self,
|
||||
update: &proto::UpdateWorktreeSettings,
|
||||
|
@ -499,6 +506,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Adds the given connection to the specified project.
|
||||
pub async fn join_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
@ -704,6 +712,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Removes the given connection from the specified project.
|
||||
pub async fn leave_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
@ -805,6 +814,7 @@ impl Database {
|
|||
.map(|guard| guard.into_inner())
|
||||
}
|
||||
|
||||
/// Returns the host connection for a read-only request to join a shared project.
|
||||
pub async fn host_for_read_only_project_request(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
@ -842,6 +852,7 @@ impl Database {
|
|||
.map(|guard| guard.into_inner())
|
||||
}
|
||||
|
||||
/// Returns the host connection for a request to join a shared project.
|
||||
pub async fn host_for_mutating_project_request(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
@ -927,6 +938,10 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns the connection IDs in the given project.
|
||||
///
|
||||
/// The provided `connection_id` must also be a collaborator in the project,
|
||||
/// otherwise an error will be returned.
|
||||
pub async fn project_connection_ids(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
@ -976,6 +991,7 @@ impl Database {
|
|||
Ok(guest_connection_ids)
|
||||
}
|
||||
|
||||
/// Returns the [`RoomId`] for the given project.
|
||||
pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
|
||||
self.transaction(|tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
|
@ -1020,6 +1036,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Adds the given follower connection as a follower of the given leader connection.
|
||||
pub async fn follow(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
|
@ -1050,6 +1067,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Removes the given follower connection as a follower of the given leader connection.
|
||||
pub async fn unfollow(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::*;
|
||||
|
||||
impl Database {
|
||||
/// Clears all room participants in rooms attached to a stale server.
|
||||
pub async fn clear_stale_room_participants(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
|
@ -78,6 +79,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns the incoming calls for user with the given ID.
|
||||
pub async fn incoming_call_for_user(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
@ -102,6 +104,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Creates a new room.
|
||||
pub async fn create_room(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
|
@ -394,6 +397,7 @@ impl Database {
|
|||
Ok(participant_index)
|
||||
}
|
||||
|
||||
/// Returns the channel ID for the given room, if it has one.
|
||||
pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result<Option<ChannelId>> {
|
||||
self.transaction(|tx| async move {
|
||||
let room: Option<room::Model> = room::Entity::find()
|
||||
|
@ -944,6 +948,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Updates the location of a participant in the given room.
|
||||
pub async fn update_room_participant_location(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
|
@ -1004,6 +1009,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Sets the role of a participant in the given room.
|
||||
pub async fn set_room_participant_role(
|
||||
&self,
|
||||
admin_id: UserId,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::*;
|
||||
|
||||
impl Database {
|
||||
/// Creates a new server in the given environment.
|
||||
pub async fn create_server(&self, environment: &str) -> Result<ServerId> {
|
||||
self.transaction(|tx| async move {
|
||||
let server = server::ActiveModel {
|
||||
|
@ -14,6 +15,10 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns the IDs of resources associated with stale servers.
|
||||
///
|
||||
/// A server is stale if it is in the specified `environment` and does not
|
||||
/// match the provided `new_server_id`.
|
||||
pub async fn stale_server_resource_ids(
|
||||
&self,
|
||||
environment: &str,
|
||||
|
@ -61,6 +66,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Deletes any stale servers in the environment that don't match the `new_server_id`.
|
||||
pub async fn delete_stale_servers(
|
||||
&self,
|
||||
environment: &str,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::*;
|
||||
|
||||
impl Database {
|
||||
/// Creates a new user.
|
||||
pub async fn create_user(
|
||||
&self,
|
||||
email_address: &str,
|
||||
|
@ -35,11 +36,13 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns a user by ID. There are no access checks here, so this should only be used internally.
|
||||
pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
|
||||
self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all users by ID. There are no access checks here, so this should only be used internally.
|
||||
pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
|
||||
self.transaction(|tx| async {
|
||||
let tx = tx;
|
||||
|
@ -51,6 +54,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
|
||||
pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(user::Entity::find()
|
||||
|
@ -111,6 +115,8 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// get_all_users returns the next page of users. To get more call again with
|
||||
/// the same limit and the page incremented by 1.
|
||||
pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(user::Entity::find()
|
||||
|
@ -123,6 +129,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns the metrics id for the user.
|
||||
pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryAs {
|
||||
|
@ -142,6 +149,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Set "connected_once" on the user for analytics.
|
||||
pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
user::Entity::update_many()
|
||||
|
@ -157,6 +165,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// hard delete the user.
|
||||
pub async fn destroy_user(&self, id: UserId) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
access_token::Entity::delete_many()
|
||||
|
@ -169,6 +178,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Find users where github_login ILIKE name_query.
|
||||
pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
|
||||
self.transaction(|tx| async {
|
||||
let tx = tx;
|
||||
|
@ -193,6 +203,8 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
|
||||
/// e.g. "cir" would become "%c%i%r%"
|
||||
pub fn fuzzy_like_string(string: &str) -> String {
|
||||
let mut result = String::with_capacity(string.len() * 2 + 1);
|
||||
for c in string.chars() {
|
||||
|
@ -205,6 +217,7 @@ impl Database {
|
|||
result
|
||||
}
|
||||
|
||||
/// Creates a new feature flag.
|
||||
pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
|
||||
self.transaction(|tx| async move {
|
||||
let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
|
||||
|
@ -220,6 +233,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Add the given user to the feature flag
|
||||
pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
user_feature::Entity::insert(user_feature::ActiveModel {
|
||||
|
@ -234,6 +248,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Return the active flags for the user.
|
||||
pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
|
||||
self.transaction(|tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::db::UserId;
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
/// A user model.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
|
|
|
@ -932,11 +932,13 @@ async fn connection_lost(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Acknowledges a ping from a client, used to keep the connection alive.
|
||||
async fn ping(_: proto::Ping, response: Response<proto::Ping>, _session: Session) -> Result<()> {
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new room for calling (outside of channels)
|
||||
async fn create_room(
|
||||
_request: proto::CreateRoom,
|
||||
response: Response<proto::CreateRoom>,
|
||||
|
@ -984,6 +986,7 @@ async fn create_room(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Join a room from an invitation. Equivalent to joining a channel if there is one.
|
||||
async fn join_room(
|
||||
request: proto::JoinRoom,
|
||||
response: Response<proto::JoinRoom>,
|
||||
|
@ -1058,6 +1061,7 @@ async fn join_room(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Rejoin room is used to reconnect to a room after connection errors.
|
||||
async fn rejoin_room(
|
||||
request: proto::RejoinRoom,
|
||||
response: Response<proto::RejoinRoom>,
|
||||
|
@ -1249,6 +1253,7 @@ async fn rejoin_room(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// leave room disonnects from the room.
|
||||
async fn leave_room(
|
||||
_: proto::LeaveRoom,
|
||||
response: Response<proto::LeaveRoom>,
|
||||
|
@ -1259,6 +1264,7 @@ async fn leave_room(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the permissions of someone else in the room.
|
||||
async fn set_room_participant_role(
|
||||
request: proto::SetRoomParticipantRole,
|
||||
response: Response<proto::SetRoomParticipantRole>,
|
||||
|
@ -1303,6 +1309,7 @@ async fn set_room_participant_role(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Call someone else into the current room
|
||||
async fn call(
|
||||
request: proto::Call,
|
||||
response: Response<proto::Call>,
|
||||
|
@ -1371,6 +1378,7 @@ async fn call(
|
|||
Err(anyhow!("failed to ring user"))?
|
||||
}
|
||||
|
||||
/// Cancel an outgoing call.
|
||||
async fn cancel_call(
|
||||
request: proto::CancelCall,
|
||||
response: Response<proto::CancelCall>,
|
||||
|
@ -1408,6 +1416,7 @@ async fn cancel_call(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Decline an incoming call.
|
||||
async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<()> {
|
||||
let room_id = RoomId::from_proto(message.room_id);
|
||||
{
|
||||
|
@ -1439,6 +1448,7 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update other participants in the room with your current location.
|
||||
async fn update_participant_location(
|
||||
request: proto::UpdateParticipantLocation,
|
||||
response: Response<proto::UpdateParticipantLocation>,
|
||||
|
@ -1459,6 +1469,7 @@ async fn update_participant_location(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Share a project into the room.
|
||||
async fn share_project(
|
||||
request: proto::ShareProject,
|
||||
response: Response<proto::ShareProject>,
|
||||
|
@ -1481,6 +1492,7 @@ async fn share_project(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Unshare a project from the room.
|
||||
async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(message.project_id);
|
||||
|
||||
|
@ -1500,6 +1512,7 @@ async fn unshare_project(message: proto::UnshareProject, session: Session) -> Re
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Join someone elses shared project.
|
||||
async fn join_project(
|
||||
request: proto::JoinProject,
|
||||
response: Response<proto::JoinProject>,
|
||||
|
@ -1625,6 +1638,7 @@ async fn join_project(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Leave someone elses shared project.
|
||||
async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> {
|
||||
let sender_id = session.connection_id;
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
|
@ -1647,6 +1661,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update other participants with changes to the project
|
||||
async fn update_project(
|
||||
request: proto::UpdateProject,
|
||||
response: Response<proto::UpdateProject>,
|
||||
|
@ -1673,6 +1688,7 @@ async fn update_project(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update other participants with changes to the worktree
|
||||
async fn update_worktree(
|
||||
request: proto::UpdateWorktree,
|
||||
response: Response<proto::UpdateWorktree>,
|
||||
|
@ -1697,6 +1713,7 @@ async fn update_worktree(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update other participants with changes to the diagnostics
|
||||
async fn update_diagnostic_summary(
|
||||
message: proto::UpdateDiagnosticSummary,
|
||||
session: Session,
|
||||
|
@ -1720,6 +1737,7 @@ async fn update_diagnostic_summary(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Update other participants with changes to the worktree settings
|
||||
async fn update_worktree_settings(
|
||||
message: proto::UpdateWorktreeSettings,
|
||||
session: Session,
|
||||
|
@ -1743,6 +1761,7 @@ async fn update_worktree_settings(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that a language server has started.
|
||||
async fn start_language_server(
|
||||
request: proto::StartLanguageServer,
|
||||
session: Session,
|
||||
|
@ -1765,6 +1784,7 @@ async fn start_language_server(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that a language server has changed.
|
||||
async fn update_language_server(
|
||||
request: proto::UpdateLanguageServer,
|
||||
session: Session,
|
||||
|
@ -1787,6 +1807,8 @@ async fn update_language_server(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// forward a project request to the host. These requests should be read only
|
||||
/// as guests are allowed to send them.
|
||||
async fn forward_read_only_project_request<T>(
|
||||
request: T,
|
||||
response: Response<T>,
|
||||
|
@ -1809,6 +1831,8 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// forward a project request to the host. These requests are disallowed
|
||||
/// for guests.
|
||||
async fn forward_mutating_project_request<T>(
|
||||
request: T,
|
||||
response: Response<T>,
|
||||
|
@ -1831,6 +1855,7 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that a new buffer has been created
|
||||
async fn create_buffer_for_peer(
|
||||
request: proto::CreateBufferForPeer,
|
||||
session: Session,
|
||||
|
@ -1850,6 +1875,8 @@ async fn create_buffer_for_peer(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that a buffer has been updated. This is
|
||||
/// allowed for guests as long as the update is limited to selections.
|
||||
async fn update_buffer(
|
||||
request: proto::UpdateBuffer,
|
||||
response: Response<proto::UpdateBuffer>,
|
||||
|
@ -1909,6 +1936,7 @@ async fn update_buffer(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that a project has been updated.
|
||||
async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
|
||||
request: T,
|
||||
session: Session,
|
||||
|
@ -1932,6 +1960,7 @@ async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProj
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Start following another user in a call.
|
||||
async fn follow(
|
||||
request: proto::Follow,
|
||||
response: Response<proto::Follow>,
|
||||
|
@ -1969,6 +1998,7 @@ async fn follow(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop following another user in a call.
|
||||
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
|
||||
let room_id = RoomId::from_proto(request.room_id);
|
||||
let project_id = request.project_id.map(ProjectId::from_proto);
|
||||
|
@ -2000,6 +2030,7 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify everyone following you of your current location.
|
||||
async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
|
||||
let room_id = RoomId::from_proto(request.room_id);
|
||||
let database = session.db.lock().await;
|
||||
|
@ -2036,6 +2067,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Get public data about users.
|
||||
async fn get_users(
|
||||
request: proto::GetUsers,
|
||||
response: Response<proto::GetUsers>,
|
||||
|
@ -2062,6 +2094,7 @@ async fn get_users(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Search for users (to invite) buy Github login
|
||||
async fn fuzzy_search_users(
|
||||
request: proto::FuzzySearchUsers,
|
||||
response: Response<proto::FuzzySearchUsers>,
|
||||
|
@ -2092,6 +2125,7 @@ async fn fuzzy_search_users(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a contact request to another user.
|
||||
async fn request_contact(
|
||||
request: proto::RequestContact,
|
||||
response: Response<proto::RequestContact>,
|
||||
|
@ -2138,6 +2172,7 @@ async fn request_contact(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Accept or decline a contact request
|
||||
async fn respond_to_contact_request(
|
||||
request: proto::RespondToContactRequest,
|
||||
response: Response<proto::RespondToContactRequest>,
|
||||
|
@ -2195,6 +2230,7 @@ async fn respond_to_contact_request(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a contact.
|
||||
async fn remove_contact(
|
||||
request: proto::RemoveContact,
|
||||
response: Response<proto::RemoveContact>,
|
||||
|
@ -2245,6 +2281,7 @@ async fn remove_contact(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new channel.
|
||||
async fn create_channel(
|
||||
request: proto::CreateChannel,
|
||||
response: Response<proto::CreateChannel>,
|
||||
|
@ -2279,6 +2316,7 @@ async fn create_channel(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a channel
|
||||
async fn delete_channel(
|
||||
request: proto::DeleteChannel,
|
||||
response: Response<proto::DeleteChannel>,
|
||||
|
@ -2308,6 +2346,7 @@ async fn delete_channel(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Invite someone to join a channel.
|
||||
async fn invite_channel_member(
|
||||
request: proto::InviteChannelMember,
|
||||
response: Response<proto::InviteChannelMember>,
|
||||
|
@ -2344,6 +2383,7 @@ async fn invite_channel_member(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// remove someone from a channel
|
||||
async fn remove_channel_member(
|
||||
request: proto::RemoveChannelMember,
|
||||
response: Response<proto::RemoveChannelMember>,
|
||||
|
@ -2385,6 +2425,7 @@ async fn remove_channel_member(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Toggle the channel between public and private
|
||||
async fn set_channel_visibility(
|
||||
request: proto::SetChannelVisibility,
|
||||
response: Response<proto::SetChannelVisibility>,
|
||||
|
@ -2423,6 +2464,7 @@ async fn set_channel_visibility(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Alter the role for a user in the channel
|
||||
async fn set_channel_member_role(
|
||||
request: proto::SetChannelMemberRole,
|
||||
response: Response<proto::SetChannelMemberRole>,
|
||||
|
@ -2470,6 +2512,7 @@ async fn set_channel_member_role(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Change the name of a channel
|
||||
async fn rename_channel(
|
||||
request: proto::RenameChannel,
|
||||
response: Response<proto::RenameChannel>,
|
||||
|
@ -2503,6 +2546,7 @@ async fn rename_channel(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Move a channel to a new parent.
|
||||
async fn move_channel(
|
||||
request: proto::MoveChannel,
|
||||
response: Response<proto::MoveChannel>,
|
||||
|
@ -2555,6 +2599,7 @@ async fn notify_channel_moved(result: Option<MoveChannelResult>, session: Sessio
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the list of channel members
|
||||
async fn get_channel_members(
|
||||
request: proto::GetChannelMembers,
|
||||
response: Response<proto::GetChannelMembers>,
|
||||
|
@ -2569,6 +2614,7 @@ async fn get_channel_members(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Accept or decline a channel invitation.
|
||||
async fn respond_to_channel_invite(
|
||||
request: proto::RespondToChannelInvite,
|
||||
response: Response<proto::RespondToChannelInvite>,
|
||||
|
@ -2609,6 +2655,7 @@ async fn respond_to_channel_invite(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Join the channels' room
|
||||
async fn join_channel(
|
||||
request: proto::JoinChannel,
|
||||
response: Response<proto::JoinChannel>,
|
||||
|
@ -2713,6 +2760,7 @@ async fn join_channel_internal(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Start editing the channel notes
|
||||
async fn join_channel_buffer(
|
||||
request: proto::JoinChannelBuffer,
|
||||
response: Response<proto::JoinChannelBuffer>,
|
||||
|
@ -2744,6 +2792,7 @@ async fn join_channel_buffer(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Edit the channel notes
|
||||
async fn update_channel_buffer(
|
||||
request: proto::UpdateChannelBuffer,
|
||||
session: Session,
|
||||
|
@ -2790,6 +2839,7 @@ async fn update_channel_buffer(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Rejoin the channel notes after a connection blip
|
||||
async fn rejoin_channel_buffers(
|
||||
request: proto::RejoinChannelBuffers,
|
||||
response: Response<proto::RejoinChannelBuffers>,
|
||||
|
@ -2824,6 +2874,7 @@ async fn rejoin_channel_buffers(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop editing the channel notes
|
||||
async fn leave_channel_buffer(
|
||||
request: proto::LeaveChannelBuffer,
|
||||
response: Response<proto::LeaveChannelBuffer>,
|
||||
|
@ -2885,6 +2936,7 @@ fn send_notifications(
|
|||
}
|
||||
}
|
||||
|
||||
/// Send a message to the channel
|
||||
async fn send_channel_message(
|
||||
request: proto::SendChannelMessage,
|
||||
response: Response<proto::SendChannelMessage>,
|
||||
|
@ -2973,6 +3025,7 @@ async fn send_channel_message(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a channel message
|
||||
async fn remove_channel_message(
|
||||
request: proto::RemoveChannelMessage,
|
||||
response: Response<proto::RemoveChannelMessage>,
|
||||
|
@ -2992,6 +3045,7 @@ async fn remove_channel_message(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a channel message as read
|
||||
async fn acknowledge_channel_message(
|
||||
request: proto::AckChannelMessage,
|
||||
session: Session,
|
||||
|
@ -3011,6 +3065,7 @@ async fn acknowledge_channel_message(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a buffer version as synced
|
||||
async fn acknowledge_buffer_version(
|
||||
request: proto::AckBufferOperation,
|
||||
session: Session,
|
||||
|
@ -3029,6 +3084,7 @@ async fn acknowledge_buffer_version(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Start receiving chat updates for a channel
|
||||
async fn join_channel_chat(
|
||||
request: proto::JoinChannelChat,
|
||||
response: Response<proto::JoinChannelChat>,
|
||||
|
@ -3049,6 +3105,7 @@ async fn join_channel_chat(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop receiving chat updates for a channel
|
||||
async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
session
|
||||
|
@ -3059,6 +3116,7 @@ async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session)
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrive the chat history for a channel
|
||||
async fn get_channel_messages(
|
||||
request: proto::GetChannelMessages,
|
||||
response: Response<proto::GetChannelMessages>,
|
||||
|
@ -3082,6 +3140,7 @@ async fn get_channel_messages(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve specific chat messages
|
||||
async fn get_channel_messages_by_id(
|
||||
request: proto::GetChannelMessagesById,
|
||||
response: Response<proto::GetChannelMessagesById>,
|
||||
|
@ -3104,6 +3163,7 @@ async fn get_channel_messages_by_id(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve the current users notifications
|
||||
async fn get_notifications(
|
||||
request: proto::GetNotifications,
|
||||
response: Response<proto::GetNotifications>,
|
||||
|
@ -3127,6 +3187,7 @@ async fn get_notifications(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark notifications as read
|
||||
async fn mark_notification_as_read(
|
||||
request: proto::MarkNotificationRead,
|
||||
response: Response<proto::MarkNotificationRead>,
|
||||
|
@ -3148,6 +3209,7 @@ async fn mark_notification_as_read(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current users information
|
||||
async fn get_private_user_info(
|
||||
_request: proto::GetPrivateUserInfo,
|
||||
response: Response<proto::GetPrivateUserInfo>,
|
||||
|
|
|
@ -185,31 +185,27 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
|||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let window_a = cx_a.add_empty_window();
|
||||
let editor_a =
|
||||
window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
|
||||
|
||||
let mut editor_cx_a = EditorTestContext {
|
||||
cx: VisualTestContext::from_window(window_a, cx_a),
|
||||
window: window_a.into(),
|
||||
cx: cx_a.clone(),
|
||||
window: cx_a.handle(),
|
||||
editor: editor_a,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
|
||||
let window_b = cx_b.add_empty_window();
|
||||
let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
|
||||
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
// Open a buffer as client B
|
||||
let buffer_b = project_b
|
||||
.update(&mut cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let editor_b = window_b.build_view(&mut cx_b, |cx| {
|
||||
Editor::for_buffer(buffer_b, Some(project_b), cx)
|
||||
});
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
|
||||
|
||||
let mut editor_cx_b = EditorTestContext {
|
||||
cx: cx_b,
|
||||
window: window_b.into(),
|
||||
cx: cx_b.clone(),
|
||||
window: cx_b.handle(),
|
||||
editor: editor_b,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
|
@ -311,10 +307,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
|||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let window_b = cx_b.add_empty_window();
|
||||
let editor_b = window_b.build_view(cx_b, |cx| {
|
||||
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
|
||||
});
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
cx_a.background_executor.run_until_parked();
|
||||
|
@ -323,10 +318,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
|||
assert!(!buffer.completion_triggers().is_empty())
|
||||
});
|
||||
|
||||
let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
|
||||
|
||||
// Type a completion trigger character as the guest.
|
||||
editor_b.update(&mut cx_b, |editor, cx| {
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(".", cx);
|
||||
});
|
||||
|
@ -392,8 +385,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
|||
});
|
||||
|
||||
// Confirm a completion on the guest.
|
||||
|
||||
editor_b.update(&mut cx_b, |editor, cx| {
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
|
||||
|
@ -431,7 +423,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
|||
);
|
||||
});
|
||||
|
||||
buffer_b.read_with(&mut cx_b, |buffer, _| {
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method() }"
|
||||
|
@ -960,7 +952,7 @@ async fn test_share_project(
|
|||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
let executor = cx_a.executor();
|
||||
let window_b = cx_b.add_empty_window();
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
@ -1075,7 +1067,7 @@ async fn test_share_project(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
|
||||
|
||||
// Client A sees client B's selection
|
||||
executor.run_until_parked();
|
||||
|
@ -1089,8 +1081,7 @@ async fn test_share_project(
|
|||
});
|
||||
|
||||
// Edit the buffer as client B and see that edit as client A.
|
||||
let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
|
||||
editor_b.update(&mut cx_b, |editor, cx| editor.handle_input("ok, ", cx));
|
||||
editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
|
||||
executor.run_until_parked();
|
||||
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
|
@ -1099,7 +1090,7 @@ async fn test_share_project(
|
|||
|
||||
// Client B can invite client C on a project shared by client A.
|
||||
active_call_b
|
||||
.update(&mut cx_b, |call, cx| {
|
||||
.update(cx_b, |call, cx| {
|
||||
call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
|
||||
})
|
||||
.await
|
||||
|
@ -1190,12 +1181,8 @@ async fn test_on_input_format_from_host_to_guest(
|
|||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let window_a = cx_a.add_empty_window();
|
||||
let editor_a = window_a
|
||||
.update(cx_a, |_, cx| {
|
||||
cx.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx))
|
||||
})
|
||||
.unwrap();
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
@ -1226,10 +1213,9 @@ async fn test_on_input_format_from_host_to_guest(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut cx_a = VisualTestContext::from_window(window_a, cx_a);
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
cx_a.focus_view(&editor_a);
|
||||
editor_a.update(&mut cx_a, |editor, cx| {
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(">", cx);
|
||||
});
|
||||
|
@ -1241,7 +1227,7 @@ async fn test_on_input_format_from_host_to_guest(
|
|||
});
|
||||
|
||||
// Undo should remove LSP edits first
|
||||
editor_a.update(&mut cx_a, |editor, cx| {
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a>~< }");
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a> }");
|
||||
|
@ -1252,7 +1238,7 @@ async fn test_on_input_format_from_host_to_guest(
|
|||
assert_eq!(buffer.text(), "fn main() { a> }")
|
||||
});
|
||||
|
||||
editor_a.update(&mut cx_a, |editor, cx| {
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a> }");
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a }");
|
||||
|
@ -1323,17 +1309,15 @@ async fn test_on_input_format_from_guest_to_host(
|
|||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let window_b = cx_b.add_empty_window();
|
||||
let editor_b = window_b.build_view(cx_b, |cx| {
|
||||
Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
|
||||
});
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
|
||||
|
||||
// Type a on type formatting trigger character as the guest.
|
||||
cx_b.focus_view(&editor_b);
|
||||
editor_b.update(&mut cx_b, |editor, cx| {
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input(":", cx);
|
||||
});
|
||||
|
@ -1374,7 +1358,7 @@ async fn test_on_input_format_from_guest_to_host(
|
|||
});
|
||||
|
||||
// Undo should remove LSP edits first
|
||||
editor_b.update(&mut cx_b, |editor, cx| {
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a:~: }");
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a: }");
|
||||
|
@ -1385,7 +1369,7 @@ async fn test_on_input_format_from_guest_to_host(
|
|||
assert_eq!(buffer.text(), "fn main() { a: }")
|
||||
});
|
||||
|
||||
editor_b.update(&mut cx_b, |editor, cx| {
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "fn main() { a: }");
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a }");
|
||||
|
|
|
@ -249,7 +249,7 @@ async fn test_basic_following(
|
|||
executor.run_until_parked();
|
||||
cx_c.cx.update(|_| {});
|
||||
|
||||
weak_workspace_c.assert_dropped();
|
||||
weak_workspace_c.assert_released();
|
||||
|
||||
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||
executor.run_until_parked();
|
||||
|
|
|
@ -125,6 +125,23 @@ impl ChatPanel {
|
|||
open_context_menu: None,
|
||||
};
|
||||
|
||||
if let Some(channel_id) = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.and_then(|room| room.read(cx).channel_id())
|
||||
{
|
||||
this.select_channel(channel_id, None, cx)
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.is_some_and(|room| room.read(cx).contains_guests())
|
||||
{
|
||||
cx.emit(PanelEvent::Activate)
|
||||
}
|
||||
}
|
||||
|
||||
this.subscriptions.push(cx.subscribe(
|
||||
&ActiveCall::global(cx),
|
||||
move |this: &mut Self, call, event: &room::Event, cx| match event {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![deny(missing_docs)]
|
||||
|
||||
mod async_context;
|
||||
mod entity_map;
|
||||
mod model_context;
|
||||
|
@ -43,6 +45,9 @@ use util::{
|
|||
ResultExt,
|
||||
};
|
||||
|
||||
/// The duration for which futures returned from [AppContext::on_app_context] or [ModelContext::on_app_quit] can run before the application fully quits.
|
||||
pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Temporary(?) wrapper around [`RefCell<AppContext>`] to help us debug any double borrows.
|
||||
/// Strongly consider removing after stabilization.
|
||||
#[doc(hidden)]
|
||||
|
@ -187,6 +192,9 @@ type QuitHandler = Box<dyn FnOnce(&mut AppContext) -> LocalBoxFuture<'static, ()
|
|||
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
|
||||
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
|
||||
|
||||
/// Contains the state of the full application, and passed as a reference to a variety of callbacks.
|
||||
/// Other contexts such as [ModelContext], [WindowContext], and [ViewContext] deref to this type, making it the most general context type.
|
||||
/// You need a reference to an `AppContext` to access the state of a [Model].
|
||||
pub struct AppContext {
|
||||
pub(crate) this: Weak<AppCell>,
|
||||
pub(crate) platform: Rc<dyn Platform>,
|
||||
|
@ -312,7 +320,7 @@ impl AppContext {
|
|||
let futures = futures::future::join_all(futures);
|
||||
if self
|
||||
.background_executor
|
||||
.block_with_timeout(Duration::from_millis(100), futures)
|
||||
.block_with_timeout(SHUTDOWN_TIMEOUT, futures)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("timed out waiting on app_will_quit");
|
||||
|
@ -446,6 +454,7 @@ impl AppContext {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a handle to the window that is currently focused at the platform level, if one exists.
|
||||
pub fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
self.platform.active_window()
|
||||
}
|
||||
|
@ -474,14 +483,17 @@ impl AppContext {
|
|||
self.platform.activate(ignoring_other_apps);
|
||||
}
|
||||
|
||||
/// Hide the application at the platform level.
|
||||
pub fn hide(&self) {
|
||||
self.platform.hide();
|
||||
}
|
||||
|
||||
/// Hide other applications at the platform level.
|
||||
pub fn hide_other_apps(&self) {
|
||||
self.platform.hide_other_apps();
|
||||
}
|
||||
|
||||
/// Unhide other applications at the platform level.
|
||||
pub fn unhide_other_apps(&self) {
|
||||
self.platform.unhide_other_apps();
|
||||
}
|
||||
|
@ -521,18 +533,25 @@ impl AppContext {
|
|||
self.platform.open_url(url);
|
||||
}
|
||||
|
||||
/// Returns the full pathname of the current app bundle.
|
||||
/// If the app is not being run from a bundle, returns an error.
|
||||
pub fn app_path(&self) -> Result<PathBuf> {
|
||||
self.platform.app_path()
|
||||
}
|
||||
|
||||
/// Returns the file URL of the executable with the specified name in the application bundle
|
||||
pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
self.platform.path_for_auxiliary_executable(name)
|
||||
}
|
||||
|
||||
/// Returns the maximum duration in which a second mouse click must occur for an event to be a double-click event.
|
||||
pub fn double_click_interval(&self) -> Duration {
|
||||
self.platform.double_click_interval()
|
||||
}
|
||||
|
||||
/// Displays a platform modal for selecting paths.
|
||||
/// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel.
|
||||
/// If cancelled, a `None` will be relayed instead.
|
||||
pub fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
|
@ -540,22 +559,30 @@ impl AppContext {
|
|||
self.platform.prompt_for_paths(options)
|
||||
}
|
||||
|
||||
/// Displays a platform modal for selecting a new path where a file can be saved.
|
||||
/// The provided directory will be used to set the iniital location.
|
||||
/// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
|
||||
/// If cancelled, a `None` will be relayed instead.
|
||||
pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
self.platform.prompt_for_new_path(directory)
|
||||
}
|
||||
|
||||
/// Reveals the specified path at the platform level, such as in Finder on macOS.
|
||||
pub fn reveal_path(&self, path: &Path) {
|
||||
self.platform.reveal_path(path)
|
||||
}
|
||||
|
||||
/// Returns whether the user has configured scrollbars to auto-hide at the platform level.
|
||||
pub fn should_auto_hide_scrollbars(&self) -> bool {
|
||||
self.platform.should_auto_hide_scrollbars()
|
||||
}
|
||||
|
||||
/// Restart the application.
|
||||
pub fn restart(&self) {
|
||||
self.platform.restart()
|
||||
}
|
||||
|
||||
/// Returns the local timezone at the platform level.
|
||||
pub fn local_timezone(&self) -> UtcOffset {
|
||||
self.platform.local_timezone()
|
||||
}
|
||||
|
@ -745,7 +772,7 @@ impl AppContext {
|
|||
}
|
||||
|
||||
/// Spawns the future returned by the given function on the thread pool. The closure will be invoked
|
||||
/// with AsyncAppContext, which allows the application state to be accessed across await points.
|
||||
/// with [AsyncAppContext], which allows the application state to be accessed across await points.
|
||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
|
||||
where
|
||||
Fut: Future<Output = R> + 'static,
|
||||
|
@ -896,6 +923,8 @@ impl AppContext {
|
|||
self.globals_by_type.insert(global_type, lease.global);
|
||||
}
|
||||
|
||||
/// Arrange for the given function to be invoked whenever a view of the specified type is created.
|
||||
/// The function will be passed a mutable reference to the view along with an appropriate context.
|
||||
pub fn observe_new_views<V: 'static>(
|
||||
&mut self,
|
||||
on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>),
|
||||
|
@ -915,6 +944,8 @@ impl AppContext {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Observe the release of a model or view. The callback is invoked after the model or view
|
||||
/// has no more strong references but before it has been dropped.
|
||||
pub fn observe_release<E, T>(
|
||||
&mut self,
|
||||
handle: &E,
|
||||
|
@ -935,6 +966,9 @@ impl AppContext {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when a keystroke is received by the application
|
||||
/// in any window. Note that this fires after all other action and event mechansims have resolved
|
||||
/// and that this API will not be invoked if the event's propogation is stopped.
|
||||
pub fn observe_keystrokes(
|
||||
&mut self,
|
||||
f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static,
|
||||
|
@ -958,6 +992,7 @@ impl AppContext {
|
|||
self.pending_effects.push_back(Effect::Refresh);
|
||||
}
|
||||
|
||||
/// Clear all key bindings in the app.
|
||||
pub fn clear_key_bindings(&mut self) {
|
||||
self.keymap.lock().clear();
|
||||
self.pending_effects.push_back(Effect::Refresh);
|
||||
|
@ -992,6 +1027,7 @@ impl AppContext {
|
|||
self.propagate_event = true;
|
||||
}
|
||||
|
||||
/// Build an action from some arbitrary data, typically a keymap entry.
|
||||
pub fn build_action(
|
||||
&self,
|
||||
name: &str,
|
||||
|
@ -1000,10 +1036,16 @@ impl AppContext {
|
|||
self.actions.build_action(name, data)
|
||||
}
|
||||
|
||||
/// Get a list of all action names that have been registered.
|
||||
/// in the application. Note that registration only allows for
|
||||
/// actions to be built dynamically, and is unrelated to binding
|
||||
/// actions in the element tree.
|
||||
pub fn all_action_names(&self) -> &[SharedString] {
|
||||
self.actions.all_action_names()
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when the application is about to quit.
|
||||
/// It is not possible to cancel the quit event at this point.
|
||||
pub fn on_app_quit<Fut>(
|
||||
&mut self,
|
||||
mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static,
|
||||
|
@ -1039,6 +1081,8 @@ impl AppContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Checks if the given action is bound in the current context, as defined by the app's current focus,
|
||||
/// the bindings in the element tree, and any global action listeners.
|
||||
pub fn is_action_available(&mut self, action: &dyn Action) -> bool {
|
||||
if let Some(window) = self.active_window() {
|
||||
if let Ok(window_action_available) =
|
||||
|
@ -1052,10 +1096,13 @@ impl AppContext {
|
|||
.contains_key(&action.as_any().type_id())
|
||||
}
|
||||
|
||||
/// Set the menu bar for this application. This will replace any existing menu bar.
|
||||
pub fn set_menus(&mut self, menus: Vec<Menu>) {
|
||||
self.platform.set_menus(menus, &self.keymap.lock());
|
||||
}
|
||||
|
||||
/// Dispatch an action to the currently active window or global action handler
|
||||
/// See [action::Action] for more information on how actions work
|
||||
pub fn dispatch_action(&mut self, action: &dyn Action) {
|
||||
if let Some(active_window) = self.active_window() {
|
||||
active_window
|
||||
|
@ -1110,6 +1157,7 @@ impl AppContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Is there currently something being dragged?
|
||||
pub fn has_active_drag(&self) -> bool {
|
||||
self.active_drag.is_some()
|
||||
}
|
||||
|
@ -1262,8 +1310,14 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
|
|||
/// Contains state associated with an active drag operation, started by dragging an element
|
||||
/// within the window or by dragging into the app from the underlying platform.
|
||||
pub struct AnyDrag {
|
||||
/// The view used to render this drag
|
||||
pub view: AnyView,
|
||||
|
||||
/// The value of the dragged item, to be dropped
|
||||
pub value: Box<dyn Any>,
|
||||
|
||||
/// This is used to render the dragged item in the same place
|
||||
/// on the original element that the drag was initiated
|
||||
pub cursor_offset: Point<Pixels>,
|
||||
}
|
||||
|
||||
|
@ -1271,12 +1325,19 @@ pub struct AnyDrag {
|
|||
/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip].
|
||||
#[derive(Clone)]
|
||||
pub struct AnyTooltip {
|
||||
/// The view used to display the tooltip
|
||||
pub view: AnyView,
|
||||
|
||||
/// The offset from the cursor to use, relative to the parent view
|
||||
pub cursor_offset: Point<Pixels>,
|
||||
}
|
||||
|
||||
/// A keystroke event, and potentially the associated action
|
||||
#[derive(Debug)]
|
||||
pub struct KeystrokeEvent {
|
||||
/// The keystroke that occurred
|
||||
pub keystroke: Keystroke,
|
||||
|
||||
/// The action that was resolved for the keystroke, if any
|
||||
pub action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ use anyhow::{anyhow, Context as _};
|
|||
use derive_more::{Deref, DerefMut};
|
||||
use std::{future::Future, rc::Weak};
|
||||
|
||||
/// An async-friendly version of [AppContext] with a static lifetime so it can be held across `await` points in async code.
|
||||
/// You're provided with an instance when calling [AppContext::spawn], and you can also create one with [AppContext::to_async].
|
||||
/// Internally, this holds a weak reference to an `AppContext`, so its methods are fallible to protect against cases where the [AppContext] is dropped.
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncAppContext {
|
||||
pub(crate) app: Weak<AppCell>,
|
||||
|
@ -139,6 +142,8 @@ impl AsyncAppContext {
|
|||
self.foreground_executor.spawn(f(self.clone()))
|
||||
}
|
||||
|
||||
/// Determine whether global state of the specified type has been assigned.
|
||||
/// Returns an error if the `AppContext` has been dropped.
|
||||
pub fn has_global<G: 'static>(&self) -> Result<bool> {
|
||||
let app = self
|
||||
.app
|
||||
|
@ -148,6 +153,9 @@ impl AsyncAppContext {
|
|||
Ok(app.has_global::<G>())
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
/// Panics if no global state of the specified type has been assigned.
|
||||
/// Returns an error if the `AppContext` has been dropped.
|
||||
pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
|
||||
let app = self
|
||||
.app
|
||||
|
@ -157,6 +165,9 @@ impl AsyncAppContext {
|
|||
Ok(read(app.global(), &app))
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
/// Similar to [read_global], but returns an error instead of panicking if no state of the specified type has been assigned.
|
||||
/// Returns an error if no state of the specified type has been assigned the `AppContext` has been dropped.
|
||||
pub fn try_read_global<G: 'static, R>(
|
||||
&self,
|
||||
read: impl FnOnce(&G, &AppContext) -> R,
|
||||
|
@ -166,6 +177,8 @@ impl AsyncAppContext {
|
|||
Some(read(app.try_global()?, &app))
|
||||
}
|
||||
|
||||
/// A convenience method for [AppContext::update_global]
|
||||
/// for updating the global state of the specified type.
|
||||
pub fn update_global<G: 'static, R>(
|
||||
&mut self,
|
||||
update: impl FnOnce(&mut G, &mut AppContext) -> R,
|
||||
|
@ -179,6 +192,8 @@ impl AsyncAppContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// A cloneable, owned handle to the application context,
|
||||
/// composed with the window associated with the current task.
|
||||
#[derive(Clone, Deref, DerefMut)]
|
||||
pub struct AsyncWindowContext {
|
||||
#[deref]
|
||||
|
@ -188,14 +203,16 @@ pub struct AsyncWindowContext {
|
|||
}
|
||||
|
||||
impl AsyncWindowContext {
|
||||
pub fn window_handle(&self) -> AnyWindowHandle {
|
||||
self.window
|
||||
}
|
||||
|
||||
pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
|
||||
Self { app, window }
|
||||
}
|
||||
|
||||
/// Get the handle of the window this context is associated with.
|
||||
pub fn window_handle(&self) -> AnyWindowHandle {
|
||||
self.window
|
||||
}
|
||||
|
||||
/// A convenience method for [WindowContext::update()]
|
||||
pub fn update<R>(
|
||||
&mut self,
|
||||
update: impl FnOnce(AnyView, &mut WindowContext) -> R,
|
||||
|
@ -203,10 +220,12 @@ impl AsyncWindowContext {
|
|||
self.app.update_window(self.window, update)
|
||||
}
|
||||
|
||||
/// A convenience method for [WindowContext::on_next_frame()]
|
||||
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
|
||||
self.window.update(self, |_, cx| cx.on_next_frame(f)).ok();
|
||||
}
|
||||
|
||||
/// A convenience method for [AppContext::global()]
|
||||
pub fn read_global<G: 'static, R>(
|
||||
&mut self,
|
||||
read: impl FnOnce(&G, &WindowContext) -> R,
|
||||
|
@ -214,6 +233,8 @@ impl AsyncWindowContext {
|
|||
self.window.update(self, |_, cx| read(cx.global(), cx))
|
||||
}
|
||||
|
||||
/// A convenience method for [AppContext::update_global()]
|
||||
/// for updating the global state of the specified type.
|
||||
pub fn update_global<G, R>(
|
||||
&mut self,
|
||||
update: impl FnOnce(&mut G, &mut WindowContext) -> R,
|
||||
|
@ -224,6 +245,8 @@ impl AsyncWindowContext {
|
|||
self.window.update(self, |_, cx| cx.update_global(update))
|
||||
}
|
||||
|
||||
/// Schedule a future to be executed on the main thread. This is used for collecting
|
||||
/// the results of background tasks and updating the UI.
|
||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task<R>
|
||||
where
|
||||
Fut: Future<Output = R> + 'static,
|
||||
|
|
|
@ -31,6 +31,7 @@ impl From<u64> for EntityId {
|
|||
}
|
||||
|
||||
impl EntityId {
|
||||
/// Converts this entity id to a [u64]
|
||||
pub fn as_u64(self) -> u64 {
|
||||
self.0.as_ffi()
|
||||
}
|
||||
|
@ -140,7 +141,7 @@ impl EntityMap {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct Lease<'a, T> {
|
||||
pub(crate) struct Lease<'a, T> {
|
||||
entity: Option<Box<dyn Any>>,
|
||||
pub model: &'a Model<T>,
|
||||
entity_type: PhantomData<T>,
|
||||
|
@ -169,8 +170,9 @@ impl<'a, T> Drop for Lease<'a, T> {
|
|||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
pub struct Slot<T>(Model<T>);
|
||||
pub(crate) struct Slot<T>(Model<T>);
|
||||
|
||||
/// A dynamically typed reference to a model, which can be downcast into a `Model<T>`.
|
||||
pub struct AnyModel {
|
||||
pub(crate) entity_id: EntityId,
|
||||
pub(crate) entity_type: TypeId,
|
||||
|
@ -195,14 +197,17 @@ impl AnyModel {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the id associated with this model.
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.entity_id
|
||||
}
|
||||
|
||||
/// Returns the [TypeId] associated with this model.
|
||||
pub fn entity_type(&self) -> TypeId {
|
||||
self.entity_type
|
||||
}
|
||||
|
||||
/// Converts this model handle into a weak variant, which does not prevent it from being released.
|
||||
pub fn downgrade(&self) -> AnyWeakModel {
|
||||
AnyWeakModel {
|
||||
entity_id: self.entity_id,
|
||||
|
@ -211,6 +216,8 @@ impl AnyModel {
|
|||
}
|
||||
}
|
||||
|
||||
/// Converts this model handle into a strongly-typed model handle of the given type.
|
||||
/// If this model handle is not of the specified type, returns itself as an error variant.
|
||||
pub fn downcast<T: 'static>(self) -> Result<Model<T>, AnyModel> {
|
||||
if TypeId::of::<T>() == self.entity_type {
|
||||
Ok(Model {
|
||||
|
@ -274,7 +281,7 @@ impl Drop for AnyModel {
|
|||
entity_map
|
||||
.write()
|
||||
.leak_detector
|
||||
.handle_dropped(self.entity_id, self.handle_id)
|
||||
.handle_released(self.entity_id, self.handle_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -307,6 +314,8 @@ impl std::fmt::Debug for AnyModel {
|
|||
}
|
||||
}
|
||||
|
||||
/// A strong, well typed reference to a struct which is managed
|
||||
/// by GPUI
|
||||
#[derive(Deref, DerefMut)]
|
||||
pub struct Model<T> {
|
||||
#[deref]
|
||||
|
@ -368,10 +377,12 @@ impl<T: 'static> Model<T> {
|
|||
self.any_model
|
||||
}
|
||||
|
||||
/// Grab a reference to this entity from the context.
|
||||
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
|
||||
cx.entities.read(self)
|
||||
}
|
||||
|
||||
/// Read the entity referenced by this model with the given function.
|
||||
pub fn read_with<R, C: Context>(
|
||||
&self,
|
||||
cx: &C,
|
||||
|
@ -437,6 +448,7 @@ impl<T> PartialEq<WeakModel<T>> for Model<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A type erased, weak reference to a model.
|
||||
#[derive(Clone)]
|
||||
pub struct AnyWeakModel {
|
||||
pub(crate) entity_id: EntityId,
|
||||
|
@ -445,10 +457,12 @@ pub struct AnyWeakModel {
|
|||
}
|
||||
|
||||
impl AnyWeakModel {
|
||||
/// Get the entity ID associated with this weak reference.
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.entity_id
|
||||
}
|
||||
|
||||
/// Check if this weak handle can be upgraded, or if the model has already been dropped
|
||||
pub fn is_upgradable(&self) -> bool {
|
||||
let ref_count = self
|
||||
.entity_ref_counts
|
||||
|
@ -458,6 +472,7 @@ impl AnyWeakModel {
|
|||
ref_count > 0
|
||||
}
|
||||
|
||||
/// Upgrade this weak model reference to a strong reference.
|
||||
pub fn upgrade(&self) -> Option<AnyModel> {
|
||||
let ref_counts = &self.entity_ref_counts.upgrade()?;
|
||||
let ref_counts = ref_counts.read();
|
||||
|
@ -485,14 +500,15 @@ impl AnyWeakModel {
|
|||
})
|
||||
}
|
||||
|
||||
/// Assert that model referenced by this weak handle has been released.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn assert_dropped(&self) {
|
||||
pub fn assert_released(&self) {
|
||||
self.entity_ref_counts
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
.write()
|
||||
.leak_detector
|
||||
.assert_dropped(self.entity_id);
|
||||
.assert_released(self.entity_id);
|
||||
|
||||
if self
|
||||
.entity_ref_counts
|
||||
|
@ -527,6 +543,7 @@ impl PartialEq for AnyWeakModel {
|
|||
|
||||
impl Eq for AnyWeakModel {}
|
||||
|
||||
/// A weak reference to a model of the given type.
|
||||
#[derive(Deref, DerefMut)]
|
||||
pub struct WeakModel<T> {
|
||||
#[deref]
|
||||
|
@ -617,12 +634,12 @@ lazy_static::lazy_static! {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||
pub struct HandleId {
|
||||
pub(crate) struct HandleId {
|
||||
id: u64, // id of the handle itself, not the pointed at object
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct LeakDetector {
|
||||
pub(crate) struct LeakDetector {
|
||||
next_handle_id: u64,
|
||||
entity_handles: HashMap<EntityId, HashMap<HandleId, Option<backtrace::Backtrace>>>,
|
||||
}
|
||||
|
@ -641,12 +658,12 @@ impl LeakDetector {
|
|||
handle_id
|
||||
}
|
||||
|
||||
pub fn handle_dropped(&mut self, entity_id: EntityId, handle_id: HandleId) {
|
||||
pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) {
|
||||
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||
handles.remove(&handle_id);
|
||||
}
|
||||
|
||||
pub fn assert_dropped(&mut self, entity_id: EntityId) {
|
||||
pub fn assert_released(&mut self, entity_id: EntityId) {
|
||||
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||
if !handles.is_empty() {
|
||||
for (_, backtrace) in handles {
|
||||
|
|
|
@ -11,6 +11,7 @@ use std::{
|
|||
future::Future,
|
||||
};
|
||||
|
||||
/// The app context, with specialized behavior for the given model.
|
||||
#[derive(Deref, DerefMut)]
|
||||
pub struct ModelContext<'a, T> {
|
||||
#[deref]
|
||||
|
@ -24,20 +25,24 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
Self { app, model_state }
|
||||
}
|
||||
|
||||
/// The entity id of the model backing this context.
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.model_state.entity_id
|
||||
}
|
||||
|
||||
/// Returns a handle to the model belonging to this context.
|
||||
pub fn handle(&self) -> Model<T> {
|
||||
self.weak_model()
|
||||
.upgrade()
|
||||
.expect("The entity must be alive if we have a model context")
|
||||
}
|
||||
|
||||
/// Returns a weak handle to the model belonging to this context.
|
||||
pub fn weak_model(&self) -> WeakModel<T> {
|
||||
self.model_state.clone()
|
||||
}
|
||||
|
||||
/// Arranges for the given function to be called whenever [ModelContext::notify] or [ViewContext::notify] is called with the given model or view.
|
||||
pub fn observe<W, E>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
|
@ -59,6 +64,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Subscribe to an event type from another model or view
|
||||
pub fn subscribe<T2, E, Evt>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
|
@ -81,6 +87,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when GPUI releases this model.
|
||||
pub fn on_release(
|
||||
&mut self,
|
||||
on_release: impl FnOnce(&mut T, &mut AppContext) + 'static,
|
||||
|
@ -99,6 +106,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to be run on the release of another model or view
|
||||
pub fn observe_release<T2, E>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
|
@ -124,6 +132,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Register a callback to for updates to the given global
|
||||
pub fn observe_global<G: 'static>(
|
||||
&mut self,
|
||||
mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + 'static,
|
||||
|
@ -140,6 +149,8 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Arrange for the given function to be invoked whenever the application is quit.
|
||||
/// The future returned from this callback will be polled for up to [gpui::SHUTDOWN_TIMEOUT] until the app fully quits.
|
||||
pub fn on_app_quit<Fut>(
|
||||
&mut self,
|
||||
mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + 'static,
|
||||
|
@ -165,6 +176,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
subscription
|
||||
}
|
||||
|
||||
/// Tell GPUI that this model has changed and observers of it should be notified.
|
||||
pub fn notify(&mut self) {
|
||||
if self
|
||||
.app
|
||||
|
@ -177,6 +189,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Update the given global
|
||||
pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||
where
|
||||
G: 'static,
|
||||
|
@ -187,6 +200,9 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
result
|
||||
}
|
||||
|
||||
/// Spawn the future returned by the given function.
|
||||
/// The function is provided a weak handle to the model owned by this context and a context that can be held across await points.
|
||||
/// The returned task must be held or detached.
|
||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(WeakModel<T>, AsyncAppContext) -> Fut) -> Task<R>
|
||||
where
|
||||
T: 'static,
|
||||
|
@ -199,6 +215,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
|
|||
}
|
||||
|
||||
impl<'a, T> ModelContext<'a, T> {
|
||||
/// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts.
|
||||
pub fn emit<Evt>(&mut self, event: Evt)
|
||||
where
|
||||
T: EventEmitter<Evt>,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
#![deny(missing_docs)]
|
||||
|
||||
use crate::{
|
||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor,
|
||||
IntoElement, Keystroke, Model, ModelContext, Pixels, Platform, Render, Result, Size, Task,
|
||||
TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext,
|
||||
WindowContext, WindowHandle, WindowOptions,
|
||||
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
AvailableSpace, BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter,
|
||||
ForegroundExecutor, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform, Point,
|
||||
Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
|
||||
ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt};
|
||||
|
@ -167,10 +167,14 @@ impl TestAppContext {
|
|||
}
|
||||
|
||||
/// Adds a new window with no content.
|
||||
pub fn add_empty_window(&mut self) -> AnyWindowHandle {
|
||||
pub fn add_empty_window(&mut self) -> &mut VisualTestContext {
|
||||
let mut cx = self.app.borrow_mut();
|
||||
cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {}))
|
||||
.any_handle
|
||||
let window = cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| ()));
|
||||
drop(cx);
|
||||
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
|
||||
cx.run_until_parked();
|
||||
// it might be nice to try and cleanup these at the end of each test.
|
||||
Box::leak(cx)
|
||||
}
|
||||
|
||||
/// Adds a new window, and returns its root view and a `VisualTestContext` which can be used
|
||||
|
@ -564,6 +568,11 @@ pub struct VisualTestContext {
|
|||
}
|
||||
|
||||
impl<'a> VisualTestContext {
|
||||
/// Get the underlying window handle underlying this context.
|
||||
pub fn handle(&self) -> AnyWindowHandle {
|
||||
self.window
|
||||
}
|
||||
|
||||
/// Provides the `WindowContext` for the duration of the closure.
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
|
||||
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
|
||||
|
@ -609,6 +618,36 @@ impl<'a> VisualTestContext {
|
|||
self.cx.simulate_input(self.window, input)
|
||||
}
|
||||
|
||||
/// Draw an element to the window. Useful for simulating events or actions
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
space: Size<AvailableSpace>,
|
||||
f: impl FnOnce(&mut WindowContext) -> AnyElement,
|
||||
) {
|
||||
self.update(|cx| {
|
||||
let entity_id = cx
|
||||
.window
|
||||
.root_view
|
||||
.as_ref()
|
||||
.expect("Can't draw to this window without a root view")
|
||||
.entity_id();
|
||||
cx.with_view_id(entity_id, |cx| {
|
||||
f(cx).draw(origin, space, cx);
|
||||
});
|
||||
|
||||
cx.refresh();
|
||||
})
|
||||
}
|
||||
|
||||
/// Simulate an event from the platform, e.g. a SrollWheelEvent
|
||||
/// Make sure you've called [VisualTestContext::draw] first!
|
||||
pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
|
||||
self.test_window(self.window)
|
||||
.simulate_input(event.to_platform_input());
|
||||
self.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Simulates the user blurring the window.
|
||||
pub fn deactivate_window(&mut self) {
|
||||
if Some(self.window) == self.test_platform.active_window() {
|
||||
|
@ -763,12 +802,3 @@ impl AnyWindowHandle {
|
|||
self.update(cx, |_, cx| cx.new_view(build_view)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// An EmptyView for testing.
|
||||
pub struct EmptyView {}
|
||||
|
||||
impl Render for EmptyView {
|
||||
fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,6 +115,12 @@ pub trait Render: 'static + Sized {
|
|||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
|
||||
}
|
||||
|
||||
impl Render for () {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
/// You can derive [`IntoElement`] on any type that implements this trait.
|
||||
/// It is used to allow views to be expressed in terms of abstract data.
|
||||
pub trait RenderOnce: 'static {
|
||||
|
|
|
@ -30,6 +30,7 @@ struct StateInner {
|
|||
logical_scroll_top: Option<ListOffset>,
|
||||
alignment: ListAlignment,
|
||||
overdraw: Pixels,
|
||||
reset: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
|
||||
}
|
||||
|
@ -92,11 +93,17 @@ impl ListState {
|
|||
alignment: orientation,
|
||||
overdraw,
|
||||
scroll_handler: None,
|
||||
reset: false,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Reset this instantiation of the list state.
|
||||
///
|
||||
/// Note that this will cause scroll events to be dropped until the next paint.
|
||||
pub fn reset(&self, element_count: usize) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
state.reset = true;
|
||||
|
||||
state.logical_scroll_top = None;
|
||||
state.items = SumTree::new();
|
||||
state
|
||||
|
@ -152,11 +159,13 @@ impl ListState {
|
|||
scroll_top.item_ix = item_count;
|
||||
scroll_top.offset_in_item = px(0.);
|
||||
}
|
||||
|
||||
state.logical_scroll_top = Some(scroll_top);
|
||||
}
|
||||
|
||||
pub fn scroll_to_reveal_item(&self, ix: usize) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
|
||||
let mut scroll_top = state.logical_scroll_top();
|
||||
let height = state
|
||||
.last_layout_bounds
|
||||
|
@ -187,9 +196,9 @@ impl ListState {
|
|||
/// Get the bounds for the given item in window coordinates.
|
||||
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
|
||||
let state = &*self.0.borrow();
|
||||
|
||||
let bounds = state.last_layout_bounds.unwrap_or_default();
|
||||
let scroll_top = state.logical_scroll_top();
|
||||
|
||||
if ix < scroll_top.item_ix {
|
||||
return None;
|
||||
}
|
||||
|
@ -230,6 +239,12 @@ impl StateInner {
|
|||
delta: Point<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
// Drop scroll events after a reset, since we can't calculate
|
||||
// the new logical scroll top without the item heights
|
||||
if self.reset {
|
||||
return;
|
||||
}
|
||||
|
||||
let scroll_max = (self.items.summary().height - height).max(px(0.));
|
||||
let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
|
||||
.max(px(0.))
|
||||
|
@ -325,6 +340,8 @@ impl Element for List {
|
|||
) {
|
||||
let state = &mut *self.state.0.borrow_mut();
|
||||
|
||||
state.reset = false;
|
||||
|
||||
// If the width of the list has changed, invalidate all cached item heights
|
||||
if state.last_layout_bounds.map_or(true, |last_bounds| {
|
||||
last_bounds.size.width != bounds.size.width
|
||||
|
@ -346,8 +363,9 @@ impl Element for List {
|
|||
height: AvailableSpace::MinContent,
|
||||
};
|
||||
|
||||
// Render items after the scroll top, including those in the trailing overdraw
|
||||
let mut cursor = old_items.cursor::<Count>();
|
||||
|
||||
// Render items after the scroll top, including those in the trailing overdraw
|
||||
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||
for (ix, item) in cursor.by_ref().enumerate() {
|
||||
let visible_height = rendered_height - scroll_top.offset_in_item;
|
||||
|
@ -461,6 +479,7 @@ impl Element for List {
|
|||
|
||||
let list_state = self.state.clone();
|
||||
let height = bounds.size.height;
|
||||
|
||||
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& bounds.contains(&event.position)
|
||||
|
@ -562,3 +581,49 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
|
|||
self.0.partial_cmp(&other.height).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use gpui::{ScrollDelta, ScrollWheelEvent};
|
||||
|
||||
use crate::{self as gpui, TestAppContext};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) {
|
||||
use crate::{div, list, point, px, size, Element, ListState, Styled};
|
||||
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| {
|
||||
div().h(px(10.)).w_full().into_any()
|
||||
});
|
||||
|
||||
// Ensure that the list is scrolled to the top
|
||||
state.scroll_to(gpui::ListOffset {
|
||||
item_ix: 0,
|
||||
offset_in_item: px(0.0),
|
||||
});
|
||||
|
||||
// Paint
|
||||
cx.draw(
|
||||
point(px(0.), px(0.)),
|
||||
size(px(100.), px(20.)).into(),
|
||||
|_| list(state.clone()).w_full().h_full().z_index(10).into_any(),
|
||||
);
|
||||
|
||||
// Reset
|
||||
state.reset(5);
|
||||
|
||||
// And then recieve a scroll event _before_ the next paint
|
||||
cx.simulate_event(ScrollWheelEvent {
|
||||
position: point(px(1.), px(1.)),
|
||||
delta: ScrollDelta::Pixels(point(px(0.), px(-500.))),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Scroll position should stay at the top of the list
|
||||
assert_eq!(state.logical_scroll_top().item_ix, 0);
|
||||
assert_eq!(state.logical_scroll_top().offset_in_item, px(0.));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,9 +109,10 @@ type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
|
|||
|
||||
/// BackgroundExecutor lets you run things on background threads.
|
||||
/// In production this is a thread pool with no ordering guarantees.
|
||||
/// In tests this is simalated by running tasks one by one in a deterministic
|
||||
/// In tests this is simulated by running tasks one by one in a deterministic
|
||||
/// (but arbitrary) order controlled by the `SEED` environment variable.
|
||||
impl BackgroundExecutor {
|
||||
#[doc(hidden)]
|
||||
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
|
||||
Self { dispatcher }
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
use crate::{
|
||||
div, point, Element, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext,
|
||||
point, seal::Sealed, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
|
||||
use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf};
|
||||
|
||||
pub trait InputEvent: Sealed + 'static {
|
||||
fn to_platform_input(self) -> PlatformInput;
|
||||
}
|
||||
pub trait KeyEvent: InputEvent {}
|
||||
pub trait MouseEvent: InputEvent {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct KeyDownEvent {
|
||||
|
@ -10,16 +16,40 @@ pub struct KeyDownEvent {
|
|||
pub is_held: bool,
|
||||
}
|
||||
|
||||
impl Sealed for KeyDownEvent {}
|
||||
impl InputEvent for KeyDownEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::KeyDown(self)
|
||||
}
|
||||
}
|
||||
impl KeyEvent for KeyDownEvent {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyUpEvent {
|
||||
pub keystroke: Keystroke,
|
||||
}
|
||||
|
||||
impl Sealed for KeyUpEvent {}
|
||||
impl InputEvent for KeyUpEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::KeyUp(self)
|
||||
}
|
||||
}
|
||||
impl KeyEvent for KeyUpEvent {}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ModifiersChangedEvent {
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Sealed for ModifiersChangedEvent {}
|
||||
impl InputEvent for ModifiersChangedEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::ModifiersChanged(self)
|
||||
}
|
||||
}
|
||||
impl KeyEvent for ModifiersChangedEvent {}
|
||||
|
||||
impl Deref for ModifiersChangedEvent {
|
||||
type Target = Modifiers;
|
||||
|
||||
|
@ -30,9 +60,10 @@ impl Deref for ModifiersChangedEvent {
|
|||
|
||||
/// The phase of a touch motion event.
|
||||
/// Based on the winit enum of the same name.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum TouchPhase {
|
||||
Started,
|
||||
#[default]
|
||||
Moved,
|
||||
Ended,
|
||||
}
|
||||
|
@ -45,6 +76,14 @@ pub struct MouseDownEvent {
|
|||
pub click_count: usize,
|
||||
}
|
||||
|
||||
impl Sealed for MouseDownEvent {}
|
||||
impl InputEvent for MouseDownEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MouseDown(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MouseDownEvent {}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseUpEvent {
|
||||
pub button: MouseButton,
|
||||
|
@ -53,38 +92,20 @@ pub struct MouseUpEvent {
|
|||
pub click_count: usize,
|
||||
}
|
||||
|
||||
impl Sealed for MouseUpEvent {}
|
||||
impl InputEvent for MouseUpEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MouseUp(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MouseUpEvent {}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ClickEvent {
|
||||
pub down: MouseDownEvent,
|
||||
pub up: MouseUpEvent,
|
||||
}
|
||||
|
||||
pub struct Drag<S, R, V, E>
|
||||
where
|
||||
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
||||
V: 'static,
|
||||
E: IntoElement,
|
||||
{
|
||||
pub state: S,
|
||||
pub render_drag_handle: R,
|
||||
view_element_types: PhantomData<(V, E)>,
|
||||
}
|
||||
|
||||
impl<S, R, V, E> Drag<S, R, V, E>
|
||||
where
|
||||
R: Fn(&mut V, &mut ViewContext<V>) -> E,
|
||||
V: 'static,
|
||||
E: Element,
|
||||
{
|
||||
pub fn new(state: S, render_drag_handle: R) -> Self {
|
||||
Drag {
|
||||
state,
|
||||
render_drag_handle,
|
||||
view_element_types: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum MouseButton {
|
||||
Left,
|
||||
|
@ -130,13 +151,21 @@ pub struct MouseMoveEvent {
|
|||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Sealed for MouseMoveEvent {}
|
||||
impl InputEvent for MouseMoveEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MouseMove(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MouseMoveEvent {}
|
||||
|
||||
impl MouseMoveEvent {
|
||||
pub fn dragging(&self) -> bool {
|
||||
self.pressed_button == Some(MouseButton::Left)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ScrollWheelEvent {
|
||||
pub position: Point<Pixels>,
|
||||
pub delta: ScrollDelta,
|
||||
|
@ -144,6 +173,14 @@ pub struct ScrollWheelEvent {
|
|||
pub touch_phase: TouchPhase,
|
||||
}
|
||||
|
||||
impl Sealed for ScrollWheelEvent {}
|
||||
impl InputEvent for ScrollWheelEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::ScrollWheel(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for ScrollWheelEvent {}
|
||||
|
||||
impl Deref for ScrollWheelEvent {
|
||||
type Target = Modifiers;
|
||||
|
||||
|
@ -201,6 +238,14 @@ pub struct MouseExitEvent {
|
|||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Sealed for MouseExitEvent {}
|
||||
impl InputEvent for MouseExitEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::MouseExited(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for MouseExitEvent {}
|
||||
|
||||
impl Deref for MouseExitEvent {
|
||||
type Target = Modifiers;
|
||||
|
||||
|
@ -220,7 +265,7 @@ impl ExternalPaths {
|
|||
|
||||
impl Render for ExternalPaths {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div() // Intentionally left empty because the platform will render icons for the dragged files
|
||||
() // Intentionally left empty because the platform will render icons for the dragged files
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,8 +284,16 @@ pub enum FileDropEvent {
|
|||
Exited,
|
||||
}
|
||||
|
||||
impl Sealed for FileDropEvent {}
|
||||
impl InputEvent for FileDropEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::FileDrop(self)
|
||||
}
|
||||
}
|
||||
impl MouseEvent for FileDropEvent {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InputEvent {
|
||||
pub enum PlatformInput {
|
||||
KeyDown(KeyDownEvent),
|
||||
KeyUp(KeyUpEvent),
|
||||
ModifiersChanged(ModifiersChangedEvent),
|
||||
|
@ -252,19 +305,19 @@ pub enum InputEvent {
|
|||
FileDrop(FileDropEvent),
|
||||
}
|
||||
|
||||
impl InputEvent {
|
||||
impl PlatformInput {
|
||||
pub fn position(&self) -> Option<Point<Pixels>> {
|
||||
match self {
|
||||
InputEvent::KeyDown { .. } => None,
|
||||
InputEvent::KeyUp { .. } => None,
|
||||
InputEvent::ModifiersChanged { .. } => None,
|
||||
InputEvent::MouseDown(event) => Some(event.position),
|
||||
InputEvent::MouseUp(event) => Some(event.position),
|
||||
InputEvent::MouseMove(event) => Some(event.position),
|
||||
InputEvent::MouseExited(event) => Some(event.position),
|
||||
InputEvent::ScrollWheel(event) => Some(event.position),
|
||||
InputEvent::FileDrop(FileDropEvent::Exited) => None,
|
||||
InputEvent::FileDrop(
|
||||
PlatformInput::KeyDown { .. } => None,
|
||||
PlatformInput::KeyUp { .. } => None,
|
||||
PlatformInput::ModifiersChanged { .. } => None,
|
||||
PlatformInput::MouseDown(event) => Some(event.position),
|
||||
PlatformInput::MouseUp(event) => Some(event.position),
|
||||
PlatformInput::MouseMove(event) => Some(event.position),
|
||||
PlatformInput::MouseExited(event) => Some(event.position),
|
||||
PlatformInput::ScrollWheel(event) => Some(event.position),
|
||||
PlatformInput::FileDrop(FileDropEvent::Exited) => None,
|
||||
PlatformInput::FileDrop(
|
||||
FileDropEvent::Entered { position, .. }
|
||||
| FileDropEvent::Pending { position, .. }
|
||||
| FileDropEvent::Submit { position, .. },
|
||||
|
@ -274,29 +327,29 @@ impl InputEvent {
|
|||
|
||||
pub fn mouse_event(&self) -> Option<&dyn Any> {
|
||||
match self {
|
||||
InputEvent::KeyDown { .. } => None,
|
||||
InputEvent::KeyUp { .. } => None,
|
||||
InputEvent::ModifiersChanged { .. } => None,
|
||||
InputEvent::MouseDown(event) => Some(event),
|
||||
InputEvent::MouseUp(event) => Some(event),
|
||||
InputEvent::MouseMove(event) => Some(event),
|
||||
InputEvent::MouseExited(event) => Some(event),
|
||||
InputEvent::ScrollWheel(event) => Some(event),
|
||||
InputEvent::FileDrop(event) => Some(event),
|
||||
PlatformInput::KeyDown { .. } => None,
|
||||
PlatformInput::KeyUp { .. } => None,
|
||||
PlatformInput::ModifiersChanged { .. } => None,
|
||||
PlatformInput::MouseDown(event) => Some(event),
|
||||
PlatformInput::MouseUp(event) => Some(event),
|
||||
PlatformInput::MouseMove(event) => Some(event),
|
||||
PlatformInput::MouseExited(event) => Some(event),
|
||||
PlatformInput::ScrollWheel(event) => Some(event),
|
||||
PlatformInput::FileDrop(event) => Some(event),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keyboard_event(&self) -> Option<&dyn Any> {
|
||||
match self {
|
||||
InputEvent::KeyDown(event) => Some(event),
|
||||
InputEvent::KeyUp(event) => Some(event),
|
||||
InputEvent::ModifiersChanged(event) => Some(event),
|
||||
InputEvent::MouseDown(_) => None,
|
||||
InputEvent::MouseUp(_) => None,
|
||||
InputEvent::MouseMove(_) => None,
|
||||
InputEvent::MouseExited(_) => None,
|
||||
InputEvent::ScrollWheel(_) => None,
|
||||
InputEvent::FileDrop(_) => None,
|
||||
PlatformInput::KeyDown(event) => Some(event),
|
||||
PlatformInput::KeyUp(event) => Some(event),
|
||||
PlatformInput::ModifiersChanged(event) => Some(event),
|
||||
PlatformInput::MouseDown(_) => None,
|
||||
PlatformInput::MouseUp(_) => None,
|
||||
PlatformInput::MouseMove(_) => None,
|
||||
PlatformInput::MouseExited(_) => None,
|
||||
PlatformInput::ScrollWheel(_) => None,
|
||||
PlatformInput::FileDrop(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -209,7 +209,6 @@ mod tests {
|
|||
);
|
||||
assert!(!matcher.has_pending_keystrokes());
|
||||
|
||||
eprintln!("PROBLEM AREA");
|
||||
// If a is prefixed, C will not be dispatched because there
|
||||
// was a pending binding for it
|
||||
assert_eq!(
|
||||
|
|
|
@ -7,7 +7,7 @@ mod test;
|
|||
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics,
|
||||
FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap, LineLayout, Pixels,
|
||||
FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout, Pixels, PlatformInput,
|
||||
Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString,
|
||||
Size, TaskLabel,
|
||||
};
|
||||
|
@ -88,7 +88,7 @@ pub(crate) trait Platform: 'static {
|
|||
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
|
||||
fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
|
@ -114,15 +114,20 @@ pub(crate) trait Platform: 'static {
|
|||
fn delete_credentials(&self, url: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||
pub trait PlatformDisplay: Send + Sync + Debug {
|
||||
/// Get the ID for this display
|
||||
fn id(&self) -> DisplayId;
|
||||
|
||||
/// Returns a stable identifier for this display that can be persisted and used
|
||||
/// across system restarts.
|
||||
fn uuid(&self) -> Result<Uuid>;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
/// Get the bounds for this display
|
||||
fn bounds(&self) -> Bounds<GlobalPixels>;
|
||||
}
|
||||
|
||||
/// An opaque identifier for a hardware display
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub struct DisplayId(pub(crate) u32);
|
||||
|
||||
|
@ -134,7 +139,7 @@ impl Debug for DisplayId {
|
|||
|
||||
unsafe impl Send for DisplayId {}
|
||||
|
||||
pub trait PlatformWindow {
|
||||
pub(crate) trait PlatformWindow {
|
||||
fn bounds(&self) -> WindowBounds;
|
||||
fn content_size(&self) -> Size<Pixels>;
|
||||
fn scale_factor(&self) -> f32;
|
||||
|
@ -155,7 +160,7 @@ pub trait PlatformWindow {
|
|||
fn zoom(&self);
|
||||
fn toggle_full_screen(&self);
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>);
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
|
||||
fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>);
|
||||
|
@ -175,6 +180,9 @@ pub trait PlatformWindow {
|
|||
}
|
||||
}
|
||||
|
||||
/// This type is public so that our test macro can generate and use it, but it should not
|
||||
/// be considered part of our public API.
|
||||
#[doc(hidden)]
|
||||
pub trait PlatformDispatcher: Send + Sync {
|
||||
fn is_main_thread(&self) -> bool;
|
||||
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
|
||||
|
@ -190,7 +198,7 @@ pub trait PlatformDispatcher: Send + Sync {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait PlatformTextSystem: Send + Sync {
|
||||
pub(crate) trait PlatformTextSystem: Send + Sync {
|
||||
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
|
||||
fn all_font_names(&self) -> Vec<String>;
|
||||
fn font_id(&self, descriptor: &Font) -> Result<FontId>;
|
||||
|
@ -214,15 +222,21 @@ pub trait PlatformTextSystem: Send + Sync {
|
|||
) -> Vec<usize>;
|
||||
}
|
||||
|
||||
/// Basic metadata about the current application and operating system.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppMetadata {
|
||||
/// The name of the current operating system
|
||||
pub os_name: &'static str,
|
||||
|
||||
/// The operating system's version
|
||||
pub os_version: Option<SemanticVersion>,
|
||||
|
||||
/// The current version of the application
|
||||
pub app_version: Option<SemanticVersion>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub enum AtlasKey {
|
||||
pub(crate) enum AtlasKey {
|
||||
Glyph(RenderGlyphParams),
|
||||
Svg(RenderSvgParams),
|
||||
Image(RenderImageParams),
|
||||
|
@ -262,7 +276,7 @@ impl From<RenderImageParams> for AtlasKey {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait PlatformAtlas: Send + Sync {
|
||||
pub(crate) trait PlatformAtlas: Send + Sync {
|
||||
fn get_or_insert_with<'a>(
|
||||
&self,
|
||||
key: &AtlasKey,
|
||||
|
@ -274,7 +288,7 @@ pub trait PlatformAtlas: Send + Sync {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct AtlasTile {
|
||||
pub(crate) struct AtlasTile {
|
||||
pub(crate) texture_id: AtlasTextureId,
|
||||
pub(crate) tile_id: TileId,
|
||||
pub(crate) bounds: Bounds<DevicePixels>,
|
||||
|
|
|
@ -11,7 +11,6 @@ use core_graphics::{
|
|||
geometry::{CGPoint, CGRect, CGSize},
|
||||
};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use std::any::Any;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -154,10 +153,6 @@ impl PlatformDisplay for MacDisplay {
|
|||
]))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<GlobalPixels> {
|
||||
unsafe {
|
||||
let native_bounds = CGDisplayBounds(self.0);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
point, px, InputEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent,
|
||||
MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection,
|
||||
Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
||||
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||
|
@ -82,7 +82,7 @@ unsafe fn read_modifiers(native_event: id) -> Modifiers {
|
|||
}
|
||||
}
|
||||
|
||||
impl InputEvent {
|
||||
impl PlatformInput {
|
||||
pub unsafe fn from_native(native_event: id, window_height: Option<Pixels>) -> Option<Self> {
|
||||
let event_type = native_event.eventType();
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use super::{events::key_to_native, BoolExt};
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker,
|
||||
MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
||||
ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
|
||||
MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
|
||||
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
|
@ -153,7 +153,7 @@ pub struct MacPlatformState {
|
|||
resign_active: Option<Box<dyn FnMut()>>,
|
||||
reopen: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
event: Option<Box<dyn FnMut(InputEvent) -> bool>>,
|
||||
event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
|
||||
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
will_open_menu: Option<Box<dyn FnMut()>>,
|
||||
|
@ -637,7 +637,7 @@ impl Platform for MacPlatform {
|
|||
self.0.lock().reopen = Some(callback);
|
||||
}
|
||||
|
||||
fn on_event(&self, callback: Box<dyn FnMut(InputEvent) -> bool>) {
|
||||
fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
|
||||
self.0.lock().event = Some(callback);
|
||||
}
|
||||
|
||||
|
@ -976,7 +976,7 @@ unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform {
|
|||
|
||||
extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
|
||||
unsafe {
|
||||
if let Some(event) = InputEvent::from_native(native_event, None) {
|
||||
if let Some(event) = PlatformInput::from_native(native_event, None) {
|
||||
let platform = get_mac_platform(this);
|
||||
let mut lock = platform.0.lock();
|
||||
if let Some(mut callback) = lock.event.take() {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
|
||||
use crate::{
|
||||
display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
|
||||
FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke,
|
||||
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
|
||||
FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||
PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
|
@ -319,7 +319,7 @@ struct MacWindowState {
|
|||
renderer: MetalRenderer,
|
||||
kind: WindowKind,
|
||||
request_frame_callback: Option<Box<dyn FnMut()>>,
|
||||
event_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
|
||||
event_callback: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
|
||||
activate_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
|
@ -333,7 +333,7 @@ struct MacWindowState {
|
|||
synthetic_drag_counter: usize,
|
||||
last_fresh_keydown: Option<Keystroke>,
|
||||
traffic_light_position: Option<Point<Pixels>>,
|
||||
previous_modifiers_changed_event: Option<InputEvent>,
|
||||
previous_modifiers_changed_event: Option<PlatformInput>,
|
||||
// State tracking what the IME did after the last request
|
||||
ime_state: ImeState,
|
||||
// Retains the last IME Text
|
||||
|
@ -928,7 +928,7 @@ impl PlatformWindow for MacWindow {
|
|||
self.0.as_ref().lock().request_frame_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>) {
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
|
||||
self.0.as_ref().lock().event_callback = Some(callback);
|
||||
}
|
||||
|
||||
|
@ -1053,9 +1053,9 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||
let mut lock = window_state.as_ref().lock();
|
||||
|
||||
let window_height = lock.content_size().height;
|
||||
let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
|
||||
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
|
||||
|
||||
if let Some(InputEvent::KeyDown(event)) = event {
|
||||
if let Some(PlatformInput::KeyDown(event)) = event {
|
||||
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
||||
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
|
||||
// makes no distinction between these two types of events, so we need to ignore
|
||||
|
@ -1102,7 +1102,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||
.flatten()
|
||||
.is_some();
|
||||
if !is_composing {
|
||||
handled = callback(InputEvent::KeyDown(event));
|
||||
handled = callback(PlatformInput::KeyDown(event));
|
||||
}
|
||||
|
||||
if !handled {
|
||||
|
@ -1146,11 +1146,11 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
|||
let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
|
||||
|
||||
let window_height = lock.content_size().height;
|
||||
let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
|
||||
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
|
||||
|
||||
if let Some(mut event) = event {
|
||||
match &mut event {
|
||||
InputEvent::MouseDown(
|
||||
PlatformInput::MouseDown(
|
||||
event @ MouseDownEvent {
|
||||
button: MouseButton::Left,
|
||||
modifiers: Modifiers { control: true, .. },
|
||||
|
@ -1172,7 +1172,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
|||
// Because we map a ctrl-left_down to a right_down -> right_up let's ignore
|
||||
// the ctrl-left_up to avoid having a mismatch in button down/up events if the
|
||||
// user is still holding ctrl when releasing the left mouse button
|
||||
InputEvent::MouseUp(
|
||||
PlatformInput::MouseUp(
|
||||
event @ MouseUpEvent {
|
||||
button: MouseButton::Left,
|
||||
modifiers: Modifiers { control: true, .. },
|
||||
|
@ -1194,7 +1194,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
|||
};
|
||||
|
||||
match &event {
|
||||
InputEvent::MouseMove(
|
||||
PlatformInput::MouseMove(
|
||||
event @ MouseMoveEvent {
|
||||
pressed_button: Some(_),
|
||||
..
|
||||
|
@ -1216,15 +1216,15 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
|||
}
|
||||
}
|
||||
|
||||
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
|
||||
PlatformInput::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
|
||||
|
||||
InputEvent::MouseUp(MouseUpEvent { .. }) => {
|
||||
PlatformInput::MouseUp(MouseUpEvent { .. }) => {
|
||||
lock.synthetic_drag_counter += 1;
|
||||
}
|
||||
|
||||
InputEvent::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
|
||||
PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
|
||||
// Only raise modifiers changed event when they have actually changed
|
||||
if let Some(InputEvent::ModifiersChanged(ModifiersChangedEvent {
|
||||
if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
|
||||
modifiers: prev_modifiers,
|
||||
})) = &lock.previous_modifiers_changed_event
|
||||
{
|
||||
|
@ -1258,7 +1258,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
|
|||
key: ".".into(),
|
||||
ime_key: None,
|
||||
};
|
||||
let event = InputEvent::KeyDown(KeyDownEvent {
|
||||
let event = PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke.clone(),
|
||||
is_held: false,
|
||||
});
|
||||
|
@ -1655,7 +1655,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr
|
|||
if send_new_event(&window_state, {
|
||||
let position = drag_event_position(&window_state, dragging_info);
|
||||
let paths = external_paths_from_event(dragging_info);
|
||||
InputEvent::FileDrop(FileDropEvent::Entered { position, paths })
|
||||
PlatformInput::FileDrop(FileDropEvent::Entered { position, paths })
|
||||
}) {
|
||||
window_state.lock().external_files_dragged = true;
|
||||
NSDragOperationCopy
|
||||
|
@ -1669,7 +1669,7 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr
|
|||
let position = drag_event_position(&window_state, dragging_info);
|
||||
if send_new_event(
|
||||
&window_state,
|
||||
InputEvent::FileDrop(FileDropEvent::Pending { position }),
|
||||
PlatformInput::FileDrop(FileDropEvent::Pending { position }),
|
||||
) {
|
||||
NSDragOperationCopy
|
||||
} else {
|
||||
|
@ -1679,7 +1679,10 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr
|
|||
|
||||
extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
|
||||
send_new_event(
|
||||
&window_state,
|
||||
PlatformInput::FileDrop(FileDropEvent::Exited),
|
||||
);
|
||||
window_state.lock().external_files_dragged = false;
|
||||
}
|
||||
|
||||
|
@ -1688,7 +1691,7 @@ extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -
|
|||
let position = drag_event_position(&window_state, dragging_info);
|
||||
if send_new_event(
|
||||
&window_state,
|
||||
InputEvent::FileDrop(FileDropEvent::Submit { position }),
|
||||
PlatformInput::FileDrop(FileDropEvent::Submit { position }),
|
||||
) {
|
||||
YES
|
||||
} else {
|
||||
|
@ -1712,7 +1715,10 @@ fn external_paths_from_event(dragging_info: *mut Object) -> ExternalPaths {
|
|||
|
||||
extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited));
|
||||
send_new_event(
|
||||
&window_state,
|
||||
PlatformInput::FileDrop(FileDropEvent::Exited),
|
||||
);
|
||||
}
|
||||
|
||||
async fn synthetic_drag(
|
||||
|
@ -1727,7 +1733,7 @@ async fn synthetic_drag(
|
|||
if lock.synthetic_drag_counter == drag_id {
|
||||
if let Some(mut callback) = lock.event_callback.take() {
|
||||
drop(lock);
|
||||
callback(InputEvent::MouseMove(event.clone()));
|
||||
callback(PlatformInput::MouseMove(event.clone()));
|
||||
window_state.lock().event_callback = Some(callback);
|
||||
}
|
||||
} else {
|
||||
|
@ -1737,7 +1743,7 @@ async fn synthetic_drag(
|
|||
}
|
||||
}
|
||||
|
||||
fn send_new_event(window_state_lock: &Mutex<MacWindowState>, e: InputEvent) -> bool {
|
||||
fn send_new_event(window_state_lock: &Mutex<MacWindowState>, e: PlatformInput) -> bool {
|
||||
let window_state = window_state_lock.lock().event_callback.take();
|
||||
if let Some(mut callback) = window_state {
|
||||
callback(e);
|
||||
|
|
|
@ -31,10 +31,6 @@ impl PlatformDisplay for TestDisplay {
|
|||
Ok(self.uuid)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {
|
||||
self.bounds
|
||||
}
|
||||
|
|
|
@ -239,7 +239,7 @@ impl Platform for TestPlatform {
|
|||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_event(&self, _callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
|
||||
fn on_event(&self, _callback: Box<dyn FnMut(crate::PlatformInput) -> bool>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, InputEvent, KeyDownEvent,
|
||||
Keystroke, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
|
||||
Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
|
||||
px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
|
||||
Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -19,7 +19,7 @@ pub struct TestWindowState {
|
|||
platform: Weak<TestPlatform>,
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
|
||||
input_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
|
||||
input_callback: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
|
||||
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
moved_callback: Option<Box<dyn FnMut()>>,
|
||||
|
@ -85,7 +85,7 @@ impl TestWindow {
|
|||
self.0.lock().active_status_change_callback = Some(callback);
|
||||
}
|
||||
|
||||
pub fn simulate_input(&mut self, event: InputEvent) -> bool {
|
||||
pub fn simulate_input(&mut self, event: PlatformInput) -> bool {
|
||||
let mut lock = self.0.lock();
|
||||
let Some(mut callback) = lock.input_callback.take() else {
|
||||
return false;
|
||||
|
@ -97,7 +97,7 @@ impl TestWindow {
|
|||
}
|
||||
|
||||
pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) {
|
||||
if self.simulate_input(InputEvent::KeyDown(KeyDownEvent {
|
||||
if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke.clone(),
|
||||
is_held,
|
||||
})) {
|
||||
|
@ -220,7 +220,7 @@ impl PlatformWindow for TestWindow {
|
|||
|
||||
fn on_request_frame(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
|
||||
fn on_input(&self, callback: Box<dyn FnMut(crate::PlatformInput) -> bool>) {
|
||||
self.0.lock().input_callback = Some(callback)
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, order: &StackingOrder, primitive: impl Into<Primitive>) {
|
||||
pub(crate) fn insert(&mut self, order: &StackingOrder, primitive: impl Into<Primitive>) {
|
||||
let primitive = primitive.into();
|
||||
let clipped_bounds = primitive
|
||||
.bounds()
|
||||
|
@ -440,7 +440,7 @@ pub enum PrimitiveKind {
|
|||
Surface,
|
||||
}
|
||||
|
||||
pub enum Primitive {
|
||||
pub(crate) enum Primitive {
|
||||
Shadow(Shadow),
|
||||
Quad(Quad),
|
||||
Path(Path<ScaledPixels>),
|
||||
|
@ -589,7 +589,7 @@ impl From<Shadow> for Primitive {
|
|||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct MonochromeSprite {
|
||||
pub(crate) struct MonochromeSprite {
|
||||
pub view_id: ViewId,
|
||||
pub layer_id: LayerId,
|
||||
pub order: DrawOrder,
|
||||
|
@ -622,7 +622,7 @@ impl From<MonochromeSprite> for Primitive {
|
|||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct PolychromeSprite {
|
||||
pub(crate) struct PolychromeSprite {
|
||||
pub view_id: ViewId,
|
||||
pub layer_id: LayerId,
|
||||
pub order: DrawOrder,
|
||||
|
|
|
@ -47,7 +47,7 @@ pub struct TextSystem {
|
|||
}
|
||||
|
||||
impl TextSystem {
|
||||
pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
|
||||
pub(crate) fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
|
||||
TextSystem {
|
||||
line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
|
||||
platform_text_system,
|
||||
|
|
|
@ -13,7 +13,7 @@ pub struct LineWrapper {
|
|||
impl LineWrapper {
|
||||
pub const MAX_INDENT: u32 = 256;
|
||||
|
||||
pub fn new(
|
||||
pub(crate) fn new(
|
||||
font_id: FontId,
|
||||
font_size: Pixels,
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![deny(missing_docs)]
|
||||
|
||||
use crate::{
|
||||
seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
|
||||
Bounds, ContentMask, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView,
|
||||
|
@ -11,12 +13,16 @@ use std::{
|
|||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
/// A view is a piece of state that can be presented on screen by implementing the [Render] trait.
|
||||
/// Views implement [Element] and can composed with other views, and every window is created with a root view.
|
||||
pub struct View<V> {
|
||||
/// A view is just a [Model] whose type implements `Render`, and the model is accessible via this field.
|
||||
pub model: Model<V>,
|
||||
}
|
||||
|
||||
impl<V> Sealed for View<V> {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct AnyViewState {
|
||||
root_style: Style,
|
||||
cache_key: Option<ViewCacheKey>,
|
||||
|
@ -58,6 +64,7 @@ impl<V: 'static> View<V> {
|
|||
Entity::downgrade(self)
|
||||
}
|
||||
|
||||
/// Update the view's state with the given function, which is passed a mutable reference and a context.
|
||||
pub fn update<C, R>(
|
||||
&self,
|
||||
cx: &mut C,
|
||||
|
@ -69,10 +76,12 @@ impl<V: 'static> View<V> {
|
|||
cx.update_view(self, f)
|
||||
}
|
||||
|
||||
/// Obtain a read-only reference to this view's state.
|
||||
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
|
||||
self.model.read(cx)
|
||||
}
|
||||
|
||||
/// Gets a [FocusHandle] for this view when its state implements [FocusableView].
|
||||
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
|
||||
where
|
||||
V: FocusableView,
|
||||
|
@ -131,19 +140,24 @@ impl<V> PartialEq for View<V> {
|
|||
|
||||
impl<V> Eq for View<V> {}
|
||||
|
||||
/// A weak variant of [View] which does not prevent the view from being released.
|
||||
pub struct WeakView<V> {
|
||||
pub(crate) model: WeakModel<V>,
|
||||
}
|
||||
|
||||
impl<V: 'static> WeakView<V> {
|
||||
/// Gets the entity id associated with this handle.
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.model.entity_id
|
||||
}
|
||||
|
||||
/// Obtain a strong handle for the view if it hasn't been released.
|
||||
pub fn upgrade(&self) -> Option<View<V>> {
|
||||
Entity::upgrade_from(self)
|
||||
}
|
||||
|
||||
/// Update this view's state if it hasn't been released.
|
||||
/// Returns an error if this view has been released.
|
||||
pub fn update<C, R>(
|
||||
&self,
|
||||
cx: &mut C,
|
||||
|
@ -157,9 +171,10 @@ impl<V: 'static> WeakView<V> {
|
|||
Ok(view.update(cx, f)).flatten()
|
||||
}
|
||||
|
||||
/// Assert that the view referenced by this handle has been released.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn assert_dropped(&self) {
|
||||
self.model.assert_dropped()
|
||||
pub fn assert_released(&self) {
|
||||
self.model.assert_released()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,6 +200,7 @@ impl<V> PartialEq for WeakView<V> {
|
|||
|
||||
impl<V> Eq for WeakView<V> {}
|
||||
|
||||
/// A dynically-typed handle to a view, which can be downcast to a [View] for a specific type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnyView {
|
||||
model: AnyModel,
|
||||
|
@ -193,11 +209,15 @@ pub struct AnyView {
|
|||
}
|
||||
|
||||
impl AnyView {
|
||||
/// Indicate that this view should be cached when using it as an element.
|
||||
/// When using this method, the view's previous layout and paint will be recycled from the previous frame if [ViewContext::notify] has not been called since it was rendered.
|
||||
/// The one exception is when [WindowContext::refresh] is called, in which case caching is ignored.
|
||||
pub fn cached(mut self) -> Self {
|
||||
self.cache = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Convert this to a weak handle.
|
||||
pub fn downgrade(&self) -> AnyWeakView {
|
||||
AnyWeakView {
|
||||
model: self.model.downgrade(),
|
||||
|
@ -205,6 +225,8 @@ impl AnyView {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convert this to a [View] of a specific type.
|
||||
/// If this handle does not contain a view of the specified type, returns itself in an `Err` variant.
|
||||
pub fn downcast<T: 'static>(self) -> Result<View<T>, Self> {
|
||||
match self.model.downcast() {
|
||||
Ok(model) => Ok(View { model }),
|
||||
|
@ -216,10 +238,12 @@ impl AnyView {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets the [TypeId] of the underlying view.
|
||||
pub fn entity_type(&self) -> TypeId {
|
||||
self.model.entity_type
|
||||
}
|
||||
|
||||
/// Gets the entity id of this handle.
|
||||
pub fn entity_id(&self) -> EntityId {
|
||||
self.model.entity_id()
|
||||
}
|
||||
|
@ -337,12 +361,14 @@ impl IntoElement for AnyView {
|
|||
}
|
||||
}
|
||||
|
||||
/// A weak, dynamically-typed view handle that does not prevent the view from being released.
|
||||
pub struct AnyWeakView {
|
||||
model: AnyWeakModel,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
|
||||
}
|
||||
|
||||
impl AnyWeakView {
|
||||
/// Convert to a strongly-typed handle if the referenced view has not yet been released.
|
||||
pub fn upgrade(&self) -> Option<AnyView> {
|
||||
let model = self.model.upgrade()?;
|
||||
Some(AnyView {
|
||||
|
|
|
@ -5,13 +5,14 @@ use crate::{
|
|||
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
|
||||
DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect,
|
||||
Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla,
|
||||
ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId,
|
||||
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent,
|
||||
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
|
||||
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
|
||||
RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, Style, SubscriberSet,
|
||||
Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
|
||||
WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
|
||||
ImageData, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeystrokeEvent, LayoutId,
|
||||
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
|
||||
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render,
|
||||
RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, Shadow,
|
||||
SharedString, Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task,
|
||||
Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions,
|
||||
SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
|
@ -971,7 +972,7 @@ impl<'a> WindowContext<'a> {
|
|||
/// Register a mouse event listener on the window for the next frame. The type of event
|
||||
/// is determined by the first parameter of the given listener. When the next frame is rendered
|
||||
/// the listener will be cleared.
|
||||
pub fn on_mouse_event<Event: 'static>(
|
||||
pub fn on_mouse_event<Event: MouseEvent>(
|
||||
&mut self,
|
||||
mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static,
|
||||
) {
|
||||
|
@ -999,7 +1000,7 @@ impl<'a> WindowContext<'a> {
|
|||
///
|
||||
/// This is a fairly low-level method, so prefer using event handlers on elements unless you have
|
||||
/// a specific need to register a global listener.
|
||||
pub fn on_key_event<Event: 'static>(
|
||||
pub fn on_key_event<Event: KeyEvent>(
|
||||
&mut self,
|
||||
listener: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
|
||||
) {
|
||||
|
@ -1620,7 +1621,7 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
|
||||
/// Dispatch a mouse or keyboard event on the window.
|
||||
pub fn dispatch_event(&mut self, event: InputEvent) -> bool {
|
||||
pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
|
||||
// Handlers may set this to false by calling `stop_propagation`.
|
||||
self.app.propagate_event = true;
|
||||
// Handlers may set this to true by calling `prevent_default`.
|
||||
|
@ -1629,37 +1630,37 @@ impl<'a> WindowContext<'a> {
|
|||
let event = match event {
|
||||
// Track the mouse position with our own state, since accessing the platform
|
||||
// API for the mouse position can only occur on the main thread.
|
||||
InputEvent::MouseMove(mouse_move) => {
|
||||
PlatformInput::MouseMove(mouse_move) => {
|
||||
self.window.mouse_position = mouse_move.position;
|
||||
self.window.modifiers = mouse_move.modifiers;
|
||||
InputEvent::MouseMove(mouse_move)
|
||||
PlatformInput::MouseMove(mouse_move)
|
||||
}
|
||||
InputEvent::MouseDown(mouse_down) => {
|
||||
PlatformInput::MouseDown(mouse_down) => {
|
||||
self.window.mouse_position = mouse_down.position;
|
||||
self.window.modifiers = mouse_down.modifiers;
|
||||
InputEvent::MouseDown(mouse_down)
|
||||
PlatformInput::MouseDown(mouse_down)
|
||||
}
|
||||
InputEvent::MouseUp(mouse_up) => {
|
||||
PlatformInput::MouseUp(mouse_up) => {
|
||||
self.window.mouse_position = mouse_up.position;
|
||||
self.window.modifiers = mouse_up.modifiers;
|
||||
InputEvent::MouseUp(mouse_up)
|
||||
PlatformInput::MouseUp(mouse_up)
|
||||
}
|
||||
InputEvent::MouseExited(mouse_exited) => {
|
||||
PlatformInput::MouseExited(mouse_exited) => {
|
||||
self.window.modifiers = mouse_exited.modifiers;
|
||||
InputEvent::MouseExited(mouse_exited)
|
||||
PlatformInput::MouseExited(mouse_exited)
|
||||
}
|
||||
InputEvent::ModifiersChanged(modifiers_changed) => {
|
||||
PlatformInput::ModifiersChanged(modifiers_changed) => {
|
||||
self.window.modifiers = modifiers_changed.modifiers;
|
||||
InputEvent::ModifiersChanged(modifiers_changed)
|
||||
PlatformInput::ModifiersChanged(modifiers_changed)
|
||||
}
|
||||
InputEvent::ScrollWheel(scroll_wheel) => {
|
||||
PlatformInput::ScrollWheel(scroll_wheel) => {
|
||||
self.window.mouse_position = scroll_wheel.position;
|
||||
self.window.modifiers = scroll_wheel.modifiers;
|
||||
InputEvent::ScrollWheel(scroll_wheel)
|
||||
PlatformInput::ScrollWheel(scroll_wheel)
|
||||
}
|
||||
// Translate dragging and dropping of external files from the operating system
|
||||
// to internal drag and drop events.
|
||||
InputEvent::FileDrop(file_drop) => match file_drop {
|
||||
PlatformInput::FileDrop(file_drop) => match file_drop {
|
||||
FileDropEvent::Entered { position, paths } => {
|
||||
self.window.mouse_position = position;
|
||||
if self.active_drag.is_none() {
|
||||
|
@ -1669,7 +1670,7 @@ impl<'a> WindowContext<'a> {
|
|||
cursor_offset: position,
|
||||
});
|
||||
}
|
||||
InputEvent::MouseMove(MouseMoveEvent {
|
||||
PlatformInput::MouseMove(MouseMoveEvent {
|
||||
position,
|
||||
pressed_button: Some(MouseButton::Left),
|
||||
modifiers: Modifiers::default(),
|
||||
|
@ -1677,7 +1678,7 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
FileDropEvent::Pending { position } => {
|
||||
self.window.mouse_position = position;
|
||||
InputEvent::MouseMove(MouseMoveEvent {
|
||||
PlatformInput::MouseMove(MouseMoveEvent {
|
||||
position,
|
||||
pressed_button: Some(MouseButton::Left),
|
||||
modifiers: Modifiers::default(),
|
||||
|
@ -1686,21 +1687,21 @@ impl<'a> WindowContext<'a> {
|
|||
FileDropEvent::Submit { position } => {
|
||||
self.activate(true);
|
||||
self.window.mouse_position = position;
|
||||
InputEvent::MouseUp(MouseUpEvent {
|
||||
PlatformInput::MouseUp(MouseUpEvent {
|
||||
button: MouseButton::Left,
|
||||
position,
|
||||
modifiers: Modifiers::default(),
|
||||
click_count: 1,
|
||||
})
|
||||
}
|
||||
FileDropEvent::Exited => InputEvent::MouseUp(MouseUpEvent {
|
||||
FileDropEvent::Exited => PlatformInput::MouseUp(MouseUpEvent {
|
||||
button: MouseButton::Left,
|
||||
position: Point::default(),
|
||||
modifiers: Modifiers::default(),
|
||||
click_count: 1,
|
||||
}),
|
||||
},
|
||||
InputEvent::KeyDown(_) | InputEvent::KeyUp(_) => event,
|
||||
PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event,
|
||||
};
|
||||
|
||||
if let Some(any_mouse_event) = event.mouse_event() {
|
||||
|
@ -2983,7 +2984,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
/// Add a listener for any mouse event that occurs in the window.
|
||||
/// This is a fairly low level method.
|
||||
/// Typically, you'll want to use methods on UI elements, which perform bounds checking etc.
|
||||
pub fn on_mouse_event<Event: 'static>(
|
||||
pub fn on_mouse_event<Event: MouseEvent>(
|
||||
&mut self,
|
||||
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
|
||||
) {
|
||||
|
@ -2996,7 +2997,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
}
|
||||
|
||||
/// Register a callback to be invoked when the given Key Event is dispatched to the window.
|
||||
pub fn on_key_event<Event: 'static>(
|
||||
pub fn on_key_event<Event: KeyEvent>(
|
||||
&mut self,
|
||||
handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
|
||||
) {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50",
|
||||
"version": "1.1.4"
|
||||
"revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff",
|
||||
"version": "1.0.12"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -24,8 +24,8 @@
|
|||
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08",
|
||||
"version": "114.5735.8"
|
||||
"revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65",
|
||||
"version": "104.5112.17"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ let package = Package(
|
|||
targets: ["LiveKitBridge"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")),
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
|
|
|
@ -39,6 +39,7 @@ type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppCon
|
|||
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
|
||||
type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
|
||||
|
||||
/// Kind of language server stdio given to an IO handler.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum IoKind {
|
||||
StdOut,
|
||||
|
@ -46,12 +47,15 @@ pub enum IoKind {
|
|||
StdErr,
|
||||
}
|
||||
|
||||
/// Represents a launchable language server. This can either be a standalone binary or the path
|
||||
/// to a runtime with arguments to instruct it to launch the actual language server file.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct LanguageServerBinary {
|
||||
pub path: PathBuf,
|
||||
pub arguments: Vec<OsString>,
|
||||
}
|
||||
|
||||
/// A running language server process.
|
||||
pub struct LanguageServer {
|
||||
server_id: LanguageServerId,
|
||||
next_id: AtomicUsize,
|
||||
|
@ -70,10 +74,12 @@ pub struct LanguageServer {
|
|||
_server: Option<Mutex<Child>>,
|
||||
}
|
||||
|
||||
/// Identifies a running language server.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct LanguageServerId(pub usize);
|
||||
|
||||
/// Handle to a language server RPC activity subscription.
|
||||
pub enum Subscription {
|
||||
Notification {
|
||||
method: &'static str,
|
||||
|
@ -85,6 +91,9 @@ pub enum Subscription {
|
|||
},
|
||||
}
|
||||
|
||||
/// Language server protocol RPC request message.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Request<'a, T> {
|
||||
jsonrpc: &'static str,
|
||||
|
@ -93,6 +102,7 @@ pub struct Request<'a, T> {
|
|||
params: T,
|
||||
}
|
||||
|
||||
/// Language server protocol RPC request response message before it is deserialized into a concrete type.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AnyResponse<'a> {
|
||||
jsonrpc: &'a str,
|
||||
|
@ -103,6 +113,9 @@ struct AnyResponse<'a> {
|
|||
result: Option<&'a RawValue>,
|
||||
}
|
||||
|
||||
/// Language server protocol RPC request response message.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage)
|
||||
#[derive(Serialize)]
|
||||
struct Response<T> {
|
||||
jsonrpc: &'static str,
|
||||
|
@ -111,6 +124,9 @@ struct Response<T> {
|
|||
error: Option<Error>,
|
||||
}
|
||||
|
||||
/// Language server protocol RPC notification message.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage)
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Notification<'a, T> {
|
||||
jsonrpc: &'static str,
|
||||
|
@ -119,6 +135,7 @@ struct Notification<'a, T> {
|
|||
params: T,
|
||||
}
|
||||
|
||||
/// Language server RPC notification message before it is deserialized into a concrete type.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct AnyNotification<'a> {
|
||||
#[serde(default)]
|
||||
|
@ -135,6 +152,7 @@ struct Error {
|
|||
}
|
||||
|
||||
impl LanguageServer {
|
||||
/// Starts a language server process.
|
||||
pub fn new(
|
||||
stderr_capture: Arc<Mutex<Option<String>>>,
|
||||
server_id: LanguageServerId,
|
||||
|
@ -277,6 +295,7 @@ impl LanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// List of code action kinds this language server reports being able to emit.
|
||||
pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
self.code_action_kinds.clone()
|
||||
}
|
||||
|
@ -427,9 +446,10 @@ impl LanguageServer {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes a language server.
|
||||
/// Note that `options` is used directly to construct [`InitializeParams`],
|
||||
/// which is why it is owned.
|
||||
/// Initializes a language server by sending the `Initialize` request.
|
||||
/// Note that `options` is used directly to construct [`InitializeParams`], which is why it is owned.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)
|
||||
pub async fn initialize(mut self, options: Option<Value>) -> Result<Arc<Self>> {
|
||||
let root_uri = Url::from_file_path(&self.root_path).unwrap();
|
||||
#[allow(deprecated)]
|
||||
|
@ -564,6 +584,7 @@ impl LanguageServer {
|
|||
Ok(Arc::new(self))
|
||||
}
|
||||
|
||||
/// Sends a shutdown request to the language server process and prepares the `LanguageServer` to be dropped.
|
||||
pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Option<()>>> {
|
||||
if let Some(tasks) = self.io_tasks.lock().take() {
|
||||
let response_handlers = self.response_handlers.clone();
|
||||
|
@ -598,6 +619,9 @@ impl LanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Register a handler to handle incoming LSP notifications.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage)
|
||||
#[must_use]
|
||||
pub fn on_notification<T, F>(&self, f: F) -> Subscription
|
||||
where
|
||||
|
@ -607,6 +631,9 @@ impl LanguageServer {
|
|||
self.on_custom_notification(T::METHOD, f)
|
||||
}
|
||||
|
||||
/// Register a handler to handle incoming LSP requests.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
|
||||
#[must_use]
|
||||
pub fn on_request<T, F, Fut>(&self, f: F) -> Subscription
|
||||
where
|
||||
|
@ -618,6 +645,7 @@ impl LanguageServer {
|
|||
self.on_custom_request(T::METHOD, f)
|
||||
}
|
||||
|
||||
/// Register a handler to inspect all language server process stdio.
|
||||
#[must_use]
|
||||
pub fn on_io<F>(&self, f: F) -> Subscription
|
||||
where
|
||||
|
@ -631,20 +659,23 @@ impl LanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Removes a request handler registers via [Self::on_request].
|
||||
pub fn remove_request_handler<T: request::Request>(&self) {
|
||||
self.notification_handlers.lock().remove(T::METHOD);
|
||||
}
|
||||
|
||||
/// Removes a notification handler registers via [Self::on_notification].
|
||||
pub fn remove_notification_handler<T: notification::Notification>(&self) {
|
||||
self.notification_handlers.lock().remove(T::METHOD);
|
||||
}
|
||||
|
||||
/// Checks if a notification handler has been registered via [Self::on_notification].
|
||||
pub fn has_notification_handler<T: notification::Notification>(&self) -> bool {
|
||||
self.notification_handlers.lock().contains_key(T::METHOD)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
|
||||
fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(Params, AsyncAppContext) + Send,
|
||||
Params: DeserializeOwned,
|
||||
|
@ -668,11 +699,7 @@ impl LanguageServer {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_custom_request<Params, Res, Fut, F>(
|
||||
&self,
|
||||
method: &'static str,
|
||||
mut f: F,
|
||||
) -> Subscription
|
||||
fn on_custom_request<Params, Res, Fut, F>(&self, method: &'static str, mut f: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(Params, AsyncAppContext) -> Fut + Send,
|
||||
Fut: 'static + Future<Output = Result<Res>>,
|
||||
|
@ -750,22 +777,29 @@ impl LanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the name of the running language server.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Get the reported capabilities of the running language server.
|
||||
pub fn capabilities(&self) -> &ServerCapabilities {
|
||||
&self.capabilities
|
||||
}
|
||||
|
||||
/// Get the id of the running language server.
|
||||
pub fn server_id(&self) -> LanguageServerId {
|
||||
self.server_id
|
||||
}
|
||||
|
||||
/// Get the root path of the project the language server is running against.
|
||||
pub fn root_path(&self) -> &PathBuf {
|
||||
&self.root_path
|
||||
}
|
||||
|
||||
/// Sends a RPC request to the language server.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
|
||||
pub fn request<T: request::Request>(
|
||||
&self,
|
||||
params: T::Params,
|
||||
|
@ -851,6 +885,9 @@ impl LanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sends a RPC notification to the language server.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage)
|
||||
pub fn notify<T: notification::Notification>(&self, params: T::Params) -> Result<()> {
|
||||
Self::notify_internal::<T>(&self.outbound_tx, params)
|
||||
}
|
||||
|
@ -879,6 +916,7 @@ impl Drop for LanguageServer {
|
|||
}
|
||||
|
||||
impl Subscription {
|
||||
/// Detaching a subscription handle prevents it from unsubscribing on drop.
|
||||
pub fn detach(&mut self) {
|
||||
match self {
|
||||
Subscription::Notification {
|
||||
|
@ -925,6 +963,7 @@ impl Drop for Subscription {
|
|||
}
|
||||
}
|
||||
|
||||
/// Mock language server for use in tests.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Clone)]
|
||||
pub struct FakeLanguageServer {
|
||||
|
@ -946,6 +985,7 @@ impl LanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Construct a fake language server.
|
||||
pub fn fake(
|
||||
name: String,
|
||||
capabilities: ServerCapabilities,
|
||||
|
@ -1015,10 +1055,12 @@ impl LanguageServer {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeLanguageServer {
|
||||
/// See [LanguageServer::notify]
|
||||
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
||||
self.server.notify::<T>(params).ok();
|
||||
}
|
||||
|
||||
/// See [LanguageServer::request]
|
||||
pub async fn request<T>(&self, params: T::Params) -> Result<T::Result>
|
||||
where
|
||||
T: request::Request,
|
||||
|
@ -1028,11 +1070,13 @@ impl FakeLanguageServer {
|
|||
self.server.request::<T>(params).await
|
||||
}
|
||||
|
||||
/// Attempts [try_receive_notification], unwrapping if it has not received the specified type yet.
|
||||
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
|
||||
self.server.executor.start_waiting();
|
||||
self.try_receive_notification::<T>().await.unwrap()
|
||||
}
|
||||
|
||||
/// Consumes the notification channel until it finds a notification for the specified type.
|
||||
pub async fn try_receive_notification<T: notification::Notification>(
|
||||
&mut self,
|
||||
) -> Option<T::Params> {
|
||||
|
@ -1048,6 +1092,7 @@ impl FakeLanguageServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Registers a handler for a specific kind of request. Removes any existing handler for specified request type.
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
mut handler: F,
|
||||
|
@ -1076,6 +1121,7 @@ impl FakeLanguageServer {
|
|||
responded_rx
|
||||
}
|
||||
|
||||
/// Registers a handler for a specific kind of notification. Removes any existing handler for specified notification type.
|
||||
pub fn handle_notification<T, F>(
|
||||
&self,
|
||||
mut handler: F,
|
||||
|
@ -1096,6 +1142,7 @@ impl FakeLanguageServer {
|
|||
handled_rx
|
||||
}
|
||||
|
||||
/// Removes any existing handler for specified notification type.
|
||||
pub fn remove_request_handler<T>(&mut self)
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
|
@ -1103,6 +1150,7 @@ impl FakeLanguageServer {
|
|||
self.server.remove_request_handler::<T>();
|
||||
}
|
||||
|
||||
/// Simulate that the server has started work and notifies about its progress with the specified token.
|
||||
pub async fn start_progress(&self, token: impl Into<String>) {
|
||||
let token = token.into();
|
||||
self.request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
|
||||
|
@ -1116,6 +1164,7 @@ impl FakeLanguageServer {
|
|||
});
|
||||
}
|
||||
|
||||
/// Simulate that the server has completed work and notifies about that with the specified token.
|
||||
pub fn end_progress(&self, token: impl Into<String>) {
|
||||
self.notify::<notification::Progress>(ProgressParams {
|
||||
token: NumberOrString::String(token.into()),
|
||||
|
|
|
@ -429,6 +429,11 @@ pub trait SearchActionsRegistrar {
|
|||
&mut self,
|
||||
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
|
||||
);
|
||||
|
||||
fn register_handler_for_dismissed_search<A: Action>(
|
||||
&mut self,
|
||||
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
|
||||
);
|
||||
}
|
||||
|
||||
type GetSearchBar<T> =
|
||||
|
@ -457,16 +462,62 @@ impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
|
|||
}
|
||||
|
||||
impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
|
||||
fn register_handler<A: gpui::Action>(
|
||||
fn register_handler<A: Action>(
|
||||
&mut self,
|
||||
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
|
||||
) {
|
||||
let getter = self.search_getter;
|
||||
self.div = self.div.take().map(|div| {
|
||||
div.on_action(self.cx.listener(move |this, action, cx| {
|
||||
(getter)(this, cx)
|
||||
let should_notify = (getter)(this, cx)
|
||||
.clone()
|
||||
.map(|search_bar| search_bar.update(cx, |this, cx| callback(this, action, cx)));
|
||||
.map(|search_bar| {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
if search_bar.is_dismissed()
|
||||
|| search_bar.active_searchable_item.is_none()
|
||||
{
|
||||
false
|
||||
} else {
|
||||
callback(search_bar, action, cx);
|
||||
true
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if should_notify {
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
fn register_handler_for_dismissed_search<A: Action>(
|
||||
&mut self,
|
||||
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
|
||||
) {
|
||||
let getter = self.search_getter;
|
||||
self.div = self.div.take().map(|div| {
|
||||
div.on_action(self.cx.listener(move |this, action, cx| {
|
||||
let should_notify = (getter)(this, cx)
|
||||
.clone()
|
||||
.map(|search_bar| {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
if search_bar.is_dismissed() {
|
||||
callback(search_bar, action, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if should_notify {
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
@ -488,44 +539,86 @@ impl SearchActionsRegistrar for Workspace {
|
|||
pane.update(cx, move |this, cx| {
|
||||
this.toolbar().update(cx, move |this, cx| {
|
||||
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, move |this, cx| callback(this, action, cx));
|
||||
cx.notify();
|
||||
let should_notify = search_bar.update(cx, move |search_bar, cx| {
|
||||
if search_bar.is_dismissed()
|
||||
|| search_bar.active_searchable_item.is_none()
|
||||
{
|
||||
false
|
||||
} else {
|
||||
callback(search_bar, action, cx);
|
||||
true
|
||||
}
|
||||
});
|
||||
if should_notify {
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn register_handler_for_dismissed_search<A: Action>(
|
||||
&mut self,
|
||||
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
|
||||
) {
|
||||
self.register_action(move |workspace, action: &A, cx| {
|
||||
if workspace.has_active_modal(cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
let pane = workspace.active_pane();
|
||||
pane.update(cx, move |this, cx| {
|
||||
this.toolbar().update(cx, move |this, cx| {
|
||||
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
|
||||
let should_notify = search_bar.update(cx, move |search_bar, cx| {
|
||||
if search_bar.is_dismissed() {
|
||||
callback(search_bar, action, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if should_notify {
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferSearchBar {
|
||||
pub fn register_inner(registrar: &mut impl SearchActionsRegistrar) {
|
||||
pub fn register(registrar: &mut impl SearchActionsRegistrar) {
|
||||
registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| {
|
||||
if this.supported_options().case {
|
||||
this.toggle_case_sensitive(action, cx);
|
||||
}
|
||||
});
|
||||
|
||||
registrar.register_handler(|this, action: &ToggleWholeWord, cx| {
|
||||
if this.supported_options().word {
|
||||
this.toggle_whole_word(action, cx);
|
||||
}
|
||||
});
|
||||
|
||||
registrar.register_handler(|this, action: &ToggleReplace, cx| {
|
||||
if this.supported_options().replacement {
|
||||
this.toggle_replace(action, cx);
|
||||
}
|
||||
});
|
||||
|
||||
registrar.register_handler(|this, _: &ActivateRegexMode, cx| {
|
||||
if this.supported_options().regex {
|
||||
this.activate_search_mode(SearchMode::Regex, cx);
|
||||
}
|
||||
});
|
||||
|
||||
registrar.register_handler(|this, _: &ActivateTextMode, cx| {
|
||||
this.activate_search_mode(SearchMode::Text, cx);
|
||||
});
|
||||
|
||||
registrar.register_handler(|this, action: &CycleMode, cx| {
|
||||
if this.supported_options().regex {
|
||||
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
|
||||
|
@ -533,7 +626,6 @@ impl BufferSearchBar {
|
|||
this.cycle_mode(action, cx)
|
||||
}
|
||||
});
|
||||
|
||||
registrar.register_handler(|this, action: &SelectNextMatch, cx| {
|
||||
this.select_next_match(action, cx);
|
||||
});
|
||||
|
@ -544,19 +636,13 @@ impl BufferSearchBar {
|
|||
this.select_all_matches(action, cx);
|
||||
});
|
||||
registrar.register_handler(|this, _: &editor::Cancel, cx| {
|
||||
if this.dismissed {
|
||||
cx.propagate();
|
||||
} else {
|
||||
this.dismiss(&Dismiss, cx);
|
||||
}
|
||||
this.dismiss(&Dismiss, cx);
|
||||
});
|
||||
registrar.register_handler(|this, deploy, cx| {
|
||||
registrar.register_handler_for_dismissed_search(|this, deploy, cx| {
|
||||
this.deploy(deploy, cx);
|
||||
})
|
||||
}
|
||||
fn register(workspace: &mut Workspace) {
|
||||
Self::register_inner(workspace);
|
||||
}
|
||||
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let query_editor = cx.new_view(|cx| Editor::single_line(cx));
|
||||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
|
@ -1081,7 +1167,7 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
use editor::{DisplayPoint, Editor};
|
||||
use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
|
||||
use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
|
||||
use language::Buffer;
|
||||
use smol::stream::StreamExt as _;
|
||||
use unindent::Unindent as _;
|
||||
|
@ -1114,7 +1200,7 @@ mod tests {
|
|||
.unindent(),
|
||||
)
|
||||
});
|
||||
let (_, cx) = cx.add_window_view(|_| EmptyView {});
|
||||
let cx = cx.add_empty_window();
|
||||
let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
|
||||
|
||||
let search_bar = cx.new_view(|cx| {
|
||||
|
@ -1461,7 +1547,7 @@ mod tests {
|
|||
"Should pick a query with multiple results"
|
||||
);
|
||||
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
|
||||
let window = cx.add_window(|_| EmptyView {});
|
||||
let window = cx.add_window(|_| ());
|
||||
|
||||
let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
|
||||
|
||||
|
@ -1657,7 +1743,7 @@ mod tests {
|
|||
"#
|
||||
.unindent();
|
||||
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
|
||||
let (_, cx) = cx.add_window_view(|_| EmptyView {});
|
||||
let cx = cx.add_empty_window();
|
||||
|
||||
let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
|
||||
|
||||
|
|
|
@ -12,10 +12,10 @@ use editor::{
|
|||
};
|
||||
use editor::{EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
actions, div, AnyElement, AnyView, AppContext, Context as _, Element, EntityId, EventEmitter,
|
||||
FocusHandle, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement, IntoElement,
|
||||
KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled,
|
||||
Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView,
|
||||
actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId,
|
||||
EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement,
|
||||
IntoElement, KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString,
|
||||
Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView,
|
||||
WhiteSpace, WindowContext,
|
||||
};
|
||||
use menu::Confirm;
|
||||
|
@ -36,6 +36,7 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use workspace::{DeploySearch, NewSearch};
|
||||
|
||||
use ui::{
|
||||
h_flex, prelude::*, v_flex, Icon, IconButton, IconName, Label, LabelCommon, LabelSize,
|
||||
|
@ -60,10 +61,64 @@ struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
|
|||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(ActiveSettings::default());
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace
|
||||
.register_action(ProjectSearchView::new_search)
|
||||
.register_action(ProjectSearchView::deploy_search)
|
||||
.register_action(ProjectSearchBar::search_in_new);
|
||||
register_workspace_action(workspace, move |search_bar, _: &ToggleFilters, cx| {
|
||||
search_bar.toggle_filters(cx);
|
||||
});
|
||||
register_workspace_action(workspace, move |search_bar, _: &ToggleCaseSensitive, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
|
||||
});
|
||||
register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, cx| {
|
||||
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
|
||||
});
|
||||
register_workspace_action(workspace, move |search_bar, action: &ToggleReplace, cx| {
|
||||
search_bar.toggle_replace(action, cx)
|
||||
});
|
||||
register_workspace_action(workspace, move |search_bar, _: &ActivateRegexMode, cx| {
|
||||
search_bar.activate_search_mode(SearchMode::Regex, cx)
|
||||
});
|
||||
register_workspace_action(workspace, move |search_bar, _: &ActivateTextMode, cx| {
|
||||
search_bar.activate_search_mode(SearchMode::Text, cx)
|
||||
});
|
||||
register_workspace_action(
|
||||
workspace,
|
||||
move |search_bar, _: &ActivateSemanticMode, cx| {
|
||||
search_bar.activate_search_mode(SearchMode::Semantic, cx)
|
||||
},
|
||||
);
|
||||
register_workspace_action(workspace, move |search_bar, action: &CycleMode, cx| {
|
||||
search_bar.cycle_mode(action, cx)
|
||||
});
|
||||
register_workspace_action(
|
||||
workspace,
|
||||
move |search_bar, action: &SelectNextMatch, cx| {
|
||||
search_bar.select_next_match(action, cx)
|
||||
},
|
||||
);
|
||||
register_workspace_action(
|
||||
workspace,
|
||||
move |search_bar, action: &SelectPrevMatch, cx| {
|
||||
search_bar.select_prev_match(action, cx)
|
||||
},
|
||||
);
|
||||
|
||||
register_workspace_action_for_dismissed_search(
|
||||
workspace,
|
||||
move |workspace, action: &NewSearch, cx| {
|
||||
ProjectSearchView::new_search(workspace, action, cx)
|
||||
},
|
||||
);
|
||||
register_workspace_action_for_dismissed_search(
|
||||
workspace,
|
||||
move |workspace, action: &DeploySearch, cx| {
|
||||
ProjectSearchView::deploy_search(workspace, action, cx)
|
||||
},
|
||||
);
|
||||
register_workspace_action_for_dismissed_search(
|
||||
workspace,
|
||||
move |workspace, action: &SearchInNew, cx| {
|
||||
ProjectSearchView::search_in_new(workspace, action, cx)
|
||||
},
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -960,6 +1015,37 @@ impl ProjectSearchView {
|
|||
Self::existing_or_new_search(workspace, existing, cx)
|
||||
}
|
||||
|
||||
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(search_view) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
let new_query = search_view.update(cx, |search_view, cx| {
|
||||
let new_query = search_view.build_search_query(cx);
|
||||
if new_query.is_some() {
|
||||
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
|
||||
search_view.query_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(old_query.as_str(), cx);
|
||||
});
|
||||
search_view.search_options = SearchOptions::from_query(&old_query);
|
||||
}
|
||||
}
|
||||
new_query
|
||||
});
|
||||
if let Some(new_query) = new_query {
|
||||
let model = cx.new_model(|cx| {
|
||||
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
|
||||
model.search(new_query, cx);
|
||||
model
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add another search tab to the workspace.
|
||||
fn new_search(
|
||||
workspace: &mut Workspace,
|
||||
|
@ -1262,17 +1348,11 @@ impl ProjectSearchView {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for ProjectSearchBar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectSearchBar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active_project_search: Default::default(),
|
||||
subscription: Default::default(),
|
||||
active_project_search: None,
|
||||
subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1303,37 +1383,6 @@ impl ProjectSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(search_view) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
let new_query = search_view.update(cx, |search_view, cx| {
|
||||
let new_query = search_view.build_search_query(cx);
|
||||
if new_query.is_some() {
|
||||
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
|
||||
search_view.query_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(old_query.as_str(), cx);
|
||||
});
|
||||
search_view.search_options = SearchOptions::from_query(&old_query);
|
||||
}
|
||||
}
|
||||
new_query
|
||||
});
|
||||
if let Some(new_query) = new_query {
|
||||
let model = cx.new_model(|cx| {
|
||||
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
|
||||
model.search(new_query, cx);
|
||||
model
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
||||
self.cycle_field(Direction::Next, cx);
|
||||
}
|
||||
|
@ -1502,6 +1551,22 @@ impl ProjectSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
|
||||
if let Some(search) = self.active_project_search.as_ref() {
|
||||
search.update(cx, |this, cx| {
|
||||
this.select_match(Direction::Next, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
|
||||
if let Some(search) = self.active_project_search.as_ref() {
|
||||
search.update(cx, |this, cx| {
|
||||
this.select_match(Direction::Prev, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn new_placeholder_text(&self, cx: &mut ViewContext<Self>) -> Option<String> {
|
||||
let previous_query_keystrokes = cx
|
||||
.bindings_for_action(&PreviousHistoryQuery {})
|
||||
|
@ -1870,6 +1935,8 @@ impl Render for ProjectSearchBar {
|
|||
}))
|
||||
})
|
||||
})
|
||||
.on_action(cx.listener(Self::select_next_match))
|
||||
.on_action(cx.listener(Self::select_prev_match))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
|
@ -1963,6 +2030,60 @@ impl ToolbarItemView for ProjectSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
fn register_workspace_action<A: Action>(
|
||||
workspace: &mut Workspace,
|
||||
callback: fn(&mut ProjectSearchBar, &A, &mut ViewContext<ProjectSearchBar>),
|
||||
) {
|
||||
workspace.register_action(move |workspace, action: &A, cx| {
|
||||
if workspace.has_active_modal(cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, move |workspace, cx| {
|
||||
if let Some(search_bar) = workspace.item_of_type::<ProjectSearchBar>() {
|
||||
search_bar.update(cx, move |search_bar, cx| {
|
||||
if search_bar.active_project_search.is_some() {
|
||||
callback(search_bar, action, cx);
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn register_workspace_action_for_dismissed_search<A: Action>(
|
||||
workspace: &mut Workspace,
|
||||
callback: fn(&mut Workspace, &A, &mut ViewContext<Workspace>),
|
||||
) {
|
||||
workspace.register_action(move |workspace, action: &A, cx| {
|
||||
if workspace.has_active_modal(cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
let should_notify = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<ProjectSearchBar>()
|
||||
.map(|search_bar| search_bar.read(cx).active_project_search.is_none())
|
||||
.unwrap_or(false);
|
||||
if should_notify {
|
||||
callback(workspace, action, cx);
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -387,7 +387,7 @@ impl Render for TerminalPanel {
|
|||
},
|
||||
cx,
|
||||
);
|
||||
BufferSearchBar::register_inner(&mut registrar);
|
||||
BufferSearchBar::register(&mut registrar);
|
||||
registrar.into_div().size_full().child(self.pane.clone())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ impl Render for IconButtonStory {
|
|||
|
||||
StoryContainer::new(
|
||||
"Icon Button",
|
||||
"crates/ui2/src/components/stories/icon_button.rs",
|
||||
"crates/ui/src/components/stories/icon_button.rs",
|
||||
)
|
||||
.child(StorySection::new().children(buttons))
|
||||
.child(
|
||||
|
|
|
@ -9,7 +9,7 @@ impl Render for ToggleButtonStory {
|
|||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
StoryContainer::new(
|
||||
"Toggle Button",
|
||||
"crates/ui2/src/components/stories/toggle_button.rs",
|
||||
"crates/ui/src/components/stories/toggle_button.rs",
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
|
|
|
@ -4,12 +4,14 @@ use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView};
|
|||
|
||||
use crate::{state::Mode, Vim};
|
||||
|
||||
/// The ModeIndicator displays the current mode in the status bar.
|
||||
pub struct ModeIndicator {
|
||||
pub mode: Option<Mode>,
|
||||
pub(crate) mode: Option<Mode>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ModeIndicator {
|
||||
/// Construct a new mode indicator in this window.
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let _subscriptions = vec![
|
||||
cx.observe_global::<Vim>(|this, cx| this.update_mode(cx)),
|
||||
|
@ -37,13 +39,6 @@ impl ModeIndicator {
|
|||
self.mode = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
if self.mode != Some(mode) {
|
||||
self.mode = Some(mode);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ModeIndicator {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Vim support for Zed.
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
|
@ -38,12 +40,18 @@ use crate::state::ReplayableAction;
|
|||
/// Default: false
|
||||
pub struct VimModeSetting(pub bool);
|
||||
|
||||
/// An Action to Switch between modes
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct SwitchMode(pub Mode);
|
||||
|
||||
/// PushOperator is used to put vim into a "minor" mode,
|
||||
/// where it's waiting for a specific next set of keystrokes.
|
||||
/// For example 'd' needs a motion to complete.
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct PushOperator(pub Operator);
|
||||
|
||||
/// Number is used to manage vim's count. Pushing a digit
|
||||
/// multiplis the current value by 10 and adds the digit.
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
struct Number(usize);
|
||||
|
||||
|
@ -51,11 +59,13 @@ actions!(
|
|||
vim,
|
||||
[Tab, Enter, Object, InnerObject, FindForward, FindBackward]
|
||||
);
|
||||
|
||||
// in the workspace namespace so it's not filtered out when vim is disabled.
|
||||
actions!(workspace, [ToggleVimMode]);
|
||||
|
||||
impl_actions!(vim, [SwitchMode, PushOperator, Number]);
|
||||
|
||||
/// Initializes the `vim` crate.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(Vim::default());
|
||||
VimModeSetting::register(cx);
|
||||
|
@ -119,6 +129,7 @@ fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
|||
visual::register(workspace, cx);
|
||||
}
|
||||
|
||||
/// Registers a keystroke observer to observe keystrokes for the Vim integration.
|
||||
pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
cx.observe_keystrokes(|keystroke_event, cx| {
|
||||
if let Some(action) = keystroke_event
|
||||
|
@ -160,6 +171,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
|||
.detach()
|
||||
}
|
||||
|
||||
/// The state pertaining to Vim mode. Stored as a global.
|
||||
#[derive(Default)]
|
||||
pub struct Vim {
|
||||
active_editor: Option<WeakView<Editor>>,
|
||||
|
@ -251,6 +263,8 @@ impl Vim {
|
|||
Some(editor.update(cx, update))
|
||||
}
|
||||
|
||||
/// When doing an action that modifies the buffer, we start recording so that `.`
|
||||
/// will replay the action.
|
||||
pub fn start_recording(&mut self, cx: &mut WindowContext) {
|
||||
if !self.workspace_state.replaying {
|
||||
self.workspace_state.recording = true;
|
||||
|
@ -295,12 +309,19 @@ impl Vim {
|
|||
}
|
||||
}
|
||||
|
||||
/// When finishing an action that modifies the buffer, stop recording.
|
||||
/// as you usually call this within a keystroke handler we also ensure that
|
||||
/// the current action is recorded.
|
||||
pub fn stop_recording(&mut self) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state.stop_recording_after_next_action = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops recording actions immediately rather than waiting until after the
|
||||
/// next action to stop recording.
|
||||
///
|
||||
/// This doesn't include the current action.
|
||||
pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state
|
||||
|
@ -311,6 +332,7 @@ impl Vim {
|
|||
}
|
||||
}
|
||||
|
||||
/// Explicitly record one action (equiavlent to start_recording and stop_recording)
|
||||
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
|
||||
self.start_recording(cx);
|
||||
self.stop_recording();
|
||||
|
@ -516,6 +538,7 @@ impl Vim {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the state of the active editor.
|
||||
pub fn state(&self) -> &EditorState {
|
||||
if let Some(active_editor) = self.active_editor.as_ref() {
|
||||
if let Some(state) = self.editor_states.get(&active_editor.entity_id()) {
|
||||
|
@ -526,6 +549,7 @@ impl Vim {
|
|||
&self.default_state
|
||||
}
|
||||
|
||||
/// Updates the state of the active editor.
|
||||
pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
|
||||
let mut state = self.state().clone();
|
||||
let ret = func(&mut state);
|
||||
|
|
|
@ -1809,9 +1809,9 @@ mod tests {
|
|||
assert!(workspace.active_item(cx).is_none());
|
||||
})
|
||||
.unwrap();
|
||||
editor_1.assert_dropped();
|
||||
editor_2.assert_dropped();
|
||||
buffer.assert_dropped();
|
||||
editor_1.assert_released();
|
||||
editor_2.assert_released();
|
||||
buffer.assert_released();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -87,10 +87,14 @@ This means that when releasing a new version of Zed that has changes to the RPC
|
|||
|
||||
1. This script will make local changes only, and print out a shell command that you can use to push the branch and tag.
|
||||
1. Pushing the new tag will trigger a CI build that, when finished will upload a new versioned docker image to the DigitalOcean docker registry.
|
||||
1. Once that CI job completes, you will be able to run the following command to deploy that docker image. The script takes two arguments: an environment (`production`, `preview`, or `staging`), and a version number (e.g. `0.10.1`).
|
||||
1. If needing a migration:
|
||||
- First check that the migration is valid. The database serves both preview and stable simultaneously, so new columns need to have defaults and old tables or columns can't be dropped.
|
||||
- Then use `script/deploy-migration` <release channel> <version number> (production, staging, preview, nightly). ex: `script/deploy-migration preview 0.19.0`
|
||||
- If there is an 'Error: container is waiting to start', you can review logs manually with: `kubectl --namespace <enviroment> logs <pod name>` to make sure the mgiration ran successfully.
|
||||
1. Once that CI job completes, you will be able to run the following command to deploy that docker image. The script takes two arguments: an environment (`production`, `preview`, or `staging`), and a version number (e.g. `0.10.1`):
|
||||
|
||||
```
|
||||
script/deploy preview 0.10.1
|
||||
```
|
||||
```
|
||||
script/deploy preview 0.10.1
|
||||
```
|
||||
|
||||
1. This command should complete quickly, updating the given environment to use the given version number of the `collab` server.
|
||||
1. This command should complete quickly, updating the given environment to use the given version number of the `collab` server.
|
|
@ -1,94 +1,120 @@
|
|||
# Building Zed
|
||||
|
||||
🚧 TODO:
|
||||
- [ ] Tidy up & update instructions
|
||||
|
||||
- [ ] Remove ZI-specific things
|
||||
- [ ] Rework any steps that currently require a ZI-specific account
|
||||
|
||||
How to build Zed from source for the first time.
|
||||
|
||||
## Prerequisites
|
||||
### Prerequisites
|
||||
|
||||
🚧 TODO 🚧 Update for open source
|
||||
|
||||
- Be added to the GitHub organization
|
||||
- Be added to the Vercel team
|
||||
- Create a [Personal Access Token](https://github.com/settings/personal-access-tokens/new) on Github
|
||||
- 🚧 TODO 🚧 What permissions are required?
|
||||
- 🚧 TODO 🚧 What changes when repo isn't private?
|
||||
- Go to https://github.com/settings/tokens and Generate new token
|
||||
- GitHub currently provides two kinds of tokens:
|
||||
- Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected
|
||||
Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories
|
||||
- (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos
|
||||
- Keep the token in the browser tab/editor for the next two steps
|
||||
|
||||
## Process
|
||||
### Dependencies
|
||||
|
||||
Expect this to take 30min to an hour! Some of these steps will take quite a while based on your connection speed, and how long your first build will be.
|
||||
- Install [Rust](https://www.rust-lang.org/tools/install)
|
||||
|
||||
- Install the [GitHub CLI](https://cli.github.com/), [Livekit](https://formulae.brew.sh/formula/livekit) & [Foreman](https://formulae.brew.sh/formula/foreman)
|
||||
|
||||
```bash
|
||||
brew install gh
|
||||
brew install livekit
|
||||
brew install foreman
|
||||
```
|
||||
|
||||
- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store
|
||||
|
||||
- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/)
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
- If `xcode-select --print-path prints /Library/Developer/CommandLineTools…` run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.`
|
||||
|
||||
* Install [Postgres](https://postgresapp.com)
|
||||
|
||||
* Install the wasm toolchain
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-wasi
|
||||
```
|
||||
|
||||
### Building Zed from Source
|
||||
|
||||
1. Install the [GitHub CLI](https://cli.github.com/):
|
||||
- `brew install gh`
|
||||
1. Clone the `zed` repo
|
||||
- `gh repo clone zed-industries/zed`
|
||||
1. Install Xcode from the macOS App Store
|
||||
1. Install Xcode command line tools
|
||||
- `xcode-select --install`
|
||||
- If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.`
|
||||
1. Install [Postgres](https://postgresapp.com)
|
||||
1. Install rust/rustup
|
||||
- `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
|
||||
1. Install the wasm toolchain
|
||||
- `rustup target add wasm32-wasi`
|
||||
1. Install Livekit & Foreman
|
||||
- `brew install livekit`
|
||||
- `brew install foreman`
|
||||
1. Generate an GitHub API Key
|
||||
- Go to https://github.com/settings/tokens and Generate new token
|
||||
- GitHub currently provides two kinds of tokens:
|
||||
- Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected
|
||||
Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories
|
||||
- (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos
|
||||
- Keep the token in the browser tab/editor for the next two steps
|
||||
|
||||
```bash
|
||||
gh repo clone zed-industries/zed
|
||||
```
|
||||
|
||||
1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken`
|
||||
1. Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies:
|
||||
```
|
||||
cd ..
|
||||
git clone https://github.com/zed-industries/zed.dev
|
||||
cd zed.dev && npm install
|
||||
npm install -g vercel
|
||||
```
|
||||
1. Link your zed.dev project to Vercel
|
||||
- `vercel link`
|
||||
- Select the `zed-industries` team. If you don't have this get someone on the team to add you to it.
|
||||
- Select the `zed.dev` project
|
||||
1. Run `vercel pull` to pull down the environment variables and project info from Vercel
|
||||
1. (🚧 TODO 🚧 - Will this be relevant for open source?) Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
git clone https://github.com/zed-industries/zed.dev
|
||||
cd zed.dev && npm install
|
||||
pnpm install -g vercel
|
||||
```
|
||||
|
||||
1. (🚧 TODO 🚧 - Will this be relevant for open source?) Link your zed.dev project to Vercel
|
||||
|
||||
- `vercel link`
|
||||
- Select the `zed-industries` team. If you don't have this get someone on the team to add you to it.
|
||||
- Select the `zed.dev` project
|
||||
|
||||
1. (🚧 TODO 🚧 - Will this be relevant for open source?) Run `vercel pull` to pull down the environment variables and project info from Vercel
|
||||
1. Open Postgres.app
|
||||
1. From `./path/to/zed/`:
|
||||
- Run:
|
||||
- `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap`
|
||||
- Replace `{yourGithubAPIToken}` with the API token you generated above.
|
||||
- You don't need to include the GITHUB_TOKEN if you exported it above.
|
||||
- Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault).
|
||||
- If you get:
|
||||
- ```bash
|
||||
Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)!
|
||||
Please create a new installation in /opt/homebrew using one of the
|
||||
"Alternative Installs" from:
|
||||
https://docs.brew.sh/Installation
|
||||
```
|
||||
- In that case try:
|
||||
- `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
|
||||
- If Homebrew is not in your PATH:
|
||||
- Replace `{username}` with your home folder name (usually your login name)
|
||||
- `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile`
|
||||
- `eval "$(/opt/homebrew/bin/brew shellenv)"`
|
||||
1. From `./path/to/zed/` run `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap`
|
||||
|
||||
- You don't need to include the GITHUB_TOKEN if you exported it above.
|
||||
- Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault).
|
||||
|
||||
1. To run the Zed app:
|
||||
- If you are working on zed:
|
||||
- `cargo run`
|
||||
- If you are just using the latest version, but not working on zed:
|
||||
- `cargo run --release`
|
||||
- If you need to run the collaboration server locally:
|
||||
- `script/zed-local`
|
||||
- If you are working on zed:
|
||||
- `cargo run`
|
||||
- If you are just using the latest version, but not working on zed:
|
||||
- `cargo run --release`
|
||||
- If you need to run the collaboration server locally:
|
||||
- `script/zed-local`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`
|
||||
**`error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`**
|
||||
|
||||
- Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer`
|
||||
|
||||
### `xcrun: error: unable to find utility "metal", not a developer tool or in PATH`
|
||||
**`xcrun: error: unable to find utility "metal", not a developer tool or in PATH`**
|
||||
|
||||
### Seeding errors during `script/bootstrap` runs
|
||||
### `script/bootstrap`
|
||||
|
||||
```bash
|
||||
Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)!
|
||||
Please create a new installation in /opt/homebrew using one of the
|
||||
"Alternative Installs" from:
|
||||
https://docs.brew.sh/Installation
|
||||
```
|
||||
|
||||
- In that case try `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
|
||||
|
||||
- If Homebrew is not in your PATH:
|
||||
- Replace `{username}` with your home folder name (usually your login name)
|
||||
- `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile`
|
||||
- `eval "$(/opt/homebrew/bin/brew shellenv)"`
|
||||
|
||||
```
|
||||
seeding database...
|
||||
|
@ -104,4 +130,4 @@ Same command
|
|||
|
||||
### If you experience errors that mention some dependency is using unstable features
|
||||
|
||||
Try `cargo clean` and `cargo build`
|
||||
Try `cargo clean` and `cargo build`,
|
||||
|
|
|
@ -4,7 +4,7 @@ set -eu
|
|||
source script/lib/deploy-helpers.sh
|
||||
|
||||
if [[ $# < 2 ]]; then
|
||||
echo "Usage: $0 <production|staging|preview> <tag-name> (nightly is not yet supported)"
|
||||
echo "Usage: $0 <production|preview|nightly|staging> <tag-name>"
|
||||
exit 1
|
||||
fi
|
||||
environment=$1
|
||||
|
|
|
@ -4,16 +4,11 @@ set -eu
|
|||
source script/lib/deploy-helpers.sh
|
||||
|
||||
if [[ $# < 1 ]]; then
|
||||
echo "Usage: $0 <production|staging|preview> (nightly is not yet supported)"
|
||||
echo "Usage: $0 <production|preview|nightly|staging>"
|
||||
exit 1
|
||||
fi
|
||||
environment=$1
|
||||
|
||||
if [[ ${environment} == "nightly" ]]; then
|
||||
echo "nightly is not yet supported"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export_vars_for_environment ${environment}
|
||||
target_zed_kube_cluster
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue