Merge branch 'main' into cursors

This commit is contained in:
Conrad Irwin 2024-01-18 14:03:27 -07:00
commit bfee008bb2
199 changed files with 3365 additions and 1735 deletions

View file

@ -23,7 +23,7 @@ env:
jobs: jobs:
style: style:
name: Check formatting and Clippy lints name: Check formatting, Clippy lints, and spelling
runs-on: runs-on:
- self-hosted - self-hosted
- test - test
@ -38,6 +38,13 @@ jobs:
- name: Set up default .cargo/config.toml - name: Set up default .cargo/config.toml
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
- name: Check spelling
run: |
if ! which typos > /dev/null; then
cargo install typos-cli
fi
typos
- name: Run style checks - name: Run style checks
uses: ./.github/actions/check_style uses: ./.github/actions/check_style

1
.gitignore vendored
View file

@ -9,6 +9,7 @@
/styles/src/types/zed.ts /styles/src/types/zed.ts
/crates/theme/schemas/theme.json /crates/theme/schemas/theme.json
/crates/collab/static/styles.css /crates/collab/static/styles.css
/crates/collab/.admins.json
/vendor/bin /vendor/bin
/assets/themes/*.json /assets/themes/*.json
/assets/*licenses.md /assets/*licenses.md

57
CONTRIBUTING.md Normal file
View 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.
Remember that smaller, incremental PRs are easier to review and merge than large PRs.

79
Cargo.lock generated
View file

@ -1452,7 +1452,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.36.1" version = "0.37.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1580,6 +1580,28 @@ dependencies = [
"rustc-hash", "rustc-hash",
] ]
[[package]]
name = "color"
version = "0.1.0"
dependencies = [
"anyhow",
"fs",
"indexmap 1.9.3",
"itertools 0.11.0",
"palette",
"parking_lot 0.11.2",
"refineable",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"story",
"toml 0.5.11",
"util",
"uuid 1.4.1",
]
[[package]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@ -4977,6 +4999,7 @@ dependencies = [
"approx", "approx",
"fast-srgb8", "fast-srgb8",
"palette_derive", "palette_derive",
"phf",
] ]
[[package]] [[package]]
@ -5165,6 +5188,48 @@ dependencies = [
"indexmap 2.0.0", "indexmap 2.0.0",
] ]
[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared",
"rand 0.8.5",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher 0.3.11",
]
[[package]] [[package]]
name = "picker" name = "picker"
version = "0.1.0" version = "0.1.0"
@ -7074,6 +7139,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -7644,7 +7715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
dependencies = [ dependencies = [
"float-cmp", "float-cmp",
"siphasher", "siphasher 0.2.3",
] ]
[[package]] [[package]]
@ -7855,6 +7926,7 @@ name = "theme"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"color",
"fs", "fs",
"gpui", "gpui",
"indexmap 1.9.3", "indexmap 1.9.3",
@ -8858,7 +8930,7 @@ dependencies = [
"roxmltree", "roxmltree",
"rustybuzz", "rustybuzz",
"simplecss", "simplecss",
"siphasher", "siphasher 0.2.3",
"svgtypes", "svgtypes",
"ttf-parser 0.12.3", "ttf-parser 0.12.3",
"unicode-bidi", "unicode-bidi",
@ -9650,6 +9722,7 @@ dependencies = [
"client", "client",
"collab_ui", "collab_ui",
"collections", "collections",
"color",
"command_palette", "command_palette",
"copilot", "copilot",
"copilot_ui", "copilot_ui",

View file

@ -1,4 +1,2 @@
web: cd ../zed.dev && PORT=3000 npm run dev
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
livekit: livekit-server --dev livekit: livekit-server --dev
postgrest: postgrest crates/collab/admin_api.conf

109
README.md
View file

@ -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 # Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) [![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](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 - [Building Zed](./docs/src/developing_zed__building_zed.md)
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
* 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.
### Licensing ### 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: 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 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 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). - 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`.

View file

@ -99,7 +99,7 @@
"ctrl-i": "pane::GoForward", "ctrl-i": "pane::GoForward",
"ctrl-]": "editor::GoToDefinition", "ctrl-]": "editor::GoToDefinition",
"escape": ["vim::SwitchMode", "Normal"], "escape": ["vim::SwitchMode", "Normal"],
"ctrl+[": ["vim::SwitchMode", "Normal"], "ctrl-[": ["vim::SwitchMode", "Normal"],
"v": "vim::ToggleVisual", "v": "vim::ToggleVisual",
"shift-v": "vim::ToggleVisualLine", "shift-v": "vim::ToggleVisualLine",
"ctrl-v": "vim::ToggleVisualBlock", "ctrl-v": "vim::ToggleVisualBlock",
@ -288,7 +288,7 @@
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": { "bindings": {
"escape": "editor::Cancel", "escape": "editor::Cancel",
"ctrl+[": "editor::Cancel" "ctrl-[": "editor::Cancel"
} }
}, },
{ {
@ -441,7 +441,7 @@
"r": ["vim::PushOperator", "Replace"], "r": ["vim::PushOperator", "Replace"],
"ctrl-c": ["vim::SwitchMode", "Normal"], "ctrl-c": ["vim::SwitchMode", "Normal"],
"escape": ["vim::SwitchMode", "Normal"], "escape": ["vim::SwitchMode", "Normal"],
"ctrl+[": ["vim::SwitchMode", "Normal"], "ctrl-[": ["vim::SwitchMode", "Normal"],
">": "editor::Indent", ">": "editor::Indent",
"<": "editor::Outdent", "<": "editor::Outdent",
"i": [ "i": [
@ -481,7 +481,7 @@
"tab": "vim::Tab", "tab": "vim::Tab",
"enter": "vim::Enter", "enter": "vim::Enter",
"escape": ["vim::SwitchMode", "Normal"], "escape": ["vim::SwitchMode", "Normal"],
"ctrl+[": ["vim::SwitchMode", "Normal"] "ctrl-[": ["vim::SwitchMode", "Normal"]
} }
}, },
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View file

@ -81,8 +81,8 @@ impl PromptChain {
pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> { pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> {
// Argsort based on Prompt Priority // Argsort based on Prompt Priority
let seperator = "\n"; let separator = "\n";
let seperator_tokens = self.args.model.count_tokens(seperator)?; let separator_tokens = self.args.model.count_tokens(separator)?;
let mut sorted_indices = (0..self.templates.len()).collect::<Vec<_>>(); let mut sorted_indices = (0..self.templates.len()).collect::<Vec<_>>();
sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0)); sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0));
@ -104,7 +104,7 @@ impl PromptChain {
prompts[idx] = template_prompt; prompts[idx] = template_prompt;
if let Some(remaining_tokens) = tokens_outstanding { if let Some(remaining_tokens) = tokens_outstanding {
let new_tokens = prompt_token_count + seperator_tokens; let new_tokens = prompt_token_count + separator_tokens;
tokens_outstanding = if remaining_tokens > new_tokens { tokens_outstanding = if remaining_tokens > new_tokens {
Some(remaining_tokens - new_tokens) Some(remaining_tokens - new_tokens)
} else { } else {
@ -117,9 +117,9 @@ impl PromptChain {
prompts.retain(|x| x != ""); prompts.retain(|x| x != "");
let full_prompt = prompts.join(seperator); let full_prompt = prompts.join(separator);
let total_token_count = self.args.model.count_tokens(&full_prompt)?; let total_token_count = self.args.model.count_tokens(&full_prompt)?;
anyhow::Ok((prompts.join(seperator), total_token_count)) anyhow::Ok((prompts.join(separator), total_token_count))
} }
} }

View file

@ -68,7 +68,7 @@ impl PromptTemplate for RepositoryContext {
let mut prompt = String::new(); let mut prompt = String::new();
let mut remaining_tokens = max_token_length.clone(); let mut remaining_tokens = max_token_length.clone();
let seperator_token_length = args.model.count_tokens("\n")?; let separator_token_length = args.model.count_tokens("\n")?;
for snippet in &args.snippets { for snippet in &args.snippets {
let mut snippet_prompt = template.to_string(); let mut snippet_prompt = template.to_string();
let content = snippet.to_string(); let content = snippet.to_string();
@ -79,9 +79,9 @@ impl PromptTemplate for RepositoryContext {
if let Some(tokens_left) = remaining_tokens { if let Some(tokens_left) = remaining_tokens {
if tokens_left >= token_count { if tokens_left >= token_count {
writeln!(prompt, "{snippet_prompt}").unwrap(); writeln!(prompt, "{snippet_prompt}").unwrap();
remaining_tokens = if tokens_left >= (token_count + seperator_token_length) remaining_tokens = if tokens_left >= (token_count + separator_token_length)
{ {
Some(tokens_left - token_count - seperator_token_length) Some(tokens_left - token_count - separator_token_length)
} else { } else {
Some(0) Some(0)
}; };

View file

@ -273,7 +273,7 @@ impl CompletionProvider for OpenAICompletionProvider {
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> { ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
// Currently the CompletionRequest for OpenAI, includes a 'model' parameter // Currently the CompletionRequest for OpenAI, includes a 'model' parameter
// This means that the model is determined by the CompletionRequest and not the CompletionProvider, // This means that the model is determined by the CompletionRequest and not the CompletionProvider,
// which is currently model based, due to the langauge model. // which is currently model based, due to the language model.
// At some point in the future we should rectify this. // At some point in the future we should rectify this.
let credential = self.credential.read().clone(); let credential = self.credential.read().clone();
let request = stream_completion(credential, self.executor.clone(), prompt); let request = stream_completion(credential, self.executor.clone(), prompt);

View file

@ -19,12 +19,13 @@ use chrono::{DateTime, Local};
use client::telemetry::AssistantKind; use client::telemetry::AssistantKind;
use collections::{hash_map, HashMap, HashSet, VecDeque}; use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{ use editor::{
actions::{MoveDown, MoveUp},
display_map::{ display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
}, },
scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, scroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MoveDown, MoveUp, MultiBufferSnapshot, Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, ToOffset,
ToOffset, ToPoint, ToPoint,
}; };
use fs::Fs; use fs::Fs;
use futures::StreamExt; use futures::StreamExt;
@ -479,7 +480,7 @@ impl AssistantPanel {
fn cancel_last_inline_assist( fn cancel_last_inline_assist(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &editor::Cancel, _: &editor::actions::Cancel,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) { if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
@ -891,7 +892,7 @@ impl AssistantPanel {
} }
} }
fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) { fn handle_editor_cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
if !search_bar.read(cx).is_dismissed() { if !search_bar.read(cx).is_dismissed() {
search_bar.update(cx, |search_bar, cx| { search_bar.update(cx, |search_bar, cx| {
@ -1148,7 +1149,7 @@ impl Render for AssistantPanel {
|panel, cx| panel.toolbar.read(cx).item_of_type::<BufferSearchBar>(), |panel, cx| panel.toolbar.read(cx).item_of_type::<BufferSearchBar>(),
cx, cx,
); );
BufferSearchBar::register_inner(&mut registrar); BufferSearchBar::register(&mut registrar);
registrar.into_div() registrar.into_div()
} else { } else {
div() div()
@ -2158,7 +2159,7 @@ impl ConversationEditor {
} }
} }
fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) { fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
if !self if !self
.conversation .conversation
.update(cx, |conversation, _| conversation.cancel_last_assist()) .update(cx, |conversation, _| conversation.cancel_last_assist())
@ -2311,8 +2312,7 @@ impl ConversationEditor {
} }
}); });
div() h_flex()
.h_flex()
.id(("message_header", message_id.0)) .id(("message_header", message_id.0))
.h_11() .h_11()
.relative() .relative()
@ -2328,6 +2328,7 @@ impl ConversationEditor {
.add_suffix(true) .add_suffix(true)
.to_string(), .to_string(),
) )
.size(LabelSize::XSmall)
.color(Color::Muted), .color(Color::Muted),
) )
.children( .children(
@ -2417,7 +2418,7 @@ impl ConversationEditor {
} }
} }
fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext<Self>) { fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
let editor = self.editor.read(cx); let editor = self.editor.read(cx);
let conversation = self.conversation.read(cx); let conversation = self.conversation.read(cx);
if editor.selections.count() == 1 { if editor.selections.count() == 1 {
@ -2828,7 +2829,7 @@ impl InlineAssistant {
cx.notify(); cx.notify();
} }
fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(InlineAssistantEvent::Canceled); cx.emit(InlineAssistantEvent::Canceled);
} }
@ -2917,7 +2918,7 @@ impl InlineAssistant {
let semantic_permissioned = self.semantic_permissioned(cx); let semantic_permissioned = self.semantic_permissioned(cx);
if let Some(semantic_index) = SemanticIndex::global(cx) { if let Some(semantic_index) = SemanticIndex::global(cx) {
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
// This has to be updated to accomodate for semantic_permissions // This has to be updated to accommodate for semantic_permissions
if semantic_permissioned.await.unwrap_or(false) { if semantic_permissioned.await.unwrap_or(false) {
semantic_index semantic_index
.update(&mut cx, |index, cx| index.index_project(project, cx))? .update(&mut cx, |index, cx| index.index_project(project, cx))?

View file

@ -93,7 +93,9 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
cx.observe_new_views(|workspace: &mut Workspace, _cx| { cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|_, action: &Check, cx| check(action, cx)); workspace.register_action(|_, action: &Check, cx| check(action, cx));
workspace.register_action(|_, action, cx| view_release_notes(action, cx)); workspace.register_action(|_, action, cx| {
view_release_notes(action, cx);
});
// @nate - code to trigger update notification on launch // @nate - code to trigger update notification on launch
// todo!("remove this when Nate is done") // todo!("remove this when Nate is done")
@ -140,24 +142,23 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
} }
} }
pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<()> {
if let Some(auto_updater) = AutoUpdater::get(cx) { let auto_updater = AutoUpdater::get(cx)?;
let release_channel = cx.try_global::<ReleaseChannel>()?;
if matches!(
release_channel,
ReleaseChannel::Stable | ReleaseChannel::Preview
) {
let auto_updater = auto_updater.read(cx); let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url; let server_url = &auto_updater.server_url;
let release_channel = release_channel.dev_name();
let current_version = auto_updater.current_version; let current_version = auto_updater.current_version;
if cx.has_global::<ReleaseChannel>() { let url = format!("{server_url}/releases/{release_channel}/{current_version}");
match cx.global::<ReleaseChannel>() { cx.open_url(&url);
ReleaseChannel::Dev => {}
ReleaseChannel::Nightly => {}
ReleaseChannel::Preview => {
cx.open_url(&format!("{server_url}/releases/preview/{current_version}"))
}
ReleaseChannel::Stable => {
cx.open_url(&format!("{server_url}/releases/stable/{current_version}"))
}
}
}
} }
None
} }
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> { pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
@ -257,12 +258,14 @@ impl AutoUpdater {
"{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg" "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
); );
cx.update(|cx| { cx.update(|cx| {
if cx.has_global::<ReleaseChannel>() { if let Some(param) = cx
if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() { .try_global::<ReleaseChannel>()
.map(|release_channel| release_channel.release_query_param())
.flatten()
{
url_string += "&"; url_string += "&";
url_string += param; url_string += param;
} }
}
})?; })?;
let mut response = client.get(&url_string, Default::default(), true).await?; let mut response = client.get(&url_string, Default::default(), true).await?;
@ -313,8 +316,8 @@ impl AutoUpdater {
let (installation_id, release_channel, telemetry) = cx.update(|cx| { let (installation_id, release_channel, telemetry) = cx.update(|cx| {
let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id(); let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
let release_channel = cx let release_channel = cx
.has_global::<ReleaseChannel>() .try_global::<ReleaseChannel>()
.then(|| cx.global::<ReleaseChannel>().display_name()); .map(|release_channel| release_channel.display_name());
let telemetry = TelemetrySettings::get_global(cx).metrics; let telemetry = TelemetrySettings::get_global(cx).metrics;
(installation_id, release_channel, telemetry) (installation_id, release_channel, telemetry)

View file

@ -40,7 +40,9 @@ impl Render for UpdateNotification {
.id("notes") .id("notes")
.child(Label::new("View the release notes")) .child(Label::new("View the release notes"))
.cursor_pointer() .cursor_pointer()
.on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), .on_click(|_, cx| {
crate::view_release_notes(&Default::default(), cx);
}),
) )
} }
} }

View file

@ -150,11 +150,9 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
impl Telemetry { impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> { pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
let release_channel = if cx.has_global::<ReleaseChannel>() { let release_channel = cx
Some(cx.global::<ReleaseChannel>().display_name()) .try_global::<ReleaseChannel>()
} else { .map(|release_channel| release_channel.display_name());
None
};
TelemetrySettings::register(cx); TelemetrySettings::register(cx);

View file

@ -0,0 +1 @@
["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler"]

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.36.1" version = "0.37.0"
publish = false publish = false
[[bin]] [[bin]]

View file

@ -19,6 +19,7 @@ CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
CREATE TABLE "access_tokens" ( CREATE TABLE "access_tokens" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT, "id" INTEGER PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER REFERENCES users (id), "user_id" INTEGER REFERENCES users (id),
"impersonated_user_id" INTEGER REFERENCES users (id),
"hash" VARCHAR(128) "hash" VARCHAR(128)
); );
CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id"); CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");

View file

@ -0,0 +1 @@
ALTER TABLE access_tokens ADD COLUMN impersonated_user_id integer;

View file

@ -156,11 +156,11 @@ async fn create_access_token(
.await? .await?
.ok_or_else(|| anyhow!("user not found"))?; .ok_or_else(|| anyhow!("user not found"))?;
let mut user_id = user.id; let mut impersonated_user_id = None;
if let Some(impersonate) = params.impersonate { if let Some(impersonate) = params.impersonate {
if user.admin { if user.admin {
if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? { if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
user_id = impersonated_user.id; impersonated_user_id = Some(impersonated_user.id);
} else { } else {
return Err(Error::Http( return Err(Error::Http(
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
@ -175,12 +175,13 @@ async fn create_access_token(
} }
} }
let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?; let access_token =
auth::create_access_token(app.db.as_ref(), user_id, impersonated_user_id).await?;
let encrypted_access_token = let encrypted_access_token =
auth::encrypt_access_token(&access_token, params.public_key.clone())?; auth::encrypt_access_token(&access_token, params.public_key.clone())?;
Ok(Json(CreateAccessTokenResponse { Ok(Json(CreateAccessTokenResponse {
user_id, user_id: impersonated_user_id.unwrap_or(user_id),
encrypted_access_token, encrypted_access_token,
})) }))
} }

View file

@ -27,6 +27,11 @@ lazy_static! {
.unwrap(); .unwrap();
} }
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Impersonator(pub Option<db::User>);
/// 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 { pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
let mut auth_header = req let mut auth_header = req
.headers() .headers()
@ -55,29 +60,51 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
})?; })?;
let state = req.extensions().get::<Arc<AppState>>().unwrap(); let state = req.extensions().get::<Arc<AppState>>().unwrap();
let credentials_valid = if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
state.config.api_token == admin_token // In development, allow impersonation using the admin API token.
// Don't allow this in production because we can't tell who is doing
// the impersonating.
let validate_result = if let (Some(admin_token), true) = (
access_token.strip_prefix("ADMIN_TOKEN:"),
state.config.is_development(),
) {
Ok(VerifyAccessTokenResult {
is_valid: state.config.api_token == admin_token,
impersonator_id: None,
})
} else { } else {
verify_access_token(&access_token, user_id, &state.db) verify_access_token(&access_token, user_id, &state.db).await
.await
.unwrap_or(false)
}; };
if credentials_valid { if let Ok(validate_result) = validate_result {
if validate_result.is_valid {
let user = state let user = state
.db .db
.get_user_by_id(user_id) .get_user_by_id(user_id)
.await? .await?
.ok_or_else(|| anyhow!("user {} not found", user_id))?; .ok_or_else(|| anyhow!("user {} not found", user_id))?;
req.extensions_mut().insert(user);
Ok::<_, Error>(next.run(req).await) let impersonator = if let Some(impersonator_id) = validate_result.impersonator_id {
let impersonator = state
.db
.get_user_by_id(impersonator_id)
.await?
.ok_or_else(|| anyhow!("user {} not found", impersonator_id))?;
Some(impersonator)
} else { } else {
None
};
req.extensions_mut().insert(user);
req.extensions_mut().insert(Impersonator(impersonator));
return Ok::<_, Error>(next.run(req).await);
}
}
Err(Error::Http( Err(Error::Http(
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
"invalid credentials".to_string(), "invalid credentials".to_string(),
)) ))
} }
}
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8; const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
@ -88,13 +115,24 @@ struct AccessTokenJson {
token: String, token: String,
} }
pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result<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,
impersonated_user_id: Option<UserId>,
) -> Result<String> {
const VERSION: usize = 1; const VERSION: usize = 1;
let access_token = rpc::auth::random_token(); let access_token = rpc::auth::random_token();
let access_token_hash = let access_token_hash =
hash_access_token(&access_token).context("failed to hash access token")?; hash_access_token(&access_token).context("failed to hash access token")?;
let id = db let id = db
.create_access_token(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE) .create_access_token(
user_id,
impersonated_user_id,
&access_token_hash,
MAX_ACCESS_TOKENS_TO_STORE,
)
.await?; .await?;
Ok(serde_json::to_string(&AccessTokenJson { Ok(serde_json::to_string(&AccessTokenJson {
version: VERSION, version: VERSION,
@ -122,6 +160,8 @@ fn hash_access_token(token: &str) -> Result<String> {
.to_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> { pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<String> {
let native_app_public_key = let native_app_public_key =
rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?; rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?;
@ -131,11 +171,22 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<St
Ok(encrypted_access_token) Ok(encrypted_access_token)
} }
pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc<Database>) -> Result<bool> { pub struct VerifyAccessTokenResult {
pub is_valid: bool,
pub impersonator_id: Option<UserId>,
}
/// Checks that the given access token is valid for the given user.
pub async fn verify_access_token(
token: &str,
user_id: UserId,
db: &Arc<Database>,
) -> Result<VerifyAccessTokenResult> {
let token: AccessTokenJson = serde_json::from_str(&token)?; let token: AccessTokenJson = serde_json::from_str(&token)?;
let db_token = db.get_access_token(token.id).await?; let db_token = db.get_access_token(token.id).await?;
if db_token.user_id != user_id { let token_user_id = db_token.impersonated_user_id.unwrap_or(db_token.user_id);
if token_user_id != user_id {
return Err(anyhow!("no such access token"))?; return Err(anyhow!("no such access token"))?;
} }
@ -147,5 +198,12 @@ pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc<Database
let duration = t0.elapsed(); let duration = t0.elapsed();
log::info!("hashed access token in {:?}", duration); log::info!("hashed access token in {:?}", duration);
METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64); METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64);
Ok(is_valid) Ok(VerifyAccessTokenResult {
is_valid,
impersonator_id: if db_token.impersonated_user_id.is_some() {
Some(db_token.user_id)
} else {
None
},
})
} }

View file

@ -1,7 +1,11 @@
use collab::{db, executor::Executor}; use collab::{
db::{self, NewUserParams},
env::load_dotenv,
executor::Executor,
};
use db::{ConnectOptions, Database}; use db::{ConnectOptions, Database};
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
use std::fmt::Write; use std::{fmt::Write, fs};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct GitHubUser { struct GitHubUser {
@ -12,70 +16,56 @@ struct GitHubUser {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
load_dotenv().expect("failed to load .env.toml file");
let mut admin_logins =
load_admins("./.admins.default.json").expect("failed to load default admins file");
if let Ok(other_admins) = load_admins("./.admins.json") {
admin_logins.extend(other_admins);
}
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
let db = Database::new(ConnectOptions::new(database_url), Executor::Production) let db = Database::new(ConnectOptions::new(database_url), Executor::Production)
.await .await
.expect("failed to connect to postgres database"); .expect("failed to connect to postgres database");
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let mut current_user = // Create admin users for all of the users in `.admins.toml` or `.admins.default.toml`.
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await; for admin_login in admin_logins {
current_user let user = fetch_github::<GitHubUser>(
.email
.get_or_insert_with(|| "placeholder@example.com".to_string());
let staff_users = fetch_github::<Vec<GitHubUser>>(
&client, &client,
&github_token, &format!("https://api.github.com/users/{admin_login}"),
"https://api.github.com/orgs/zed-industries/teams/staff/members",
) )
.await; .await;
db.create_user(
&user.email.unwrap_or(format!("{admin_login}@example.com")),
true,
NewUserParams {
github_login: user.login,
github_user_id: user.id,
},
)
.await
.expect("failed to create admin user");
}
let mut zed_users = Vec::new(); // Fetch 100 other random users from GitHub and insert them into the database.
zed_users.push((current_user, true)); let mut user_count = db
zed_users.extend(staff_users.into_iter().map(|user| (user, true)));
let user_count = db
.get_all_users(0, 200) .get_all_users(0, 200)
.await .await
.expect("failed to load users from db") .expect("failed to load users from db")
.len(); .len();
if user_count < 100 {
let mut last_user_id = None; let mut last_user_id = None;
for _ in 0..10 { while user_count < 100 {
let mut uri = "https://api.github.com/users?per_page=100".to_string(); let mut uri = "https://api.github.com/users?per_page=100".to_string();
if let Some(last_user_id) = last_user_id { if let Some(last_user_id) = last_user_id {
write!(&mut uri, "&since={}", last_user_id).unwrap(); write!(&mut uri, "&since={}", last_user_id).unwrap();
} }
let users = fetch_github::<Vec<GitHubUser>>(&client, &github_token, &uri).await; let users = fetch_github::<Vec<GitHubUser>>(&client, &uri).await;
if let Some(last_user) = users.last() {
last_user_id = Some(last_user.id);
zed_users.extend(users.into_iter().map(|user| (user, false)));
} else {
break;
}
}
}
for (github_user, admin) in zed_users { for github_user in users {
if db last_user_id = Some(github_user.id);
.get_user_by_github_login(&github_user.login) user_count += 1;
.await
.expect("failed to fetch user")
.is_none()
{
if admin {
db.create_user(
&format!("{}@zed.dev", github_user.login),
admin,
db::NewUserParams {
github_login: github_user.login,
github_user_id: github_user.id,
},
)
.await
.expect("failed to insert user");
} else {
db.get_or_create_user_by_github_account( db.get_or_create_user_by_github_account(
&github_user.login, &github_user.login,
Some(github_user.id), Some(github_user.id),
@ -86,16 +76,15 @@ async fn main() {
} }
} }
} }
fn load_admins(path: &str) -> anyhow::Result<Vec<String>> {
let file_content = fs::read_to_string(path)?;
Ok(serde_json::from_str(&file_content)?)
} }
async fn fetch_github<T: DeserializeOwned>( async fn fetch_github<T: DeserializeOwned>(client: &reqwest::Client, url: &str) -> T {
client: &reqwest::Client,
access_token: &str,
url: &str,
) -> T {
let response = client let response = client
.get(url) .get(url)
.bearer_auth(&access_token)
.header("user-agent", "zed") .header("user-agent", "zed")
.send() .send()
.await .await

View file

@ -47,6 +47,8 @@ pub use ids::*;
pub use sea_orm::ConnectOptions; pub use sea_orm::ConnectOptions;
pub use tables::user::Model as User; 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 { pub struct Database {
options: ConnectOptions, options: ConnectOptions,
pool: DatabaseConnection, pool: DatabaseConnection,
@ -62,6 +64,7 @@ pub struct Database {
// The `Database` type has so many methods that its impl blocks are split into // The `Database` type has so many methods that its impl blocks are split into
// separate files in the `queries` folder. // separate files in the `queries` folder.
impl Database { impl Database {
/// Connects to the database with the given options
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> { pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
sqlx::any::install_default_drivers(); sqlx::any::install_default_drivers();
Ok(Self { Ok(Self {
@ -82,6 +85,7 @@ impl Database {
self.rooms.clear(); self.rooms.clear();
} }
/// Runs the database migrations.
pub async fn migrate( pub async fn migrate(
&self, &self,
migrations_path: &Path, migrations_path: &Path,
@ -123,11 +127,15 @@ impl Database {
Ok(new_migrations) Ok(new_migrations)
} }
/// Initializes static data that resides in the database by upserting it.
pub async fn initialize_static_data(&mut self) -> Result<()> { pub async fn initialize_static_data(&mut self) -> Result<()> {
self.initialize_notification_kinds().await?; self.initialize_notification_kinds().await?;
Ok(()) 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> pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
where where
F: Send + Fn(TransactionHandle) -> Fut, F: Send + Fn(TransactionHandle) -> Fut,
@ -160,6 +168,7 @@ impl Database {
self.run(body).await 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>>> async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
where where
F: Send + Fn(TransactionHandle) -> Fut, F: Send + Fn(TransactionHandle) -> Fut,
@ -210,6 +219,9 @@ impl Database {
self.run(body).await 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>> async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
where where
F: Send + Fn(TransactionHandle) -> Fut, 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>>); pub struct TransactionHandle(Arc<Option<DatabaseTransaction>>);
impl Deref for TransactionHandle { 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> { pub struct RoomGuard<T> {
data: T, data: T,
_guard: OwnedMutexGuard<()>, _guard: OwnedMutexGuard<()>,
@ -361,6 +376,7 @@ impl<T> DerefMut for RoomGuard<T> {
} }
impl<T> RoomGuard<T> { impl<T> RoomGuard<T> {
/// Returns the inner value of the guard.
pub fn into_inner(self) -> T { pub fn into_inner(self) -> T {
self.data self.data
} }
@ -420,12 +436,14 @@ pub struct WaitlistSummary {
pub unknown_count: i64, pub unknown_count: i64,
} }
/// The parameters to create a new user.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct NewUserParams { pub struct NewUserParams {
pub github_login: String, pub github_login: String,
pub github_user_id: i32, pub github_user_id: i32,
} }
/// The result of creating a new user.
#[derive(Debug)] #[derive(Debug)]
pub struct NewUserResult { pub struct NewUserResult {
pub user_id: UserId, pub user_id: UserId,
@ -434,6 +452,7 @@ pub struct NewUserResult {
pub signup_device_id: Option<String>, pub signup_device_id: Option<String>,
} }
/// The result of moving a channel.
#[derive(Debug)] #[derive(Debug)]
pub struct MoveChannelResult { pub struct MoveChannelResult {
pub participants_to_update: HashMap<UserId, ChannelsForUser>, pub participants_to_update: HashMap<UserId, ChannelsForUser>,
@ -441,18 +460,21 @@ pub struct MoveChannelResult {
pub moved_channels: HashSet<ChannelId>, pub moved_channels: HashSet<ChannelId>,
} }
/// The result of renaming a channel.
#[derive(Debug)] #[derive(Debug)]
pub struct RenameChannelResult { pub struct RenameChannelResult {
pub channel: Channel, pub channel: Channel,
pub participants_to_update: HashMap<UserId, Channel>, pub participants_to_update: HashMap<UserId, Channel>,
} }
/// The result of creating a channel.
#[derive(Debug)] #[derive(Debug)]
pub struct CreateChannelResult { pub struct CreateChannelResult {
pub channel: Channel, pub channel: Channel,
pub participants_to_update: Vec<(UserId, ChannelsForUser)>, pub participants_to_update: Vec<(UserId, ChannelsForUser)>,
} }
/// The result of setting a channel's visibility.
#[derive(Debug)] #[derive(Debug)]
pub struct SetChannelVisibilityResult { pub struct SetChannelVisibilityResult {
pub participants_to_update: HashMap<UserId, ChannelsForUser>, pub participants_to_update: HashMap<UserId, ChannelsForUser>,
@ -460,6 +482,7 @@ pub struct SetChannelVisibilityResult {
pub channels_to_remove: Vec<ChannelId>, pub channels_to_remove: Vec<ChannelId>,
} }
/// The result of updating a channel membership.
#[derive(Debug)] #[derive(Debug)]
pub struct MembershipUpdated { pub struct MembershipUpdated {
pub channel_id: ChannelId, pub channel_id: ChannelId,
@ -467,12 +490,14 @@ pub struct MembershipUpdated {
pub removed_channels: Vec<ChannelId>, pub removed_channels: Vec<ChannelId>,
} }
/// The result of setting a member's role.
#[derive(Debug)] #[derive(Debug)]
pub enum SetMemberRoleResult { pub enum SetMemberRoleResult {
InviteUpdated(Channel), InviteUpdated(Channel),
MembershipUpdated(MembershipUpdated), MembershipUpdated(MembershipUpdated),
} }
/// The result of inviting a member to a channel.
#[derive(Debug)] #[derive(Debug)]
pub struct InviteMemberResult { pub struct InviteMemberResult {
pub channel: Channel, pub channel: Channel,
@ -497,6 +522,7 @@ pub struct Channel {
pub name: String, pub name: String,
pub visibility: ChannelVisibility, pub visibility: ChannelVisibility,
pub role: ChannelRole, pub role: ChannelRole,
/// parent_path is the channel ids from the root to this one (not including this one)
pub parent_path: Vec<ChannelId>, pub parent_path: Vec<ChannelId>,
} }

View file

@ -19,19 +19,23 @@ macro_rules! id_type {
Deserialize, Deserialize,
DeriveValueType, DeriveValueType,
)] )]
#[allow(missing_docs)]
#[serde(transparent)] #[serde(transparent)]
pub struct $name(pub i32); pub struct $name(pub i32);
impl $name { impl $name {
#[allow(unused)] #[allow(unused)]
#[allow(missing_docs)]
pub const MAX: Self = Self(i32::MAX); pub const MAX: Self = Self(i32::MAX);
#[allow(unused)] #[allow(unused)]
#[allow(missing_docs)]
pub fn from_proto(value: u64) -> Self { pub fn from_proto(value: u64) -> Self {
Self(value as i32) Self(value as i32)
} }
#[allow(unused)] #[allow(unused)]
#[allow(missing_docs)]
pub fn to_proto(self) -> u64 { pub fn to_proto(self) -> u64 {
self.0 as u64 self.0 as u64
} }
@ -84,21 +88,28 @@ id_type!(FlagId);
id_type!(NotificationId); id_type!(NotificationId);
id_type!(NotificationKindId); id_type!(NotificationKindId);
/// ChannelRole gives you permissions for both channels and calls.
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
#[sea_orm(rs_type = "String", db_type = "String(None)")] #[sea_orm(rs_type = "String", db_type = "String(None)")]
pub enum ChannelRole { pub enum ChannelRole {
/// Admin can read/write and change permissions.
#[sea_orm(string_value = "admin")] #[sea_orm(string_value = "admin")]
Admin, Admin,
/// Member can read/write, but not change pemissions.
#[sea_orm(string_value = "member")] #[sea_orm(string_value = "member")]
#[default] #[default]
Member, Member,
/// Guest can read, but not write.
/// (thought they can use the channel chat)
#[sea_orm(string_value = "guest")] #[sea_orm(string_value = "guest")]
Guest, Guest,
/// Banned may not read.
#[sea_orm(string_value = "banned")] #[sea_orm(string_value = "banned")]
Banned, Banned,
} }
impl ChannelRole { impl ChannelRole {
/// Returns true if this role is more powerful than the other role.
pub fn should_override(&self, other: Self) -> bool { pub fn should_override(&self, other: Self) -> bool {
use ChannelRole::*; use ChannelRole::*;
match self { match self {
@ -109,6 +120,7 @@ impl ChannelRole {
} }
} }
/// Returns the maximal role between the two
pub fn max(&self, other: Self) -> Self { pub fn max(&self, other: Self) -> Self {
if self.should_override(other) { if self.should_override(other) {
*self *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 { pub fn can_see_all_descendants(&self) -> bool {
use ChannelRole::*; use ChannelRole::*;
match self { 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 { pub fn can_only_see_public_descendants(&self) -> bool {
use ChannelRole::*; use ChannelRole::*;
match self { 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 { pub fn can_publish_to_rooms(&self) -> bool {
use ChannelRole::*; use ChannelRole::*;
match self { match self {
@ -141,6 +156,7 @@ impl ChannelRole {
} }
} }
/// True if the role can edit shared projects.
pub fn can_edit_projects(&self) -> bool { pub fn can_edit_projects(&self) -> bool {
use ChannelRole::*; use ChannelRole::*;
match self { match self {
@ -149,6 +165,7 @@ impl ChannelRole {
} }
} }
/// True if the role can read shared projects.
pub fn can_read_projects(&self) -> bool { pub fn can_read_projects(&self) -> bool {
use ChannelRole::*; use ChannelRole::*;
match self { 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)] #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
#[sea_orm(rs_type = "String", db_type = "String(None)")] #[sea_orm(rs_type = "String", db_type = "String(None)")]
pub enum ChannelVisibility { 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")] #[sea_orm(string_value = "public")]
Public, Public,
/// Members channels are only visible to members of this channel or its parents.
#[sea_orm(string_value = "members")] #[sea_orm(string_value = "members")]
#[default] #[default]
Members, Members,

View file

@ -2,9 +2,11 @@ use super::*;
use sea_orm::sea_query::Query; use sea_orm::sea_query::Query;
impl Database { impl Database {
/// Creates a new access token for the given user.
pub async fn create_access_token( pub async fn create_access_token(
&self, &self,
user_id: UserId, user_id: UserId,
impersonated_user_id: Option<UserId>,
access_token_hash: &str, access_token_hash: &str,
max_access_token_count: usize, max_access_token_count: usize,
) -> Result<AccessTokenId> { ) -> Result<AccessTokenId> {
@ -13,6 +15,7 @@ impl Database {
let token = access_token::ActiveModel { let token = access_token::ActiveModel {
user_id: ActiveValue::set(user_id), user_id: ActiveValue::set(user_id),
impersonated_user_id: ActiveValue::set(impersonated_user_id),
hash: ActiveValue::set(access_token_hash.into()), hash: ActiveValue::set(access_token_hash.into()),
..Default::default() ..Default::default()
} }
@ -39,6 +42,7 @@ impl Database {
.await .await
} }
/// Retrieves the access token with the given ID.
pub async fn get_access_token( pub async fn get_access_token(
&self, &self,
access_token_id: AccessTokenId, access_token_id: AccessTokenId,

View file

@ -9,6 +9,8 @@ pub struct LeftChannelBuffer {
} }
impl Database { 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( pub async fn join_channel_buffer(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -121,6 +123,7 @@ impl Database {
.await .await
} }
/// Rejoin a channel buffer (after a connection interruption)
pub async fn rejoin_channel_buffers( pub async fn rejoin_channel_buffers(
&self, &self,
buffers: &[proto::ChannelBufferVersion], buffers: &[proto::ChannelBufferVersion],
@ -149,7 +152,7 @@ impl Database {
.await?; .await?;
// If the buffer epoch hasn't changed since the client lost // If the buffer epoch hasn't changed since the client lost
// connection, then the client's buffer can be syncronized with // connection, then the client's buffer can be synchronized with
// the server's buffer. // the server's buffer.
if buffer.epoch as u64 != client_buffer.epoch { if buffer.epoch as u64 != client_buffer.epoch {
log::info!("can't rejoin buffer, epoch has changed"); log::info!("can't rejoin buffer, epoch has changed");
@ -232,6 +235,7 @@ impl Database {
.await .await
} }
/// Clear out any buffer collaborators who are no longer collaborating.
pub async fn clear_stale_channel_buffer_collaborators( pub async fn clear_stale_channel_buffer_collaborators(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -274,6 +278,7 @@ impl Database {
.await .await
} }
/// Close the channel buffer, and stop receiving updates for it.
pub async fn leave_channel_buffer( pub async fn leave_channel_buffer(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -286,6 +291,7 @@ impl Database {
.await .await
} }
/// Close the channel buffer, and stop receiving updates for it.
pub async fn channel_buffer_connection_lost( pub async fn channel_buffer_connection_lost(
&self, &self,
connection: ConnectionId, connection: ConnectionId,
@ -309,6 +315,7 @@ impl Database {
Ok(()) Ok(())
} }
/// Close all open channel buffers
pub async fn leave_channel_buffers( pub async fn leave_channel_buffers(
&self, &self,
connection: ConnectionId, connection: ConnectionId,
@ -342,7 +349,7 @@ impl Database {
.await .await
} }
pub async fn leave_channel_buffer_internal( async fn leave_channel_buffer_internal(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
connection: ConnectionId, connection: ConnectionId,
@ -798,6 +805,7 @@ impl Database {
Ok(changes) Ok(changes)
} }
/// Returns the latest operations for the buffers with the specified IDs.
pub async fn get_latest_operations_for_buffers( pub async fn get_latest_operations_for_buffers(
&self, &self,
buffer_ids: impl IntoIterator<Item = BufferId>, buffer_ids: impl IntoIterator<Item = BufferId>,
@ -962,7 +970,7 @@ fn version_from_storage(version: &Vec<storage::VectorClockEntry>) -> Vec<proto::
.collect() .collect()
} }
// This is currently a manual copy of the deserialization code in the client's langauge crate // This is currently a manual copy of the deserialization code in the client's language crate
pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operation> { pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operation> {
match operation.variant? { match operation.variant? {
proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation {

View file

@ -40,6 +40,7 @@ impl Database {
.id) .id)
} }
/// Creates a new channel.
pub async fn create_channel( pub async fn create_channel(
&self, &self,
name: &str, name: &str,
@ -97,6 +98,7 @@ impl Database {
.await .await
} }
/// Adds a user to the specified channel.
pub async fn join_channel( pub async fn join_channel(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -179,6 +181,7 @@ impl Database {
.await .await
} }
/// Sets the visibiltity of the given channel.
pub async fn set_channel_visibility( pub async fn set_channel_visibility(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -258,6 +261,7 @@ impl Database {
.await .await
} }
/// Deletes the channel with the specified ID.
pub async fn delete_channel( pub async fn delete_channel(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -294,6 +298,7 @@ impl Database {
.await .await
} }
/// Invites a user to a channel as a member.
pub async fn invite_channel_member( pub async fn invite_channel_member(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -349,6 +354,7 @@ impl Database {
Ok(new_name) Ok(new_name)
} }
/// Renames the specified channel.
pub async fn rename_channel( pub async fn rename_channel(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -387,6 +393,7 @@ impl Database {
.await .await
} }
/// accept or decline an invite to join a channel
pub async fn respond_to_channel_invite( pub async fn respond_to_channel_invite(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -486,6 +493,7 @@ impl Database {
}) })
} }
/// Removes a channel member.
pub async fn remove_channel_member( pub async fn remove_channel_member(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -530,6 +538,7 @@ impl Database {
.await .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>> { pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let mut role_for_channel: HashMap<ChannelId, ChannelRole> = HashMap::default(); let mut role_for_channel: HashMap<ChannelId, ChannelRole> = HashMap::default();
@ -565,6 +574,7 @@ impl Database {
.await .await
} }
/// Returns all channels for the user with the given ID.
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> { pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let tx = tx; let tx = tx;
@ -574,6 +584,8 @@ impl Database {
.await .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( pub async fn get_user_channels(
&self, &self,
user_id: UserId, user_id: UserId,
@ -743,6 +755,7 @@ impl Database {
Ok(results) Ok(results)
} }
/// Sets the role for the specified channel member.
pub async fn set_channel_member_role( pub async fn set_channel_member_role(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -786,6 +799,7 @@ impl Database {
.await .await
} }
/// Returns the details for the specified channel member.
pub async fn get_channel_participant_details( pub async fn get_channel_participant_details(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -911,6 +925,7 @@ impl Database {
.collect()) .collect())
} }
/// Returns the participants in the given channel.
pub async fn get_channel_participants( pub async fn get_channel_participants(
&self, &self,
channel: &channel::Model, channel: &channel::Model,
@ -925,6 +940,7 @@ impl Database {
.collect()) .collect())
} }
/// Returns whether the given user is an admin in the specified channel.
pub async fn check_user_is_channel_admin( pub async fn check_user_is_channel_admin(
&self, &self,
channel: &channel::Model, 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( pub async fn check_user_is_channel_member(
&self, &self,
channel: &channel::Model, 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( pub async fn check_user_is_channel_participant(
&self, &self,
channel: &channel::Model, 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( pub async fn pending_invite_for_channel(
&self, &self,
channel: &channel::Model, channel: &channel::Model,
@ -991,7 +1010,7 @@ impl Database {
Ok(row) Ok(row)
} }
pub async fn public_parent_channel( async fn public_parent_channel(
&self, &self,
channel: &channel::Model, channel: &channel::Model,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
@ -1003,7 +1022,7 @@ impl Database {
Ok(path.pop()) Ok(path.pop())
} }
pub async fn public_ancestors_including_self( pub(crate) async fn public_ancestors_including_self(
&self, &self,
channel: &channel::Model, channel: &channel::Model,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
@ -1018,6 +1037,7 @@ impl Database {
Ok(visible_channels) Ok(visible_channels)
} }
/// Returns the role for a user in the given channel.
pub async fn channel_role_for_user( pub async fn channel_role_for_user(
&self, &self,
channel: &channel::Model, channel: &channel::Model,
@ -1143,7 +1163,7 @@ impl Database {
.await?) .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> { pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &*tx).await?; let channel = self.get_channel_internal(channel_id, &*tx).await?;
@ -1156,7 +1176,7 @@ impl Database {
.await .await
} }
pub async fn get_channel_internal( pub(crate) async fn get_channel_internal(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,

View file

@ -1,6 +1,7 @@
use super::*; use super::*;
impl Database { impl Database {
/// Retrieves the contacts for the user with the given ID.
pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> { pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
#[derive(Debug, FromQueryResult)] #[derive(Debug, FromQueryResult)]
struct ContactWithUserBusyStatuses { struct ContactWithUserBusyStatuses {
@ -86,6 +87,7 @@ impl Database {
.await .await
} }
/// Returns whether the given user is a busy (on a call).
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> { pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let participant = room_participant::Entity::find() let participant = room_participant::Entity::find()
@ -97,6 +99,9 @@ impl Database {
.await .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> { pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let (id_a, id_b) = if user_id_1 < user_id_2 { let (id_a, id_b) = if user_id_1 < user_id_2 {
@ -119,6 +124,7 @@ impl Database {
.await .await
} }
/// Invite the user with `receiver_id` to be a contact of the user with `sender_id`.
pub async fn send_contact_request( pub async fn send_contact_request(
&self, &self,
sender_id: UserId, sender_id: UserId,
@ -231,6 +237,7 @@ impl Database {
.await .await
} }
/// Dismisses a contact notification for the given user.
pub async fn dismiss_contact_notification( pub async fn dismiss_contact_notification(
&self, &self,
user_id: UserId, user_id: UserId,
@ -272,6 +279,7 @@ impl Database {
.await .await
} }
/// Accept or decline a contact request
pub async fn respond_to_contact_request( pub async fn respond_to_contact_request(
&self, &self,
responder_id: UserId, responder_id: UserId,

View file

@ -4,6 +4,7 @@ use sea_orm::TryInsertResult;
use time::OffsetDateTime; use time::OffsetDateTime;
impl Database { impl Database {
/// Inserts a record representing a user joining the chat for a given channel.
pub async fn join_channel_chat( pub async fn join_channel_chat(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -28,6 +29,7 @@ impl Database {
.await .await
} }
/// Removes `channel_chat_participant` records associated with the given connection ID.
pub async fn channel_chat_connection_lost( pub async fn channel_chat_connection_lost(
&self, &self,
connection_id: ConnectionId, connection_id: ConnectionId,
@ -47,6 +49,8 @@ impl Database {
Ok(()) 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( pub async fn leave_channel_chat(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -72,6 +76,9 @@ impl Database {
.await .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( pub async fn get_channel_messages(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -103,6 +110,7 @@ impl Database {
.await .await
} }
/// Returns the channel messages with the given IDs.
pub async fn get_channel_messages_by_id( pub async fn get_channel_messages_by_id(
&self, &self,
user_id: UserId, user_id: UserId,
@ -190,6 +198,7 @@ impl Database {
Ok(messages) Ok(messages)
} }
/// Creates a new channel message.
pub async fn create_channel_message( pub async fn create_channel_message(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
@ -376,6 +385,7 @@ impl Database {
Ok(()) Ok(())
} }
/// Returns the unseen messages for the given user in the specified channels.
pub async fn unseen_channel_messages( pub async fn unseen_channel_messages(
&self, &self,
user_id: UserId, user_id: UserId,
@ -449,6 +459,7 @@ impl Database {
Ok(changes) Ok(changes)
} }
/// Removes the channel message with the given ID.
pub async fn remove_channel_message( pub async fn remove_channel_message(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,

View file

@ -2,6 +2,7 @@ use super::*;
use rpc::Notification; use rpc::Notification;
impl Database { impl Database {
/// Initializes the different kinds of notifications by upserting records for them.
pub async fn initialize_notification_kinds(&mut self) -> Result<()> { pub async fn initialize_notification_kinds(&mut self) -> Result<()> {
notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map( notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map(
|kind| notification_kind::ActiveModel { |kind| notification_kind::ActiveModel {
@ -28,6 +29,7 @@ impl Database {
Ok(()) Ok(())
} }
/// Returns the notifications for the given recipient.
pub async fn get_notifications( pub async fn get_notifications(
&self, &self,
recipient_id: UserId, recipient_id: UserId,
@ -140,6 +142,7 @@ impl Database {
.await .await
} }
/// Marks the given notification as read.
pub async fn mark_notification_as_read( pub async fn mark_notification_as_read(
&self, &self,
recipient_id: UserId, recipient_id: UserId,
@ -150,6 +153,7 @@ impl Database {
.await .await
} }
/// Marks the notification with the given ID as read.
pub async fn mark_notification_as_read_by_id( pub async fn mark_notification_as_read_by_id(
&self, &self,
recipient_id: UserId, recipient_id: UserId,

View file

@ -1,6 +1,7 @@
use super::*; use super::*;
impl Database { impl Database {
/// Returns the count of all projects, excluding ones marked as admin.
pub async fn project_count_excluding_admins(&self) -> Result<usize> { pub async fn project_count_excluding_admins(&self) -> Result<usize> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs { enum QueryAs {
@ -21,6 +22,7 @@ impl Database {
.await .await
} }
/// Shares a project with the given room.
pub async fn share_project( pub async fn share_project(
&self, &self,
room_id: RoomId, room_id: RoomId,
@ -100,6 +102,7 @@ impl Database {
.await .await
} }
/// Unshares the given project.
pub async fn unshare_project( pub async fn unshare_project(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -126,6 +129,7 @@ impl Database {
.await .await
} }
/// Updates the worktrees associated with the given project.
pub async fn update_project( pub async fn update_project(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -346,6 +350,7 @@ impl Database {
.await .await
} }
/// Updates the diagnostic summary for the given connection.
pub async fn update_diagnostic_summary( pub async fn update_diagnostic_summary(
&self, &self,
update: &proto::UpdateDiagnosticSummary, update: &proto::UpdateDiagnosticSummary,
@ -401,6 +406,7 @@ impl Database {
.await .await
} }
/// Starts the language server for the given connection.
pub async fn start_language_server( pub async fn start_language_server(
&self, &self,
update: &proto::StartLanguageServer, update: &proto::StartLanguageServer,
@ -447,6 +453,7 @@ impl Database {
.await .await
} }
/// Updates the worktree settings for the given connection.
pub async fn update_worktree_settings( pub async fn update_worktree_settings(
&self, &self,
update: &proto::UpdateWorktreeSettings, update: &proto::UpdateWorktreeSettings,
@ -499,6 +506,7 @@ impl Database {
.await .await
} }
/// Adds the given connection to the specified project.
pub async fn join_project( pub async fn join_project(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -704,6 +712,7 @@ impl Database {
.await .await
} }
/// Removes the given connection from the specified project.
pub async fn leave_project( pub async fn leave_project(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -805,6 +814,7 @@ impl Database {
.map(|guard| guard.into_inner()) .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( pub async fn host_for_read_only_project_request(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -842,6 +852,7 @@ impl Database {
.map(|guard| guard.into_inner()) .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( pub async fn host_for_mutating_project_request(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -927,6 +938,10 @@ impl Database {
.await .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( pub async fn project_connection_ids(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -976,6 +991,7 @@ impl Database {
Ok(guest_connection_ids) Ok(guest_connection_ids)
} }
/// Returns the [`RoomId`] for the given project.
pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> { pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id) let project = project::Entity::find_by_id(project_id)
@ -1020,6 +1036,7 @@ impl Database {
.await .await
} }
/// Adds the given follower connection as a follower of the given leader connection.
pub async fn follow( pub async fn follow(
&self, &self,
room_id: RoomId, room_id: RoomId,
@ -1050,6 +1067,7 @@ impl Database {
.await .await
} }
/// Removes the given follower connection as a follower of the given leader connection.
pub async fn unfollow( pub async fn unfollow(
&self, &self,
room_id: RoomId, room_id: RoomId,

View file

@ -1,6 +1,7 @@
use super::*; use super::*;
impl Database { impl Database {
/// Clears all room participants in rooms attached to a stale server.
pub async fn clear_stale_room_participants( pub async fn clear_stale_room_participants(
&self, &self,
room_id: RoomId, room_id: RoomId,
@ -78,6 +79,7 @@ impl Database {
.await .await
} }
/// Returns the incoming calls for user with the given ID.
pub async fn incoming_call_for_user( pub async fn incoming_call_for_user(
&self, &self,
user_id: UserId, user_id: UserId,
@ -102,6 +104,7 @@ impl Database {
.await .await
} }
/// Creates a new room.
pub async fn create_room( pub async fn create_room(
&self, &self,
user_id: UserId, user_id: UserId,
@ -394,6 +397,7 @@ impl Database {
Ok(participant_index) 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>> { pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result<Option<ChannelId>> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let room: Option<room::Model> = room::Entity::find() let room: Option<room::Model> = room::Entity::find()
@ -944,6 +948,7 @@ impl Database {
.await .await
} }
/// Updates the location of a participant in the given room.
pub async fn update_room_participant_location( pub async fn update_room_participant_location(
&self, &self,
room_id: RoomId, room_id: RoomId,
@ -1004,6 +1009,7 @@ impl Database {
.await .await
} }
/// Sets the role of a participant in the given room.
pub async fn set_room_participant_role( pub async fn set_room_participant_role(
&self, &self,
admin_id: UserId, admin_id: UserId,

View file

@ -1,6 +1,7 @@
use super::*; use super::*;
impl Database { impl Database {
/// Creates a new server in the given environment.
pub async fn create_server(&self, environment: &str) -> Result<ServerId> { pub async fn create_server(&self, environment: &str) -> Result<ServerId> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let server = server::ActiveModel { let server = server::ActiveModel {
@ -14,6 +15,10 @@ impl Database {
.await .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( pub async fn stale_server_resource_ids(
&self, &self,
environment: &str, environment: &str,
@ -61,6 +66,7 @@ impl Database {
.await .await
} }
/// Deletes any stale servers in the environment that don't match the `new_server_id`.
pub async fn delete_stale_servers( pub async fn delete_stale_servers(
&self, &self,
environment: &str, environment: &str,

View file

@ -1,6 +1,7 @@
use super::*; use super::*;
impl Database { impl Database {
/// Creates a new user.
pub async fn create_user( pub async fn create_user(
&self, &self,
email_address: &str, email_address: &str,
@ -19,7 +20,11 @@ impl Database {
}) })
.on_conflict( .on_conflict(
OnConflict::column(user::Column::GithubLogin) OnConflict::column(user::Column::GithubLogin)
.update_column(user::Column::GithubLogin) .update_columns([
user::Column::Admin,
user::Column::EmailAddress,
user::Column::GithubUserId,
])
.to_owned(), .to_owned(),
) )
.exec_with_returning(&*tx) .exec_with_returning(&*tx)
@ -35,11 +40,13 @@ impl Database {
.await .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>> { 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?) }) self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
.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>> { pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
self.transaction(|tx| async { self.transaction(|tx| async {
let tx = tx; let tx = tx;
@ -51,6 +58,7 @@ impl Database {
.await .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>> { pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
Ok(user::Entity::find() Ok(user::Entity::find()
@ -111,6 +119,8 @@ impl Database {
.await .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>> { pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
Ok(user::Entity::find() Ok(user::Entity::find()
@ -123,6 +133,7 @@ impl Database {
.await .await
} }
/// Returns the metrics id for the user.
pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> { pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs { enum QueryAs {
@ -142,6 +153,7 @@ impl Database {
.await .await
} }
/// Set "connected_once" on the user for analytics.
pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
user::Entity::update_many() user::Entity::update_many()
@ -157,6 +169,7 @@ impl Database {
.await .await
} }
/// hard delete the user.
pub async fn destroy_user(&self, id: UserId) -> Result<()> { pub async fn destroy_user(&self, id: UserId) -> Result<()> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
access_token::Entity::delete_many() access_token::Entity::delete_many()
@ -169,6 +182,7 @@ impl Database {
.await .await
} }
/// Find users where github_login ILIKE name_query.
pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> { pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
self.transaction(|tx| async { self.transaction(|tx| async {
let tx = tx; let tx = tx;
@ -193,6 +207,8 @@ impl Database {
.await .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 { pub fn fuzzy_like_string(string: &str) -> String {
let mut result = String::with_capacity(string.len() * 2 + 1); let mut result = String::with_capacity(string.len() * 2 + 1);
for c in string.chars() { for c in string.chars() {
@ -205,6 +221,7 @@ impl Database {
result result
} }
/// Creates a new feature flag.
pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> { pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
@ -220,6 +237,7 @@ impl Database {
.await .await
} }
/// Add the given user to the feature flag
pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
user_feature::Entity::insert(user_feature::ActiveModel { user_feature::Entity::insert(user_feature::ActiveModel {
@ -234,6 +252,7 @@ impl Database {
.await .await
} }
/// Return the active flags for the user.
pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> { pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]

View file

@ -7,6 +7,7 @@ pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: AccessTokenId, pub id: AccessTokenId,
pub user_id: UserId, pub user_id: UserId,
pub impersonated_user_id: Option<UserId>,
pub hash: String, pub hash: String,
} }

View file

@ -2,6 +2,7 @@ use crate::db::UserId;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::Serialize; use serde::Serialize;
/// A user model.
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)] #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "users")] #[sea_orm(table_name = "users")]
pub struct Model { pub struct Model {

View file

@ -146,7 +146,7 @@ test_both_dbs!(
); );
async fn test_create_access_tokens(db: &Arc<Database>) { async fn test_create_access_tokens(db: &Arc<Database>) {
let user = db let user_1 = db
.create_user( .create_user(
"u1@example.com", "u1@example.com",
false, false,
@ -158,14 +158,27 @@ async fn test_create_access_tokens(db: &Arc<Database>) {
.await .await
.unwrap() .unwrap()
.user_id; .user_id;
let user_2 = db
.create_user(
"u2@example.com",
false,
NewUserParams {
github_login: "u2".into(),
github_user_id: 2,
},
)
.await
.unwrap()
.user_id;
let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); let token_1 = db.create_access_token(user_1, None, "h1", 2).await.unwrap();
let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); let token_2 = db.create_access_token(user_1, None, "h2", 2).await.unwrap();
assert_eq!( assert_eq!(
db.get_access_token(token_1).await.unwrap(), db.get_access_token(token_1).await.unwrap(),
access_token::Model { access_token::Model {
id: token_1, id: token_1,
user_id: user, user_id: user_1,
impersonated_user_id: None,
hash: "h1".into(), hash: "h1".into(),
} }
); );
@ -173,17 +186,19 @@ async fn test_create_access_tokens(db: &Arc<Database>) {
db.get_access_token(token_2).await.unwrap(), db.get_access_token(token_2).await.unwrap(),
access_token::Model { access_token::Model {
id: token_2, id: token_2,
user_id: user, user_id: user_1,
impersonated_user_id: None,
hash: "h2".into() hash: "h2".into()
} }
); );
let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); let token_3 = db.create_access_token(user_1, None, "h3", 2).await.unwrap();
assert_eq!( assert_eq!(
db.get_access_token(token_3).await.unwrap(), db.get_access_token(token_3).await.unwrap(),
access_token::Model { access_token::Model {
id: token_3, id: token_3,
user_id: user, user_id: user_1,
impersonated_user_id: None,
hash: "h3".into() hash: "h3".into()
} }
); );
@ -191,18 +206,20 @@ async fn test_create_access_tokens(db: &Arc<Database>) {
db.get_access_token(token_2).await.unwrap(), db.get_access_token(token_2).await.unwrap(),
access_token::Model { access_token::Model {
id: token_2, id: token_2,
user_id: user, user_id: user_1,
impersonated_user_id: None,
hash: "h2".into() hash: "h2".into()
} }
); );
assert!(db.get_access_token(token_1).await.is_err()); assert!(db.get_access_token(token_1).await.is_err());
let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); let token_4 = db.create_access_token(user_1, None, "h4", 2).await.unwrap();
assert_eq!( assert_eq!(
db.get_access_token(token_4).await.unwrap(), db.get_access_token(token_4).await.unwrap(),
access_token::Model { access_token::Model {
id: token_4, id: token_4,
user_id: user, user_id: user_1,
impersonated_user_id: None,
hash: "h4".into() hash: "h4".into()
} }
); );
@ -210,12 +227,77 @@ async fn test_create_access_tokens(db: &Arc<Database>) {
db.get_access_token(token_3).await.unwrap(), db.get_access_token(token_3).await.unwrap(),
access_token::Model { access_token::Model {
id: token_3, id: token_3,
user_id: user, user_id: user_1,
impersonated_user_id: None,
hash: "h3".into() hash: "h3".into()
} }
); );
assert!(db.get_access_token(token_2).await.is_err()); assert!(db.get_access_token(token_2).await.is_err());
assert!(db.get_access_token(token_1).await.is_err()); assert!(db.get_access_token(token_1).await.is_err());
// An access token for user 2 impersonating user 1 does not
// count against user 1's access token limit (of 2).
let token_5 = db
.create_access_token(user_2, Some(user_1), "h5", 2)
.await
.unwrap();
assert_eq!(
db.get_access_token(token_5).await.unwrap(),
access_token::Model {
id: token_5,
user_id: user_2,
impersonated_user_id: Some(user_1),
hash: "h5".into()
}
);
assert_eq!(
db.get_access_token(token_3).await.unwrap(),
access_token::Model {
id: token_3,
user_id: user_1,
impersonated_user_id: None,
hash: "h3".into()
}
);
// Only a limited number (2) of access tokens are stored for user 2
// impersonating other users.
let token_6 = db
.create_access_token(user_2, Some(user_1), "h6", 2)
.await
.unwrap();
let token_7 = db
.create_access_token(user_2, Some(user_1), "h7", 2)
.await
.unwrap();
assert_eq!(
db.get_access_token(token_6).await.unwrap(),
access_token::Model {
id: token_6,
user_id: user_2,
impersonated_user_id: Some(user_1),
hash: "h6".into()
}
);
assert_eq!(
db.get_access_token(token_7).await.unwrap(),
access_token::Model {
id: token_7,
user_id: user_2,
impersonated_user_id: Some(user_1),
hash: "h7".into()
}
);
assert!(db.get_access_token(token_5).await.is_err());
assert_eq!(
db.get_access_token(token_3).await.unwrap(),
access_token::Model {
id: token_3,
user_id: user_1,
impersonated_user_id: None,
hash: "h3".into()
}
);
} }
test_both_dbs!( test_both_dbs!(

View file

@ -1,7 +1,7 @@
mod connection_pool; mod connection_pool;
use crate::{ use crate::{
auth, auth::{self, Impersonator},
db::{ db::{
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult, self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult,
CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
@ -65,7 +65,7 @@ use std::{
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::sync::{watch, Semaphore}; use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument}; use tracing::{field, info_span, instrument, Instrument};
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
@ -561,13 +561,17 @@ impl Server {
connection: Connection, connection: Connection,
address: String, address: String,
user: User, user: User,
impersonator: Option<User>,
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>, mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
executor: Executor, executor: Executor,
) -> impl Future<Output = Result<()>> { ) -> impl Future<Output = Result<()>> {
let this = self.clone(); let this = self.clone();
let user_id = user.id; let user_id = user.id;
let login = user.github_login; let login = user.github_login;
let span = info_span!("handle connection", %user_id, %login, %address); let span = info_span!("handle connection", %user_id, %login, %address, impersonator = field::Empty);
if let Some(impersonator) = impersonator {
span.record("impersonator", &impersonator.github_login);
}
let mut teardown = self.teardown.subscribe(); let mut teardown = self.teardown.subscribe();
async move { async move {
let (connection_id, handle_io, mut incoming_rx) = this let (connection_id, handle_io, mut incoming_rx) = this
@ -839,6 +843,7 @@ pub async fn handle_websocket_request(
ConnectInfo(socket_address): ConnectInfo<SocketAddr>, ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
Extension(server): Extension<Arc<Server>>, Extension(server): Extension<Arc<Server>>,
Extension(user): Extension<User>, Extension(user): Extension<User>,
Extension(impersonator): Extension<Impersonator>,
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
) -> axum::response::Response { ) -> axum::response::Response {
if protocol_version != rpc::PROTOCOL_VERSION { if protocol_version != rpc::PROTOCOL_VERSION {
@ -858,7 +863,14 @@ pub async fn handle_websocket_request(
let connection = Connection::new(Box::pin(socket)); let connection = Connection::new(Box::pin(socket));
async move { async move {
server server
.handle_connection(connection, socket_address, user, None, Executor::Production) .handle_connection(
connection,
socket_address,
user,
impersonator.0,
None,
Executor::Production,
)
.await .await
.log_err(); .log_err();
} }
@ -932,11 +944,13 @@ async fn connection_lost(
Ok(()) 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<()> { async fn ping(_: proto::Ping, response: Response<proto::Ping>, _session: Session) -> Result<()> {
response.send(proto::Ack {})?; response.send(proto::Ack {})?;
Ok(()) Ok(())
} }
/// Create a new room for calling (outside of channels)
async fn create_room( async fn create_room(
_request: proto::CreateRoom, _request: proto::CreateRoom,
response: Response<proto::CreateRoom>, response: Response<proto::CreateRoom>,
@ -984,6 +998,7 @@ async fn create_room(
Ok(()) Ok(())
} }
/// Join a room from an invitation. Equivalent to joining a channel if there is one.
async fn join_room( async fn join_room(
request: proto::JoinRoom, request: proto::JoinRoom,
response: Response<proto::JoinRoom>, response: Response<proto::JoinRoom>,
@ -1058,6 +1073,7 @@ async fn join_room(
Ok(()) Ok(())
} }
/// Rejoin room is used to reconnect to a room after connection errors.
async fn rejoin_room( async fn rejoin_room(
request: proto::RejoinRoom, request: proto::RejoinRoom,
response: Response<proto::RejoinRoom>, response: Response<proto::RejoinRoom>,
@ -1249,6 +1265,7 @@ async fn rejoin_room(
Ok(()) Ok(())
} }
/// leave room disonnects from the room.
async fn leave_room( async fn leave_room(
_: proto::LeaveRoom, _: proto::LeaveRoom,
response: Response<proto::LeaveRoom>, response: Response<proto::LeaveRoom>,
@ -1259,6 +1276,7 @@ async fn leave_room(
Ok(()) Ok(())
} }
/// Update the permissions of someone else in the room.
async fn set_room_participant_role( async fn set_room_participant_role(
request: proto::SetRoomParticipantRole, request: proto::SetRoomParticipantRole,
response: Response<proto::SetRoomParticipantRole>, response: Response<proto::SetRoomParticipantRole>,
@ -1303,6 +1321,7 @@ async fn set_room_participant_role(
Ok(()) Ok(())
} }
/// Call someone else into the current room
async fn call( async fn call(
request: proto::Call, request: proto::Call,
response: Response<proto::Call>, response: Response<proto::Call>,
@ -1371,6 +1390,7 @@ async fn call(
Err(anyhow!("failed to ring user"))? Err(anyhow!("failed to ring user"))?
} }
/// Cancel an outgoing call.
async fn cancel_call( async fn cancel_call(
request: proto::CancelCall, request: proto::CancelCall,
response: Response<proto::CancelCall>, response: Response<proto::CancelCall>,
@ -1408,6 +1428,7 @@ async fn cancel_call(
Ok(()) Ok(())
} }
/// Decline an incoming call.
async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<()> { async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<()> {
let room_id = RoomId::from_proto(message.room_id); let room_id = RoomId::from_proto(message.room_id);
{ {
@ -1439,6 +1460,7 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<(
Ok(()) Ok(())
} }
/// Update other participants in the room with your current location.
async fn update_participant_location( async fn update_participant_location(
request: proto::UpdateParticipantLocation, request: proto::UpdateParticipantLocation,
response: Response<proto::UpdateParticipantLocation>, response: Response<proto::UpdateParticipantLocation>,
@ -1459,6 +1481,7 @@ async fn update_participant_location(
Ok(()) Ok(())
} }
/// Share a project into the room.
async fn share_project( async fn share_project(
request: proto::ShareProject, request: proto::ShareProject,
response: Response<proto::ShareProject>, response: Response<proto::ShareProject>,
@ -1481,6 +1504,7 @@ async fn share_project(
Ok(()) Ok(())
} }
/// Unshare a project from the room.
async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> { async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(message.project_id); let project_id = ProjectId::from_proto(message.project_id);
@ -1500,6 +1524,7 @@ async fn unshare_project(message: proto::UnshareProject, session: Session) -> Re
Ok(()) Ok(())
} }
/// Join someone elses shared project.
async fn join_project( async fn join_project(
request: proto::JoinProject, request: proto::JoinProject,
response: Response<proto::JoinProject>, response: Response<proto::JoinProject>,
@ -1625,6 +1650,7 @@ async fn join_project(
Ok(()) Ok(())
} }
/// Leave someone elses shared project.
async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> { async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> {
let sender_id = session.connection_id; let sender_id = session.connection_id;
let project_id = ProjectId::from_proto(request.project_id); let project_id = ProjectId::from_proto(request.project_id);
@ -1647,6 +1673,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
Ok(()) Ok(())
} }
/// Update other participants with changes to the project
async fn update_project( async fn update_project(
request: proto::UpdateProject, request: proto::UpdateProject,
response: Response<proto::UpdateProject>, response: Response<proto::UpdateProject>,
@ -1673,6 +1700,7 @@ async fn update_project(
Ok(()) Ok(())
} }
/// Update other participants with changes to the worktree
async fn update_worktree( async fn update_worktree(
request: proto::UpdateWorktree, request: proto::UpdateWorktree,
response: Response<proto::UpdateWorktree>, response: Response<proto::UpdateWorktree>,
@ -1697,6 +1725,7 @@ async fn update_worktree(
Ok(()) Ok(())
} }
/// Update other participants with changes to the diagnostics
async fn update_diagnostic_summary( async fn update_diagnostic_summary(
message: proto::UpdateDiagnosticSummary, message: proto::UpdateDiagnosticSummary,
session: Session, session: Session,
@ -1720,6 +1749,7 @@ async fn update_diagnostic_summary(
Ok(()) Ok(())
} }
/// Update other participants with changes to the worktree settings
async fn update_worktree_settings( async fn update_worktree_settings(
message: proto::UpdateWorktreeSettings, message: proto::UpdateWorktreeSettings,
session: Session, session: Session,
@ -1743,6 +1773,7 @@ async fn update_worktree_settings(
Ok(()) Ok(())
} }
/// Notify other participants that a language server has started.
async fn start_language_server( async fn start_language_server(
request: proto::StartLanguageServer, request: proto::StartLanguageServer,
session: Session, session: Session,
@ -1765,6 +1796,7 @@ async fn start_language_server(
Ok(()) Ok(())
} }
/// Notify other participants that a language server has changed.
async fn update_language_server( async fn update_language_server(
request: proto::UpdateLanguageServer, request: proto::UpdateLanguageServer,
session: Session, session: Session,
@ -1787,6 +1819,8 @@ async fn update_language_server(
Ok(()) 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>( async fn forward_read_only_project_request<T>(
request: T, request: T,
response: Response<T>, response: Response<T>,
@ -1809,6 +1843,8 @@ where
Ok(()) Ok(())
} }
/// forward a project request to the host. These requests are disallowed
/// for guests.
async fn forward_mutating_project_request<T>( async fn forward_mutating_project_request<T>(
request: T, request: T,
response: Response<T>, response: Response<T>,
@ -1831,6 +1867,7 @@ where
Ok(()) Ok(())
} }
/// Notify other participants that a new buffer has been created
async fn create_buffer_for_peer( async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer, request: proto::CreateBufferForPeer,
session: Session, session: Session,
@ -1850,6 +1887,8 @@ async fn create_buffer_for_peer(
Ok(()) 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( async fn update_buffer(
request: proto::UpdateBuffer, request: proto::UpdateBuffer,
response: Response<proto::UpdateBuffer>, response: Response<proto::UpdateBuffer>,
@ -1909,6 +1948,7 @@ async fn update_buffer(
Ok(()) Ok(())
} }
/// Notify other participants that a project has been updated.
async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>( async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
request: T, request: T,
session: Session, session: Session,
@ -1932,6 +1972,7 @@ async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProj
Ok(()) Ok(())
} }
/// Start following another user in a call.
async fn follow( async fn follow(
request: proto::Follow, request: proto::Follow,
response: Response<proto::Follow>, response: Response<proto::Follow>,
@ -1969,6 +2010,7 @@ async fn follow(
Ok(()) Ok(())
} }
/// Stop following another user in a call.
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
let room_id = RoomId::from_proto(request.room_id); let room_id = RoomId::from_proto(request.room_id);
let project_id = request.project_id.map(ProjectId::from_proto); let project_id = request.project_id.map(ProjectId::from_proto);
@ -2000,6 +2042,7 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
Ok(()) Ok(())
} }
/// Notify everyone following you of your current location.
async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> { async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
let room_id = RoomId::from_proto(request.room_id); let room_id = RoomId::from_proto(request.room_id);
let database = session.db.lock().await; let database = session.db.lock().await;
@ -2036,6 +2079,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
Ok(()) Ok(())
} }
/// Get public data about users.
async fn get_users( async fn get_users(
request: proto::GetUsers, request: proto::GetUsers,
response: Response<proto::GetUsers>, response: Response<proto::GetUsers>,
@ -2062,6 +2106,7 @@ async fn get_users(
Ok(()) Ok(())
} }
/// Search for users (to invite) buy Github login
async fn fuzzy_search_users( async fn fuzzy_search_users(
request: proto::FuzzySearchUsers, request: proto::FuzzySearchUsers,
response: Response<proto::FuzzySearchUsers>, response: Response<proto::FuzzySearchUsers>,
@ -2092,6 +2137,7 @@ async fn fuzzy_search_users(
Ok(()) Ok(())
} }
/// Send a contact request to another user.
async fn request_contact( async fn request_contact(
request: proto::RequestContact, request: proto::RequestContact,
response: Response<proto::RequestContact>, response: Response<proto::RequestContact>,
@ -2138,6 +2184,7 @@ async fn request_contact(
Ok(()) Ok(())
} }
/// Accept or decline a contact request
async fn respond_to_contact_request( async fn respond_to_contact_request(
request: proto::RespondToContactRequest, request: proto::RespondToContactRequest,
response: Response<proto::RespondToContactRequest>, response: Response<proto::RespondToContactRequest>,
@ -2195,6 +2242,7 @@ async fn respond_to_contact_request(
Ok(()) Ok(())
} }
/// Remove a contact.
async fn remove_contact( async fn remove_contact(
request: proto::RemoveContact, request: proto::RemoveContact,
response: Response<proto::RemoveContact>, response: Response<proto::RemoveContact>,
@ -2245,6 +2293,7 @@ async fn remove_contact(
Ok(()) Ok(())
} }
/// Create a new channel.
async fn create_channel( async fn create_channel(
request: proto::CreateChannel, request: proto::CreateChannel,
response: Response<proto::CreateChannel>, response: Response<proto::CreateChannel>,
@ -2279,6 +2328,7 @@ async fn create_channel(
Ok(()) Ok(())
} }
/// Delete a channel
async fn delete_channel( async fn delete_channel(
request: proto::DeleteChannel, request: proto::DeleteChannel,
response: Response<proto::DeleteChannel>, response: Response<proto::DeleteChannel>,
@ -2308,6 +2358,7 @@ async fn delete_channel(
Ok(()) Ok(())
} }
/// Invite someone to join a channel.
async fn invite_channel_member( async fn invite_channel_member(
request: proto::InviteChannelMember, request: proto::InviteChannelMember,
response: Response<proto::InviteChannelMember>, response: Response<proto::InviteChannelMember>,
@ -2344,6 +2395,7 @@ async fn invite_channel_member(
Ok(()) Ok(())
} }
/// remove someone from a channel
async fn remove_channel_member( async fn remove_channel_member(
request: proto::RemoveChannelMember, request: proto::RemoveChannelMember,
response: Response<proto::RemoveChannelMember>, response: Response<proto::RemoveChannelMember>,
@ -2385,6 +2437,7 @@ async fn remove_channel_member(
Ok(()) Ok(())
} }
/// Toggle the channel between public and private
async fn set_channel_visibility( async fn set_channel_visibility(
request: proto::SetChannelVisibility, request: proto::SetChannelVisibility,
response: Response<proto::SetChannelVisibility>, response: Response<proto::SetChannelVisibility>,
@ -2423,6 +2476,7 @@ async fn set_channel_visibility(
Ok(()) Ok(())
} }
/// Alter the role for a user in the channel
async fn set_channel_member_role( async fn set_channel_member_role(
request: proto::SetChannelMemberRole, request: proto::SetChannelMemberRole,
response: Response<proto::SetChannelMemberRole>, response: Response<proto::SetChannelMemberRole>,
@ -2470,6 +2524,7 @@ async fn set_channel_member_role(
Ok(()) Ok(())
} }
/// Change the name of a channel
async fn rename_channel( async fn rename_channel(
request: proto::RenameChannel, request: proto::RenameChannel,
response: Response<proto::RenameChannel>, response: Response<proto::RenameChannel>,
@ -2503,6 +2558,7 @@ async fn rename_channel(
Ok(()) Ok(())
} }
/// Move a channel to a new parent.
async fn move_channel( async fn move_channel(
request: proto::MoveChannel, request: proto::MoveChannel,
response: Response<proto::MoveChannel>, response: Response<proto::MoveChannel>,
@ -2555,6 +2611,7 @@ async fn notify_channel_moved(result: Option<MoveChannelResult>, session: Sessio
Ok(()) Ok(())
} }
/// Get the list of channel members
async fn get_channel_members( async fn get_channel_members(
request: proto::GetChannelMembers, request: proto::GetChannelMembers,
response: Response<proto::GetChannelMembers>, response: Response<proto::GetChannelMembers>,
@ -2569,6 +2626,7 @@ async fn get_channel_members(
Ok(()) Ok(())
} }
/// Accept or decline a channel invitation.
async fn respond_to_channel_invite( async fn respond_to_channel_invite(
request: proto::RespondToChannelInvite, request: proto::RespondToChannelInvite,
response: Response<proto::RespondToChannelInvite>, response: Response<proto::RespondToChannelInvite>,
@ -2609,6 +2667,7 @@ async fn respond_to_channel_invite(
Ok(()) Ok(())
} }
/// Join the channels' room
async fn join_channel( async fn join_channel(
request: proto::JoinChannel, request: proto::JoinChannel,
response: Response<proto::JoinChannel>, response: Response<proto::JoinChannel>,
@ -2713,6 +2772,7 @@ async fn join_channel_internal(
Ok(()) Ok(())
} }
/// Start editing the channel notes
async fn join_channel_buffer( async fn join_channel_buffer(
request: proto::JoinChannelBuffer, request: proto::JoinChannelBuffer,
response: Response<proto::JoinChannelBuffer>, response: Response<proto::JoinChannelBuffer>,
@ -2744,6 +2804,7 @@ async fn join_channel_buffer(
Ok(()) Ok(())
} }
/// Edit the channel notes
async fn update_channel_buffer( async fn update_channel_buffer(
request: proto::UpdateChannelBuffer, request: proto::UpdateChannelBuffer,
session: Session, session: Session,
@ -2790,6 +2851,7 @@ async fn update_channel_buffer(
Ok(()) Ok(())
} }
/// Rejoin the channel notes after a connection blip
async fn rejoin_channel_buffers( async fn rejoin_channel_buffers(
request: proto::RejoinChannelBuffers, request: proto::RejoinChannelBuffers,
response: Response<proto::RejoinChannelBuffers>, response: Response<proto::RejoinChannelBuffers>,
@ -2824,6 +2886,7 @@ async fn rejoin_channel_buffers(
Ok(()) Ok(())
} }
/// Stop editing the channel notes
async fn leave_channel_buffer( async fn leave_channel_buffer(
request: proto::LeaveChannelBuffer, request: proto::LeaveChannelBuffer,
response: Response<proto::LeaveChannelBuffer>, response: Response<proto::LeaveChannelBuffer>,
@ -2885,6 +2948,7 @@ fn send_notifications(
} }
} }
/// Send a message to the channel
async fn send_channel_message( async fn send_channel_message(
request: proto::SendChannelMessage, request: proto::SendChannelMessage,
response: Response<proto::SendChannelMessage>, response: Response<proto::SendChannelMessage>,
@ -2973,6 +3037,7 @@ async fn send_channel_message(
Ok(()) Ok(())
} }
/// Delete a channel message
async fn remove_channel_message( async fn remove_channel_message(
request: proto::RemoveChannelMessage, request: proto::RemoveChannelMessage,
response: Response<proto::RemoveChannelMessage>, response: Response<proto::RemoveChannelMessage>,
@ -2992,6 +3057,7 @@ async fn remove_channel_message(
Ok(()) Ok(())
} }
/// Mark a channel message as read
async fn acknowledge_channel_message( async fn acknowledge_channel_message(
request: proto::AckChannelMessage, request: proto::AckChannelMessage,
session: Session, session: Session,
@ -3011,6 +3077,7 @@ async fn acknowledge_channel_message(
Ok(()) Ok(())
} }
/// Mark a buffer version as synced
async fn acknowledge_buffer_version( async fn acknowledge_buffer_version(
request: proto::AckBufferOperation, request: proto::AckBufferOperation,
session: Session, session: Session,
@ -3029,6 +3096,7 @@ async fn acknowledge_buffer_version(
Ok(()) Ok(())
} }
/// Start receiving chat updates for a channel
async fn join_channel_chat( async fn join_channel_chat(
request: proto::JoinChannelChat, request: proto::JoinChannelChat,
response: Response<proto::JoinChannelChat>, response: Response<proto::JoinChannelChat>,
@ -3049,6 +3117,7 @@ async fn join_channel_chat(
Ok(()) Ok(())
} }
/// Stop receiving chat updates for a channel
async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> { async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
session session
@ -3059,6 +3128,7 @@ async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session)
Ok(()) Ok(())
} }
/// Retrieve the chat history for a channel
async fn get_channel_messages( async fn get_channel_messages(
request: proto::GetChannelMessages, request: proto::GetChannelMessages,
response: Response<proto::GetChannelMessages>, response: Response<proto::GetChannelMessages>,
@ -3082,6 +3152,7 @@ async fn get_channel_messages(
Ok(()) Ok(())
} }
/// Retrieve specific chat messages
async fn get_channel_messages_by_id( async fn get_channel_messages_by_id(
request: proto::GetChannelMessagesById, request: proto::GetChannelMessagesById,
response: Response<proto::GetChannelMessagesById>, response: Response<proto::GetChannelMessagesById>,
@ -3104,6 +3175,7 @@ async fn get_channel_messages_by_id(
Ok(()) Ok(())
} }
/// Retrieve the current users notifications
async fn get_notifications( async fn get_notifications(
request: proto::GetNotifications, request: proto::GetNotifications,
response: Response<proto::GetNotifications>, response: Response<proto::GetNotifications>,
@ -3127,6 +3199,7 @@ async fn get_notifications(
Ok(()) Ok(())
} }
/// Mark notifications as read
async fn mark_notification_as_read( async fn mark_notification_as_read(
request: proto::MarkNotificationRead, request: proto::MarkNotificationRead,
response: Response<proto::MarkNotificationRead>, response: Response<proto::MarkNotificationRead>,
@ -3148,6 +3221,7 @@ async fn mark_notification_as_read(
Ok(()) Ok(())
} }
/// Get the current users information
async fn get_private_user_info( async fn get_private_user_info(
_request: proto::GetPrivateUserInfo, _request: proto::GetPrivateUserInfo,
response: Response<proto::GetPrivateUserInfo>, response: Response<proto::GetPrivateUserInfo>,

View file

@ -203,7 +203,7 @@ async fn test_core_channels(
executor.run_until_parked(); executor.run_until_parked();
// Observe that client B is now an admin of channel A, and that // Observe that client B is now an admin of channel A, and that
// their admin priveleges extend to subchannels of channel A. // their admin privileges extend to subchannels of channel A.
assert_channel_invitations(client_b.channel_store(), cx_b, &[]); assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
assert_channels( assert_channels(
client_b.channel_store(), client_b.channel_store(),

View file

@ -8,9 +8,11 @@ use std::{
use call::ActiveCall; use call::ActiveCall;
use editor::{ use editor::{
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, ToggleCodeActions, Undo,
},
test::editor_test_context::{AssertionContextManager, EditorTestContext}, test::editor_test_context::{AssertionContextManager, EditorTestContext},
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Editor,
Undo,
}; };
use futures::StreamExt; use futures::StreamExt;
use gpui::{TestAppContext, VisualContext, VisualTestContext}; use gpui::{TestAppContext, VisualContext, VisualTestContext};
@ -185,31 +187,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)) .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await .await
.unwrap(); .unwrap();
let window_a = cx_a.add_empty_window(); let cx_a = cx_a.add_empty_window();
let editor_a = let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
let mut editor_cx_a = EditorTestContext { let mut editor_cx_a = EditorTestContext {
cx: VisualTestContext::from_window(window_a, cx_a), cx: cx_a.clone(),
window: window_a.into(), window: cx_a.handle(),
editor: editor_a, editor: editor_a,
assertion_cx: AssertionContextManager::new(), assertion_cx: AssertionContextManager::new(),
}; };
let window_b = cx_b.add_empty_window(); let cx_b = cx_b.add_empty_window();
let mut cx_b = VisualTestContext::from_window(window_b, cx_b);
// Open a buffer as client B // Open a buffer as client B
let buffer_b = project_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 .await
.unwrap(); .unwrap();
let editor_b = window_b.build_view(&mut cx_b, |cx| { let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
Editor::for_buffer(buffer_b, Some(project_b), cx)
});
let mut editor_cx_b = EditorTestContext { let mut editor_cx_b = EditorTestContext {
cx: cx_b, cx: cx_b.clone(),
window: window_b.into(), window: cx_b.handle(),
editor: editor_b, editor: editor_b,
assertion_cx: AssertionContextManager::new(), assertion_cx: AssertionContextManager::new(),
}; };
@ -221,7 +219,8 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
editor_cx_b.set_selections_state(indoc! {" editor_cx_b.set_selections_state(indoc! {"
Some textˇ Some textˇ
"}); "});
editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); editor_cx_a
.update_editor(|editor, cx| editor.newline_above(&editor::actions::NewlineAbove, cx));
executor.run_until_parked(); executor.run_until_parked();
editor_cx_a.assert_editor_state(indoc! {" editor_cx_a.assert_editor_state(indoc! {"
ˇ ˇ
@ -241,7 +240,8 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
Some textˇ Some textˇ
"}); "});
editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); editor_cx_a
.update_editor(|editor, cx| editor.newline_below(&editor::actions::NewlineBelow, cx));
executor.run_until_parked(); executor.run_until_parked();
editor_cx_a.assert_editor_state(indoc! {" editor_cx_a.assert_editor_state(indoc! {"
@ -311,10 +311,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)) .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
let window_b = cx_b.add_empty_window(); let cx_b = cx_b.add_empty_window();
let editor_b = window_b.build_view(cx_b, |cx| { let editor_b =
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) 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(); let fake_language_server = fake_language_servers.next().await.unwrap();
cx_a.background_executor.run_until_parked(); cx_a.background_executor.run_until_parked();
@ -323,10 +322,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
assert!(!buffer.completion_triggers().is_empty()) 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. // 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.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.handle_input(".", cx); editor.handle_input(".", cx);
}); });
@ -392,8 +389,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
}); });
// Confirm a completion on the guest. // Confirm a completion on the guest.
editor_b.update(cx_b, |editor, cx| {
editor_b.update(&mut cx_b, |editor, cx| {
assert!(editor.context_menu_visible()); assert!(editor.context_menu_visible());
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
@ -431,7 +427,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!( assert_eq!(
buffer.text(), buffer.text(),
"use d::SomeTrait;\nfn main() { a.first_method() }" "use d::SomeTrait;\nfn main() { a.first_method() }"
@ -960,7 +956,7 @@ async fn test_share_project(
cx_c: &mut TestAppContext, cx_c: &mut TestAppContext,
) { ) {
let executor = cx_a.executor(); 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 mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
@ -1075,7 +1071,7 @@ async fn test_share_project(
.await .await
.unwrap(); .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 // Client A sees client B's selection
executor.run_until_parked(); executor.run_until_parked();
@ -1089,8 +1085,7 @@ async fn test_share_project(
}); });
// Edit the buffer as client B and see that edit as client A. // 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(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
editor_b.update(&mut cx_b, |editor, cx| editor.handle_input("ok, ", cx));
executor.run_until_parked(); executor.run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| { buffer_a.read_with(cx_a, |buffer, _| {
@ -1099,7 +1094,7 @@ async fn test_share_project(
// Client B can invite client C on a project shared by client A. // Client B can invite client C on a project shared by client A.
active_call_b 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) call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
}) })
.await .await
@ -1190,18 +1185,14 @@ async fn test_on_input_format_from_host_to_guest(
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
let window_a = cx_a.add_empty_window(); let cx_a = cx_a.add_empty_window();
let editor_a = window_a let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
.update(cx_a, |_, cx| {
cx.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx))
})
.unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked(); executor.run_until_parked();
// Receive an OnTypeFormatting request as the host's language server. // Receive an OnTypeFormatting request as the host's language server.
// Return some formattings from the host's language server. // Return some formatting from the host's language server.
fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>( fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
|params, _| async move { |params, _| async move {
assert_eq!( assert_eq!(
@ -1220,16 +1211,15 @@ async fn test_on_input_format_from_host_to_guest(
}, },
); );
// Open the buffer on the guest and see that the formattings worked // Open the buffer on the guest and see that the formatting worked
let buffer_b = project_b let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
let mut cx_a = VisualTestContext::from_window(window_a, cx_a);
// Type a on type formatting trigger character as the guest. // Type a on type formatting trigger character as the guest.
cx_a.focus_view(&editor_a); 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.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.handle_input(">", cx); editor.handle_input(">", cx);
}); });
@ -1241,7 +1231,7 @@ async fn test_on_input_format_from_host_to_guest(
}); });
// Undo should remove LSP edits first // 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>~< }"); assert_eq!(editor.text(cx), "fn main() { a>~< }");
editor.undo(&Undo, cx); editor.undo(&Undo, cx);
assert_eq!(editor.text(cx), "fn main() { a> }"); assert_eq!(editor.text(cx), "fn main() { a> }");
@ -1252,7 +1242,7 @@ async fn test_on_input_format_from_host_to_guest(
assert_eq!(buffer.text(), "fn main() { a> }") 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> }"); assert_eq!(editor.text(cx), "fn main() { a> }");
editor.undo(&Undo, cx); editor.undo(&Undo, cx);
assert_eq!(editor.text(cx), "fn main() { a }"); assert_eq!(editor.text(cx), "fn main() { a }");
@ -1323,23 +1313,21 @@ async fn test_on_input_format_from_guest_to_host(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
let window_b = cx_b.add_empty_window(); let cx_b = cx_b.add_empty_window();
let editor_b = window_b.build_view(cx_b, |cx| { let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked(); 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. // Type a on type formatting trigger character as the guest.
cx_b.focus_view(&editor_b); 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.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.handle_input(":", cx); editor.handle_input(":", cx);
}); });
// Receive an OnTypeFormatting request as the host's language server. // Receive an OnTypeFormatting request as the host's language server.
// Return some formattings from the host's language server. // Return some formatting from the host's language server.
executor.start_waiting(); executor.start_waiting();
fake_language_server fake_language_server
.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move { .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
@ -1362,7 +1350,7 @@ async fn test_on_input_format_from_guest_to_host(
.unwrap(); .unwrap();
executor.finish_waiting(); executor.finish_waiting();
// Open the buffer on the host and see that the formattings worked // Open the buffer on the host and see that the formatting worked
let buffer_a = project_a let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
@ -1374,7 +1362,7 @@ async fn test_on_input_format_from_guest_to_host(
}); });
// Undo should remove LSP edits first // 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:~: }"); assert_eq!(editor.text(cx), "fn main() { a:~: }");
editor.undo(&Undo, cx); editor.undo(&Undo, cx);
assert_eq!(editor.text(cx), "fn main() { a: }"); assert_eq!(editor.text(cx), "fn main() { a: }");
@ -1385,7 +1373,7 @@ async fn test_on_input_format_from_guest_to_host(
assert_eq!(buffer.text(), "fn main() { a: }") 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: }"); assert_eq!(editor.text(cx), "fn main() { a: }");
editor.undo(&Undo, cx); editor.undo(&Undo, cx);
assert_eq!(editor.text(cx), "fn main() { a }"); assert_eq!(editor.text(cx), "fn main() { a }");
@ -1836,7 +1824,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
assert_eq!( assert_eq!(
inlay_cache.version(), inlay_cache.version(),
1, 1,
"Should update cache verison after first hints" "Should update cache version after first hints"
); );
}); });

View file

@ -249,7 +249,7 @@ async fn test_basic_following(
executor.run_until_parked(); executor.run_until_parked();
cx_c.cx.update(|_| {}); 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. // Clients A and B see that client B is following A, and client C is not present in the followers.
executor.run_until_parked(); executor.run_until_parked();
@ -1229,7 +1229,9 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
}); });
// When client B moves, it automatically stops following client A. // When client B moves, it automatically stops following client A.
editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); editor_b2.update(cx_b, |editor, cx| {
editor.move_right(&editor::actions::MoveRight, cx)
});
assert_eq!( assert_eq!(
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
None None
@ -1735,6 +1737,11 @@ async fn test_following_into_excluded_file(
vec![18..17] vec![18..17]
); );
editor_for_excluded_a.update(cx_a, |editor, cx| {
editor.select_right(&Default::default(), cx);
});
executor.run_until_parked();
// Changes from B to the excluded file are replicated in A's editor // Changes from B to the excluded file are replicated in A's editor
editor_for_excluded_b.update(cx_b, |editor, cx| { editor_for_excluded_b.update(cx_b, |editor, cx| {
editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx); editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
@ -1743,7 +1750,7 @@ async fn test_following_into_excluded_file(
editor_for_excluded_a.update(cx_a, |editor, cx| { editor_for_excluded_a.update(cx_a, |editor, cx| {
assert_eq!( assert_eq!(
editor.text(cx), editor.text(cx),
"new commit messag\nCo-Authored-By: B <b@b.b>" "new commit message\nCo-Authored-By: B <b@b.b>"
); );
}); });
} }

View file

@ -7,7 +7,10 @@ use client::{User, RECEIVE_TIMEOUT};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _; use futures::StreamExt as _;
use gpui::{AppContext, BackgroundExecutor, Model, TestAppContext}; use gpui::{
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
TestAppContext,
};
use language::{ use language::{
language_settings::{AllLanguageSettings, Formatter}, language_settings::{AllLanguageSettings, Formatter},
tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
@ -5903,3 +5906,42 @@ async fn test_join_call_after_screen_was_shared(
); );
}); });
} }
#[gpui::test]
async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
let mut server = TestServer::start(cx.executor().clone()).await;
let client_a = server.create_client(cx, "user_a").await;
let (_workspace_a, cx) = client_a.build_test_workspace(cx).await;
cx.simulate_resize(size(px(300.), px(300.)));
cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
cx.update(|cx| cx.refresh());
let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
assert!(
tab_bounds.intersects(&new_tab_button_bounds),
"Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
);
cx.simulate_event(MouseDownEvent {
button: MouseButton::Right,
position: new_tab_button_bounds.center(),
modifiers: Modifiers::default(),
click_count: 1,
});
// regression test that the right click menu for tabs does not open.
assert!(cx.debug_bounds("MENU_ITEM-Close").is_none());
let tab_bounds = cx.debug_bounds("TAB-1").unwrap();
cx.simulate_event(MouseDownEvent {
button: MouseButton::Right,
position: tab_bounds.center(),
modifiers: Modifiers::default(),
click_count: 1,
});
assert!(cx.debug_bounds("MENU_ITEM-Close").is_some());
}

View file

@ -213,6 +213,7 @@ impl TestServer {
server_conn, server_conn,
client_name, client_name,
user, user,
None,
Some(connection_id_tx), Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()), Executor::Deterministic(cx.background_executor().clone()),
)) ))

View file

@ -125,6 +125,23 @@ impl ChatPanel {
open_context_menu: None, 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( this.subscriptions.push(cx.subscribe(
&ActiveCall::global(cx), &ActiveCall::global(cx),
move |this: &mut Self, call, event: &room::Event, cx| match event { move |this: &mut Self, call, event: &room::Event, cx| match event {

View file

@ -2314,7 +2314,7 @@ impl CollabPanel {
.child( .child(
IconButton::new("channel_chat", IconName::MessageBubbles) IconButton::new("channel_chat", IconName::MessageBubbles)
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.size(ButtonSize::Compact) .shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(if has_messages_notification { .icon_color(if has_messages_notification {
Color::Default Color::Default
@ -2332,7 +2332,7 @@ impl CollabPanel {
.child( .child(
IconButton::new("channel_notes", IconName::File) IconButton::new("channel_notes", IconName::File)
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.size(ButtonSize::Compact) .shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(if has_notes_notification { .icon_color(if has_notes_notification {
Color::Default Color::Default

View file

@ -111,7 +111,7 @@ impl ChannelModal {
.detach(); .detach();
} }
fn set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) { fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
self.channel_store.update(cx, |channel_store, cx| { self.channel_store.update(cx, |channel_store, cx| {
channel_store channel_store
.set_channel_visibility( .set_channel_visibility(
@ -189,7 +189,7 @@ impl Render for ChannelModal {
ui::Selection::Unselected ui::Selection::Unselected
}, },
) )
.on_click(cx.listener(Self::set_channel_visiblity)), .on_click(cx.listener(Self::set_channel_visibility)),
) )
.child(Label::new("Public").size(LabelSize::Small)), .child(Label::new("Public").size(LabelSize::Small)),
) )

View file

@ -3,7 +3,7 @@ use auto_update::AutoUpdateStatus;
use call::{ActiveCall, ParticipantLocation, Room}; use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, User, UserStore}; use client::{proto::PeerId, Client, User, UserStore};
use gpui::{ use gpui::{
actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla, actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
InteractiveElement, IntoElement, Model, ParentElement, Path, Render, InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView, StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
WindowBounds, WindowBounds,
@ -19,7 +19,7 @@ use ui::{
}; };
use util::ResultExt; use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu}; use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
use workspace::{notifications::NotifyResultExt, Workspace}; use workspace::{notifications::NotifyResultExt, titlebar_height, Workspace};
const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_PROJECT_NAME_LENGTH: usize = 40;
const MAX_BRANCH_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40;
@ -62,10 +62,7 @@ impl Render for CollabTitlebarItem {
.id("titlebar") .id("titlebar")
.justify_between() .justify_between()
.w_full() .w_full()
.h(rems(1.75)) .h(titlebar_height(cx))
// Set a non-scaling min-height here to ensure the titlebar is
// always at least the height of the traffic lights.
.min_h(px(32.))
.map(|this| { .map(|this| {
if matches!(cx.window_bounds(), WindowBounds::Fullscreen) { if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
this.pl_2() this.pl_2()
@ -480,14 +477,16 @@ impl CollabTitlebarItem {
return None; return None;
} }
const FACEPILE_LIMIT: usize = 3;
let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT);
let pile = FacePile::default() let pile = FacePile::default()
.child( .child(
Avatar::new(user.avatar_uri.clone()) Avatar::new(user.avatar_uri.clone())
.grayscale(!is_present) .grayscale(!is_present)
.border_color(if is_speaking { .border_color(if is_speaking {
cx.theme().status().info_border cx.theme().status().info
} else { } else {
// We draw the border in a transparent color rather to avoid // We draw the border in a transparent color rather to avoid
// the layout shift that would come with adding/removing the border. // the layout shift that would come with adding/removing the border.
@ -502,18 +501,34 @@ impl CollabTitlebarItem {
) )
}), }),
) )
.children(followers.iter().filter_map(|follower_peer_id| { .children(
followers
.iter()
.take(FACEPILE_LIMIT)
.filter_map(|follower_peer_id| {
let follower = room let follower = room
.remote_participants() .remote_participants()
.values() .values()
.find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user)) .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
.or_else(|| { .or_else(|| {
(self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user) (self.client.peer_id() == Some(*follower_peer_id))
.then_some(current_user)
})? })?
.clone(); .clone();
Some(Avatar::new(follower.avatar_uri.clone())) Some(Avatar::new(follower.avatar_uri.clone()))
})); }),
)
.children(if extra_count > 0 {
Some(
div()
.ml_1()
.child(Label::new(format!("+{extra_count}")))
.into_any_element(),
)
} else {
None
});
Some(pile) Some(pile)
} }

32
crates/color/Cargo.toml Normal file
View file

@ -0,0 +1,32 @@
[package]
name = "color"
version = "0.1.0"
edition = "2021"
publish = false
[features]
default = []
stories = ["dep:itertools", "dep:story"]
[lib]
path = "src/color.rs"
doctest = true
[dependencies]
# TODO: Clean up dependencies
anyhow.workspace = true
fs = { path = "../fs" }
indexmap = "1.6.2"
parking_lot.workspace = true
refineable.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings = { path = "../settings" }
story = { path = "../story", optional = true }
toml.workspace = true
uuid.workspace = true
util = { path = "../util" }
itertools = { version = "0.11.0", optional = true }
palette = "0.7.3"

234
crates/color/src/color.rs Normal file
View file

@ -0,0 +1,234 @@
//! # Color
//!
//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality.
//!
//! It is used to create a manipulate colors when building themes.
//!
//! === In development note ===
//!
//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff.
//!
//! It could be folded into gpui, ui or theme potentially but for now we'll continue
//! to develop it in isolation.
//!
//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths:
//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system.
//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed.
//! 3. Build the needed functionality into gpui and keep using it's color system everywhere.
//!
//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more.
//!
//! === End development note ===
use palette::{
blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha,
};
/// The types of blend modes supported
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BlendMode {
/// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows.
Multiply,
/// Lightens the color by adding the source and destination colors. It results in a lighter color.
Screen,
/// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened.
Overlay,
/// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast.
Darken,
/// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast.
Lighten,
/// Brightens the base color to reflect the blend color. The result is a lightened image.
Dodge,
/// Darkens the base color to reflect the blend color. The result is a darkened image.
Burn,
/// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color.
HardLight,
/// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color.
SoftLight,
/// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors.
Difference,
/// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity.
Exclusion,
}
/// Converts a hexadecimal color string to a `palette::Hsla` color.
///
/// This function supports the following hex formats:
/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
let hex = s.trim_start_matches('#');
// Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
let hex = match hex.len() {
3 => hex
.chars()
.map(|c| c.to_string().repeat(2))
.collect::<String>(),
4 => {
let (rgb, alpha) = hex.split_at(3);
let rgb = rgb
.chars()
.map(|c| c.to_string().repeat(2))
.collect::<String>();
let alpha = alpha.chars().next().unwrap().to_string().repeat(2);
format!("{}{}", rgb, alpha)
}
6 => format!("{}ff", hex), // Add alpha if missing
8 => hex.to_string(), // Already in full format
_ => return Err("Invalid hexadecimal string length".to_string()),
};
let hex_val =
u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
let r = ((hex_val >> 24) & 0xFF) as f32 / 255.0;
let g = ((hex_val >> 16) & 0xFF) as f32 / 255.0;
let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0;
let a = (hex_val & 0xFF) as f32 / 255.0;
let color = RGBAColor { r, g, b, a };
Ok(color)
}
// These derives implement to and from palette's color types.
#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
pub struct RGBAColor {
r: f32,
g: f32,
b: f32,
// Let Palette know this is our alpha channel.
#[palette(alpha)]
a: f32,
}
impl FromColorUnclamped<RGBAColor> for RGBAColor {
fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
color
}
}
impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
where
Srgb: FromColorUnclamped<Rgb<S, f32>>,
{
fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
let srgb = Srgb::from_color_unclamped(color);
RGBAColor {
r: srgb.red,
g: srgb.green,
b: srgb.blue,
a: 1.0,
}
}
}
impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
where
Rgb<S, f32>: FromColorUnclamped<Srgb>,
{
fn from_color_unclamped(color: RGBAColor) -> Self {
let srgb = Srgb::new(color.r, color.g, color.b);
Self::from_color_unclamped(srgb)
}
}
impl Clamp for RGBAColor {
fn clamp(self) -> Self {
RGBAColor {
r: self.r.min(1.0).max(0.0),
g: self.g.min(1.0).max(0.0),
b: self.b.min(1.0).max(0.0),
a: self.a.min(1.0).max(0.0),
}
}
}
impl RGBAColor {
/// Creates a new color from the given RGBA values.
///
/// This color can be used to convert to any [`palette::Color`] type.
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
RGBAColor { r, g, b, a }
}
/// Returns a set of states for this color.
pub fn states(self, is_light: bool) -> ColorStates {
states_for_color(self, is_light)
}
/// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
let srgb_self = Srgb::new(self.r, self.g, self.b);
let srgb_other = Srgb::new(other.r, other.g, other.b);
// Directly mix the colors as sRGB values
let mixed = srgb_self.mix(srgb_other, mix_ratio);
RGBAColor::from_color_unclamped(mixed)
}
pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
let srgb_self = Srgb::new(self.r, self.g, self.b);
let srgb_other = Srgb::new(other.r, other.g, other.b);
let blended = match blend_mode {
// replace hsl methods with the respective sRGB methods
BlendMode::Multiply => srgb_self.multiply(srgb_other),
_ => unimplemented!(),
};
Self {
r: blended.red,
g: blended.green,
b: blended.blue,
a: self.a,
}
}
}
/// A set of colors for different states of an element.
#[derive(Debug, Clone)]
pub struct ColorStates {
/// The default color.
pub default: RGBAColor,
/// The color when the mouse is hovering over the element.
pub hover: RGBAColor,
/// The color when the mouse button is held down on the element.
pub active: RGBAColor,
/// The color when the element is focused with the keyboard.
pub focused: RGBAColor,
/// The color when the element is disabled.
pub disabled: RGBAColor,
}
/// Returns a set of colors for different states of an element.
///
/// todo!("This should take a theme and use appropriate colors from it")
pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
let adjustment_factor = if is_light { 0.1 } else { -0.1 };
let hover_adjustment = 1.0 - adjustment_factor;
let active_adjustment = 1.0 - 2.0 * adjustment_factor;
let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
// Adjust lightness for each state
// Note: Adjustment logic may differ; simplify as needed for sRGB
RGBAColor::new(
color.r * adjustment,
color.g * adjustment,
color.b * adjustment,
color.a,
)
};
let color = color.clamp();
ColorStates {
default: color.clone(),
hover: make_adjustment(color.clone(), hover_adjustment),
active: make_adjustment(color.clone(), active_adjustment),
focused: make_adjustment(color.clone(), focused_adjustment),
disabled: make_adjustment(color.clone(), disabled_adjustment),
}
}

View file

@ -308,11 +308,7 @@ impl EventEmitter<Event> for Copilot {}
impl Copilot { impl Copilot {
pub fn global(cx: &AppContext) -> Option<Model<Self>> { pub fn global(cx: &AppContext) -> Option<Model<Self>> {
if cx.has_global::<Model<Self>>() { cx.try_global::<Model<Self>>().map(|model| model.clone())
Some(cx.global::<Model<Self>>().clone())
} else {
None
}
} }
fn start( fn start(
@ -373,10 +369,11 @@ impl Copilot {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn fake(cx: &mut gpui::TestAppContext) -> (Model<Self>, lsp::FakeLanguageServer) { pub fn fake(cx: &mut gpui::TestAppContext) -> (Model<Self>, lsp::FakeLanguageServer) {
use lsp::FakeLanguageServer;
use node_runtime::FakeNodeRuntime; use node_runtime::FakeNodeRuntime;
let (server, fake_server) = let (server, fake_server) =
LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); FakeLanguageServer::new("copilot".into(), Default::default(), cx.to_async());
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
let node_runtime = FakeNodeRuntime::new(); let node_runtime = FakeNodeRuntime::new();
let this = cx.new_model(|cx| Self { let this = cx.new_model(|cx| Self {

View file

@ -1,7 +1,7 @@
use crate::sign_in::CopilotCodeVerification; use crate::sign_in::CopilotCodeVerification;
use anyhow::Result; use anyhow::Result;
use copilot::{Copilot, SignOut, Status}; use copilot::{Copilot, SignOut, Status};
use editor::{scroll::autoscroll::Autoscroll, Editor}; use editor::{scroll::Autoscroll, Editor};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement, div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,

View file

@ -96,7 +96,7 @@ impl CopilotCodeVerification {
.items_center() .items_center()
.child(Headline::new("Use Github Copilot in Zed.").size(HeadlineSize::Large)) .child(Headline::new("Use Github Copilot in Zed.").size(HeadlineSize::Large))
.child( .child(
Label::new("Using Copilot requres an active subscription on Github.") Label::new("Using Copilot requires an active subscription on Github.")
.color(Color::Muted), .color(Color::Muted),
) )
.child(Self::render_device_code(data, cx)) .child(Self::render_device_code(data, cx))
@ -139,7 +139,7 @@ impl CopilotCodeVerification {
"You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.", "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
).color(Color::Warning)) ).color(Color::Warning))
.child( .child(
Button::new("copilot-subscribe-button", "Subscibe on Github") Button::new("copilot-subscribe-button", "Subscribe on Github")
.full_width() .full_width()
.on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)), .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
) )

View file

@ -8,7 +8,7 @@ use editor::{
diagnostic_block_renderer, diagnostic_block_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock}, display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
highlight_diagnostic_message, highlight_diagnostic_message,
scroll::autoscroll::Autoscroll, scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
}; };
use futures::future::try_join_all; use futures::future::try_join_all;

View file

@ -80,7 +80,7 @@ impl Render for DiagnosticIndicator {
Button::new("diagnostic_message", message) Button::new("diagnostic_message", message)
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
.tooltip(|cx| { .tooltip(|cx| {
Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx) Tooltip::for_action("Next Diagnostic", &editor::actions::GoToDiagnostic, cx)
}) })
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
this.go_to_next_diagnostic(cx); this.go_to_next_diagnostic(cx);

View file

@ -0,0 +1,218 @@
//! This module contains all actions supported by [`Editor`].
use super::*;
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectNext {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectPrevious {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectAllMatches {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
pub(super) stop_at_soft_wraps: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MovePageUp {
#[serde(default)]
pub(super) center_cursor: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MovePageDown {
#[serde(default)]
pub(super) center_cursor: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToEndOfLine {
#[serde(default)]
pub(super) stop_at_soft_wraps: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleCodeActions {
#[serde(default)]
pub deployed_from_indicator: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCompletion {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCodeAction {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct FoldAt {
pub buffer_row: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct UnfoldAt {
pub buffer_row: u32,
}
impl_actions!(
editor,
[
SelectNext,
SelectPrevious,
SelectAllMatches,
SelectToBeginningOfLine,
MovePageUp,
MovePageDown,
SelectToEndOfLine,
ToggleCodeActions,
ConfirmCompletion,
ConfirmCodeAction,
ToggleComments,
FoldAt,
UnfoldAt
]
);
gpui::actions!(
editor,
[
AddSelectionAbove,
AddSelectionBelow,
Backspace,
Cancel,
ConfirmRename,
ContextMenuFirst,
ContextMenuLast,
ContextMenuNext,
ContextMenuPrev,
ConvertToKebabCase,
ConvertToLowerCamelCase,
ConvertToLowerCase,
ConvertToSnakeCase,
ConvertToTitleCase,
ConvertToUpperCamelCase,
ConvertToUpperCase,
Copy,
CopyHighlightJson,
CopyPath,
CopyRelativePath,
Cut,
CutToEndOfLine,
Delete,
DeleteLine,
DeleteToBeginningOfLine,
DeleteToEndOfLine,
DeleteToNextSubwordEnd,
DeleteToNextWordEnd,
DeleteToPreviousSubwordStart,
DeleteToPreviousWordStart,
DuplicateLine,
ExpandMacroRecursively,
FindAllReferences,
Fold,
FoldSelectedRanges,
Format,
GoToDefinition,
GoToDefinitionSplit,
GoToDiagnostic,
GoToHunk,
GoToPrevDiagnostic,
GoToPrevHunk,
GoToTypeDefinition,
GoToTypeDefinitionSplit,
HalfPageDown,
HalfPageUp,
Hover,
Indent,
JoinLines,
LineDown,
LineUp,
MoveDown,
MoveLeft,
MoveLineDown,
MoveLineUp,
MoveRight,
MoveToBeginning,
MoveToBeginningOfLine,
MoveToEnclosingBracket,
MoveToEnd,
MoveToEndOfLine,
MoveToEndOfParagraph,
MoveToNextSubwordEnd,
MoveToNextWordEnd,
MoveToPreviousSubwordStart,
MoveToPreviousWordStart,
MoveToStartOfParagraph,
MoveUp,
Newline,
NewlineAbove,
NewlineBelow,
NextScreen,
OpenExcerpts,
Outdent,
PageDown,
PageUp,
Paste,
Redo,
RedoSelection,
Rename,
RestartLanguageServer,
RevealInFinder,
ReverseLines,
ScrollCursorBottom,
ScrollCursorCenter,
ScrollCursorTop,
SelectAll,
SelectDown,
SelectLargerSyntaxNode,
SelectLeft,
SelectLine,
SelectRight,
SelectSmallerSyntaxNode,
SelectToBeginning,
SelectToEnd,
SelectToEndOfParagraph,
SelectToNextSubwordEnd,
SelectToNextWordEnd,
SelectToPreviousSubwordStart,
SelectToPreviousWordStart,
SelectToStartOfParagraph,
SelectUp,
ShowCharacterPalette,
ShowCompletions,
ShuffleLines,
SortLinesCaseInsensitive,
SortLinesCaseSensitive,
SplitSelectionIntoLines,
Tab,
TabPrev,
ToggleInlayHints,
ToggleSoftWrap,
Transpose,
Undo,
UndoSelection,
UnfoldLines,
]
);

View file

@ -1,3 +1,22 @@
//! This module defines where the text should be displayed in an [`Editor`][Editor].
//!
//! Not literally though - rendering, layout and all that jazz is a responsibility of [`EditorElement`][EditorElement].
//! Instead, [`DisplayMap`] decides where Inlays/Inlay hints are displayed, when
//! to apply a soft wrap, where to add fold indicators, whether there are any tabs in the buffer that
//! we display as spaces and where to display custom blocks (like diagnostics).
//! Seems like a lot? That's because it is. [`DisplayMap`] is conceptually made up
//! of several smaller structures that form a hierarchy (starting at the bottom):
//! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.
//! - [`FoldMap`] that decides where the fold indicators should be; it also tracks parts of a source file that are currently folded.
//! - [`TabMap`] that keeps track of hard tabs in a buffer.
//! - [`WrapMap`] that handles soft wrapping.
//! - [`BlockMap`] that tracks custom blocks such as diagnostics that should be displayed within buffer.
//! - [`DisplayMap`] that adds background highlights to the regions of text.
//! Each one of those builds on top of preceding map.
//!
//! [Editor]: crate::Editor
//! [EditorElement]: crate::element::EditorElement
mod block_map; mod block_map;
mod fold_map; mod fold_map;
mod inlay_map; mod inlay_map;
@ -30,7 +49,8 @@ pub use block_map::{
}; };
pub use self::fold_map::{Fold, FoldPoint}; pub use self::fold_map::{Fold, FoldPoint};
pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; pub use self::inlay_map::{InlayOffset, InlayPoint};
pub(crate) use inlay_map::Inlay;
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus { pub enum FoldStatus {
@ -220,7 +240,7 @@ impl DisplayMap {
.insert(Some(type_id), Arc::new((style, ranges))); .insert(Some(type_id), Arc::new((style, ranges)));
} }
pub fn highlight_inlays( pub(crate) fn highlight_inlays(
&mut self, &mut self,
type_id: TypeId, type_id: TypeId,
highlights: Vec<InlayHighlight>, highlights: Vec<InlayHighlight>,
@ -258,11 +278,11 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_wrap_width(width, cx)) .update(cx, |map, cx| map.set_wrap_width(width, cx))
} }
pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> { pub(crate) fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
self.inlay_map.current_inlays() self.inlay_map.current_inlays()
} }
pub fn splice_inlays( pub(crate) fn splice_inlays(
&mut self, &mut self,
to_remove: Vec<InlayId>, to_remove: Vec<InlayId>,
to_insert: Vec<Inlay>, to_insert: Vec<Inlay>,
@ -306,7 +326,7 @@ impl DisplayMap {
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Highlights<'a> { pub(crate) struct Highlights<'a> {
pub text_highlights: Option<&'a TextHighlights>, pub text_highlights: Option<&'a TextHighlights>,
pub inlay_highlights: Option<&'a InlayHighlights>, pub inlay_highlights: Option<&'a InlayHighlights>,
pub inlay_highlight_style: Option<HighlightStyle>, pub inlay_highlight_style: Option<HighlightStyle>,
@ -880,8 +900,9 @@ impl DisplaySnapshot {
self.text_highlights.get(&Some(type_id)).cloned() self.text_highlights.get(&Some(type_id)).cloned()
} }
#[allow(unused)]
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn inlay_highlights<Tag: ?Sized + 'static>( pub(crate) fn inlay_highlights<Tag: ?Sized + 'static>(
&self, &self,
) -> Option<&HashMap<InlayId, (HighlightStyle, InlayHighlight)>> { ) -> Option<&HashMap<InlayId, (HighlightStyle, InlayHighlight)>> {
let type_id = TypeId::of::<Tag>(); let type_id = TypeId::of::<Tag>();
@ -969,24 +990,6 @@ impl ToDisplayPoint for Anchor {
} }
} }
pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator<Item = u32> {
let max_row = display_map.max_point().row();
let start_row = display_row + 1;
let mut current = None;
std::iter::from_fn(move || {
if current == None {
current = Some(start_row);
} else {
current = Some(current.unwrap() + 1)
}
if current.unwrap() > max_row {
None
} else {
current
}
})
}
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::*; use super::*;

View file

@ -582,7 +582,7 @@ impl BlockSnapshot {
.collect() .collect()
} }
pub fn chunks<'a>( pub(crate) fn chunks<'a>(
&'a self, &'a self,
rows: Range<u32>, rows: Range<u32>,
language_aware: bool, language_aware: bool,

View file

@ -71,10 +71,10 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
} }
} }
pub struct FoldMapWriter<'a>(&'a mut FoldMap); pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap);
impl<'a> FoldMapWriter<'a> { impl<'a> FoldMapWriter<'a> {
pub fn fold<T: ToOffset>( pub(crate) fn fold<T: ToOffset>(
&mut self, &mut self,
ranges: impl IntoIterator<Item = Range<T>>, ranges: impl IntoIterator<Item = Range<T>>,
) -> (FoldSnapshot, Vec<FoldEdit>) { ) -> (FoldSnapshot, Vec<FoldEdit>) {
@ -129,7 +129,7 @@ impl<'a> FoldMapWriter<'a> {
(self.0.snapshot.clone(), edits) (self.0.snapshot.clone(), edits)
} }
pub fn unfold<T: ToOffset>( pub(crate) fn unfold<T: ToOffset>(
&mut self, &mut self,
ranges: impl IntoIterator<Item = Range<T>>, ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool, inclusive: bool,
@ -178,14 +178,14 @@ impl<'a> FoldMapWriter<'a> {
} }
} }
pub struct FoldMap { pub(crate) struct FoldMap {
snapshot: FoldSnapshot, snapshot: FoldSnapshot,
ellipses_color: Option<Hsla>, ellipses_color: Option<Hsla>,
next_fold_id: FoldId, next_fold_id: FoldId,
} }
impl FoldMap { impl FoldMap {
pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) { pub(crate) fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
let this = Self { let this = Self {
snapshot: FoldSnapshot { snapshot: FoldSnapshot {
folds: Default::default(), folds: Default::default(),
@ -655,7 +655,7 @@ impl FoldSnapshot {
} }
} }
pub fn chunks<'a>( pub(crate) fn chunks<'a>(
&'a self, &'a self,
range: Range<FoldOffset>, range: Range<FoldOffset>,
language_aware: bool, language_aware: bool,

View file

@ -35,8 +35,8 @@ enum Transform {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Inlay { pub(crate) struct Inlay {
pub id: InlayId, pub(crate) id: InlayId,
pub position: Anchor, pub position: Anchor,
pub text: text::Rope, pub text: text::Rope,
} }
@ -1016,7 +1016,7 @@ impl InlaySnapshot {
(line_end - line_start) as u32 (line_end - line_start) as u32
} }
pub fn chunks<'a>( pub(crate) fn chunks<'a>(
&'a self, &'a self,
range: Range<InlayOffset>, range: Range<InlayOffset>,
language_aware: bool, language_aware: bool,

View file

@ -568,7 +568,7 @@ impl WrapSnapshot {
Patch::new(wrap_edits) Patch::new(wrap_edits)
} }
pub fn chunks<'a>( pub(crate) fn chunks<'a>(
&'a self, &'a self,
rows: Range<u32>, rows: Range<u32>,
language_aware: bool, language_aware: bool,

View file

@ -1,3 +1,18 @@
#![allow(rustdoc::private_intra_doc_links)]
//! This is the place where everything editor-related is stored (data-wise) and displayed (ui-wise).
//! The main point of interest in this crate is [`Editor`] type, which is used in every other Zed part as a user input element.
//! It comes in different flavors: single line, multiline and a fixed height one.
//!
//! Editor contains of multiple large submodules:
//! * [`element`] — the place where all rendering happens
//! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them.
//! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.).
//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly.
//!
//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s).
//!
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides it's behaviour.
pub mod actions;
mod blink_manager; mod blink_manager;
pub mod display_map; pub mod display_map;
mod editor_settings; mod editor_settings;
@ -14,13 +29,14 @@ pub mod movement;
mod persistence; mod persistence;
mod rust_analyzer_ext; mod rust_analyzer_ext;
pub mod scroll; pub mod scroll;
pub mod selections_collection; mod selections_collection;
#[cfg(test)] #[cfg(test)]
mod editor_tests; mod editor_tests;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test; pub mod test;
use ::git::diff::DiffHunk; use ::git::diff::DiffHunk;
pub(crate) use actions::*;
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
@ -32,14 +48,13 @@ use copilot::Copilot;
pub use display_map::DisplayPoint; pub use display_map::DisplayPoint;
use display_map::*; use display_map::*;
pub use editor_settings::EditorSettings; pub use editor_settings::EditorSettings;
pub use element::{ use element::LineWithInvisibles;
Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles, pub use element::{Cursor, EditorElement, HighlightedRange, HighlightedRangeLine};
};
use futures::FutureExt; use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use git::diff_hunk_to_display; use git::diff_hunk_to_display;
use gpui::{ use gpui::{
actions, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight,
HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton,
@ -51,7 +66,7 @@ use hover_popover::{hide_hover, HoverState};
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use items::MAX_TAB_TITLE_LEN; pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools; use itertools::Itertools;
pub use language::{char_kind, CharKind}; use language::{char_kind, CharKind};
use language::{ use language::{
language_settings::{self, all_language_settings, InlayHintSettings}, language_settings::{self, all_language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
@ -74,9 +89,7 @@ use parking_lot::RwLock;
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
use rand::prelude::*; use rand::prelude::*;
use rpc::proto::{self, *}; use rpc::proto::{self, *};
use scroll::{ use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
@ -113,10 +126,12 @@ const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024; const MAX_SELECTION_HISTORY_LEN: usize = 1024;
const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
#[doc(hidden)]
pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
pub fn render_parsed_markdown( pub fn render_parsed_markdown(
element_id: impl Into<ElementId>, element_id: impl Into<ElementId>,
@ -181,103 +196,8 @@ pub fn render_parsed_markdown(
}) })
} }
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectNext {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectPrevious {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectAllMatches {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
stop_at_soft_wraps: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MovePageUp {
#[serde(default)]
center_cursor: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MovePageDown {
#[serde(default)]
center_cursor: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToEndOfLine {
#[serde(default)]
stop_at_soft_wraps: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleCodeActions {
#[serde(default)]
pub deployed_from_indicator: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCompletion {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCodeAction {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct FoldAt {
pub buffer_row: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct UnfoldAt {
pub buffer_row: u32,
}
impl_actions!(
editor,
[
SelectNext,
SelectPrevious,
SelectAllMatches,
SelectToBeginningOfLine,
MovePageUp,
MovePageDown,
SelectToEndOfLine,
ToggleCodeActions,
ConfirmCompletion,
ConfirmCodeAction,
ToggleComments,
FoldAt,
UnfoldAt
]
);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InlayId { pub(crate) enum InlayId {
Suggestion(usize), Suggestion(usize),
Hint(usize), Hint(usize),
} }
@ -291,128 +211,6 @@ impl InlayId {
} }
} }
actions!(
editor,
[
AddSelectionAbove,
AddSelectionBelow,
Backspace,
Cancel,
ConfirmRename,
ContextMenuFirst,
ContextMenuLast,
ContextMenuNext,
ContextMenuPrev,
ConvertToKebabCase,
ConvertToLowerCamelCase,
ConvertToLowerCase,
ConvertToSnakeCase,
ConvertToTitleCase,
ConvertToUpperCamelCase,
ConvertToUpperCase,
Copy,
CopyHighlightJson,
CopyPath,
CopyRelativePath,
Cut,
CutToEndOfLine,
Delete,
DeleteLine,
DeleteToBeginningOfLine,
DeleteToEndOfLine,
DeleteToNextSubwordEnd,
DeleteToNextWordEnd,
DeleteToPreviousSubwordStart,
DeleteToPreviousWordStart,
DuplicateLine,
ExpandMacroRecursively,
FindAllReferences,
Fold,
FoldSelectedRanges,
Format,
GoToDefinition,
GoToDefinitionSplit,
GoToDiagnostic,
GoToHunk,
GoToPrevDiagnostic,
GoToPrevHunk,
GoToTypeDefinition,
GoToTypeDefinitionSplit,
HalfPageDown,
HalfPageUp,
Hover,
Indent,
JoinLines,
LineDown,
LineUp,
MoveDown,
MoveLeft,
MoveLineDown,
MoveLineUp,
MoveRight,
MoveToBeginning,
MoveToBeginningOfLine,
MoveToEnclosingBracket,
MoveToEnd,
MoveToEndOfLine,
MoveToEndOfParagraph,
MoveToNextSubwordEnd,
MoveToNextWordEnd,
MoveToPreviousSubwordStart,
MoveToPreviousWordStart,
MoveToStartOfParagraph,
MoveUp,
Newline,
NewlineAbove,
NewlineBelow,
NextScreen,
OpenExcerpts,
Outdent,
PageDown,
PageUp,
Paste,
Redo,
RedoSelection,
Rename,
RestartLanguageServer,
RevealInFinder,
ReverseLines,
ScrollCursorBottom,
ScrollCursorCenter,
ScrollCursorTop,
SelectAll,
SelectDown,
SelectLargerSyntaxNode,
SelectLeft,
SelectLine,
SelectRight,
SelectSmallerSyntaxNode,
SelectToBeginning,
SelectToEnd,
SelectToEndOfParagraph,
SelectToNextSubwordEnd,
SelectToNextWordEnd,
SelectToPreviousSubwordStart,
SelectToPreviousWordStart,
SelectToStartOfParagraph,
SelectUp,
ShowCharacterPalette,
ShowCompletions,
ShuffleLines,
SortLinesCaseInsensitive,
SortLinesCaseSensitive,
SplitSelectionIntoLines,
Tab,
TabPrev,
ToggleInlayHints,
ToggleSoftWrap,
Transpose,
Undo,
UndoSelection,
UnfoldLines,
]
);
enum DocumentHighlightRead {} enum DocumentHighlightRead {}
enum DocumentHighlightWrite {} enum DocumentHighlightWrite {}
enum InputComposition {} enum InputComposition {}
@ -489,7 +287,7 @@ pub enum SelectPhase {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum SelectMode { pub(crate) enum SelectMode {
Character, Character,
Word(Range<Anchor>), Word(Range<Anchor>),
Line(Range<Anchor>), Line(Range<Anchor>),
@ -763,6 +561,7 @@ struct SnippetState {
active_index: usize, active_index: usize,
} }
#[doc(hidden)]
pub struct RenameState { pub struct RenameState {
pub range: Range<Anchor>, pub range: Range<Anchor>,
pub old_name: Arc<str>, pub old_name: Arc<str>,
@ -1502,7 +1301,7 @@ impl CodeActionsMenu {
} }
} }
pub struct CopilotState { pub(crate) struct CopilotState {
excerpt_id: Option<ExcerptId>, excerpt_id: Option<ExcerptId>,
pending_refresh: Task<Option<()>>, pending_refresh: Task<Option<()>>,
pending_cycling_refresh: Task<Option<()>>, pending_cycling_refresh: Task<Option<()>>,
@ -1622,15 +1421,13 @@ pub struct ClipboardSelection {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct NavigationData { pub(crate) struct NavigationData {
cursor_anchor: Anchor, cursor_anchor: Anchor,
cursor_position: Point, cursor_position: Point,
scroll_anchor: ScrollAnchor, scroll_anchor: ScrollAnchor,
scroll_top_row: u32, scroll_top_row: u32,
} }
pub struct EditorCreated(pub View<Editor>);
enum GotoDefinitionKind { enum GotoDefinitionKind {
Symbol, Symbol,
Type, Type,
@ -8130,7 +7927,7 @@ impl Editor {
} }
} }
pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext<Self>) { pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
let mut fold_ranges = Vec::new(); let mut fold_ranges = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@ -8489,7 +8286,7 @@ impl Editor {
cx.notify(); cx.notify();
} }
pub fn highlight_inlay_background<T: 'static>( pub(crate) fn highlight_inlay_background<T: 'static>(
&mut self, &mut self,
ranges: Vec<InlayHighlight>, ranges: Vec<InlayHighlight>,
color_fetcher: fn(&ThemeColors) -> Hsla, color_fetcher: fn(&ThemeColors) -> Hsla,
@ -8696,7 +8493,7 @@ impl Editor {
cx.notify(); cx.notify();
} }
pub fn highlight_inlays<T: 'static>( pub(crate) fn highlight_inlays<T: 'static>(
&mut self, &mut self,
highlights: Vec<InlayHighlight>, highlights: Vec<InlayHighlight>,
style: HighlightStyle, style: HighlightStyle,
@ -8741,7 +8538,7 @@ impl Editor {
) { ) {
match event { match event {
multi_buffer::Event::Edited { multi_buffer::Event::Edited {
sigleton_buffer_edited, singleton_buffer_edited,
} => { } => {
self.refresh_active_diagnostics(cx); self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx); self.refresh_code_actions(cx);
@ -8751,7 +8548,7 @@ impl Editor {
cx.emit(EditorEvent::BufferEdited); cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated); cx.emit(SearchEvent::MatchesInvalidated);
if *sigleton_buffer_edited { if *singleton_buffer_edited {
if let Some(project) = &self.project { if let Some(project) = &self.project {
let project = project.read(cx); let project = project.read(cx);
let languages_affected = multibuffer let languages_affected = multibuffer
@ -9926,7 +9723,7 @@ pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, V
(text_without_backticks.into(), code_ranges) (text_without_backticks.into(), code_ranges)
} }
pub fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla { fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla {
match (severity, valid) { match (severity, valid) {
(DiagnosticSeverity::ERROR, true) => colors.error, (DiagnosticSeverity::ERROR, true) => colors.error,
(DiagnosticSeverity::ERROR, false) => colors.error, (DiagnosticSeverity::ERROR, false) => colors.error,
@ -9985,7 +9782,7 @@ pub fn styled_runs_for_code_label<'a>(
}) })
} }
pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a { pub(crate) fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
let mut index = 0; let mut index = 0;
let mut codepoints = text.char_indices().peekable(); let mut codepoints = text.char_indices().peekable();

View file

@ -459,6 +459,7 @@ impl EditorElement {
event: &MouseUpEvent, event: &MouseUpEvent,
position_map: &PositionMap, position_map: &PositionMap,
text_bounds: Bounds<Pixels>, text_bounds: Bounds<Pixels>,
interactive_bounds: &InteractiveBounds,
stacking_order: &StackingOrder, stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
@ -469,7 +470,8 @@ impl EditorElement {
editor.select(SelectPhase::End, cx); editor.select(SelectPhase::End, cx);
} }
if !pending_nonempty_selections if interactive_bounds.visibly_contains(&event.position, cx)
&& !pending_nonempty_selections
&& event.modifiers.command && event.modifiers.command
&& text_bounds.contains(&event.position) && text_bounds.contains(&event.position)
&& cx.was_top_layer(&event.position, stacking_order) && cx.was_top_layer(&event.position, stacking_order)
@ -2590,15 +2592,14 @@ impl EditorElement {
let interactive_bounds = interactive_bounds.clone(); let interactive_bounds = interactive_bounds.clone();
move |event: &MouseUpEvent, phase, cx| { move |event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble if phase == DispatchPhase::Bubble {
&& interactive_bounds.visibly_contains(&event.position, cx)
{
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
Self::mouse_up( Self::mouse_up(
editor, editor,
event, event,
&position_map, &position_map,
text_bounds, text_bounds,
&interactive_bounds,
&stacking_order, &stacking_order,
cx, cx,
) )
@ -2647,7 +2648,7 @@ impl EditorElement {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct LineWithInvisibles { pub(crate) struct LineWithInvisibles {
pub line: ShapedLine, pub line: ShapedLine,
invisibles: Vec<Invisible>, invisibles: Vec<Invisible>,
} }

View file

@ -339,6 +339,7 @@ fn show_hover(
this.hover_state.info_popover = hover_popover; this.hover_state.info_popover = hover_popover;
cx.notify(); cx.notify();
cx.refresh();
})?; })?;
Ok::<_, anyhow::Error>(()) Ok::<_, anyhow::Error>(())

View file

@ -1,3 +1,11 @@
/// Stores and updates all data received from LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint">textDocument/inlayHint</a> requests.
/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere.
/// On every update, cache may query for more inlay hints and update inlays on the screen.
///
/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map.
/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work.
///
/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes.
use std::{ use std::{
cmp, cmp,
ops::{ControlFlow, Range}, ops::{ControlFlow, Range},
@ -39,7 +47,7 @@ struct TasksForRanges {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct CachedExcerptHints { struct CachedExcerptHints {
version: usize, version: usize,
buffer_version: Global, buffer_version: Global,
buffer_id: u64, buffer_id: u64,
@ -47,15 +55,30 @@ pub struct CachedExcerptHints {
hints_by_id: HashMap<InlayId, InlayHint>, hints_by_id: HashMap<InlayId, InlayHint>,
} }
/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum InvalidationStrategy { pub(super) enum InvalidationStrategy {
/// Hints reset is <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">requested</a> by the LSP server.
/// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation.
///
/// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise.
RefreshRequested, RefreshRequested,
/// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place.
/// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence.
BufferEdited, BufferEdited,
/// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position.
/// No invalidation should be done at all, all new hints are added to the cache.
///
/// A special case is the settings change: in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other).
/// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen.
None, None,
} }
#[derive(Debug, Default)] /// A splice to send into the `inlay_map` for updating the visible inlays on the screen.
pub struct InlaySplice { /// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
pub(super) struct InlaySplice {
pub to_remove: Vec<InlayId>, pub to_remove: Vec<InlayId>,
pub to_insert: Vec<Inlay>, pub to_insert: Vec<Inlay>,
} }
@ -237,7 +260,7 @@ impl TasksForRanges {
} }
impl InlayHintCache { impl InlayHintCache {
pub fn new(inlay_hint_settings: InlayHintSettings) -> Self { pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self {
Self { Self {
allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(), allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
enabled: inlay_hint_settings.enabled, enabled: inlay_hint_settings.enabled,
@ -248,7 +271,10 @@ impl InlayHintCache {
} }
} }
pub fn update_settings( /// Checks inlay hint settings for enabled hint kinds and general enabled state.
/// Generates corresponding inlay_map splice updates on settings changes.
/// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries.
pub(super) fn update_settings(
&mut self, &mut self,
multi_buffer: &Model<MultiBuffer>, multi_buffer: &Model<MultiBuffer>,
new_hint_settings: InlayHintSettings, new_hint_settings: InlayHintSettings,
@ -299,7 +325,11 @@ impl InlayHintCache {
} }
} }
pub fn spawn_hint_refresh( /// If needed, queries LSP for new inlay hints, using the invalidation strategy given.
/// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first,
/// followed by the delayed queries of the same range above and below the visible one.
/// This way, concequent refresh invocations are less likely to trigger LSP queries for the invisible ranges.
pub(super) fn spawn_hint_refresh(
&mut self, &mut self,
reason: &'static str, reason: &'static str,
excerpts_to_query: HashMap<ExcerptId, (Model<Buffer>, Global, Range<usize>)>, excerpts_to_query: HashMap<ExcerptId, (Model<Buffer>, Global, Range<usize>)>,
@ -460,7 +490,11 @@ impl InlayHintCache {
} }
} }
pub fn remove_excerpts(&mut self, excerpts_removed: Vec<ExcerptId>) -> Option<InlaySplice> { /// Completely forget of certain excerpts that were removed from the multibuffer.
pub(super) fn remove_excerpts(
&mut self,
excerpts_removed: Vec<ExcerptId>,
) -> Option<InlaySplice> {
let mut to_remove = Vec::new(); let mut to_remove = Vec::new();
for excerpt_to_remove in excerpts_removed { for excerpt_to_remove in excerpts_removed {
self.update_tasks.remove(&excerpt_to_remove); self.update_tasks.remove(&excerpt_to_remove);
@ -480,7 +514,7 @@ impl InlayHintCache {
} }
} }
pub fn clear(&mut self) { pub(super) fn clear(&mut self) {
if !self.update_tasks.is_empty() || !self.hints.is_empty() { if !self.update_tasks.is_empty() || !self.hints.is_empty() {
self.version += 1; self.version += 1;
} }
@ -488,7 +522,7 @@ impl InlayHintCache {
self.hints.clear(); self.hints.clear();
} }
pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> { pub(super) fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> {
self.hints self.hints
.get(&excerpt_id)? .get(&excerpt_id)?
.read() .read()
@ -516,7 +550,8 @@ impl InlayHintCache {
self.version self.version
} }
pub fn spawn_hint_resolve( /// Queries a certain hint from the cache for extra data via the LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHint_resolve">resolve</a> request.
pub(super) fn spawn_hint_resolve(
&self, &self,
buffer_id: u64, buffer_id: u64,
excerpt_id: ExcerptId, excerpt_id: ExcerptId,
@ -925,14 +960,14 @@ async fn fetch_and_update_hints(
log::trace!("Fetched hints: {new_hints:?}"); log::trace!("Fetched hints: {new_hints:?}");
let background_task_buffer_snapshot = buffer_snapshot.clone(); let background_task_buffer_snapshot = buffer_snapshot.clone();
let backround_fetch_range = fetch_range.clone(); let background_fetch_range = fetch_range.clone();
let new_update = cx let new_update = cx
.background_executor() .background_executor()
.spawn(async move { .spawn(async move {
calculate_hint_updates( calculate_hint_updates(
query.excerpt_id, query.excerpt_id,
invalidate, invalidate,
backround_fetch_range, background_fetch_range,
new_hints, new_hints,
&background_task_buffer_snapshot, &background_task_buffer_snapshot,
cached_excerpt_hints, cached_excerpt_hints,
@ -1199,7 +1234,7 @@ pub mod tests {
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
use crate::{ use crate::{
scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, scroll::{scroll_amount::ScrollAmount, Autoscroll},
ExcerptRange, ExcerptRange,
}; };
use futures::StreamExt; use futures::StreamExt;
@ -1449,7 +1484,7 @@ pub mod tests {
assert_eq!( assert_eq!(
editor.inlay_hint_cache().version, editor.inlay_hint_cache().version,
edits_made, edits_made,
"Cache version should udpate once after the work task is done" "Cache version should update once after the work task is done"
); );
}); });
} }
@ -1599,7 +1634,7 @@ pub mod tests {
assert_eq!( assert_eq!(
expected_hints, expected_hints,
cached_hint_labels(editor), cached_hint_labels(editor),
"Markdown editor should have a separate verison, repeating Rust editor rules" "Markdown editor should have a separate version, repeating Rust editor rules"
); );
assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(editor.inlay_hint_cache().version, 1); assert_eq!(editor.inlay_hint_cache().version, 1);
@ -2612,7 +2647,7 @@ pub mod tests {
"When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
); );
assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the version");
}); });
_ = editor.update(cx, |editor, cx| { _ = editor.update(cx, |editor, cx| {
@ -2728,7 +2763,7 @@ pub mod tests {
expected_hints, expected_hints,
cached_hint_labels(editor), cached_hint_labels(editor),
"After multibuffer edit, editor gets scolled back to the last selection; \ "After multibuffer edit, editor gets scolled back to the last selection; \
all hints should be invalidated and requeried for all of its visible excerpts" all hints should be invalidated and required for all of its visible excerpts"
); );
assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx));

View file

@ -69,7 +69,7 @@ pub enum GoToDefinitionLink {
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlayHighlight { pub(crate) struct InlayHighlight {
pub inlay: InlayId, pub inlay: InlayId,
pub inlay_position: Anchor, pub inlay_position: Anchor,
pub range: Range<usize>, pub range: Range<usize>,

View file

@ -1,3 +1,6 @@
//! Movement module contains helper functions for calculating intended position
//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate.
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{px, Pixels, TextSystem}; use gpui::{px, Pixels, TextSystem};
@ -5,6 +8,9 @@ use language::Point;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
/// Defines search strategy for items in `movement` module.
/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
/// `FindRange::MultiLine` keeps going until the end of a string.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum FindRange { pub enum FindRange {
SingleLine, SingleLine,
@ -14,11 +20,13 @@ pub enum FindRange {
/// TextLayoutDetails encompasses everything we need to move vertically /// TextLayoutDetails encompasses everything we need to move vertically
/// taking into account variable width characters. /// taking into account variable width characters.
pub struct TextLayoutDetails { pub struct TextLayoutDetails {
pub text_system: Arc<TextSystem>, pub(crate) text_system: Arc<TextSystem>,
pub editor_style: EditorStyle, pub(crate) editor_style: EditorStyle,
pub rem_size: Pixels, pub(crate) rem_size: Pixels,
} }
/// Returns a column to the left of the current point, wrapping
/// to the previous line if that point is at the start of line.
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 { if point.column() > 0 {
*point.column_mut() -= 1; *point.column_mut() -= 1;
@ -29,6 +37,8 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
map.clip_point(point, Bias::Left) map.clip_point(point, Bias::Left)
} }
/// Returns a column to the left of the current point, doing nothing if
/// that point is already at the start of line.
pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 { if point.column() > 0 {
*point.column_mut() -= 1; *point.column_mut() -= 1;
@ -36,6 +46,8 @@ pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displa
map.clip_point(point, Bias::Left) map.clip_point(point, Bias::Left)
} }
/// Returns a column to the right of the current point, wrapping
/// to the next line if that point is at the end of line.
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let max_column = map.line_len(point.row()); let max_column = map.line_len(point.row());
if point.column() < max_column { if point.column() < max_column {
@ -47,11 +59,14 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
map.clip_point(point, Bias::Right) map.clip_point(point, Bias::Right)
} }
/// Returns a column to the right of the current point, not performing any wrapping
/// if that point is already at the end of line.
pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
*point.column_mut() += 1; *point.column_mut() += 1;
map.clip_point(point, Bias::Right) map.clip_point(point, Bias::Right)
} }
/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
pub fn up( pub fn up(
map: &DisplaySnapshot, map: &DisplaySnapshot,
start: DisplayPoint, start: DisplayPoint,
@ -69,6 +84,7 @@ pub fn up(
) )
} }
/// Returns a display point for the next displayed line (which might be a soft-wrapped line).
pub fn down( pub fn down(
map: &DisplaySnapshot, map: &DisplaySnapshot,
start: DisplayPoint, start: DisplayPoint,
@ -86,7 +102,7 @@ pub fn down(
) )
} }
pub fn up_by_rows( pub(crate) fn up_by_rows(
map: &DisplaySnapshot, map: &DisplaySnapshot,
start: DisplayPoint, start: DisplayPoint,
row_count: u32, row_count: u32,
@ -125,7 +141,7 @@ pub fn up_by_rows(
) )
} }
pub fn down_by_rows( pub(crate) fn down_by_rows(
map: &DisplaySnapshot, map: &DisplaySnapshot,
start: DisplayPoint, start: DisplayPoint,
row_count: u32, row_count: u32,
@ -161,6 +177,10 @@ pub fn down_by_rows(
) )
} }
/// Returns a position of the start of line.
/// If `stop_at_soft_boundaries` is true, the returned position is that of the
/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
/// Otherwise it's always going to be the start of a logical line.
pub fn line_beginning( pub fn line_beginning(
map: &DisplaySnapshot, map: &DisplaySnapshot,
display_point: DisplayPoint, display_point: DisplayPoint,
@ -177,6 +197,10 @@ pub fn line_beginning(
} }
} }
/// Returns the last indented position on a given line.
/// If `stop_at_soft_boundaries` is true, the returned [`DisplayPoint`] is that of a
/// displayed line (e.g. if there's soft wrap it's gonna be returned),
/// otherwise it's always going to be a start of a logical line.
pub fn indented_line_beginning( pub fn indented_line_beginning(
map: &DisplaySnapshot, map: &DisplaySnapshot,
display_point: DisplayPoint, display_point: DisplayPoint,
@ -201,6 +225,11 @@ pub fn indented_line_beginning(
} }
} }
/// Returns a position of the end of line.
/// If `stop_at_soft_boundaries` is true, the returned position is that of the
/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
/// Otherwise it's always going to be the end of a logical line.
pub fn line_end( pub fn line_end(
map: &DisplaySnapshot, map: &DisplaySnapshot,
display_point: DisplayPoint, display_point: DisplayPoint,
@ -217,6 +246,8 @@ pub fn line_end(
} }
} }
/// Returns a position of the previous word boundary, where a word character is defined as either
/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map); let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point); let scope = map.buffer_snapshot.language_scope_at(raw_point);
@ -227,6 +258,9 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
}) })
} }
/// Returns a position of the previous subword boundary, where a subword is defined as a run of
/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
/// lowerspace characters and uppercase characters.
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map); let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point); let scope = map.buffer_snapshot.language_scope_at(raw_point);
@ -240,6 +274,8 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
}) })
} }
/// Returns a position of the next word boundary, where a word character is defined as either
/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map); let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point); let scope = map.buffer_snapshot.language_scope_at(raw_point);
@ -250,6 +286,9 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
}) })
} }
/// Returns a position of the next subword boundary, where a subword is defined as a run of
/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
/// lowerspace characters and uppercase characters.
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map); let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point); let scope = map.buffer_snapshot.language_scope_at(raw_point);
@ -263,6 +302,8 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
}) })
} }
/// Returns a position of the start of the current paragraph, where a paragraph
/// is defined as a run of non-blank lines.
pub fn start_of_paragraph( pub fn start_of_paragraph(
map: &DisplaySnapshot, map: &DisplaySnapshot,
display_point: DisplayPoint, display_point: DisplayPoint,
@ -290,6 +331,8 @@ pub fn start_of_paragraph(
DisplayPoint::zero() DisplayPoint::zero()
} }
/// Returns a position of the end of the current paragraph, where a paragraph
/// is defined as a run of non-blank lines.
pub fn end_of_paragraph( pub fn end_of_paragraph(
map: &DisplaySnapshot, map: &DisplaySnapshot,
display_point: DisplayPoint, display_point: DisplayPoint,
@ -376,6 +419,9 @@ pub fn find_boundary(
map.clip_point(offset.to_display_point(map), Bias::Right) map.clip_point(offset.to_display_point(map), Bias::Right)
} }
/// Returns an iterator over the characters following a given offset in the [`DisplaySnapshot`].
/// The returned value also contains a range of the start/end of a returned character in
/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
pub fn chars_after( pub fn chars_after(
map: &DisplaySnapshot, map: &DisplaySnapshot,
mut offset: usize, mut offset: usize,
@ -387,6 +433,9 @@ pub fn chars_after(
}) })
} }
/// Returns a reverse iterator over the characters following a given offset in the [`DisplaySnapshot`].
/// The returned value also contains a range of the start/end of a returned character in
/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
pub fn chars_before( pub fn chars_before(
map: &DisplaySnapshot, map: &DisplaySnapshot,
mut offset: usize, mut offset: usize,
@ -400,7 +449,7 @@ pub fn chars_before(
}) })
} }
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { pub(crate) fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let raw_point = point.to_point(map); let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point); let scope = map.buffer_snapshot.language_scope_at(raw_point);
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
@ -413,7 +462,10 @@ pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
} }
pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> { pub(crate) fn surrounding_word(
map: &DisplaySnapshot,
position: DisplayPoint,
) -> Range<DisplayPoint> {
let position = map let position = map
.clip_point(position, Bias::Left) .clip_point(position, Bias::Left)
.to_offset(map, Bias::Left); .to_offset(map, Bias::Left);
@ -429,6 +481,12 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
start..end start..end
} }
/// Returns a list of lines (represented as a [`DisplayPoint`] range) contained
/// within a passed range.
///
/// The line ranges are **always* going to be in bounds of a requested range, which means that
/// the first and the last lines might not necessarily represent the
/// full range of a logical line (as their `.start`/`.end` values are clipped to those of a passed in range).
pub fn split_display_range_by_lines( pub fn split_display_range_by_lines(
map: &DisplaySnapshot, map: &DisplaySnapshot,
range: Range<DisplayPoint>, range: Range<DisplayPoint>,

View file

@ -1,6 +1,6 @@
pub mod actions; mod actions;
pub mod autoscroll; pub(crate) mod autoscroll;
pub mod scroll_amount; pub(crate) mod scroll_amount;
use crate::{ use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, ToDisplayPoint},
@ -9,8 +9,10 @@ use crate::{
Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason, Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
MultiBufferSnapshot, ToPoint, MultiBufferSnapshot, ToPoint,
}; };
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use gpui::{point, px, AppContext, Entity, Pixels, Task, ViewContext}; use gpui::{point, px, AppContext, Entity, Pixels, Task, ViewContext};
use language::{Bias, Point}; use language::{Bias, Point};
pub use scroll_amount::ScrollAmount;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
time::{Duration, Instant}, time::{Duration, Instant},
@ -18,11 +20,6 @@ use std::{
use util::ResultExt; use util::ResultExt;
use workspace::{ItemId, WorkspaceId}; use workspace::{ItemId, WorkspaceId};
use self::{
autoscroll::{Autoscroll, AutoscrollStrategy},
scroll_amount::ScrollAmount,
};
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
pub const VERTICAL_SCROLL_MARGIN: f32 = 3.; pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);

View file

@ -175,7 +175,7 @@ impl Editor {
true true
} }
pub fn autoscroll_horizontally( pub(crate) fn autoscroll_horizontally(
&mut self, &mut self,
start_row: u32, start_row: u32,
viewport_width: Pixels, viewport_width: Pixels,

View file

@ -99,7 +99,7 @@ impl SelectionsCollection {
.map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx)))) .map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
} }
pub fn pending_mode(&self) -> Option<SelectMode> { pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
self.pending.as_ref().map(|pending| pending.mode.clone()) self.pending.as_ref().map(|pending| pending.mode.clone())
} }
@ -398,7 +398,7 @@ impl<'a> MutableSelectionsCollection<'a> {
} }
} }
pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) { pub(crate) fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection { self.collection.pending = Some(PendingSelection {
selection: Selection { selection: Selection {
id: post_inc(&mut self.collection.next_selection_id), id: post_inc(&mut self.collection.next_selection_id),
@ -412,7 +412,11 @@ impl<'a> MutableSelectionsCollection<'a> {
self.selections_changed = true; self.selections_changed = true;
} }
pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) { pub(crate) fn set_pending_display_range(
&mut self,
range: Range<DisplayPoint>,
mode: SelectMode,
) {
let (start, end, reversed) = { let (start, end, reversed) = {
let display_map = self.display_map(); let display_map = self.display_map();
let buffer = self.buffer(); let buffer = self.buffer();
@ -448,7 +452,7 @@ impl<'a> MutableSelectionsCollection<'a> {
self.selections_changed = true; self.selections_changed = true;
} }
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) { pub(crate) fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection { selection, mode }); self.collection.pending = Some(PendingSelection { selection, mode });
self.selections_changed = true; self.selections_changed = true;
} }
@ -855,7 +859,7 @@ impl<'a> DerefMut for MutableSelectionsCollection<'a> {
} }
// Panics if passed selections are not in order // Panics if passed selections are not in order
pub fn resolve_multiple<'a, D, I>( pub(crate) fn resolve_multiple<'a, D, I>(
selections: I, selections: I,
snapshot: &MultiBufferSnapshot, snapshot: &MultiBufferSnapshot,
) -> impl 'a + Iterator<Item = Selection<D>> ) -> impl 'a + Iterator<Item = Selection<D>>

View file

@ -57,18 +57,14 @@ impl FeatureFlagAppExt for AppContext {
} }
fn has_flag<T: FeatureFlag>(&self) -> bool { fn has_flag<T: FeatureFlag>(&self) -> bool {
if self.has_global::<FeatureFlags>() { self.try_global::<FeatureFlags>()
self.global::<FeatureFlags>().has_flag(T::NAME) .map(|flags| flags.has_flag(T::NAME))
} else { .unwrap_or(false)
false
}
} }
fn is_staff(&self) -> bool { fn is_staff(&self) -> bool {
if self.has_global::<FeatureFlags>() { self.try_global::<FeatureFlags>()
return self.global::<FeatureFlags>().staff; .map(|flags| flags.staff)
} else { .unwrap_or(false)
false
}
} }
} }

View file

@ -1,5 +1,5 @@
use collections::HashMap; use collections::HashMap;
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use editor::{scroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{ use gpui::{
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,

View file

@ -29,7 +29,7 @@ pub trait GitRepository: Send {
fn branch_name(&self) -> Option<String>; fn branch_name(&self) -> Option<String>;
/// Get the statuses of all of the files in the index that start with the given /// Get the statuses of all of the files in the index that start with the given
/// path and have changes with resepect to the HEAD commit. This is fast because /// path and have changes with respect to the HEAD commit. This is fast because
/// the index stores hashes of trees, so that unchanged directories can be skipped. /// the index stores hashes of trees, so that unchanged directories can be skipped.
fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus>; fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus>;

View file

@ -1,4 +1,4 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Editor};
use gpui::{ use gpui::{
actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle, actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,

View file

@ -0,0 +1,35 @@
use gpui::{prelude::*, App, AppContext, EventEmitter, Model, ModelContext};
struct Counter {
count: usize,
}
struct Change {
increment: usize,
}
impl EventEmitter<Change> for Counter {}
fn main() {
App::new().run(|cx: &mut AppContext| {
let counter: Model<Counter> = cx.new_model(|_cx| Counter { count: 0 });
let subscriber = cx.new_model(|cx: &mut ModelContext<Counter>| {
cx.subscribe(&counter, |subscriber, _emitter, event, _cx| {
subscriber.count += event.increment * 2;
})
.detach();
Counter {
count: counter.read(cx).count * 2,
}
});
counter.update(cx, |counter, cx| {
counter.count += 2;
cx.notify();
cx.emit(Change { increment: 2 });
});
assert_eq!(subscriber.read(cx).count, 4);
});
}

View file

@ -1,3 +1,5 @@
#![deny(missing_docs)]
mod async_context; mod async_context;
mod entity_map; mod entity_map;
mod model_context; mod model_context;
@ -43,6 +45,9 @@ use util::{
ResultExt, 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. /// Temporary(?) wrapper around [`RefCell<AppContext>`] to help us debug any double borrows.
/// Strongly consider removing after stabilization. /// Strongly consider removing after stabilization.
#[doc(hidden)] #[doc(hidden)]
@ -106,14 +111,24 @@ pub struct App(Rc<AppCell>);
/// configured, you'll start the app with `App::run`. /// configured, you'll start the app with `App::run`.
impl App { impl App {
/// Builds an app with the given asset source. /// Builds an app with the given asset source.
pub fn production(asset_source: Arc<dyn AssetSource>) -> Self { pub fn new() -> Self {
Self(AppContext::new( Self(AppContext::new(
current_platform(), current_platform(),
asset_source, Arc::new(()),
http::client(), http::client(),
)) ))
} }
/// Assign
pub fn with_assets(self, asset_source: impl AssetSource) -> Self {
let mut context_lock = self.0.borrow_mut();
let asset_source = Arc::new(asset_source);
context_lock.asset_source = asset_source.clone();
context_lock.svg_renderer = SvgRenderer::new(asset_source);
drop(context_lock);
self
}
/// Start the application. The provided callback will be called once the /// Start the application. The provided callback will be called once the
/// app is fully launched. /// app is fully launched.
pub fn run<F>(self, on_finish_launching: F) pub fn run<F>(self, on_finish_launching: F)
@ -187,6 +202,9 @@ type QuitHandler = Box<dyn FnOnce(&mut AppContext) -> LocalBoxFuture<'static, ()
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>; type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + '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 struct AppContext {
pub(crate) this: Weak<AppCell>, pub(crate) this: Weak<AppCell>,
pub(crate) platform: Rc<dyn Platform>, pub(crate) platform: Rc<dyn Platform>,
@ -312,7 +330,7 @@ impl AppContext {
let futures = futures::future::join_all(futures); let futures = futures::future::join_all(futures);
if self if self
.background_executor .background_executor
.block_with_timeout(Duration::from_millis(100), futures) .block_with_timeout(SHUTDOWN_TIMEOUT, futures)
.is_err() .is_err()
{ {
log::error!("timed out waiting on app_will_quit"); log::error!("timed out waiting on app_will_quit");
@ -446,6 +464,7 @@ impl AppContext {
.collect() .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> { pub fn active_window(&self) -> Option<AnyWindowHandle> {
self.platform.active_window() self.platform.active_window()
} }
@ -474,14 +493,17 @@ impl AppContext {
self.platform.activate(ignoring_other_apps); self.platform.activate(ignoring_other_apps);
} }
/// Hide the application at the platform level.
pub fn hide(&self) { pub fn hide(&self) {
self.platform.hide(); self.platform.hide();
} }
/// Hide other applications at the platform level.
pub fn hide_other_apps(&self) { pub fn hide_other_apps(&self) {
self.platform.hide_other_apps(); self.platform.hide_other_apps();
} }
/// Unhide other applications at the platform level.
pub fn unhide_other_apps(&self) { pub fn unhide_other_apps(&self) {
self.platform.unhide_other_apps(); self.platform.unhide_other_apps();
} }
@ -521,18 +543,25 @@ impl AppContext {
self.platform.open_url(url); 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> { pub fn app_path(&self) -> Result<PathBuf> {
self.platform.app_path() 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> { pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
self.platform.path_for_auxiliary_executable(name) 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 { pub fn double_click_interval(&self) -> Duration {
self.platform.double_click_interval() 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( pub fn prompt_for_paths(
&self, &self,
options: PathPromptOptions, options: PathPromptOptions,
@ -540,22 +569,30 @@ impl AppContext {
self.platform.prompt_for_paths(options) 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>> { pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
self.platform.prompt_for_new_path(directory) 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) { pub fn reveal_path(&self, path: &Path) {
self.platform.reveal_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 { pub fn should_auto_hide_scrollbars(&self) -> bool {
self.platform.should_auto_hide_scrollbars() self.platform.should_auto_hide_scrollbars()
} }
/// Restart the application.
pub fn restart(&self) { pub fn restart(&self) {
self.platform.restart() self.platform.restart()
} }
/// Returns the local timezone at the platform level.
pub fn local_timezone(&self) -> UtcOffset { pub fn local_timezone(&self) -> UtcOffset {
self.platform.local_timezone() self.platform.local_timezone()
} }
@ -745,7 +782,7 @@ impl AppContext {
} }
/// Spawns the future returned by the given function on the thread pool. The closure will be invoked /// 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> pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
where where
Fut: Future<Output = R> + 'static, Fut: Future<Output = R> + 'static,
@ -896,6 +933,8 @@ impl AppContext {
self.globals_by_type.insert(global_type, lease.global); 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>( pub fn observe_new_views<V: 'static>(
&mut self, &mut self,
on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>), on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>),
@ -915,6 +954,8 @@ impl AppContext {
subscription 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>( pub fn observe_release<E, T>(
&mut self, &mut self,
handle: &E, handle: &E,
@ -935,6 +976,9 @@ impl AppContext {
subscription 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 mechanisms have resolved
/// and that this API will not be invoked if the event's propagation is stopped.
pub fn observe_keystrokes( pub fn observe_keystrokes(
&mut self, &mut self,
f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static, f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static,
@ -958,6 +1002,7 @@ impl AppContext {
self.pending_effects.push_back(Effect::Refresh); self.pending_effects.push_back(Effect::Refresh);
} }
/// Clear all key bindings in the app.
pub fn clear_key_bindings(&mut self) { pub fn clear_key_bindings(&mut self) {
self.keymap.lock().clear(); self.keymap.lock().clear();
self.pending_effects.push_back(Effect::Refresh); self.pending_effects.push_back(Effect::Refresh);
@ -992,6 +1037,7 @@ impl AppContext {
self.propagate_event = true; self.propagate_event = true;
} }
/// Build an action from some arbitrary data, typically a keymap entry.
pub fn build_action( pub fn build_action(
&self, &self,
name: &str, name: &str,
@ -1000,10 +1046,16 @@ impl AppContext {
self.actions.build_action(name, data) 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] { pub fn all_action_names(&self) -> &[SharedString] {
self.actions.all_action_names() 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>( pub fn on_app_quit<Fut>(
&mut self, &mut self,
mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static, mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static,
@ -1039,6 +1091,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 { pub fn is_action_available(&mut self, action: &dyn Action) -> bool {
if let Some(window) = self.active_window() { if let Some(window) = self.active_window() {
if let Ok(window_action_available) = if let Ok(window_action_available) =
@ -1052,10 +1106,13 @@ impl AppContext {
.contains_key(&action.as_any().type_id()) .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>) { pub fn set_menus(&mut self, menus: Vec<Menu>) {
self.platform.set_menus(menus, &self.keymap.lock()); 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) { pub fn dispatch_action(&mut self, action: &dyn Action) {
if let Some(active_window) = self.active_window() { if let Some(active_window) = self.active_window() {
active_window active_window
@ -1110,6 +1167,7 @@ impl AppContext {
} }
} }
/// Is there currently something being dragged?
pub fn has_active_drag(&self) -> bool { pub fn has_active_drag(&self) -> bool {
self.active_drag.is_some() self.active_drag.is_some()
} }
@ -1119,7 +1177,7 @@ impl Context for AppContext {
type Result<T> = T; type Result<T> = T;
/// Build an entity that is owned by the application. The given function will be invoked with /// Build an entity that is owned by the application. The given function will be invoked with
/// a `ModelContext` and must return an object representing the entity. A `Model` will be returned /// a `ModelContext` and must return an object representing the entity. A `Model` handle will be returned,
/// which can be used to access the entity in a context. /// which can be used to access the entity in a context.
fn new_model<T: 'static>( fn new_model<T: 'static>(
&mut self, &mut self,
@ -1262,8 +1320,14 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
/// Contains state associated with an active drag operation, started by dragging an element /// 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. /// within the window or by dragging into the app from the underlying platform.
pub struct AnyDrag { pub struct AnyDrag {
/// The view used to render this drag
pub view: AnyView, pub view: AnyView,
/// The value of the dragged item, to be dropped
pub value: Box<dyn Any>, 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>, pub cursor_offset: Point<Pixels>,
} }
@ -1271,12 +1335,19 @@ pub struct AnyDrag {
/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip]. /// tooltip behavior on a custom element. Otherwise, use [Div::tooltip].
#[derive(Clone)] #[derive(Clone)]
pub struct AnyTooltip { pub struct AnyTooltip {
/// The view used to display the tooltip
pub view: AnyView, pub view: AnyView,
/// The offset from the cursor to use, relative to the parent view
pub cursor_offset: Point<Pixels>, pub cursor_offset: Point<Pixels>,
} }
/// A keystroke event, and potentially the associated action
#[derive(Debug)] #[derive(Debug)]
pub struct KeystrokeEvent { pub struct KeystrokeEvent {
/// The keystroke that occurred
pub keystroke: Keystroke, pub keystroke: Keystroke,
/// The action that was resolved for the keystroke, if any
pub action: Option<Box<dyn Action>>, pub action: Option<Box<dyn Action>>,
} }

View file

@ -7,6 +7,9 @@ use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use std::{future::Future, rc::Weak}; 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)] #[derive(Clone)]
pub struct AsyncAppContext { pub struct AsyncAppContext {
pub(crate) app: Weak<AppCell>, pub(crate) app: Weak<AppCell>,
@ -139,6 +142,8 @@ impl AsyncAppContext {
self.foreground_executor.spawn(f(self.clone())) 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> { pub fn has_global<G: 'static>(&self) -> Result<bool> {
let app = self let app = self
.app .app
@ -148,6 +153,9 @@ impl AsyncAppContext {
Ok(app.has_global::<G>()) 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> { pub fn read_global<G: 'static, R>(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result<R> {
let app = self let app = self
.app .app
@ -157,6 +165,9 @@ impl AsyncAppContext {
Ok(read(app.global(), &app)) 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>( pub fn try_read_global<G: 'static, R>(
&self, &self,
read: impl FnOnce(&G, &AppContext) -> R, read: impl FnOnce(&G, &AppContext) -> R,
@ -166,6 +177,8 @@ impl AsyncAppContext {
Some(read(app.try_global()?, &app)) 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>( pub fn update_global<G: 'static, R>(
&mut self, &mut self,
update: impl FnOnce(&mut G, &mut AppContext) -> R, 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)] #[derive(Clone, Deref, DerefMut)]
pub struct AsyncWindowContext { pub struct AsyncWindowContext {
#[deref] #[deref]
@ -188,14 +203,16 @@ pub struct AsyncWindowContext {
} }
impl AsyncWindowContext { impl AsyncWindowContext {
pub fn window_handle(&self) -> AnyWindowHandle {
self.window
}
pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self { pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
Self { app, window } 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>( pub fn update<R>(
&mut self, &mut self,
update: impl FnOnce(AnyView, &mut WindowContext) -> R, update: impl FnOnce(AnyView, &mut WindowContext) -> R,
@ -203,10 +220,12 @@ impl AsyncWindowContext {
self.app.update_window(self.window, update) 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) { pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
self.window.update(self, |_, cx| cx.on_next_frame(f)).ok(); self.window.update(self, |_, cx| cx.on_next_frame(f)).ok();
} }
/// A convenience method for [AppContext::global()]
pub fn read_global<G: 'static, R>( pub fn read_global<G: 'static, R>(
&mut self, &mut self,
read: impl FnOnce(&G, &WindowContext) -> R, read: impl FnOnce(&G, &WindowContext) -> R,
@ -214,6 +233,8 @@ impl AsyncWindowContext {
self.window.update(self, |_, cx| read(cx.global(), cx)) 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>( pub fn update_global<G, R>(
&mut self, &mut self,
update: impl FnOnce(&mut G, &mut WindowContext) -> R, update: impl FnOnce(&mut G, &mut WindowContext) -> R,
@ -224,6 +245,8 @@ impl AsyncWindowContext {
self.window.update(self, |_, cx| cx.update_global(update)) 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> pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task<R>
where where
Fut: Future<Output = R> + 'static, Fut: Future<Output = R> + 'static,

View file

@ -31,6 +31,7 @@ impl From<u64> for EntityId {
} }
impl EntityId { impl EntityId {
/// Converts this entity id to a [u64]
pub fn as_u64(self) -> u64 { pub fn as_u64(self) -> u64 {
self.0.as_ffi() 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>>, entity: Option<Box<dyn Any>>,
pub model: &'a Model<T>, pub model: &'a Model<T>,
entity_type: PhantomData<T>, entity_type: PhantomData<T>,
@ -169,8 +170,9 @@ impl<'a, T> Drop for Lease<'a, T> {
} }
#[derive(Deref, DerefMut)] #[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 struct AnyModel {
pub(crate) entity_id: EntityId, pub(crate) entity_id: EntityId,
pub(crate) entity_type: TypeId, pub(crate) entity_type: TypeId,
@ -195,14 +197,17 @@ impl AnyModel {
} }
} }
/// Returns the id associated with this model.
pub fn entity_id(&self) -> EntityId { pub fn entity_id(&self) -> EntityId {
self.entity_id self.entity_id
} }
/// Returns the [TypeId] associated with this model.
pub fn entity_type(&self) -> TypeId { pub fn entity_type(&self) -> TypeId {
self.entity_type self.entity_type
} }
/// Converts this model handle into a weak variant, which does not prevent it from being released.
pub fn downgrade(&self) -> AnyWeakModel { pub fn downgrade(&self) -> AnyWeakModel {
AnyWeakModel { AnyWeakModel {
entity_id: self.entity_id, 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> { pub fn downcast<T: 'static>(self) -> Result<Model<T>, AnyModel> {
if TypeId::of::<T>() == self.entity_type { if TypeId::of::<T>() == self.entity_type {
Ok(Model { Ok(Model {
@ -274,7 +281,7 @@ impl Drop for AnyModel {
entity_map entity_map
.write() .write()
.leak_detector .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)] #[derive(Deref, DerefMut)]
pub struct Model<T> { pub struct Model<T> {
#[deref] #[deref]
@ -368,10 +377,12 @@ impl<T: 'static> Model<T> {
self.any_model self.any_model
} }
/// Grab a reference to this entity from the context.
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T { pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
cx.entities.read(self) cx.entities.read(self)
} }
/// Read the entity referenced by this model with the given function.
pub fn read_with<R, C: Context>( pub fn read_with<R, C: Context>(
&self, &self,
cx: &C, cx: &C,
@ -437,6 +448,7 @@ impl<T> PartialEq<WeakModel<T>> for Model<T> {
} }
} }
/// A type erased, weak reference to a model.
#[derive(Clone)] #[derive(Clone)]
pub struct AnyWeakModel { pub struct AnyWeakModel {
pub(crate) entity_id: EntityId, pub(crate) entity_id: EntityId,
@ -445,10 +457,12 @@ pub struct AnyWeakModel {
} }
impl AnyWeakModel { impl AnyWeakModel {
/// Get the entity ID associated with this weak reference.
pub fn entity_id(&self) -> EntityId { pub fn entity_id(&self) -> EntityId {
self.entity_id 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 { pub fn is_upgradable(&self) -> bool {
let ref_count = self let ref_count = self
.entity_ref_counts .entity_ref_counts
@ -458,6 +472,7 @@ impl AnyWeakModel {
ref_count > 0 ref_count > 0
} }
/// Upgrade this weak model reference to a strong reference.
pub fn upgrade(&self) -> Option<AnyModel> { pub fn upgrade(&self) -> Option<AnyModel> {
let ref_counts = &self.entity_ref_counts.upgrade()?; let ref_counts = &self.entity_ref_counts.upgrade()?;
let ref_counts = ref_counts.read(); 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"))] #[cfg(any(test, feature = "test-support"))]
pub fn assert_dropped(&self) { pub fn assert_released(&self) {
self.entity_ref_counts self.entity_ref_counts
.upgrade() .upgrade()
.unwrap() .unwrap()
.write() .write()
.leak_detector .leak_detector
.assert_dropped(self.entity_id); .assert_released(self.entity_id);
if self if self
.entity_ref_counts .entity_ref_counts
@ -527,6 +543,7 @@ impl PartialEq for AnyWeakModel {
impl Eq for AnyWeakModel {} impl Eq for AnyWeakModel {}
/// A weak reference to a model of the given type.
#[derive(Deref, DerefMut)] #[derive(Deref, DerefMut)]
pub struct WeakModel<T> { pub struct WeakModel<T> {
#[deref] #[deref]
@ -617,12 +634,12 @@ lazy_static::lazy_static! {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] #[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 id: u64, // id of the handle itself, not the pointed at object
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub struct LeakDetector { pub(crate) struct LeakDetector {
next_handle_id: u64, next_handle_id: u64,
entity_handles: HashMap<EntityId, HashMap<HandleId, Option<backtrace::Backtrace>>>, entity_handles: HashMap<EntityId, HashMap<HandleId, Option<backtrace::Backtrace>>>,
} }
@ -641,12 +658,12 @@ impl LeakDetector {
handle_id 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(); let handles = self.entity_handles.entry(entity_id).or_default();
handles.remove(&handle_id); 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(); let handles = self.entity_handles.entry(entity_id).or_default();
if !handles.is_empty() { if !handles.is_empty() {
for (_, backtrace) in handles { for (_, backtrace) in handles {

View file

@ -11,6 +11,7 @@ use std::{
future::Future, future::Future,
}; };
/// The app context, with specialized behavior for the given model.
#[derive(Deref, DerefMut)] #[derive(Deref, DerefMut)]
pub struct ModelContext<'a, T> { pub struct ModelContext<'a, T> {
#[deref] #[deref]
@ -24,20 +25,24 @@ impl<'a, T: 'static> ModelContext<'a, T> {
Self { app, model_state } Self { app, model_state }
} }
/// The entity id of the model backing this context.
pub fn entity_id(&self) -> EntityId { pub fn entity_id(&self) -> EntityId {
self.model_state.entity_id self.model_state.entity_id
} }
/// Returns a handle to the model belonging to this context.
pub fn handle(&self) -> Model<T> { pub fn handle(&self) -> Model<T> {
self.weak_model() self.weak_model()
.upgrade() .upgrade()
.expect("The entity must be alive if we have a model context") .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> { pub fn weak_model(&self) -> WeakModel<T> {
self.model_state.clone() 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>( pub fn observe<W, E>(
&mut self, &mut self,
entity: &E, 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>( pub fn subscribe<T2, E, Evt>(
&mut self, &mut self,
entity: &E, 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( pub fn on_release(
&mut self, &mut self,
on_release: impl FnOnce(&mut T, &mut AppContext) + 'static, on_release: impl FnOnce(&mut T, &mut AppContext) + 'static,
@ -99,6 +106,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
subscription subscription
} }
/// Register a callback to be run on the release of another model or view
pub fn observe_release<T2, E>( pub fn observe_release<T2, E>(
&mut self, &mut self,
entity: &E, entity: &E,
@ -124,6 +132,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
subscription subscription
} }
/// Register a callback to for updates to the given global
pub fn observe_global<G: 'static>( pub fn observe_global<G: 'static>(
&mut self, &mut self,
mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + 'static, mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + 'static,
@ -140,6 +149,8 @@ impl<'a, T: 'static> ModelContext<'a, T> {
subscription 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>( pub fn on_app_quit<Fut>(
&mut self, &mut self,
mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + 'static, mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + 'static,
@ -165,6 +176,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
subscription subscription
} }
/// Tell GPUI that this model has changed and observers of it should be notified.
pub fn notify(&mut self) { pub fn notify(&mut self) {
if self if self
.app .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 pub fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where where
G: 'static, G: 'static,
@ -187,6 +200,9 @@ impl<'a, T: 'static> ModelContext<'a, T> {
result 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> pub fn spawn<Fut, R>(&self, f: impl FnOnce(WeakModel<T>, AsyncAppContext) -> Fut) -> Task<R>
where where
T: 'static, T: 'static,
@ -199,6 +215,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
} }
impl<'a, T> 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) pub fn emit<Evt>(&mut self, event: Evt)
where where
T: EventEmitter<Evt>, T: EventEmitter<Evt>,

View file

@ -1,11 +1,11 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use crate::{ use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter,
IntoElement, Keystroke, Model, ModelContext, Pixels, Platform, Render, Result, Size, Task, ForegroundExecutor, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform, Point,
TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
WindowContext, WindowHandle, WindowOptions, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
}; };
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
@ -167,10 +167,14 @@ impl TestAppContext {
} }
/// Adds a new window with no content. /// 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(); let mut cx = self.app.borrow_mut();
cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {})) let window = cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| ()));
.any_handle 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 /// 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 { 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. /// Provides the `WindowContext` for the duration of the closure.
pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { pub fn update<R>(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R {
self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() self.cx.update_window(self.window, |_, cx| f(cx)).unwrap()
@ -609,6 +618,46 @@ impl<'a> VisualTestContext {
self.cx.simulate_input(self.window, input) self.cx.simulate_input(self.window, input)
} }
/// Simulates the user resizing the window to the new size.
pub fn simulate_resize(&self, size: Size<Pixels>) {
self.simulate_window_resize(self.window, size)
}
/// debug_bounds returns the bounds of the element with the given selector.
pub fn debug_bounds(&mut self, selector: &'static str) -> Option<Bounds<Pixels>> {
self.update(|cx| cx.window.rendered_frame.debug_bounds.get(selector).copied())
}
/// 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. /// Simulates the user blurring the window.
pub fn deactivate_window(&mut self) { pub fn deactivate_window(&mut self) {
if Some(self.window) == self.test_platform.active_window() { if Some(self.window) == self.test_platform.active_window() {
@ -763,12 +812,3 @@ impl AnyWindowHandle {
self.update(cx, |_, cx| cx.new_view(build_view)).unwrap() 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()
}
}

View file

@ -115,6 +115,12 @@ pub trait Render: 'static + Sized {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement; 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. /// 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. /// It is used to allow views to be expressed in terms of abstract data.
pub trait RenderOnce: 'static { pub trait RenderOnce: 'static {

View file

@ -416,6 +416,18 @@ pub trait InteractiveElement: Sized {
self self
} }
#[cfg(any(test, feature = "test-support"))]
fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self {
self.interactivity().debug_selector = Some(f());
self
}
#[cfg(not(any(test, feature = "test-support")))]
#[inline]
fn debug_selector(self, _: impl FnOnce() -> String) -> Self {
self
}
fn capture_any_mouse_down( fn capture_any_mouse_down(
mut self, mut self,
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
@ -911,6 +923,9 @@ pub struct Interactivity {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub location: Option<core::panic::Location<'static>>, pub location: Option<core::panic::Location<'static>>,
#[cfg(any(test, feature = "test-support"))]
pub debug_selector: Option<String>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -980,6 +995,14 @@ impl Interactivity {
let style = self.compute_style(Some(bounds), element_state, cx); let style = self.compute_style(Some(bounds), element_state, cx);
let z_index = style.z_index.unwrap_or(0); let z_index = style.z_index.unwrap_or(0);
#[cfg(any(feature = "test-support", test))]
if let Some(debug_selector) = &self.debug_selector {
cx.window
.next_frame
.debug_bounds
.insert(debug_selector.clone(), bounds);
}
let paint_hover_group_handler = |cx: &mut WindowContext| { let paint_hover_group_handler = |cx: &mut WindowContext| {
let hover_group_bounds = self let hover_group_bounds = self
.group_hover_style .group_hover_style

View file

@ -30,6 +30,7 @@ struct StateInner {
logical_scroll_top: Option<ListOffset>, logical_scroll_top: Option<ListOffset>,
alignment: ListAlignment, alignment: ListAlignment,
overdraw: Pixels, overdraw: Pixels,
reset: bool,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>, scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
} }
@ -92,11 +93,17 @@ impl ListState {
alignment: orientation, alignment: orientation,
overdraw, overdraw,
scroll_handler: None, 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) { pub fn reset(&self, element_count: usize) {
let state = &mut *self.0.borrow_mut(); let state = &mut *self.0.borrow_mut();
state.reset = true;
state.logical_scroll_top = None; state.logical_scroll_top = None;
state.items = SumTree::new(); state.items = SumTree::new();
state state
@ -152,11 +159,13 @@ impl ListState {
scroll_top.item_ix = item_count; scroll_top.item_ix = item_count;
scroll_top.offset_in_item = px(0.); scroll_top.offset_in_item = px(0.);
} }
state.logical_scroll_top = Some(scroll_top); state.logical_scroll_top = Some(scroll_top);
} }
pub fn scroll_to_reveal_item(&self, ix: usize) { pub fn scroll_to_reveal_item(&self, ix: usize) {
let state = &mut *self.0.borrow_mut(); let state = &mut *self.0.borrow_mut();
let mut scroll_top = state.logical_scroll_top(); let mut scroll_top = state.logical_scroll_top();
let height = state let height = state
.last_layout_bounds .last_layout_bounds
@ -187,9 +196,9 @@ impl ListState {
/// Get the bounds for the given item in window coordinates. /// Get the bounds for the given item in window coordinates.
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> { pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
let state = &*self.0.borrow(); let state = &*self.0.borrow();
let bounds = state.last_layout_bounds.unwrap_or_default(); let bounds = state.last_layout_bounds.unwrap_or_default();
let scroll_top = state.logical_scroll_top(); let scroll_top = state.logical_scroll_top();
if ix < scroll_top.item_ix { if ix < scroll_top.item_ix {
return None; return None;
} }
@ -230,6 +239,12 @@ impl StateInner {
delta: Point<Pixels>, delta: Point<Pixels>,
cx: &mut WindowContext, 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 scroll_max = (self.items.summary().height - height).max(px(0.));
let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
.max(px(0.)) .max(px(0.))
@ -325,6 +340,8 @@ impl Element for List {
) { ) {
let state = &mut *self.state.0.borrow_mut(); 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 the width of the list has changed, invalidate all cached item heights
if state.last_layout_bounds.map_or(true, |last_bounds| { if state.last_layout_bounds.map_or(true, |last_bounds| {
last_bounds.size.width != bounds.size.width last_bounds.size.width != bounds.size.width
@ -346,8 +363,9 @@ impl Element for List {
height: AvailableSpace::MinContent, height: AvailableSpace::MinContent,
}; };
// Render items after the scroll top, including those in the trailing overdraw
let mut cursor = old_items.cursor::<Count>(); 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, &()); cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
for (ix, item) in cursor.by_ref().enumerate() { for (ix, item) in cursor.by_ref().enumerate() {
let visible_height = rendered_height - scroll_top.offset_in_item; 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 list_state = self.state.clone();
let height = bounds.size.height; let height = bounds.size.height;
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble if phase == DispatchPhase::Bubble
&& bounds.contains(&event.position) && 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() 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 receive 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.));
}
}

View file

@ -109,9 +109,10 @@ type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
/// BackgroundExecutor lets you run things on background threads. /// BackgroundExecutor lets you run things on background threads.
/// In production this is a thread pool with no ordering guarantees. /// 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. /// (but arbitrary) order controlled by the `SEED` environment variable.
impl BackgroundExecutor { impl BackgroundExecutor {
#[doc(hidden)]
pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self { pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
Self { dispatcher } Self { dispatcher }
} }
@ -149,7 +150,7 @@ impl BackgroundExecutor {
Task::Spawned(task) Task::Spawned(task)
} }
/// Used by the test harness to run an async test in a syncronous fashion. /// Used by the test harness to run an async test in a synchronous fashion.
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
#[track_caller] #[track_caller]
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R { pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
@ -276,7 +277,7 @@ impl BackgroundExecutor {
/// Returns a task that will complete after the given duration. /// Returns a task that will complete after the given duration.
/// Depending on other concurrent tasks the elapsed duration may be longer /// Depending on other concurrent tasks the elapsed duration may be longer
/// than reqested. /// than requested.
pub fn timer(&self, duration: Duration) -> Task<()> { pub fn timer(&self, duration: Duration) -> Task<()> {
let (runnable, task) = async_task::spawn(async move {}, { let (runnable, task) = async_task::spawn(async move {}, {
let dispatcher = self.dispatcher.clone(); let dispatcher = self.dispatcher.clone();

View file

@ -1,8 +1,14 @@
use crate::{ 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 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)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeyDownEvent { pub struct KeyDownEvent {
@ -10,16 +16,40 @@ pub struct KeyDownEvent {
pub is_held: bool, 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)] #[derive(Clone, Debug)]
pub struct KeyUpEvent { pub struct KeyUpEvent {
pub keystroke: Keystroke, 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)] #[derive(Clone, Debug, Default)]
pub struct ModifiersChangedEvent { pub struct ModifiersChangedEvent {
pub modifiers: Modifiers, 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 { impl Deref for ModifiersChangedEvent {
type Target = Modifiers; type Target = Modifiers;
@ -30,9 +60,10 @@ impl Deref for ModifiersChangedEvent {
/// The phase of a touch motion event. /// The phase of a touch motion event.
/// Based on the winit enum of the same name. /// Based on the winit enum of the same name.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Default)]
pub enum TouchPhase { pub enum TouchPhase {
Started, Started,
#[default]
Moved, Moved,
Ended, Ended,
} }
@ -45,6 +76,14 @@ pub struct MouseDownEvent {
pub click_count: usize, 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)] #[derive(Clone, Debug, Default)]
pub struct MouseUpEvent { pub struct MouseUpEvent {
pub button: MouseButton, pub button: MouseButton,
@ -53,38 +92,20 @@ pub struct MouseUpEvent {
pub click_count: usize, 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)] #[derive(Clone, Debug, Default)]
pub struct ClickEvent { pub struct ClickEvent {
pub down: MouseDownEvent, pub down: MouseDownEvent,
pub up: MouseUpEvent, 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)] #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
pub enum MouseButton { pub enum MouseButton {
Left, Left,
@ -130,13 +151,21 @@ pub struct MouseMoveEvent {
pub modifiers: Modifiers, 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 { impl MouseMoveEvent {
pub fn dragging(&self) -> bool { pub fn dragging(&self) -> bool {
self.pressed_button == Some(MouseButton::Left) self.pressed_button == Some(MouseButton::Left)
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Default)]
pub struct ScrollWheelEvent { pub struct ScrollWheelEvent {
pub position: Point<Pixels>, pub position: Point<Pixels>,
pub delta: ScrollDelta, pub delta: ScrollDelta,
@ -144,6 +173,14 @@ pub struct ScrollWheelEvent {
pub touch_phase: TouchPhase, 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 { impl Deref for ScrollWheelEvent {
type Target = Modifiers; type Target = Modifiers;
@ -201,6 +238,14 @@ pub struct MouseExitEvent {
pub modifiers: Modifiers, 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 { impl Deref for MouseExitEvent {
type Target = Modifiers; type Target = Modifiers;
@ -220,7 +265,7 @@ impl ExternalPaths {
impl Render for ExternalPaths { impl Render for ExternalPaths {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement { 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, 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)] #[derive(Clone, Debug)]
pub enum InputEvent { pub enum PlatformInput {
KeyDown(KeyDownEvent), KeyDown(KeyDownEvent),
KeyUp(KeyUpEvent), KeyUp(KeyUpEvent),
ModifiersChanged(ModifiersChangedEvent), ModifiersChanged(ModifiersChangedEvent),
@ -252,19 +305,19 @@ pub enum InputEvent {
FileDrop(FileDropEvent), FileDrop(FileDropEvent),
} }
impl InputEvent { impl PlatformInput {
pub fn position(&self) -> Option<Point<Pixels>> { pub fn position(&self) -> Option<Point<Pixels>> {
match self { match self {
InputEvent::KeyDown { .. } => None, PlatformInput::KeyDown { .. } => None,
InputEvent::KeyUp { .. } => None, PlatformInput::KeyUp { .. } => None,
InputEvent::ModifiersChanged { .. } => None, PlatformInput::ModifiersChanged { .. } => None,
InputEvent::MouseDown(event) => Some(event.position), PlatformInput::MouseDown(event) => Some(event.position),
InputEvent::MouseUp(event) => Some(event.position), PlatformInput::MouseUp(event) => Some(event.position),
InputEvent::MouseMove(event) => Some(event.position), PlatformInput::MouseMove(event) => Some(event.position),
InputEvent::MouseExited(event) => Some(event.position), PlatformInput::MouseExited(event) => Some(event.position),
InputEvent::ScrollWheel(event) => Some(event.position), PlatformInput::ScrollWheel(event) => Some(event.position),
InputEvent::FileDrop(FileDropEvent::Exited) => None, PlatformInput::FileDrop(FileDropEvent::Exited) => None,
InputEvent::FileDrop( PlatformInput::FileDrop(
FileDropEvent::Entered { position, .. } FileDropEvent::Entered { position, .. }
| FileDropEvent::Pending { position, .. } | FileDropEvent::Pending { position, .. }
| FileDropEvent::Submit { position, .. }, | FileDropEvent::Submit { position, .. },
@ -274,29 +327,29 @@ impl InputEvent {
pub fn mouse_event(&self) -> Option<&dyn Any> { pub fn mouse_event(&self) -> Option<&dyn Any> {
match self { match self {
InputEvent::KeyDown { .. } => None, PlatformInput::KeyDown { .. } => None,
InputEvent::KeyUp { .. } => None, PlatformInput::KeyUp { .. } => None,
InputEvent::ModifiersChanged { .. } => None, PlatformInput::ModifiersChanged { .. } => None,
InputEvent::MouseDown(event) => Some(event), PlatformInput::MouseDown(event) => Some(event),
InputEvent::MouseUp(event) => Some(event), PlatformInput::MouseUp(event) => Some(event),
InputEvent::MouseMove(event) => Some(event), PlatformInput::MouseMove(event) => Some(event),
InputEvent::MouseExited(event) => Some(event), PlatformInput::MouseExited(event) => Some(event),
InputEvent::ScrollWheel(event) => Some(event), PlatformInput::ScrollWheel(event) => Some(event),
InputEvent::FileDrop(event) => Some(event), PlatformInput::FileDrop(event) => Some(event),
} }
} }
pub fn keyboard_event(&self) -> Option<&dyn Any> { pub fn keyboard_event(&self) -> Option<&dyn Any> {
match self { match self {
InputEvent::KeyDown(event) => Some(event), PlatformInput::KeyDown(event) => Some(event),
InputEvent::KeyUp(event) => Some(event), PlatformInput::KeyUp(event) => Some(event),
InputEvent::ModifiersChanged(event) => Some(event), PlatformInput::ModifiersChanged(event) => Some(event),
InputEvent::MouseDown(_) => None, PlatformInput::MouseDown(_) => None,
InputEvent::MouseUp(_) => None, PlatformInput::MouseUp(_) => None,
InputEvent::MouseMove(_) => None, PlatformInput::MouseMove(_) => None,
InputEvent::MouseExited(_) => None, PlatformInput::MouseExited(_) => None,
InputEvent::ScrollWheel(_) => None, PlatformInput::ScrollWheel(_) => None,
InputEvent::FileDrop(_) => None, PlatformInput::FileDrop(_) => None,
} }
} }
} }

View file

@ -209,7 +209,6 @@ mod tests {
); );
assert!(!matcher.has_pending_keystrokes()); assert!(!matcher.has_pending_keystrokes());
eprintln!("PROBLEM AREA");
// If a is prefixed, C will not be dispatched because there // If a is prefixed, C will not be dispatched because there
// was a pending binding for it // was a pending binding for it
assert_eq!( assert_eq!(
@ -445,7 +444,7 @@ mod tests {
KeyMatch::Some(vec![Box::new(Dollar)]) KeyMatch::Some(vec![Box::new(Dollar)])
); );
// handle Brazillian quote (quote key then space key) // handle Brazilian quote (quote key then space key)
assert_eq!( assert_eq!(
matcher.match_keystroke( matcher.match_keystroke(
&Keystroke::parse("space->\"").unwrap(), &Keystroke::parse("space->\"").unwrap(),
@ -454,7 +453,7 @@ mod tests {
KeyMatch::Some(vec![Box::new(Quote)]) KeyMatch::Some(vec![Box::new(Quote)])
); );
// handle ctrl+` on a brazillian keyboard // handle ctrl+` on a brazilian keyboard
assert_eq!( assert_eq!(
matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]), matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]),
KeyMatch::Some(vec![Box::new(Backtick)]) KeyMatch::Some(vec![Box::new(Backtick)])

View file

@ -7,7 +7,7 @@ mod test;
use crate::{ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics, 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, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString,
Size, TaskLabel, Size, TaskLabel,
}; };
@ -88,7 +88,7 @@ pub(crate) trait Platform: 'static {
fn on_resign_active(&self, callback: Box<dyn FnMut()>); fn on_resign_active(&self, callback: Box<dyn FnMut()>);
fn on_quit(&self, callback: Box<dyn FnMut()>); fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_reopen(&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 set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>); 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<()>; 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 { pub trait PlatformDisplay: Send + Sync + Debug {
/// Get the ID for this display
fn id(&self) -> DisplayId; fn id(&self) -> DisplayId;
/// Returns a stable identifier for this display that can be persisted and used /// Returns a stable identifier for this display that can be persisted and used
/// across system restarts. /// across system restarts.
fn uuid(&self) -> Result<Uuid>; fn uuid(&self) -> Result<Uuid>;
fn as_any(&self) -> &dyn Any;
/// Get the bounds for this display
fn bounds(&self) -> Bounds<GlobalPixels>; fn bounds(&self) -> Bounds<GlobalPixels>;
} }
/// An opaque identifier for a hardware display
#[derive(PartialEq, Eq, Hash, Copy, Clone)] #[derive(PartialEq, Eq, Hash, Copy, Clone)]
pub struct DisplayId(pub(crate) u32); pub struct DisplayId(pub(crate) u32);
@ -134,7 +139,7 @@ impl Debug for DisplayId {
unsafe impl Send for DisplayId {} unsafe impl Send for DisplayId {}
pub trait PlatformWindow { pub(crate) trait PlatformWindow {
fn bounds(&self) -> WindowBounds; fn bounds(&self) -> WindowBounds;
fn content_size(&self) -> Size<Pixels>; fn content_size(&self) -> Size<Pixels>;
fn scale_factor(&self) -> f32; fn scale_factor(&self) -> f32;
@ -155,7 +160,7 @@ pub trait PlatformWindow {
fn zoom(&self); fn zoom(&self);
fn toggle_full_screen(&self); fn toggle_full_screen(&self);
fn on_request_frame(&self, callback: Box<dyn FnMut()>); 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_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>); fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>); 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 { pub trait PlatformDispatcher: Send + Sync {
fn is_main_thread(&self) -> bool; fn is_main_thread(&self) -> bool;
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>); fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
@ -190,9 +198,10 @@ 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 add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
fn all_font_names(&self) -> Vec<String>; fn all_font_names(&self) -> Vec<String>;
fn all_font_families(&self) -> Vec<String>;
fn font_id(&self, descriptor: &Font) -> Result<FontId>; fn font_id(&self, descriptor: &Font) -> Result<FontId>;
fn font_metrics(&self, font_id: FontId) -> FontMetrics; fn font_metrics(&self, font_id: FontId) -> FontMetrics;
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>; fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
@ -214,15 +223,21 @@ pub trait PlatformTextSystem: Send + Sync {
) -> Vec<usize>; ) -> Vec<usize>;
} }
/// Basic metadata about the current application and operating system.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AppMetadata { pub struct AppMetadata {
/// The name of the current operating system
pub os_name: &'static str, pub os_name: &'static str,
/// The operating system's version
pub os_version: Option<SemanticVersion>, pub os_version: Option<SemanticVersion>,
/// The current version of the application
pub app_version: Option<SemanticVersion>, pub app_version: Option<SemanticVersion>,
} }
#[derive(PartialEq, Eq, Hash, Clone)] #[derive(PartialEq, Eq, Hash, Clone)]
pub enum AtlasKey { pub(crate) enum AtlasKey {
Glyph(RenderGlyphParams), Glyph(RenderGlyphParams),
Svg(RenderSvgParams), Svg(RenderSvgParams),
Image(RenderImageParams), Image(RenderImageParams),
@ -262,19 +277,17 @@ impl From<RenderImageParams> for AtlasKey {
} }
} }
pub trait PlatformAtlas: Send + Sync { pub(crate) trait PlatformAtlas: Send + Sync {
fn get_or_insert_with<'a>( fn get_or_insert_with<'a>(
&self, &self,
key: &AtlasKey, key: &AtlasKey,
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>, build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
) -> Result<AtlasTile>; ) -> Result<AtlasTile>;
fn clear(&self);
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
#[repr(C)] #[repr(C)]
pub struct AtlasTile { pub(crate) struct AtlasTile {
pub(crate) texture_id: AtlasTextureId, pub(crate) texture_id: AtlasTextureId,
pub(crate) tile_id: TileId, pub(crate) tile_id: TileId,
pub(crate) bounds: Bounds<DevicePixels>, pub(crate) bounds: Bounds<DevicePixels>,

View file

@ -19,7 +19,7 @@ impl Keystroke {
// the ime_key or the key. On some non-US keyboards keys we use in our // the ime_key or the key. On some non-US keyboards keys we use in our
// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), // bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard),
// and on some keyboards the IME handler converts a sequence of keys into a // and on some keyboards the IME handler converts a sequence of keys into a
// specific character (for example `"` is typed as `" space` on a brazillian keyboard). // specific character (for example `"` is typed as `" space` on a brazilian keyboard).
pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> {
let mut possibilities = SmallVec::new(); let mut possibilities = SmallVec::new();
match self.ime_key.as_ref() { match self.ime_key.as_ref() {

View file

@ -10,7 +10,7 @@ mod open_type;
mod platform; mod platform;
mod text_system; mod text_system;
mod window; mod window;
mod window_appearence; mod window_appearance;
use crate::{px, size, GlobalPixels, Pixels, Size}; use crate::{px, size, GlobalPixels, Pixels, Size};
use cocoa::{ use cocoa::{

View file

@ -11,7 +11,6 @@ use core_graphics::{
geometry::{CGPoint, CGRect, CGSize}, geometry::{CGPoint, CGRect, CGSize},
}; };
use objc::{msg_send, sel, sel_impl}; use objc::{msg_send, sel, sel_impl};
use std::any::Any;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug)] #[derive(Debug)]
@ -154,10 +153,6 @@ impl PlatformDisplay for MacDisplay {
])) ]))
} }
fn as_any(&self) -> &dyn Any {
self
}
fn bounds(&self) -> Bounds<GlobalPixels> { fn bounds(&self) -> Bounds<GlobalPixels> {
unsafe { unsafe {
let native_bounds = CGDisplayBounds(self.0); let native_bounds = CGDisplayBounds(self.0);

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
point, px, InputEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase, PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
}; };
use cocoa::{ use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, 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> { pub unsafe fn from_native(native_event: id, window_height: Option<Pixels>) -> Option<Self> {
let event_type = native_event.eventType(); let event_type = native_event.eventType();

View file

@ -74,20 +74,6 @@ impl PlatformAtlas for MetalAtlas {
Ok(tile) Ok(tile)
} }
} }
fn clear(&self) {
let mut lock = self.0.lock();
lock.tiles_by_key.clear();
for texture in &mut lock.monochrome_textures {
texture.clear();
}
for texture in &mut lock.polychrome_textures {
texture.clear();
}
for texture in &mut lock.path_textures {
texture.clear();
}
}
} }
impl MetalAtlasState { impl MetalAtlasState {

View file

@ -14,6 +14,7 @@ use foreign_types::ForeignType;
use media::core_video::CVMetalTextureCache; use media::core_video::CVMetalTextureCache;
use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl}; use objc::{self, msg_send, sel, sel_impl};
use smallvec::SmallVec;
use std::{ffi::c_void, mem, ptr, sync::Arc}; use std::{ffi::c_void, mem, ptr, sync::Arc};
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
@ -81,7 +82,7 @@ impl MetalRenderer {
]; ];
let unit_vertices = device.new_buffer_with_data( let unit_vertices = device.new_buffer_with_data(
unit_vertices.as_ptr() as *const c_void, unit_vertices.as_ptr() as *const c_void,
(unit_vertices.len() * mem::size_of::<u64>()) as u64, mem::size_of_val(&unit_vertices) as u64,
MTLResourceOptions::StorageModeManaged, MTLResourceOptions::StorageModeManaged,
); );
let instances = device.new_buffer( let instances = device.new_buffer(
@ -339,7 +340,8 @@ impl MetalRenderer {
for (texture_id, vertices) in vertices_by_texture_id { for (texture_id, vertices) in vertices_by_texture_id {
align_offset(offset); align_offset(offset);
let next_offset = *offset + vertices.len() * mem::size_of::<PathVertex<ScaledPixels>>(); let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
let next_offset = *offset + vertices_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE { if next_offset > INSTANCE_BUFFER_SIZE {
return None; return None;
} }
@ -372,7 +374,6 @@ impl MetalRenderer {
&texture_size as *const Size<DevicePixels> as *const _, &texture_size as *const Size<DevicePixels> as *const _,
); );
let vertices_bytes_len = mem::size_of::<PathVertex<ScaledPixels>>() * vertices.len();
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
unsafe { unsafe {
ptr::copy_nonoverlapping( ptr::copy_nonoverlapping(
@ -429,7 +430,7 @@ impl MetalRenderer {
&viewport_size as *const Size<DevicePixels> as *const _, &viewport_size as *const Size<DevicePixels> as *const _,
); );
let shadow_bytes_len = std::mem::size_of_val(shadows); let shadow_bytes_len = mem::size_of_val(shadows);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + shadow_bytes_len; let next_offset = *offset + shadow_bytes_len;
@ -490,7 +491,7 @@ impl MetalRenderer {
&viewport_size as *const Size<DevicePixels> as *const _, &viewport_size as *const Size<DevicePixels> as *const _,
); );
let quad_bytes_len = std::mem::size_of_val(quads); let quad_bytes_len = mem::size_of_val(quads);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + quad_bytes_len; let next_offset = *offset + quad_bytes_len;
@ -537,7 +538,7 @@ impl MetalRenderer {
); );
let mut prev_texture_id = None; let mut prev_texture_id = None;
let mut sprites = Vec::new(); let mut sprites = SmallVec::<[_; 1]>::new();
let mut paths_and_tiles = paths let mut paths_and_tiles = paths
.iter() .iter()
.map(|path| (path, tiles_by_path_id.get(&path.id).unwrap())) .map(|path| (path, tiles_by_path_id.get(&path.id).unwrap()))
@ -590,7 +591,7 @@ impl MetalRenderer {
command_encoder command_encoder
.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of::<MonochromeSprite>() * sprites.len(); let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
let next_offset = *offset + sprite_bytes_len; let next_offset = *offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE { if next_offset > INSTANCE_BUFFER_SIZE {
return false; return false;
@ -655,21 +656,22 @@ impl MetalRenderer {
&viewport_size as *const Size<DevicePixels> as *const _, &viewport_size as *const Size<DevicePixels> as *const _,
); );
let quad_bytes_len = std::mem::size_of_val(underlines); let underline_bytes_len = mem::size_of_val(underlines);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + underline_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
unsafe { unsafe {
ptr::copy_nonoverlapping( ptr::copy_nonoverlapping(
underlines.as_ptr() as *const u8, underlines.as_ptr() as *const u8,
buffer_contents, buffer_contents,
quad_bytes_len, underline_bytes_len,
); );
} }
let next_offset = *offset + quad_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
command_encoder.draw_primitives_instanced( command_encoder.draw_primitives_instanced(
metal::MTLPrimitiveType::Triangle, metal::MTLPrimitiveType::Triangle,
0, 0,
@ -726,7 +728,7 @@ impl MetalRenderer {
); );
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = std::mem::size_of_val(sprites); let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + sprite_bytes_len; let next_offset = *offset + sprite_bytes_len;
@ -798,7 +800,7 @@ impl MetalRenderer {
); );
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = std::mem::size_of_val(sprites); let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let next_offset = *offset + sprite_bytes_len; let next_offset = *offset + sprite_bytes_len;

View file

@ -1,8 +1,8 @@
use super::{events::key_to_native, BoolExt}; use super::{events::key_to_native, BoolExt};
use crate::{ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
}; };
use anyhow::anyhow; use anyhow::anyhow;
@ -153,7 +153,7 @@ pub struct MacPlatformState {
resign_active: Option<Box<dyn FnMut()>>, resign_active: Option<Box<dyn FnMut()>>,
reopen: Option<Box<dyn FnMut()>>, reopen: Option<Box<dyn FnMut()>>,
quit: 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)>>, menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>, validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
will_open_menu: Option<Box<dyn FnMut()>>, will_open_menu: Option<Box<dyn FnMut()>>,
@ -637,7 +637,7 @@ impl Platform for MacPlatform {
self.0.lock().reopen = Some(callback); 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); 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) { extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
unsafe { 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 platform = get_mac_platform(this);
let mut lock = platform.0.lock(); let mut lock = platform.0.lock();
if let Some(mut callback) = lock.event.take() { if let Some(mut callback) = lock.event.take() {

View file

@ -85,11 +85,24 @@ impl PlatformTextSystem for MacTextSystem {
}; };
let mut names = BTreeSet::new(); let mut names = BTreeSet::new();
for descriptor in descriptors.into_iter() { for descriptor in descriptors.into_iter() {
names.insert(descriptor.display_name()); names.insert(descriptor.font_name());
names.insert(descriptor.family_name());
names.insert(descriptor.style_name());
}
if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() {
names.extend(fonts_in_memory);
} }
names.into_iter().collect() names.into_iter().collect()
} }
fn all_font_families(&self) -> Vec<String> {
self.0
.read()
.system_source
.all_families()
.expect("core text should never return an error")
}
fn font_id(&self, font: &Font) -> Result<FontId> { fn font_id(&self, font: &Font) -> Result<FontId> {
let lock = self.0.upgradable_read(); let lock = self.0.upgradable_read();
if let Some(font_id) = lock.font_selections.get(font) { if let Some(font_id) = lock.font_selections.get(font) {

View file

@ -1,9 +1,9 @@
use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange}; use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
use crate::{ use crate::{
display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths, display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke, FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers,
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
}; };
use block::ConcreteBlock; use block::ConcreteBlock;
@ -319,7 +319,7 @@ struct MacWindowState {
renderer: MetalRenderer, renderer: MetalRenderer,
kind: WindowKind, kind: WindowKind,
request_frame_callback: Option<Box<dyn FnMut()>>, 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)>>, activate_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>, resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
fullscreen_callback: Option<Box<dyn FnMut(bool)>>, fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
@ -333,7 +333,7 @@ struct MacWindowState {
synthetic_drag_counter: usize, synthetic_drag_counter: usize,
last_fresh_keydown: Option<Keystroke>, last_fresh_keydown: Option<Keystroke>,
traffic_light_position: Option<Point<Pixels>>, 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 // State tracking what the IME did after the last request
ime_state: ImeState, ime_state: ImeState,
// Retains the last IME Text // Retains the last IME Text
@ -928,7 +928,7 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().request_frame_callback = Some(callback); 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); 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 mut lock = window_state.as_ref().lock();
let window_height = lock.content_size().height; 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. // 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 // 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 // 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() .flatten()
.is_some(); .is_some();
if !is_composing { if !is_composing {
handled = callback(InputEvent::KeyDown(event)); handled = callback(PlatformInput::KeyDown(event));
} }
if !handled { 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 is_active = unsafe { lock.native_window.isKeyWindow() == YES };
let window_height = lock.content_size().height; 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 { if let Some(mut event) = event {
match &mut event { match &mut event {
InputEvent::MouseDown( PlatformInput::MouseDown(
event @ MouseDownEvent { event @ MouseDownEvent {
button: MouseButton::Left, button: MouseButton::Left,
modifiers: Modifiers { control: true, .. }, 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 // 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 // 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 // user is still holding ctrl when releasing the left mouse button
InputEvent::MouseUp( PlatformInput::MouseUp(
event @ MouseUpEvent { event @ MouseUpEvent {
button: MouseButton::Left, button: MouseButton::Left,
modifiers: Modifiers { control: true, .. }, modifiers: Modifiers { control: true, .. },
@ -1194,7 +1194,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
}; };
match &event { match &event {
InputEvent::MouseMove( PlatformInput::MouseMove(
event @ MouseMoveEvent { event @ MouseMoveEvent {
pressed_button: Some(_), 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; lock.synthetic_drag_counter += 1;
} }
InputEvent::ModifiersChanged(ModifiersChangedEvent { modifiers }) => { PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
// Only raise modifiers changed event when they have actually changed // 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, modifiers: prev_modifiers,
})) = &lock.previous_modifiers_changed_event })) = &lock.previous_modifiers_changed_event
{ {
@ -1258,7 +1258,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
key: ".".into(), key: ".".into(),
ime_key: None, ime_key: None,
}; };
let event = InputEvent::KeyDown(KeyDownEvent { let event = PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(), keystroke: keystroke.clone(),
is_held: false, 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, { if send_new_event(&window_state, {
let position = drag_event_position(&window_state, dragging_info); let position = drag_event_position(&window_state, dragging_info);
let paths = external_paths_from_event(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; window_state.lock().external_files_dragged = true;
NSDragOperationCopy 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); let position = drag_event_position(&window_state, dragging_info);
if send_new_event( if send_new_event(
&window_state, &window_state,
InputEvent::FileDrop(FileDropEvent::Pending { position }), PlatformInput::FileDrop(FileDropEvent::Pending { position }),
) { ) {
NSDragOperationCopy NSDragOperationCopy
} else { } 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) { extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) }; 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; 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); let position = drag_event_position(&window_state, dragging_info);
if send_new_event( if send_new_event(
&window_state, &window_state,
InputEvent::FileDrop(FileDropEvent::Submit { position }), PlatformInput::FileDrop(FileDropEvent::Submit { position }),
) { ) {
YES YES
} else { } 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) { extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) }; 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( async fn synthetic_drag(
@ -1727,7 +1733,7 @@ async fn synthetic_drag(
if lock.synthetic_drag_counter == drag_id { if lock.synthetic_drag_counter == drag_id {
if let Some(mut callback) = lock.event_callback.take() { if let Some(mut callback) = lock.event_callback.take() {
drop(lock); drop(lock);
callback(InputEvent::MouseMove(event.clone())); callback(PlatformInput::MouseMove(event.clone()));
window_state.lock().event_callback = Some(callback); window_state.lock().event_callback = Some(callback);
} }
} else { } 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(); let window_state = window_state_lock.lock().event_callback.take();
if let Some(mut callback) = window_state { if let Some(mut callback) = window_state {
callback(e); callback(e);

View file

@ -31,10 +31,6 @@ impl PlatformDisplay for TestDisplay {
Ok(self.uuid) Ok(self.uuid)
} }
fn as_any(&self) -> &dyn std::any::Any {
unimplemented!()
}
fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> { fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {
self.bounds self.bounds
} }

View file

@ -239,7 +239,7 @@ impl Platform for TestPlatform {
unimplemented!() unimplemented!()
} }
fn on_event(&self, _callback: Box<dyn FnMut(crate::InputEvent) -> bool>) { fn on_event(&self, _callback: Box<dyn FnMut(crate::PlatformInput) -> bool>) {
unimplemented!() unimplemented!()
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, InputEvent, KeyDownEvent, px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke,
Keystroke, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions, Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
}; };
use collections::HashMap; use collections::HashMap;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -19,7 +19,7 @@ pub struct TestWindowState {
platform: Weak<TestPlatform>, platform: Weak<TestPlatform>,
sprite_atlas: Arc<dyn PlatformAtlas>, sprite_atlas: Arc<dyn PlatformAtlas>,
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>, 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)>>, active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>, resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>, moved_callback: Option<Box<dyn FnMut()>>,
@ -85,7 +85,7 @@ impl TestWindow {
self.0.lock().active_status_change_callback = Some(callback); 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 mut lock = self.0.lock();
let Some(mut callback) = lock.input_callback.take() else { let Some(mut callback) = lock.input_callback.take() else {
return false; return false;
@ -97,7 +97,7 @@ impl TestWindow {
} }
pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) { 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(), keystroke: keystroke.clone(),
is_held, is_held,
})) { })) {
@ -220,7 +220,7 @@ impl PlatformWindow for TestWindow {
fn on_request_frame(&self, _callback: Box<dyn FnMut()>) {} 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) self.0.lock().input_callback = Some(callback)
} }
@ -325,10 +325,4 @@ impl PlatformAtlas for TestAtlas {
Ok(state.tiles[key].clone()) Ok(state.tiles[key].clone())
} }
fn clear(&self) {
let mut state = self.0.lock();
state.tiles = HashMap::default();
state.next_id = 0;
}
} }

View file

@ -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 primitive = primitive.into();
let clipped_bounds = primitive let clipped_bounds = primitive
.bounds() .bounds()
@ -299,8 +299,8 @@ impl<'a> Iterator for BatchIterator<'a> {
let first = orders_and_kinds[0]; let first = orders_and_kinds[0];
let second = orders_and_kinds[1]; let second = orders_and_kinds[1];
let (batch_kind, max_order) = if first.0.is_some() { let (batch_kind, max_order_and_kind) = if first.0.is_some() {
(first.1, second.0.unwrap_or(u32::MAX)) (first.1, (second.0.unwrap_or(u32::MAX), second.1))
} else { } else {
return None; return None;
}; };
@ -312,7 +312,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.shadows_iter.next(); self.shadows_iter.next();
while self while self
.shadows_iter .shadows_iter
.next_if(|shadow| shadow.order < max_order) .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind)
.is_some() .is_some()
{ {
shadows_end += 1; shadows_end += 1;
@ -328,7 +328,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.quads_iter.next(); self.quads_iter.next();
while self while self
.quads_iter .quads_iter
.next_if(|quad| quad.order < max_order) .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind)
.is_some() .is_some()
{ {
quads_end += 1; quads_end += 1;
@ -342,7 +342,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.paths_iter.next(); self.paths_iter.next();
while self while self
.paths_iter .paths_iter
.next_if(|path| path.order < max_order) .next_if(|path| (path.order, batch_kind) < max_order_and_kind)
.is_some() .is_some()
{ {
paths_end += 1; paths_end += 1;
@ -356,7 +356,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.underlines_iter.next(); self.underlines_iter.next();
while self while self
.underlines_iter .underlines_iter
.next_if(|underline| underline.order < max_order) .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind)
.is_some() .is_some()
{ {
underlines_end += 1; underlines_end += 1;
@ -374,7 +374,8 @@ impl<'a> Iterator for BatchIterator<'a> {
while self while self
.monochrome_sprites_iter .monochrome_sprites_iter
.next_if(|sprite| { .next_if(|sprite| {
sprite.order < max_order && sprite.tile.texture_id == texture_id (sprite.order, batch_kind) < max_order_and_kind
&& sprite.tile.texture_id == texture_id
}) })
.is_some() .is_some()
{ {
@ -394,7 +395,8 @@ impl<'a> Iterator for BatchIterator<'a> {
while self while self
.polychrome_sprites_iter .polychrome_sprites_iter
.next_if(|sprite| { .next_if(|sprite| {
sprite.order < max_order && sprite.tile.texture_id == texture_id (sprite.order, batch_kind) < max_order_and_kind
&& sprite.tile.texture_id == texture_id
}) })
.is_some() .is_some()
{ {
@ -412,7 +414,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.surfaces_iter.next(); self.surfaces_iter.next();
while self while self
.surfaces_iter .surfaces_iter
.next_if(|surface| surface.order < max_order) .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind)
.is_some() .is_some()
{ {
surfaces_end += 1; surfaces_end += 1;
@ -438,7 +440,7 @@ pub enum PrimitiveKind {
Surface, Surface,
} }
pub enum Primitive { pub(crate) enum Primitive {
Shadow(Shadow), Shadow(Shadow),
Quad(Quad), Quad(Quad),
Path(Path<ScaledPixels>), Path(Path<ScaledPixels>),
@ -587,7 +589,7 @@ impl From<Shadow> for Primitive {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)] #[repr(C)]
pub struct MonochromeSprite { pub(crate) struct MonochromeSprite {
pub view_id: ViewId, pub view_id: ViewId,
pub layer_id: LayerId, pub layer_id: LayerId,
pub order: DrawOrder, pub order: DrawOrder,
@ -620,7 +622,7 @@ impl From<MonochromeSprite> for Primitive {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)] #[repr(C)]
pub struct PolychromeSprite { pub(crate) struct PolychromeSprite {
pub view_id: ViewId, pub view_id: ViewId,
pub layer_id: LayerId, pub layer_id: LayerId,
pub order: DrawOrder, pub order: DrawOrder,

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