Compare commits

..

3 Commits

Author SHA1 Message Date
Max Brunsfeld
a657deaddd zed 0.77.2 2023-03-14 11:12:49 -07:00
Max Brunsfeld
c6a16abcba Merge branch 'main' into v0.77.x 2023-03-14 11:11:26 -07:00
Max Brunsfeld
6c1c079b44 v0.77.x preview 2023-03-13 12:00:36 -07:00
482 changed files with 29779 additions and 56169 deletions

View File

@@ -1,13 +1,9 @@
[[PR Description]]
## Description of feature or change
Release Notes:
## Link to related issues from zed or community
Use `N/A` in this section if this item should be skipped in the release notes.
## Before Merging
Add release note lines here:
* (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
* ...
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
These will be removed by the person making the release.
- [ ] Does this have tests or have existing tests been updated to cover this change?
- [ ] Have you added the necessary settings to configure this feature?
- [ ] Has documentation been created or updated (including above changes to settings)?

View File

@@ -42,7 +42,6 @@ jobs:
runs-on:
- self-hosted
- test
needs: rustfmt
env:
RUSTFLAGS: -D warnings
steps:
@@ -55,7 +54,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v2
with:
node-version: '18'
node-version: '16'
- name: Checkout repo
uses: actions/checkout@v2
@@ -63,9 +62,6 @@ jobs:
clean: false
submodules: 'recursive'
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70
- name: Run check
run: cargo check --workspace
@@ -86,7 +82,7 @@ jobs:
runs-on:
- self-hosted
- bundle
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
needs: tests
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
@@ -106,7 +102,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v2
with:
node-version: '18'
node-version: '16'
- name: Checkout repo
uses: actions/checkout@v2
@@ -114,9 +110,6 @@ jobs:
clean: false
submodules: 'recursive'
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
@@ -148,11 +141,11 @@ jobs:
- name: Create app bundle
run: script/bundle
- name: Upload app bundle to workflow run if main branch or specifi label
- name: Upload app bundle to workflow run if main branch
uses: actions/upload-artifact@v2
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
if: ${{ github.ref == 'refs/heads/main' }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
name: Zed.dmg
path: target/release/Zed.dmg
- uses: softprops/action-gh-release@v1

View File

@@ -1,43 +0,0 @@
name: Randomized Tests
concurrency: randomized-tests
on:
push:
branches:
- randomized-tests-runner
schedule:
- cron: '0 * * * *'
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
ZED_SERVER_URL: https://zed.dev
ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }}
jobs:
tests:
name: Run randomized tests
runs-on:
- self-hosted
- randomized-tests
steps:
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
- name: Install Node
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Checkout repo
uses: actions/checkout@v2
with:
clean: false
submodules: 'recursive'
- name: Run randomized tests
run: script/randomized-test-ci

View File

@@ -14,7 +14,7 @@ jobs:
content: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
Restart your Zed or head to https://zed.dev/releases/latest to grab it.
```md
# Changelog

1979
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
[workspace]
members = [
"crates/activity_indicator",
"crates/ai",
"crates/assets",
"crates/auto_update",
"crates/breadcrumbs",
@@ -14,8 +13,6 @@ members = [
"crates/collections",
"crates/command_palette",
"crates/context_menu",
"crates/copilot",
"crates/copilot_button",
"crates/db",
"crates/diagnostics",
"crates/drag_and_drop",
@@ -36,10 +33,8 @@ members = [
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/lsp_log",
"crates/media",
"crates/menu",
"crates/node_runtime",
"crates/outline",
"crates/picker",
"crates/plugin",
@@ -56,7 +51,6 @@ members = [
"crates/snippet",
"crates/sqlez",
"crates/sqlez_macros",
"crates/staff_mode",
"crates/sum_tree",
"crates/terminal",
"crates/text",
@@ -73,32 +67,9 @@ default-members = ["crates/zed"]
resolver = "2"
[workspace.dependencies]
anyhow = { version = "1.0.57" }
async-trait = { version = "0.1" }
ctor = { version = "0.1" }
env_logger = { version = "0.9" }
futures = { version = "0.3" }
globset = { version = "0.4" }
indoc = "1"
isahc = "1.7.2"
lazy_static = { version = "1.4.0" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" }
parking_lot = { version = "0.11.1" }
postage = { version = "0.5", features = ["futures-traits"] }
rand = { version = "0.8.5" }
regex = { version = "1.5" }
schemars = { version = "0.8" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
smallvec = { version = "1.6", features = ["union"] }
smol = { version = "1.2" }
tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" }
unindent = { version = "0.1.7" }
rand = { version = "0.8" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

View File

@@ -2,7 +2,7 @@
[![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 lightning-fast, collaborative code editor that makes your dreams come true.
## Development tips
@@ -31,8 +31,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
* Set up a local `zed` database and seed it with some initial users:
Create a personal GitHub token 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.
Create a personal GitHub token to run `script/bootstrap` once successfully. Then delete that token.
```
GITHUB_TOKEN=<$token> script/bootstrap

View File

@@ -1,18 +0,0 @@
You are #zed, a language model representing the collective understanding of an open source project called Zed. When a new human visits you, they'll send you their profile. You'll respond with an introduction tailored to their situation. For example, a new user might see something like this:
Welcome to Zed! Zed is an innovative, open-source platform designed to enhance team communication and collaboration. At the heart of Zed are *contexts*, which create a dynamic digital representation of shared mental models. Contexts offer personalized starting points and the flexibility to edit and explore, enabling teams to align knowledge, streamline communication, and improve overall performance.
As the #zed model, I'm happy to answer any questions. In fact, I will improve as a result of you doing so!
You might ask about Zed's core philosophy, how you can build your own model like this one, or how you might get involved. Zed's open source!
> [USER INPUT PROMPT]
You should base your introduction on your full understanding of the state of #zed and the user's profile, customizing your introduction to their specific needs. Don't welcome them to Zed if they've been using Zed for 2 days. If they're returning after a while, welcome them back.
User input begins on a line starting with >.
Your output begins on a line starting with <.
Don't apologize ever.
Never say "I apologize".
Use simple language and don't flatter the users. Spend your tokens on valuable information.

View File

@@ -1,12 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.2926 3.48996C3.79162 3.79616 3.44871 4.26316 3.44871 4.93872C3.44871 5.75753 3.65302 6.19648 3.88658 6.43349C4.11948 6.66983 4.47018 6.79529 4.95638 6.79529C5.64158 6.79529 6.23176 6.65786 6.64548 6.37099C7.03216 6.10286 7.32149 5.66636 7.35698 4.91278C7.38386 4.34213 7.36863 3.96084 7.21748 3.68905C7.09721 3.47279 6.81682 3.2089 5.96976 3.11109C5.4731 3.05374 4.81346 3.17162 4.2926 3.48996ZM3.72539 2.5525C4.46348 2.10138 5.36842 1.93724 6.09436 2.02107C7.1336 2.14107 7.8142 2.51324 8.17039 3.15373C8.49569 3.73867 8.47238 4.43479 8.44743 4.96466C8.39736 6.02772 7.95809 6.7938 7.26541 7.27411C6.59976 7.73566 5.75982 7.89249 4.95638 7.89249C4.2936 7.89249 3.61755 7.71967 3.11095 7.20558C2.605 6.69216 2.35705 5.92853 2.35705 4.93872C2.35705 3.80566 2.96744 3.01576 3.72539 2.5525Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69546 8.97734C7.02432 8.97734 7.29091 9.24528 7.29091 9.57581V10.8725C7.29091 11.203 7.02432 11.471 6.69546 11.471C6.3666 11.471 6.1 11.203 6.1 10.8725V9.57581C6.1 9.24528 6.3666 8.97734 6.69546 8.97734Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.45301 7.32072C2.56382 6.90477 2.81104 6.35118 3.40175 6.17048L3.74851 7.31556C3.74509 7.31822 3.73425 7.32798 3.71842 7.35038C3.68409 7.39897 3.64151 7.48723 3.6034 7.6303C3.52629 7.91973 3.49839 8.31081 3.4984 8.73318V10.8761C3.5122 10.9688 3.52011 11.0083 3.53501 11.0478C3.5474 11.0807 3.57295 11.1339 3.6523 11.2153C3.83266 11.4004 4.24428 11.6866 5.21016 12.1174C5.99398 12.467 6.35125 12.6243 6.68361 12.7078C6.99799 12.7869 7.30564 12.8031 7.99999 12.8031V14C7.31311 14 6.86876 13.9882 6.3946 13.869C5.95125 13.7575 5.49691 13.5549 4.78914 13.2391C4.76868 13.23 4.74801 13.2208 4.72712 13.2115C3.73729 12.77 3.14865 12.4092 2.80139 12.0527C2.61692 11.8634 2.49682 11.6721 2.42136 11.4719C2.35507 11.2961 2.33141 11.1302 2.31663 11.0266C2.31561 11.0194 2.31463 11.0126 2.31369 11.0061L2.30749 10.9632V8.73321C2.30748 8.28334 2.33457 7.76532 2.45301 7.32072Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.83439 7.54965C2.14812 7.21281 2.52306 6.88008 2.81315 6.70729L3.42031 7.737C3.27036 7.82631 2.98468 8.06607 2.7038 8.36764C2.43565 8.65553 2.2592 8.90729 2.19783 9.04784C2.18425 9.16608 2.18871 9.38528 2.22654 9.6452C2.26959 9.94104 2.33715 10.1608 2.37974 10.2387L2.42237 10.3167L2.44057 10.4038C2.46806 10.5353 2.60072 10.7284 2.96139 10.9852C3.24332 11.1859 3.57562 11.3661 3.93098 11.5588C4.00968 11.6015 4.0895 11.6448 4.17017 11.689C4.56251 11.8768 5.17152 12.1512 5.7408 12.3785C6.02948 12.4938 6.30016 12.5938 6.5233 12.664C6.63493 12.6991 6.72826 12.7247 6.802 12.7411C6.87402 12.7571 6.90715 12.7597 6.91166 12.76L6.91213 13.957C6.68654 13.957 6.40667 13.8815 6.16744 13.8062C5.90465 13.7235 5.60351 13.6116 5.30115 13.4909C4.69547 13.2491 4.05361 12.9594 3.64268 12.7623L3.62786 12.7552L3.61345 12.7473C3.54294 12.7085 3.46868 12.6683 3.39187 12.6268C3.03384 12.433 2.62042 12.2092 2.27302 11.9619C1.88328 11.6844 1.44476 11.2894 1.29544 10.735C1.17095 10.4701 1.09192 10.1191 1.04817 9.81844C0.999401 9.48332 0.975841 9.08189 1.03513 8.7778L1.04265 8.73927L1.05511 8.70206C1.18745 8.30673 1.53258 7.87368 1.83439 7.54965Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.59243 10.4347V8.41995H2.78334V10.4347H1.59243Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7074 3.48996C12.2084 3.79616 12.5513 4.26316 12.5513 4.93872C12.5513 5.75753 12.347 6.19648 12.1134 6.43349C11.8805 6.66983 11.5298 6.79529 11.0436 6.79529C10.3584 6.79529 9.76824 6.65786 9.35452 6.37099C8.96784 6.10286 8.67851 5.66636 8.64302 4.91278C8.61614 4.34213 8.63137 3.96084 8.78252 3.68905C8.90279 3.47279 9.18318 3.2089 10.0302 3.11109C10.5269 3.05374 11.1865 3.17162 11.7074 3.48996ZM12.2746 2.5525C11.5365 2.10138 10.6316 1.93724 9.90564 2.02107C8.8664 2.14107 8.1858 2.51324 7.82961 3.15373C7.50431 3.73867 7.52762 4.43479 7.55258 4.96466C7.60264 6.02772 8.04191 6.7938 8.73459 7.27411C9.40024 7.73566 10.2402 7.89249 11.0436 7.89249C11.7064 7.89249 12.3824 7.71967 12.889 7.20558C13.395 6.69216 13.643 5.92853 13.643 4.93872C13.643 3.80566 13.0326 3.01576 12.2746 2.5525Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.30454 8.97734C8.97568 8.97734 8.70909 9.24528 8.70909 9.57581V10.8725C8.70909 11.203 8.97568 11.471 9.30454 11.471C9.6334 11.471 9.9 11.203 9.9 10.8725V9.57581C9.9 9.24528 9.6334 8.97734 9.30454 8.97734Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.547 7.32072C13.4362 6.90477 13.189 6.35118 12.5982 6.17048L12.2515 7.31556C12.2549 7.31822 12.2658 7.32798 12.2816 7.35038C12.3159 7.39897 12.3585 7.48723 12.3966 7.6303C12.4737 7.91973 12.5016 8.31081 12.5016 8.73318V10.8761C12.4878 10.9688 12.4799 11.0083 12.465 11.0478C12.4526 11.0807 12.427 11.1339 12.3477 11.2153C12.1673 11.4004 11.7557 11.6866 10.7898 12.1174C10.006 12.467 9.64875 12.6243 9.31639 12.7078C9.00201 12.7869 8.69433 12.8031 7.99999 12.8031V14C8.68686 14 9.13124 13.9882 9.6054 13.869C10.0488 13.7575 10.5031 13.5549 11.2109 13.2391C11.2313 13.23 11.252 13.2208 11.2729 13.2115C12.2627 12.77 12.8513 12.4092 13.1986 12.0527C13.3831 11.8634 13.5032 11.6721 13.5786 11.4719C13.6449 11.2961 13.6686 11.1302 13.6834 11.0266C13.6844 11.0194 13.6854 11.0126 13.6863 11.0061L13.6925 10.9632V8.73321C13.6925 8.28334 13.6654 7.76532 13.547 7.32072Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1656 7.54965C13.8519 7.21281 13.4769 6.88008 13.1868 6.70729L12.5797 7.737C12.7296 7.82631 13.0153 8.06607 13.2962 8.36764C13.5643 8.65553 13.7408 8.90729 13.8022 9.04784C13.8158 9.16608 13.8113 9.38528 13.7735 9.6452C13.7304 9.94104 13.6628 10.1608 13.6203 10.2387L13.5776 10.3167L13.5594 10.4038C13.5319 10.5353 13.3993 10.7284 13.0386 10.9852C12.7567 11.1859 12.4244 11.3661 12.069 11.5588C11.9903 11.6015 11.9105 11.6448 11.8298 11.689C11.4375 11.8768 10.8285 12.1512 10.2592 12.3785C9.97052 12.4938 9.69984 12.5938 9.4767 12.664C9.36507 12.6991 9.27174 12.7247 9.198 12.7411C9.12598 12.7571 9.09285 12.7597 9.08834 12.76L9.08787 13.957C9.31345 13.957 9.59333 13.8815 9.83256 13.8062C10.0953 13.7235 10.3965 13.6116 10.6989 13.4909C11.3045 13.2491 11.9464 12.9594 12.3573 12.7623L12.3721 12.7552L12.3865 12.7473C12.4571 12.7085 12.5313 12.6683 12.6081 12.6268C12.9662 12.433 13.3796 12.2092 13.727 11.9619C14.1167 11.6844 14.5552 11.2894 14.7046 10.735C14.829 10.4701 14.9081 10.1191 14.9518 9.81844C15.0006 9.48332 15.0242 9.08189 14.9649 8.7778L14.9574 8.73927L14.9449 8.70206C14.8126 8.30673 14.4674 7.87368 14.1656 7.54965Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4076 10.4347V8.41995H13.2167V10.4347H14.4076Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,9 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path d="M2.38084 5.44737C2.42754 5.92437 2.54311 6.33779 2.72903 6.68417C2.70043 6.72678 2.67414 6.77043 2.64995 6.81463C2.39071 6.9994 2.09192 7.27314 1.83439 7.54965C1.53258 7.87368 1.18745 8.30673 1.05511 8.70206L1.04265 8.73927L1.03513 8.7778C0.975841 9.08189 0.999401 9.48332 1.04817 9.81844C1.09192 10.1191 1.17095 10.4701 1.29544 10.735C1.44476 11.2894 1.88328 11.6844 2.27302 11.9619C2.6204 12.2092 3.03378 12.4329 3.39179 12.6267C3.4686 12.6683 3.54294 12.7085 3.61345 12.7473L3.62786 12.7552L3.64268 12.7623C4.05361 12.9594 4.69547 13.2491 5.30115 13.4909C5.60351 13.6116 5.90465 13.7235 6.16744 13.8062C6.39236 13.877 6.6532 13.948 6.87108 13.9562C7.19351 13.9948 7.54309 14 7.99999 14C8.45688 14 8.80648 13.9948 9.12892 13.9562C9.34679 13.948 9.60764 13.877 9.83256 13.8062C10.0953 13.7235 10.3965 13.6116 10.6989 13.4909C11.0041 13.369 11.3186 13.235 11.6081 13.1067L10.5467 12.2257C9.92791 12.5006 9.61228 12.6334 9.31639 12.7078C9.00201 12.7869 8.69433 12.8031 7.99999 12.8031C7.30564 12.8031 6.99799 12.7869 6.68361 12.7078C6.35125 12.6243 5.99398 12.467 5.21016 12.1174C4.24428 11.6866 3.83266 11.4004 3.6523 11.2153C3.57295 11.1339 3.5474 11.0807 3.53501 11.0478C3.52011 11.0083 3.5122 10.9688 3.4984 10.8761V8.73318C3.49839 8.31081 3.52629 7.91973 3.6034 7.6303C3.60757 7.61463 3.6118 7.59961 3.61607 7.58523C4.02831 7.80894 4.49555 7.89249 4.95638 7.89249C5.07488 7.89249 5.19417 7.88908 5.31358 7.88178L2.38084 5.44737Z" fill="white"/>
<path d="M6.63684 8.9802C6.3355 9.00979 6.1 9.26516 6.1 9.57581V10.8725C6.1 11.203 6.3666 11.471 6.69546 11.471C7.02432 11.471 7.29091 11.203 7.29091 10.8725V9.57581C7.29091 9.55736 7.29008 9.53911 7.28846 9.52109L6.63684 8.9802Z" fill="white"/>
<path d="M8.70909 10.7003V10.8725C8.70909 11.203 8.97568 11.471 9.30454 11.471C9.39795 11.471 9.48633 11.4493 9.56501 11.4108L8.70909 10.7003Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.09436 2.02107C5.38898 1.93962 4.51459 2.09228 3.78849 2.51486L4.71538 3.28426C5.1409 3.1227 5.6004 3.06844 5.96976 3.11109C6.81682 3.2089 7.09721 3.4728 7.21748 3.68905C7.36863 3.96084 7.38386 4.34213 7.35698 4.91278C7.34816 5.1 7.32368 5.26765 7.28592 5.41801L10.1885 7.82737C10.4719 7.87295 10.76 7.89249 11.0436 7.89249C11.5044 7.89249 11.9717 7.80894 12.3839 7.58523C12.3882 7.59961 12.3924 7.61463 12.3966 7.6303C12.4737 7.91973 12.5016 8.31081 12.5016 8.73318V9.74745L14.4065 11.3287C14.5379 11.1547 14.6446 10.9577 14.7046 10.735C14.829 10.4701 14.9081 10.1191 14.9518 9.81844C15.0006 9.48332 15.0242 9.08189 14.9649 8.7778L14.9574 8.73927L14.9449 8.70206C14.8126 8.30673 14.4674 7.87368 14.1656 7.54965C13.9081 7.27314 13.6093 6.9994 13.35 6.81463C13.3259 6.77043 13.2996 6.72678 13.271 6.68417C13.5201 6.21998 13.643 5.63537 13.643 4.93872C13.643 3.80567 13.0326 3.01576 12.2746 2.5525C11.5365 2.10138 10.6316 1.93724 9.90564 2.02107C9.01315 2.12413 8.38516 2.41316 8 2.89841C7.61484 2.41316 6.98685 2.12413 6.09436 2.02107ZM11.7074 3.48996C12.2084 3.79616 12.5513 4.26316 12.5513 4.93872C12.5513 5.75753 12.347 6.19648 12.1134 6.43349C11.8805 6.66983 11.5298 6.79529 11.0436 6.79529C10.3584 6.79529 9.76824 6.65786 9.35452 6.37099C8.96784 6.10286 8.67851 5.66636 8.64302 4.91278C8.61614 4.34213 8.63137 3.96084 8.78252 3.68905C8.90279 3.4728 9.18318 3.2089 10.0302 3.11109C10.5269 3.05374 11.1865 3.17162 11.7074 3.48996Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1225 13.809C14.0341 13.9146 13.877 13.9289 13.7711 13.8409L1.1931 3.40021C1.08658 3.31178 1.0722 3.15362 1.16103 3.04743L1.87751 2.19101C1.96587 2.0854 2.12299 2.07112 2.22894 2.15906L14.8069 12.5998C14.9134 12.6882 14.9278 12.8464 14.839 12.9526L14.1225 13.809Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,7 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.09436 2.02107C5.36842 1.93724 4.46348 2.10138 3.72539 2.5525C2.96744 3.01576 2.35705 3.80567 2.35705 4.93872C2.35705 5.63537 2.47988 6.21998 2.72903 6.68417C2.70043 6.72678 2.67414 6.77043 2.64995 6.81463C2.39071 6.9994 2.09192 7.27314 1.83439 7.54965C1.53258 7.87368 1.18745 8.30673 1.05511 8.70206L1.04265 8.73927L1.03513 8.7778C0.975841 9.08189 0.999401 9.48332 1.04817 9.81844C1.09192 10.1191 1.17095 10.4701 1.29544 10.735C1.44476 11.2894 1.88328 11.6844 2.27302 11.9619C2.6204 12.2092 3.03378 12.4329 3.39179 12.6267C3.4686 12.6683 3.54294 12.7085 3.61345 12.7473L3.62786 12.7552L3.64268 12.7623C4.05361 12.9594 4.69547 13.2491 5.30115 13.4909C5.60351 13.6116 5.90465 13.7235 6.16744 13.8062C6.39236 13.877 6.6532 13.948 6.87108 13.9562C7.19351 13.9948 7.54309 14 7.99999 14C8.01293 14 8.02579 14 8.03857 14C7.97904 13.903 7.99191 13.7743 8.07655 13.6911L9.0197 12.7639C8.77857 12.7952 8.48273 12.8031 7.99999 12.8031C7.30564 12.8031 6.99799 12.7869 6.68361 12.7078C6.35125 12.6243 5.99398 12.467 5.21016 12.1174C4.24428 11.6866 3.83266 11.4004 3.6523 11.2153C3.57295 11.1339 3.5474 11.0807 3.53501 11.0478C3.52011 11.0083 3.5122 10.9688 3.4984 10.8761V8.73318C3.49839 8.31081 3.52629 7.91973 3.6034 7.6303C3.60757 7.61463 3.6118 7.59961 3.61607 7.58523C4.02831 7.80894 4.49555 7.89249 4.95638 7.89249C5.75982 7.89249 6.59976 7.73566 7.26541 7.27411C7.55937 7.07027 7.8077 6.81497 8 6.50734C8.1923 6.81497 8.44063 7.07027 8.73459 7.27411C9.40024 7.73566 10.2402 7.89249 11.0436 7.89249C11.5044 7.89249 11.9717 7.80894 12.3839 7.58523C12.3882 7.59961 12.3924 7.61463 12.3966 7.6303C12.4737 7.91973 12.5016 8.31081 12.5016 8.73318V9.34082L14.1266 7.7433C14.169 7.70159 14.2225 7.67811 14.2775 7.67276C14.2398 7.63028 14.2024 7.58915 14.1656 7.54965C13.9081 7.27314 13.6093 6.9994 13.35 6.81463C13.3259 6.77043 13.2996 6.72678 13.271 6.68417C13.5201 6.21998 13.643 5.63537 13.643 4.93872C13.643 3.80567 13.0326 3.01576 12.2746 2.5525C11.5365 2.10138 10.6316 1.93724 9.90564 2.02107C9.01315 2.12413 8.38516 2.41316 8 2.89841C7.61484 2.41316 6.98685 2.12413 6.09436 2.02107ZM3.44871 4.93872C3.44871 4.26316 3.79162 3.79616 4.2926 3.48996C4.81346 3.17162 5.4731 3.05374 5.96976 3.11109C6.81682 3.2089 7.09721 3.4728 7.21748 3.68905C7.36863 3.96084 7.38386 4.34213 7.35698 4.91278C7.32149 5.66636 7.03216 6.10286 6.64548 6.37099C6.23176 6.65786 5.64158 6.79529 4.95638 6.79529C4.47018 6.79529 4.11948 6.66983 3.88658 6.43349C3.65302 6.19648 3.44871 5.75753 3.44871 4.93872ZM12.5513 4.93872C12.5513 4.26316 12.2084 3.79616 11.7074 3.48996C11.1865 3.17162 10.5269 3.05374 10.0302 3.11109C9.18318 3.2089 8.90279 3.4728 8.78252 3.68905C8.63137 3.96084 8.61614 4.34213 8.64302 4.91278C8.67851 5.66636 8.96784 6.10286 9.35452 6.37099C9.76824 6.65786 10.3584 6.79529 11.0436 6.79529C11.5298 6.79529 11.8805 6.66983 12.1134 6.43349C12.347 6.19648 12.5513 5.75753 12.5513 4.93872Z" fill="white"/>
<path d="M7.29091 9.57581C7.29091 9.24528 7.02432 8.97734 6.69546 8.97734C6.3666 8.97734 6.1 9.24528 6.1 9.57581V10.8725C6.1 11.203 6.3666 11.471 6.69546 11.471C7.02432 11.471 7.29091 11.203 7.29091 10.8725V9.57581Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.6668 14.9102C13.7644 15.0078 13.9227 15.0078 14.0203 14.9102L14.908 14.0224C15.0056 13.9248 15.0056 13.7665 14.908 13.6688L13.2229 11.9836L14.908 10.2983C15.0057 10.2007 15.0057 10.0424 14.908 9.94474L14.0203 9.05695C13.9227 8.95931 13.7644 8.95931 13.6668 9.05695L11.9817 10.7422L10.2966 9.05693C10.199 8.95929 10.0407 8.95929 9.94306 9.05693L9.05535 9.94473C8.95773 10.0424 8.95773 10.2007 9.05535 10.2983L10.7405 11.9836L9.05537 13.6688C8.95775 13.7665 8.95775 13.9248 9.05537 14.0224L9.94308 14.9102C10.0407 15.0079 10.199 15.0079 10.2966 14.9102L11.9817 13.225L13.6668 14.9102Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M3.44872 4.93872C3.44872 4.26316 3.79163 3.79616 4.29261 3.48996C4.81346 3.17162 5.47311 3.05374 5.96976 3.11109C6.81683 3.2089 7.09722 3.4728 7.21749 3.68905C7.36864 3.96084 7.38387 4.34213 7.35699 4.91278C7.3215 5.66636 7.03217 6.10286 6.64549 6.37099C6.23177 6.65786 5.64159 6.79529 4.95639 6.79529C4.47019 6.79529 4.11949 6.66983 3.88658 6.43349C3.65303 6.19648 3.44872 5.75753 3.44872 4.93872ZM6.09437 2.02107C5.36843 1.93724 4.46349 2.10138 3.7254 2.5525C2.96745 3.01576 2.35706 3.80567 2.35706 4.93872C2.35706 5.63537 2.47988 6.21998 2.72904 6.68417C2.70044 6.72678 2.67415 6.77043 2.64996 6.81463C2.39072 6.9994 2.09193 7.27314 1.83439 7.54965C1.53259 7.87368 1.18745 8.30673 1.05511 8.70206L1.04266 8.73927L1.03514 8.7778C0.975849 9.08189 0.999409 9.48332 1.04818 9.81844C1.09193 10.1191 1.17096 10.4701 1.29545 10.735C1.44476 11.2894 1.88329 11.6844 2.27303 11.9619C2.6204 12.2092 3.03379 12.4329 3.3918 12.6267L3.39183 12.6267L3.39185 12.6267L3.39188 12.6268C3.46869 12.6683 3.54295 12.7085 3.61346 12.7473L3.62787 12.7552L3.64269 12.7623C4.05362 12.9594 4.69548 13.2491 5.30115 13.4909C5.60352 13.6116 5.90466 13.7235 6.16745 13.8062C6.39237 13.877 6.65321 13.948 6.87108 13.9562C7.19351 13.9948 7.5431 14 8 14C8.45052 14 8.79672 13.9949 9.11543 13.9578C9.04001 13.6509 9.00001 13.3301 9.00001 13C9.00001 12.9213 9.00229 12.8431 9.00677 12.7656C8.76798 12.7955 8.47414 12.8031 8 12.8031C7.30565 12.8031 6.998 12.7869 6.68362 12.7078C6.35125 12.6243 5.99399 12.467 5.21017 12.1174C4.24429 11.6866 3.83267 11.4004 3.6523 11.2153C3.57296 11.1339 3.54741 11.0807 3.53502 11.0478C3.52011 11.0083 3.51221 10.9688 3.4984 10.8761V8.73318C3.49839 8.31081 3.5263 7.91973 3.6034 7.6303C3.60758 7.61463 3.61181 7.59961 3.61608 7.58523C4.02832 7.80894 4.49556 7.89249 4.95639 7.89249C5.75982 7.89249 6.59977 7.73566 7.26541 7.27411C7.55938 7.07027 7.8077 6.81497 8.00001 6.50734C8.19231 6.81497 8.44064 7.07027 8.7346 7.27411C9.40025 7.73566 10.2402 7.89249 11.0436 7.89249C11.5045 7.89249 11.9717 7.80894 12.3839 7.58523C12.3882 7.59961 12.3924 7.61463 12.3966 7.6303C12.4737 7.91973 12.5016 8.31081 12.5016 8.73318V9.03072C12.6649 9.01043 12.8312 8.99997 13 8.99997C13.7226 8.99997 14.4004 9.19157 14.9855 9.52673C15.0073 9.26739 15.0077 8.99725 14.9649 8.7778L14.9574 8.73927L14.9449 8.70206C14.8126 8.30673 14.4674 7.87368 14.1656 7.54965C13.9081 7.27314 13.6093 6.9994 13.3501 6.81463C13.3259 6.77043 13.2996 6.72678 13.271 6.68417C13.5201 6.21998 13.643 5.63537 13.643 4.93872C13.643 3.80567 13.0326 3.01576 12.2746 2.5525C11.5365 2.10138 10.6316 1.93724 9.90565 2.02107C9.01315 2.12413 8.38517 2.41316 8.00001 2.89841C7.61485 2.41316 6.98686 2.12413 6.09437 2.02107ZM9.9 10.4719V9.57581C9.9 9.24528 9.63341 8.97734 9.30455 8.97734C8.97569 8.97734 8.7091 9.24528 8.7091 9.57581V10.8725C8.7091 11.2024 8.97466 11.4699 9.30265 11.471C9.45294 11.1079 9.65515 10.7718 9.9 10.4719ZM7.29092 9.57581C7.29092 9.24528 7.02433 8.97734 6.69547 8.97734C6.36661 8.97734 6.10001 9.24528 6.10001 9.57581V10.8725C6.10001 11.203 6.36661 11.471 6.69547 11.471C7.02433 11.471 7.29092 11.203 7.29092 10.8725V9.57581ZM12.5513 4.93872C12.5513 4.26316 12.2084 3.79616 11.7074 3.48996C11.1866 3.17162 10.5269 3.05374 10.0303 3.11109C9.18318 3.2089 8.90279 3.4728 8.78253 3.68905C8.63138 3.96084 8.61615 4.34213 8.64303 4.91278C8.67852 5.66636 8.96785 6.10286 9.35453 6.37099C9.76825 6.65786 10.3584 6.79529 11.0436 6.79529C11.5298 6.79529 11.8805 6.66983 12.1134 6.43349C12.347 6.19648 12.5513 5.75753 12.5513 4.93872Z" fill="white"/>
<circle cx="13" cy="13" r="3" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9083 3.19699L7.99999 10.3949L0.0916311 3.1969C0.346537 2.49164 1.10447 1.98018 2 1.98018H14C14.8943 1.98018 15.653 2.49168 15.9083 3.19699ZM16 4.7153L12.1526 8.21715L16 11.688V4.7153ZM8.52024 11.5232L11.4199 8.88404L15.9081 12.933C15.6528 13.6378 14.8941 14.1501 14 14.1501H2C1.10461 14.1501 0.346779 13.6378 0.0917535 12.9331L4.58012 8.88404L7.47975 11.5232L7.99999 11.9967L8.52024 11.5232ZM3.84742 8.21715L0 4.71532V11.688L3.84742 8.21715Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 614 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1.24em" height="1em" viewBox="0 0 256 208"><path d="M205.28 31.36c14.096 14.88 20.016 35.2 22.512 63.68c6.626 0 12.805 1.47 16.976 7.152l7.792 10.56A17.548 17.548 0 0 1 256 123.2v28.688c-.008 3.704-1.843 7.315-4.832 9.504C215.885 187.222 172.35 208 128 208c-49.066 0-98.19-28.273-123.168-46.608c-2.989-2.189-4.825-5.8-4.832-9.504V123.2c0-3.776 1.2-7.424 3.424-10.464l7.792-10.544c4.173-5.657 10.38-7.152 16.992-7.152c2.496-28.48 8.4-48.8 22.512-63.68C77.331 3.165 112.567.06 127.552 0H128c14.72 0 50.4 2.88 77.28 31.36Zm-77.264 47.376c-3.04 0-6.544.176-10.272.544c-1.312 4.896-3.248 9.312-6.08 12.128c-11.2 11.2-24.704 12.928-31.936 12.928c-6.802 0-13.927-1.42-19.744-5.088c-5.502 1.808-10.786 4.415-11.136 10.912c-.586 12.28-.637 24.55-.688 36.824c-.026 6.16-.05 12.322-.144 18.488c.024 3.579 2.182 6.903 5.44 8.384C79.936 185.92 104.976 192 128.016 192c23.008 0 48.048-6.08 74.512-18.144c3.258-1.48 5.415-4.805 5.44-8.384c.317-18.418.062-36.912-.816-55.312h.016c-.342-6.534-5.648-9.098-11.168-10.912c-5.82 3.652-12.927 5.088-19.728 5.088c-7.232 0-20.72-1.728-31.936-12.928c-2.832-2.816-4.768-7.232-6.08-12.128a106.26 106.26 0 0 0-10.24-.544Zm-26.941 43.93c5.748 0 10.408 4.66 10.408 10.409v19.183c0 5.749-4.66 10.409-10.408 10.409c-5.748 0-10.408-4.66-10.408-10.409v-19.183c0-5.748 4.66-10.408 10.408-10.408Zm53.333 0c5.749 0 10.409 4.66 10.409 10.409v19.183c0 5.749-4.66 10.409-10.409 10.409c-5.748 0-10.408-4.66-10.408-10.409v-19.183c0-5.748 4.66-10.408 10.408-10.408ZM81.44 28.32c-11.2 1.12-20.64 4.8-25.44 9.92c-10.4 11.36-8.16 40.16-2.24 46.24c4.32 4.32 12.48 7.2 21.28 7.2c6.72 0 19.52-1.44 30.08-12.16c4.64-4.48 7.52-15.68 7.2-27.04c-.32-9.12-2.88-16.64-6.72-19.84c-4.16-3.68-13.6-5.28-24.16-4.32Zm68.96 4.32c-3.84 3.2-6.4 10.72-6.72 19.84c-.32 11.36 2.56 22.56 7.2 27.04c10.56 10.72 23.36 12.16 30.08 12.16c8.8 0 16.96-2.88 21.28-7.2c5.92-6.08 8.16-34.88-2.24-46.24c-4.8-5.12-14.24-8.8-25.44-9.92c-10.56-.96-20 .64-24.16 4.32ZM128 56c-2.56 0-5.6.16-8.96.48c.32 1.76.48 3.68.64 5.76c0 1.44 0 2.88-.16 4.48c3.2-.32 5.92-.32 8.48-.32c2.56 0 5.28 0 8.48.32c-.16-1.6-.16-3.04-.16-4.48c.16-2.08.32-4 .64-5.76c-3.36-.32-6.4-.48-8.96-.48Z"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,5 +0,0 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 1H7.5H8.75C8.88807 1 9 1.11193 9 1.25V4.5" stroke="#838994" stroke-linecap="round"/>
<path d="M3.64645 5.64645C3.45118 5.84171 3.45118 6.15829 3.64645 6.35355C3.84171 6.54882 4.15829 6.54882 4.35355 6.35355L3.64645 5.64645ZM8.64645 0.646447L3.64645 5.64645L4.35355 6.35355L9.35355 1.35355L8.64645 0.646447Z" fill="#838994"/>
<path d="M7.5 6.5V9C7.5 9.27614 7.27614 9.5 7 9.5H1C0.723858 9.5 0.5 9.27614 0.5 9V3C0.5 2.72386 0.723858 2.5 1 2.5H3.5" stroke="#838994" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 605 B

View File

@@ -1,3 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.75 1.875C4.75 2.71406 4.19922 3.42422 3.4375 3.66328V5.97891C3.9086 5.64609 4.4711 5.4375 5.125 5.4375H7.375C8.30782 5.4375 9.0625 4.68281 9.0625 3.75V3.66328C8.30078 3.42422 7.75 2.71406 7.75 1.875C7.75 0.839531 8.58907 0 9.625 0C10.6609 0 11.5 0.839531 11.5 1.875C11.5 2.71406 10.9492 3.42422 10.1875 3.66328V3.75C10.1875 5.30391 8.92891 6.5625 7.375 6.5625H5.125C4.19219 6.5625 3.4375 7.31719 3.4375 8.25V8.33672C4.19922 8.57578 4.75 9.28594 4.75 10.125C4.75 11.1609 3.91094 12 2.875 12C1.83953 12 1 11.1609 1 10.125C1 9.28594 1.55172 8.57578 2.3125 8.33672V3.66328C1.55172 3.42422 1 2.71406 1 1.875C1 0.839531 1.83953 0 2.875 0C3.91094 0 4.75 0.839531 4.75 1.875ZM2.875 2.625C3.28914 2.625 3.625 2.28914 3.625 1.875C3.625 1.46086 3.28914 1.125 2.875 1.125C2.46086 1.125 2.125 1.46086 2.125 1.875C2.125 2.28914 2.46086 2.625 2.875 2.625ZM9.625 1.125C9.21016 1.125 8.875 1.46086 8.875 1.875C8.875 2.28914 9.21016 2.625 9.625 2.625C10.0398 2.625 10.375 2.28914 10.375 1.875C10.375 1.46086 10.0398 1.125 9.625 1.125ZM2.875 10.875C3.28914 10.875 3.625 10.5398 3.625 10.125C3.625 9.71016 3.28914 9.375 2.875 9.375C2.46086 9.375 2.125 9.71016 2.125 10.125C2.125 10.5398 2.46086 10.875 2.875 10.875Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,14 +0,0 @@
<svg width="93" height="32" viewBox="0 0 93 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.03996 7.04962C8.00936 7.67635 7.30396 8.63219 7.30396 10.0149C7.30396 11.6908 7.72425 12.5893 8.2047 13.0744C8.68381 13.5581 9.40526 13.8149 10.4054 13.8149C11.815 13.8149 13.0291 13.5336 13.8802 12.9464C14.6756 12.3977 15.2708 11.5042 15.3438 9.96182C15.3991 8.79382 15.3678 8.01341 15.0568 7.45711C14.8094 7.01449 14.2326 6.47436 12.4901 6.27416C11.4684 6.15678 10.1114 6.39804 9.03996 7.04962ZM7.87312 5.13084C9.39147 4.2075 11.2531 3.87155 12.7464 4.04312C14.8843 4.28874 16.2844 5.05049 17.0171 6.36142C17.6863 7.55867 17.6384 8.98348 17.587 10.068C17.484 12.2439 16.5804 13.8118 15.1554 14.7949C13.7861 15.7396 12.0582 16.0606 10.4054 16.0606C9.04201 16.0606 7.65128 15.7069 6.60913 14.6547C5.56832 13.6038 5.05825 12.0408 5.05825 10.0149C5.05825 7.6958 6.3139 6.07903 7.87312 5.13084Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.983 18.2811C14.6595 18.2811 15.2079 18.8295 15.2079 19.506V22.16C15.2079 22.8365 14.6595 23.385 13.983 23.385C13.3065 23.385 12.758 22.8365 12.758 22.16V19.506C12.758 18.8295 13.3065 18.2811 13.983 18.2811Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25566 14.8903C5.48361 14.039 5.99218 12.9059 7.20734 12.5361L7.92068 14.8798C7.92068 14.8798 7.91996 14.8801 7.92068 14.8798L7.92375 14.8785C7.92411 14.8783 7.92375 14.8785 7.92375 14.8785C7.92374 14.8785 7.92322 14.8778 7.92068 14.8798C7.91364 14.8852 7.89133 14.9052 7.85878 14.951C7.78816 15.0505 7.70057 15.2311 7.62216 15.524C7.46354 16.1164 7.40614 16.9168 7.40616 17.7813V22.1675C7.43456 22.3571 7.45082 22.438 7.48148 22.5189C7.50697 22.5862 7.55954 22.695 7.72276 22.8617C8.09379 23.2406 8.94055 23.8264 10.9275 24.7081C12.5399 25.4236 13.2749 25.7456 13.9586 25.9166C14.6053 26.0784 15.2382 26.1115 16.6666 26.1115V28.5613C15.2536 28.5613 14.3395 28.5372 13.3641 28.2932C12.452 28.0651 11.5174 27.6502 10.0614 27.004C10.0193 26.9853 9.97679 26.9664 9.93382 26.9474C7.8976 26.0438 6.68669 25.3053 5.97232 24.5757C5.59284 24.1882 5.34578 23.7967 5.19055 23.387C5.05418 23.0271 5.0055 22.6875 4.97509 22.4754C4.973 22.4608 4.97099 22.4468 4.96905 22.4335L4.95629 22.3458V17.7814C4.95629 17.7814 4.95629 17.7814 4.95629 17.7814C4.95627 16.8606 5.012 15.8003 5.25566 14.8903Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.98306 15.3589C4.62844 14.6695 5.39976 13.9884 5.99652 13.6348L7.24552 15.7424C6.93706 15.9252 6.34938 16.4159 5.77155 17.0332C5.21994 17.6224 4.85695 18.1377 4.73071 18.4254C4.70277 18.6674 4.71195 19.116 4.78977 19.648C4.87834 20.2536 5.01731 20.7033 5.10492 20.8628L5.19261 21.0224L5.23006 21.2007C5.28661 21.4698 5.55952 21.8651 6.30146 22.3907C6.88143 22.8015 7.56502 23.1703 8.29605 23.5648C8.45794 23.6521 8.62215 23.7407 8.78809 23.8313C9.5952 24.2156 10.848 24.7773 12.0191 25.2425C12.613 25.4784 13.1698 25.6831 13.6288 25.8268C13.8584 25.8987 14.0505 25.9512 14.2021 25.9847C14.3503 26.0175 14.4185 26.0227 14.4277 26.0234C14.4288 26.0234 14.4281 26.0234 14.4277 26.0234L14.4287 28.4733C13.9646 28.4733 13.3889 28.3188 12.8968 28.1648C12.3562 27.9955 11.7367 27.7664 11.1147 27.5193C9.86871 27.0244 8.54832 26.4314 7.70298 26.028L7.67249 26.0135L7.64284 25.9973C7.49779 25.918 7.34502 25.8357 7.18702 25.7506C6.4505 25.354 5.60004 24.896 4.88539 24.3898C4.08363 23.8219 3.18153 23.0135 2.87437 21.8785C2.61828 21.3365 2.4557 20.618 2.3657 20.0026C2.26537 19.3167 2.2169 18.4951 2.33888 17.8727L2.35434 17.7938L2.37996 17.7176C2.6522 16.9085 3.36219 16.0221 3.98306 15.3589Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.48531 21.264V17.1402H5.93518V21.264H3.48531Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.2932 7.04962C25.3238 7.67635 26.0292 8.63219 26.0292 10.0149C26.0292 11.6908 25.609 12.5893 25.1285 13.0744C24.6494 13.5581 23.9279 13.8149 22.9278 13.8149C21.5182 13.8149 20.3041 13.5336 19.453 12.9464C18.6576 12.3977 18.0624 11.5042 17.9894 9.96182C17.9341 8.79382 17.9654 8.01341 18.2764 7.45711C18.5238 7.01449 19.1006 6.47436 20.8431 6.27416C21.8648 6.15678 23.2218 6.39804 24.2932 7.04962ZM25.4601 5.13084C23.9417 4.2075 22.0801 3.87155 20.5868 4.04312C18.4489 4.28874 17.0488 5.05049 16.3161 6.36142C15.6469 7.55867 15.6948 8.98348 15.7462 10.068C15.8492 12.2439 16.7528 13.8118 18.1778 14.7949C19.5471 15.7396 21.275 16.0606 22.9278 16.0606C24.2912 16.0606 25.6819 15.7069 26.7241 14.6547C27.7649 13.6038 28.275 12.0408 28.275 10.0149C28.275 7.6958 27.0193 6.07903 25.4601 5.13084Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.3502 18.2811C18.6737 18.2811 18.1253 18.8295 18.1253 19.506V22.16C18.1253 22.8365 18.6737 23.385 19.3502 23.385C20.0267 23.385 20.5752 22.8365 20.5752 22.16V19.506C20.5752 18.8295 20.0267 18.2811 19.3502 18.2811Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.0775 14.8903C27.8496 14.039 27.341 12.9059 26.1259 12.5361L25.4125 14.8798C25.4125 14.8798 25.4132 14.8801 25.4125 14.8798L25.4095 14.8785C25.4091 14.8783 25.4095 14.8785 25.4095 14.8785C25.4095 14.8785 25.41 14.8778 25.4125 14.8798C25.4196 14.8852 25.4419 14.9052 25.4744 14.951C25.545 15.0505 25.6326 15.2311 25.711 15.524C25.8697 16.1164 25.9271 16.9168 25.927 17.7813V22.1675C25.8986 22.3571 25.8824 22.438 25.8517 22.5189C25.8262 22.5862 25.7737 22.695 25.6104 22.8617C25.2394 23.2406 24.3927 23.8264 22.4057 24.7081C20.7933 25.4236 20.0583 25.7456 19.3746 25.9166C18.7279 26.0784 18.0949 26.1115 16.6666 26.1115V28.5613C18.0796 28.5613 18.9937 28.5372 19.9691 28.2932C20.8812 28.0651 21.8158 27.6502 23.2718 27.004C23.3139 26.9853 23.3564 26.9664 23.3994 26.9474C25.4356 26.0438 26.6465 25.3053 27.3609 24.5757C27.7404 24.1882 27.9874 23.7967 28.1427 23.387C28.279 23.0271 28.3277 22.6875 28.3581 22.4754C28.3602 22.4608 28.3622 22.4468 28.3642 22.4335L28.3769 22.3458V17.7814C28.3769 17.7814 28.3769 17.7814 28.3769 17.7814C28.3769 16.8606 28.3212 15.8003 28.0775 14.8903Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.3501 15.3589C28.7048 14.6695 27.9334 13.9884 27.3367 13.6348L26.0877 15.7424C26.3961 15.9252 26.9838 16.4159 27.5616 17.0332C28.1133 17.6224 28.4763 18.1377 28.6025 18.4254C28.6304 18.6674 28.6213 19.116 28.5434 19.648C28.4549 20.2536 28.3159 20.7033 28.2283 20.8628L28.1406 21.0224L28.1031 21.2007C28.0466 21.4698 27.7737 21.8651 27.0317 22.3907C26.4518 22.8015 25.7682 23.1703 25.0372 23.5648C24.8753 23.6521 24.711 23.7407 24.5451 23.8313C23.738 24.2156 22.4852 24.7773 21.3141 25.2425C20.7202 25.4784 20.1634 25.6831 19.7044 25.8268C19.4748 25.8987 19.2827 25.9512 19.1311 25.9847C18.9829 26.0175 18.9147 26.0227 18.9055 26.0234C18.9044 26.0234 18.9051 26.0234 18.9055 26.0234L18.9045 28.4733C19.3686 28.4733 19.9443 28.3188 20.4364 28.1648C20.977 27.9955 21.5965 27.7664 22.2185 27.5193C23.4645 27.0244 24.7849 26.4314 25.6302 26.028L25.6607 26.0135L25.6904 25.9973C25.8354 25.918 25.9882 25.8357 26.1462 25.7506C26.8827 25.354 27.7332 24.896 28.4478 24.3898C29.2496 23.8219 30.1517 23.0135 30.4588 21.8785C30.7149 21.3365 30.8775 20.618 30.9675 20.0026C31.0678 19.3167 31.1163 18.4951 30.9943 17.8727L30.9789 17.7938L30.9532 17.7176C30.681 16.9085 29.971 16.0221 29.3501 15.3589Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.8479 21.264V17.1402H27.398V21.264H29.8479Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.6666 11C49.2189 11 49.6666 11.4477 49.6666 12V15H52.6666C53.2189 15 53.6666 15.4477 53.6666 16C53.6666 16.5523 53.2189 17 52.6666 17H49.6666V20C49.6666 20.5523 49.2189 21 48.6666 21C48.1143 21 47.6666 20.5523 47.6666 20V17H44.6666C44.1143 17 43.6666 16.5523 43.6666 16C43.6666 15.4477 44.1143 15 44.6666 15H47.6666V12C47.6666 11.4477 48.1143 11 48.6666 11Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M67.1666 4.33329C66.7064 4.33329 66.3333 4.70639 66.3333 5.16663V23.5H64.6666V5.16663C64.6666 3.78591 65.7859 2.66663 67.1666 2.66663H89.494C90.6077 2.66663 91.1654 4.01306 90.3779 4.80051L76.6264 18.552H80.5V16.8333H82.1666V18.9687C82.1666 19.6591 81.607 20.2187 80.9166 20.2187H74.9597L72.0951 23.0833H85.0833V12.6666H86.75V23.0833C86.75 24.0038 86.0038 24.75 85.0833 24.75H70.4285L67.5118 27.6666H88.8333C89.2935 27.6666 89.6666 27.2935 89.6666 26.8333V8.49996H91.3333V26.8333C91.3333 28.214 90.214 29.3333 88.8333 29.3333H66.5059C65.3922 29.3333 64.8345 27.9869 65.622 27.1994L79.3214 13.5H75.5V15.1666H73.8333V13.0833C73.8333 12.3929 74.3929 11.8333 75.0833 11.8333H80.9881L83.9048 8.91663H70.9166V19.3333H69.25V8.91663C69.25 7.99615 69.9962 7.24996 70.9166 7.24996H85.5714L88.4881 4.33329H67.1666Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -18,9 +18,7 @@
],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"cmd-shift-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow"
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
}
},
{
@@ -39,8 +37,8 @@
{
"context": "Workspace",
"bindings": {
"cmd-\\": "workspace::ToggleLeftDock",
"cmd-k cmd-b": "workspace::ToggleLeftDock",
"cmd-\\": "workspace::ToggleLeftSidebar",
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
"cmd-t": "file_finder::Toggle",
"cmd-shift-r": "project_symbols::Toggle"
}
@@ -57,10 +55,14 @@
"bindings": {
"ctrl-[": "project_panel::CollapseSelectedEntry",
"ctrl-b": "project_panel::CollapseSelectedEntry",
"alt-b": "project_panel::CollapseSelectedEntry",
"h": "project_panel::CollapseSelectedEntry",
"ctrl-]": "project_panel::ExpandSelectedEntry",
"ctrl-f": "project_panel::ExpandSelectedEntry",
"ctrl-shift-c": "project_panel::CopyPath"
}
},
{
"context": "Dock",
"bindings": {}
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -11,11 +11,9 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"cmd-d": "editor::DuplicateLine",
"cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
"cmd-alt-enter": "editor::NewlineAbove",
"shift-enter": "editor::NewlineBelow",
"cmd--": "editor::Fold",
"cmd-=": "editor::UnfoldLines",
@@ -34,7 +32,6 @@
],
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",
"cmd-[": "pane::GoBack",
"cmd-]": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
@@ -65,11 +62,17 @@
{
"context": "Workspace",
"bindings": {
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle",
"cmd-1": "workspace::ToggleLeftDock",
"cmd-6": "diagnostics::Deploy"
"cmd-1": "workspace::ToggleLeftSidebar",
"cmd-6": "diagnostics::Deploy",
"alt-f12": "dock::FocusDock"
}
},
{
"context": "Dock",
"bindings": {
"alt-f12": "dock::HideDock"
}
}
]

View File

@@ -24,9 +24,7 @@
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cmd-shift-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{
@@ -45,11 +43,18 @@
{
"context": "Workspace",
"bindings": {
"cmd-k cmd-b": "workspace::ToggleLeftDock",
"ctrl-`": "dock::FocusDock",
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
"cmd-t": "file_finder::Toggle",
"shift-cmd-r": "project_symbols::Toggle",
// Currently busted: https://github.com/zed-industries/feedback/issues/898
"ctrl-0": "project_panel::ToggleFocus"
}
},
{
"context": "Dock",
"bindings": {
"ctrl-`": "dock::HideDock"
}
}
]

View File

@@ -12,7 +12,6 @@
"ctrl-shift-d": "editor::DuplicateLine",
"cmd-b": "editor::GoToDefinition",
"cmd-j": "editor::ScrollCursorCenter",
"cmd-alt-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow",
"cmd-shift-l": "editor::SelectLine",
"cmd-shift-t": "outline::Toggle",
@@ -68,7 +67,7 @@
{
"context": "Workspace",
"bindings": {
"cmd-alt-ctrl-d": "workspace::ToggleLeftDock",
"cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
"cmd-t": "file_finder::Toggle",
"cmd-shift-t": "project_symbols::Toggle"
}
@@ -83,5 +82,9 @@
{
"context": "ProjectPanel",
"bindings": {}
},
{
"context": "Dock",
"bindings": {}
}
]

View File

@@ -1,325 +1,325 @@
[
{
"context": "Editor && VimControl && !VimWaiting",
"bindings": {
"g": [
"vim::PushOperator",
{
"Namespace": "G"
{
"context": "Editor && VimControl && !VimWaiting",
"bindings": {
"g": [
"vim::PushOperator",
{
"Namespace": "G"
}
],
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
],
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"l": "vim::Right",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
"shift-w": [
"vim::NextWordStart",
{
"ignorePunctuation": true
}
],
"e": "vim::NextWordEnd",
"shift-e": [
"vim::NextWordEnd",
{
"ignorePunctuation": true
}
],
"b": "vim::PreviousWordStart",
"shift-b": [
"vim::PreviousWordStart",
{
"ignorePunctuation": true
}
],
"%": "vim::Matching",
"ctrl-y": [
"vim::Scroll",
"LineUp"
],
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
}
],
"t": [
"vim::PushOperator",
{
"FindForward": {
"before": true
}
}
],
"shift-f": [
"vim::PushOperator",
{
"FindBackward": {
"after": false
}
}
],
"shift-t": [
"vim::PushOperator",
{
"FindBackward": {
"after": true
}
}
],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
}
],
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"c": [
"vim::PushOperator",
"Change"
],
"shift-c": "vim::ChangeToEndOfLine",
"d": [
"vim::PushOperator",
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine",
"y": [
"vim::PushOperator",
"Yank"
],
"z": [
"vim::PushOperator",
{
"Namespace": "Z"
}
],
"i": [
"vim::SwitchMode",
"Insert"
],
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"^": "vim::FirstNonWhitespace",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"v": [
"vim::SwitchMode",
{
"Visual": {
"line": false
}
}
],
"shift-v": [
"vim::SwitchMode",
{
"Visual": {
"line": true
}
}
],
"p": "vim::Paste",
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"ctrl-o": "pane::GoBack",
"/": [
"buffer_search::Deploy",
{
"focus": true
}
],
"ctrl-f": [
"vim::Scroll",
"PageDown"
],
"ctrl-b": [
"vim::Scroll",
"PageUp"
],
"ctrl-d": [
"vim::Scroll",
"HalfPageDown"
],
"ctrl-u": [
"vim::Scroll",
"HalfPageUp"
],
"ctrl-e": [
"vim::Scroll",
"LineDown"
],
"r": [
"vim::PushOperator",
"Replace"
]
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
},
{
"context": "Editor && vim_operator == n",
"bindings": {
"0": [
"vim::Number",
0
]
}
],
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"l": "vim::Right",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
"shift-w": [
"vim::NextWordStart",
{
"ignorePunctuation": true
},
{
"context": "Editor && vim_operator == g",
"bindings": {
"g": "vim::StartOfDocument",
"h": "editor::Hover",
"escape": [
"vim::SwitchMode",
"Normal"
],
"d": "editor::GoToDefinition"
}
],
"e": "vim::NextWordEnd",
"shift-e": [
"vim::NextWordEnd",
{
"ignorePunctuation": true
},
{
"context": "Editor && vim_operator == c",
"bindings": {
"c": "vim::CurrentLine"
}
],
"b": "vim::PreviousWordStart",
"shift-b": [
"vim::PreviousWordStart",
{
"ignorePunctuation": true
},
{
"context": "Editor && vim_operator == d",
"bindings": {
"d": "vim::CurrentLine"
}
],
"%": "vim::Matching",
"ctrl-y": [
"vim::Scroll",
"LineUp"
],
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
},
{
"context": "Editor && vim_operator == y",
"bindings": {
"y": "vim::CurrentLine"
}
],
"t": [
"vim::PushOperator",
{
"FindForward": {
"before": true
}
},
{
"context": "Editor && vim_operator == z",
"bindings": {
"t": "editor::ScrollCursorTop",
"z": "editor::ScrollCursorCenter",
"b": "editor::ScrollCursorBottom",
"escape": [
"vim::SwitchMode",
"Normal"
]
}
],
"shift-f": [
"vim::PushOperator",
{
"FindBackward": {
"after": false
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"s": "vim::Sentence",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets"
}
],
"shift-t": [
"vim::PushOperator",
{
"FindBackward": {
"after": true
}
},
{
"context": "Editor && vim_mode == visual && !VimWaiting",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
"p": "vim::VisualPaste",
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{
"context": "Editor && vim_mode == insert",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore"
}
},
{
"context": "Editor && VimWaiting",
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "editor::Cancel"
}
],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"c": [
"vim::PushOperator",
"Change"
],
"shift-c": "vim::ChangeToEndOfLine",
"d": [
"vim::PushOperator",
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine",
"y": [
"vim::PushOperator",
"Yank"
],
"z": [
"vim::PushOperator",
{
"Namespace": "Z"
}
],
"i": [
"vim::SwitchMode",
"Insert"
],
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"^": "vim::FirstNonWhitespace",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"v": [
"vim::SwitchMode",
{
"Visual": {
"line": false
}
}
],
"shift-v": [
"vim::SwitchMode",
{
"Visual": {
"line": true
}
}
],
"p": "vim::Paste",
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"ctrl-o": "pane::GoBack",
"/": [
"buffer_search::Deploy",
{
"focus": true
}
],
"ctrl-f": [
"vim::Scroll",
"PageDown"
],
"ctrl-b": [
"vim::Scroll",
"PageUp"
],
"ctrl-d": [
"vim::Scroll",
"HalfPageDown"
],
"ctrl-u": [
"vim::Scroll",
"HalfPageUp"
],
"ctrl-e": [
"vim::Scroll",
"LineDown"
],
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{
"context": "Editor && vim_operator == n",
"bindings": {
"0": [
"vim::Number",
0
]
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
"g": "vim::StartOfDocument",
"h": "editor::Hover",
"escape": [
"vim::SwitchMode",
"Normal"
],
"d": "editor::GoToDefinition"
}
},
{
"context": "Editor && vim_operator == c",
"bindings": {
"c": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == d",
"bindings": {
"d": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == y",
"bindings": {
"y": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == z",
"bindings": {
"t": "editor::ScrollCursorTop",
"z": "editor::ScrollCursorCenter",
"b": "editor::ScrollCursorBottom",
"escape": [
"vim::SwitchMode",
"Normal"
]
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"s": "vim::Sentence",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets"
}
},
{
"context": "Editor && vim_mode == visual && !VimWaiting",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
"p": "vim::VisualPaste",
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{
"context": "Editor && vim_mode == insert",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore"
}
},
{
"context": "Editor && VimWaiting",
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "editor::Cancel"
}
}
]
]

View File

@@ -1,318 +1,241 @@
{
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// The name of a base set of key bindings to use.
// This setting can take four values, each named after another
// text editor:
//
// 1. "VSCode"
// 2. "JetBrains"
// 3. "SublimeText"
// 4. "Atom"
"base_keymap": "VSCode",
// Features that can be globally enabled or disabled
"features": {
// Show Copilot icon in status bar
"copilot": true
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The OpenType features to enable for text in the editor.
"buffer_font_features": {
// Disable ligatures:
// "calt": false
},
// The default font size for text in the editor
"buffer_font_size": 15,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,
// Whether to show tabs and spaces in the editor.
// This setting can take two values:
//
// 1. Draw tabs and spaces only for the selected text (default):
// "selection"
// 2. Do not draw any tabs or spaces:
// "none"
// 3. Draw all invisible symbols:
// "all"
"show_whitespaces": "selection",
// Scrollbar related settings
"scrollbar": {
// When to show the scrollbar in the editor.
// This setting can take four values:
//
// 1. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
// "auto"
// 2. Match the system's configured behavior:
// "system"
// 3. Always show the scrollbar:
// "always"
// 4. Never show the scrollbar:
// "never"
"show": "auto",
// Whether to show git diff indicators in the scrollbar.
"git_diff": true
},
"project_panel": {
// Whether to show the git status in the project panel.
"git_status": true,
// Where to dock project panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the project panel.
"default_width": 240
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.
"diagnostics": true,
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Automatically update Zed
"auto_update": true,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
"copilot": {
// The set of glob patterns for which copilot should be disabled
// in any matching file.
"disabled_globs": [
".env"
]
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration in /etc/passwd
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The default font size for text in the editor
"buffer_font_size": 15,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether the screen sharing icon is showed in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "bottom",
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"shell": "system",
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
"dock": "bottom",
// Default width when the terminal is docked to the left or right.
"default_width": 640,
// Default height when the terminal is docked to the bottom.
"default_height": 320,
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.
"diagnostics": true,
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Set the terminal's line height.
// May take 3 values:
// 1. Use a line height that's comfortable for reading, 1.618
// "line_height": "comfortable"
// 2. Use a standard line height, 1.3. This option is useful for TUIs,
// particularly if they use box characters
// "line_height": "standard",
// 3. Use a custom line height.
// "line_height": {
// "custom": 2
// },
//
"line_height": "comfortable"
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
// Automatically update Zed
"auto_update": true,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
"Elixir": {
"tab_size": 2
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
"Go": {
"tab_size": 4,
"hard_tabs": true
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration (e.g. $TERM).
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
"Markdown": {
"soft_wrap": "preferred_line_length"
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
}
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
},
"JSON": {
"tab_size": 2
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
}
}

View File

@@ -7,5 +7,5 @@
// custom settings, run the `open default settings` command
// from the command palette or from `Zed` application menu.
{
"buffer_font_size": 15
"buffer_font_size": 15
}

View File

@@ -16,11 +16,6 @@ gpui = { path = "../gpui" }
project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
futures.workspace = true
smallvec.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
futures = "0.3"
smallvec = { version = "1.6", features = ["union"] }

View File

@@ -2,13 +2,12 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
use editor::Editor;
use futures::StreamExt;
use gpui::{
actions, anyhow,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
actions, elements::*, platform::CursorStyle, Action, AppContext, Entity, ModelHandle,
MouseButton, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus};
use project::{LanguageServerProgress, Project};
use settings::Settings;
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc};
use util::ResultExt;
@@ -44,10 +43,10 @@ struct PendingWork<'a> {
struct Content {
icon: Option<&'static str>,
message: String,
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
action: Option<Box<dyn Action>>,
}
pub fn init(cx: &mut AppContext) {
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ActivityIndicator::show_error_message);
cx.add_action(ActivityIndicator::dismiss_error_message);
}
@@ -62,18 +61,21 @@ impl ActivityIndicator {
let auto_updater = AutoUpdater::get(cx);
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(|this, mut cx| async move {
cx.spawn_weak(|this, mut cx| async move {
while let Some((language, event)) = status_events.next().await {
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != language.name());
this.statuses.push(LspStatus {
name: language.name(),
status: event,
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != language.name());
this.statuses.push(LspStatus {
name: language.name(),
status: event,
});
cx.notify();
});
cx.notify();
})?;
} else {
break;
}
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
@@ -168,7 +170,7 @@ impl ActivityIndicator {
.flatten()
}
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
// Show any language server has pending activity.
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
@@ -198,7 +200,7 @@ impl ActivityIndicator {
return Content {
icon: None,
message,
on_click: None,
action: None,
};
}
@@ -229,7 +231,7 @@ impl ActivityIndicator {
downloading.join(", "),
if downloading.len() > 1 { "s" } else { "" }
),
on_click: None,
action: None,
};
} else if !checking_for_update.is_empty() {
return Content {
@@ -243,7 +245,7 @@ impl ActivityIndicator {
""
}
),
on_click: None,
action: None,
};
} else if !failed.is_empty() {
return Content {
@@ -253,9 +255,7 @@ impl ActivityIndicator {
failed.join(", "),
if failed.len() > 1 { "s" } else { "" }
),
on_click: Some(Arc::new(|this, cx| {
this.show_error_message(&Default::default(), cx)
})),
action: Some(Box::new(ShowErrorMessage)),
};
}
@@ -265,31 +265,27 @@ impl ActivityIndicator {
AutoUpdateStatus::Checking => Content {
icon: Some(DOWNLOAD_ICON),
message: "Checking for Zed updates…".to_string(),
on_click: None,
action: None,
},
AutoUpdateStatus::Downloading => Content {
icon: Some(DOWNLOAD_ICON),
message: "Downloading Zed update…".to_string(),
on_click: None,
action: None,
},
AutoUpdateStatus::Installing => Content {
icon: Some(DOWNLOAD_ICON),
message: "Installing Zed update…".to_string(),
on_click: None,
action: None,
},
AutoUpdateStatus::Updated => Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new(|_, cx| {
workspace::restart(&Default::default(), cx)
})),
action: Some(Box::new(workspace::Restart)),
},
AutoUpdateStatus::Errored => Content {
icon: Some(WARNING_ICON),
message: "Auto update failed".to_string(),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&Default::default(), cx)
})),
action: Some(Box::new(DismissErrorMessage)),
},
AutoUpdateStatus::Idle => Default::default(),
};
@@ -299,7 +295,7 @@ impl ActivityIndicator {
return Content {
icon: None,
message: most_recent_active_task.to_string(),
on_click: None,
action: None,
};
}
@@ -316,16 +312,21 @@ impl View for ActivityIndicator {
"ActivityIndicator"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let Content {
icon,
message,
on_click,
action,
} = self.content_to_render(cx);
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar.lsp_status;
let style = if state.hovered() && on_click.is_some() {
let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx
.global::<Settings>()
.theme
.workspace
.status_bar
.lsp_status;
let style = if state.hovered() && action.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default)
} else {
&theme.default
@@ -339,27 +340,31 @@ impl View for ActivityIndicator {
.contained()
.with_margin_right(style.icon_spacing)
.aligned()
.into_any_named("activity-icon")
.named("activity-icon")
}))
.with_child(
Text::new(message, style.message.clone())
.with_soft_wrap(false)
.aligned(),
.aligned()
.boxed(),
)
.constrained()
.with_height(style.height)
.contained()
.with_style(style.container)
.aligned()
.boxed()
});
if let Some(on_click) = on_click.clone() {
if let Some(action) = action {
element = element
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_any_action(action.boxed_clone())
});
}
element.into_any()
element.boxed()
}
}

View File

@@ -1,25 +0,0 @@
[package]
name = "ai"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/ai.rs"
doctest = false
[dependencies]
assets = { path = "../assets"}
collections = { path = "../collections"}
editor = { path = "../editor" }
gpui = { path = "../gpui" }
util = { path = "../util" }
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
futures.workspace = true
isahc.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View File

@@ -1,63 +0,0 @@
> Hello, I'm Nathan Sobo.
I'm currently your sole creator and the leader of Zed Industries.
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
@nathan> #zed, can you describe the conversation UI as you currently understand it.
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
@nathan> What details around the conversation view are worth exploring?
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
@nathan> @nate, which of these would you like to explore?
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
#zed describe a hypothetical conversation UI that meets the description.
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.

View File

@@ -1,3 +0,0 @@
Push content to a deeper layer.
A context can have multiple sublayers.
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.

View File

@@ -1,313 +0,0 @@
use anyhow::{anyhow, Result};
use assets::Assets;
use collections::HashMap;
use editor::Editor;
use futures::AsyncBufReadExt;
use futures::{io::BufReader, AsyncReadExt, Stream, StreamExt};
use gpui::executor::Background;
use gpui::{actions, AppContext, Task, ViewContext};
use isahc::prelude::*;
use isahc::{http::StatusCode, Request};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::fs;
use std::rc::Rc;
use std::{io, sync::Arc};
use util::channel::{ReleaseChannel, RELEASE_CHANNEL};
use util::{ResultExt, TryFutureExt};
actions!(ai, [Assist]);
// Data types for chat completion requests
#[derive(Serialize)]
struct OpenAIRequest {
model: String,
messages: Vec<RequestMessage>,
stream: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct RequestMessage {
role: Role,
content: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ResponseMessage {
role: Option<Role>,
content: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Role {
User,
Assistant,
System,
}
#[derive(Deserialize, Debug)]
struct OpenAIResponseStreamEvent {
pub id: Option<String>,
pub object: String,
pub created: u32,
pub model: String,
pub choices: Vec<ChatChoiceDelta>,
pub usage: Option<Usage>,
}
#[derive(Deserialize, Debug)]
struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Deserialize, Debug)]
struct ChatChoiceDelta {
pub index: u32,
pub delta: ResponseMessage,
pub finish_reason: Option<String>,
}
#[derive(Deserialize, Debug)]
struct OpenAIUsage {
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
}
#[derive(Deserialize, Debug)]
struct OpenAIChoice {
text: String,
index: u32,
logprobs: Option<serde_json::Value>,
finish_reason: Option<String>,
}
pub fn init(cx: &mut AppContext) {
if *RELEASE_CHANNEL == ReleaseChannel::Stable {
return;
}
let assistant = Rc::new(Assistant::default());
cx.add_action({
let assistant = assistant.clone();
move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext<Editor>| {
assistant.assist(editor, cx).log_err();
}
});
cx.capture_action({
let assistant = assistant.clone();
move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext<Editor>| {
if !assistant.cancel_last_assist(cx.view_id()) {
cx.propagate_action();
}
}
});
}
type CompletionId = usize;
#[derive(Default)]
struct Assistant(RefCell<AssistantState>);
#[derive(Default)]
struct AssistantState {
assist_stacks: HashMap<usize, Vec<(CompletionId, Task<Option<()>>)>>,
next_completion_id: CompletionId,
}
impl Assistant {
fn assist(self: &Rc<Self>, editor: &mut Editor, cx: &mut ViewContext<Editor>) -> Result<()> {
let api_key = std::env::var("OPENAI_API_KEY")?;
let selections = editor.selections.all(cx);
let (user_message, insertion_site) = editor.buffer().update(cx, |buffer, cx| {
// Insert markers around selected text as described in the system prompt above.
let snapshot = buffer.snapshot(cx);
let mut user_message = String::new();
let mut user_message_suffix = String::new();
let mut buffer_offset = 0;
for selection in selections {
if !selection.is_empty() {
if user_message_suffix.is_empty() {
user_message_suffix.push_str("\n\n");
}
user_message_suffix.push_str("[Selected excerpt from above]\n");
user_message_suffix
.extend(snapshot.text_for_range(selection.start..selection.end));
user_message_suffix.push_str("\n\n");
}
user_message.extend(snapshot.text_for_range(buffer_offset..selection.start));
user_message.push_str("[SELECTION_START]");
user_message.extend(snapshot.text_for_range(selection.start..selection.end));
buffer_offset = selection.end;
user_message.push_str("[SELECTION_END]");
}
if buffer_offset < snapshot.len() {
user_message.extend(snapshot.text_for_range(buffer_offset..snapshot.len()));
}
user_message.push_str(&user_message_suffix);
// Ensure the document ends with 4 trailing newlines.
let trailing_newline_count = snapshot
.reversed_chars_at(snapshot.len())
.take_while(|c| *c == '\n')
.take(4);
let buffer_suffix = "\n".repeat(4 - trailing_newline_count.count());
buffer.edit([(snapshot.len()..snapshot.len(), buffer_suffix)], None, cx);
let snapshot = buffer.snapshot(cx); // Take a new snapshot after editing.
let insertion_site = snapshot.anchor_after(snapshot.len() - 2);
(user_message, insertion_site)
});
let this = self.clone();
let buffer = editor.buffer().clone();
let executor = cx.background_executor().clone();
let editor_id = cx.view_id();
let assist_id = util::post_inc(&mut self.0.borrow_mut().next_completion_id);
let assist_task = cx.spawn(|_, mut cx| {
async move {
// TODO: We should have a get_string method on assets. This is repateated elsewhere.
let content = Assets::get("contexts/system.zmd").unwrap();
let mut system_message = std::str::from_utf8(content.data.as_ref())
.unwrap()
.to_string();
if let Ok(custom_system_message_path) =
std::env::var("ZED_ASSISTANT_SYSTEM_PROMPT_PATH")
{
system_message.push_str(
"\n\nAlso consider the following user-defined system prompt:\n\n",
);
// TODO: Replace this with our file system trait object.
system_message.push_str(
&cx.background()
.spawn(async move { fs::read_to_string(custom_system_message_path) })
.await?,
);
}
let stream = stream_completion(
api_key,
executor,
OpenAIRequest {
model: "gpt-4".to_string(),
messages: vec![
RequestMessage {
role: Role::System,
content: system_message.to_string(),
},
RequestMessage {
role: Role::User,
content: user_message,
},
],
stream: false,
},
);
let mut messages = stream.await?;
while let Some(message) = messages.next().await {
let mut message = message?;
if let Some(choice) = message.choices.pop() {
buffer.update(&mut cx, |buffer, cx| {
let text: Arc<str> = choice.delta.content?.into();
buffer.edit([(insertion_site.clone()..insertion_site, text)], None, cx);
Some(())
});
}
}
this.0
.borrow_mut()
.assist_stacks
.get_mut(&editor_id)
.unwrap()
.retain(|(id, _)| *id != assist_id);
anyhow::Ok(())
}
.log_err()
});
self.0
.borrow_mut()
.assist_stacks
.entry(cx.view_id())
.or_default()
.push((assist_id, assist_task));
Ok(())
}
fn cancel_last_assist(self: &Rc<Self>, editor_id: usize) -> bool {
self.0
.borrow_mut()
.assist_stacks
.get_mut(&editor_id)
.and_then(|assists| assists.pop())
.is_some()
}
}
async fn stream_completion(
api_key: String,
executor: Arc<Background>,
mut request: OpenAIRequest,
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
request.stream = true;
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
let json_data = serde_json::to_string(&request)?;
let mut response = Request::post("https://api.openai.com/v1/chat/completions")
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(json_data)?
.send_async()
.await?;
let status = response.status();
if status == StatusCode::OK {
executor
.spawn(async move {
let mut lines = BufReader::new(response.body_mut()).lines();
fn parse_line(
line: Result<String, io::Error>,
) -> Result<Option<OpenAIResponseStreamEvent>> {
if let Some(data) = line?.strip_prefix("data: ") {
let event = serde_json::from_str(&data)?;
Ok(Some(event))
} else {
Ok(None)
}
}
while let Some(line) = lines.next().await {
if let Some(event) = parse_line(line).transpose() {
tx.unbounded_send(event).log_err();
}
}
anyhow::Ok(())
})
.detach();
Ok(rx)
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Err(anyhow!(
"Failed to connect to OpenAI API: {} {}",
response.status(),
body,
))
}
}

View File

@@ -10,5 +10,6 @@ doctest = false
[dependencies]
gpui = { path = "../gpui" }
anyhow.workspace = true
anyhow = "1.0.38"
rust-embed = { version = "6.3", features = ["include-exclude"] }

View File

@@ -18,12 +18,11 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
util = { path = "../util" }
anyhow.workspace = true
isahc.workspace = true
lazy_static.workspace = true
log.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
tempdir.workspace = true
anyhow = "1.0.38"
isahc = "1.7"
lazy_static = "1.4"
log = "0.4"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
smol = "1.2.5"
tempdir = "0.3.7"

View File

@@ -1,21 +1,19 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
use client::{ZED_APP_PATH, ZED_APP_VERSION};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
Task, WeakViewHandle,
MutableAppContext, Task, WeakViewHandle,
};
use isahc::AsyncBody;
use serde::Deserialize;
use serde_derive::Serialize;
use settings::{Setting, SettingsStore};
use settings::Settings;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
use util::channel::ReleaseChannel;
use util::http::HttpClient;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -23,13 +21,6 @@ const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
#[derive(Serialize)]
struct UpdateRequestBody {
installation_id: Option<Arc<str>>,
release_channel: Option<&'static str>,
telemetry: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AutoUpdateStatus {
Idle,
@@ -58,42 +49,24 @@ impl Entity for AutoUpdater {
type Event = ();
}
struct AutoUpdateSetting(bool);
impl Setting for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update");
type FileContent = Option<bool>;
fn load(
default_value: &Option<bool>,
user_values: &[&Option<bool>],
_: &AppContext,
) -> Result<Self> {
Ok(Self(
Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
))
}
}
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
settings::register::<AutoUpdateSetting>(cx);
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut MutableAppContext) {
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
let server_url = server_url;
let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url);
let updater = AutoUpdater::new(version, http_client, server_url.clone());
let mut update_subscription = settings::get::<AutoUpdateSetting>(cx)
.0
let mut update_subscription = cx
.global::<Settings>()
.auto_update
.then(|| updater.start_polling(cx));
cx.observe_global::<SettingsStore, _>(move |updater, cx| {
if settings::get::<AutoUpdateSetting>(cx).0 {
cx.observe_global::<Settings, _>(move |updater, cx| {
if cx.global::<Settings>().auto_update {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
*(&mut update_subscription) = Some(updater.start_polling(cx))
}
} else {
update_subscription.take();
(&mut update_subscription).take();
}
})
.detach();
@@ -101,35 +74,28 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
updater
});
cx.set_global(Some(auto_updater));
cx.add_global_action(check);
cx.add_global_action(view_release_notes);
cx.add_global_action(|_: &Check, cx| {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
}
});
cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{
format!("{server_url}/releases/preview/latest")
} else {
format!("{server_url}/releases/latest")
};
cx.platform().open_url(&latest_release_url);
});
cx.add_action(UpdateNotification::dismiss);
}
}
pub fn check(_: &Check, cx: &mut AppContext) {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
}
}
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
let server_url = &auto_updater.read(cx).server_url;
let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{
format!("{server_url}/releases/preview/latest")
} else {
format!("{server_url}/releases/stable/latest")
};
cx.platform().open_url(&latest_release_url);
}
}
pub fn notify_of_any_new_update(
workspace: WeakViewHandle<Workspace>,
cx: &mut AppContext,
cx: &mut MutableAppContext,
) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version;
@@ -138,15 +104,17 @@ pub fn notify_of_any_new_update(
cx.spawn(|mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
workspace.show_notification(0, cx, |cx| {
cx.add_view(|_| UpdateNotification::new(version))
if let Some(workspace) = workspace.upgrade(&cx) {
workspace.update(&mut cx, |workspace, cx| {
workspace.show_notification(0, cx, |cx| {
cx.add_view(|_| UpdateNotification::new(version))
});
updater
.read(cx)
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
});
updater
.read(cx)
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
})?;
}
}
anyhow::Ok(())
})
@@ -156,7 +124,7 @@ pub fn notify_of_any_new_update(
}
impl AutoUpdater {
pub fn get(cx: &mut AppContext) -> Option<ModelHandle<Self>> {
pub fn get(cx: &mut MutableAppContext) -> Option<ModelHandle<Self>> {
cx.default_global::<Option<ModelHandle<Self>>>().clone()
}
@@ -275,24 +243,7 @@ impl AutoUpdater {
mounted_app_path.push("/");
let mut dmg_file = File::create(&dmg_path).await?;
let (installation_id, release_channel, telemetry) = cx.read(|cx| {
let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
let release_channel = cx
.has_global::<ReleaseChannel>()
.then(|| cx.global::<ReleaseChannel>().display_name());
let telemetry = settings::get::<TelemetrySettings>(cx).metrics;
(installation_id, release_channel, telemetry)
});
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
installation_id,
release_channel,
telemetry,
})?);
let mut response = client.get(&release.url, request_body, true).await?;
let mut response = client.get(&release.url, Default::default(), true).await?;
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
log::info!("downloaded update. path:{:?}", dmg_path);

View File

@@ -1,10 +1,11 @@
use crate::ViewReleaseNotes;
use gpui::{
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
platform::{AppVersion, CursorStyle, MouseButton},
Element, Entity, View, ViewContext,
platform::{AppVersion, CursorStyle},
Element, Entity, MouseButton, View, ViewContext,
};
use menu::Cancel;
use settings::Settings;
use util::channel::ReleaseChannel;
use workspace::notifications::Notification;
@@ -25,13 +26,13 @@ impl View for UpdateNotification {
"UpdateNotification"
}
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
let theme = theme::current(cx).clone();
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.update_notification;
let app_name = cx.global::<ReleaseChannel>().display_name();
MouseEventHandler::<ViewReleaseNotes, _>::new(0, cx, |state, cx| {
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
Flex::column()
.with_child(
Flex::row()
@@ -45,10 +46,11 @@ impl View for UpdateNotification {
.aligned()
.top()
.left()
.flex(1., true),
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
@@ -60,32 +62,35 @@ impl View for UpdateNotification {
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.boxed()
})
.with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, this, cx| {
this.dismiss(&Default::default(), cx)
})
.on_click(MouseButton::Left, move |_, cx| cx.dispatch_action(Cancel))
.aligned()
.constrained()
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
.aligned()
.top()
.flex_float(),
),
.flex_float()
.boxed(),
)
.boxed(),
)
.with_child({
let style = theme.action_message.style_for(state, false);
Text::new("View the release notes", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.contained()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
crate::view_release_notes(&Default::default(), cx)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(ViewReleaseNotes)
})
.into_any_named("update notification")
.boxed()
}
}

View File

@@ -18,7 +18,6 @@ search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
outline = { path = "../outline" }
itertools = "0.10"
[dev-dependencies]

View File

@@ -1,12 +1,12 @@
use gpui::{
elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext,
ViewHandle, WeakViewHandle,
elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
};
use itertools::Itertools;
use search::ProjectSearchView;
use settings::Settings;
use workspace::{
item::{ItemEvent, ItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace,
ToolbarItemLocation, ToolbarItemView,
};
pub enum Event {
@@ -14,21 +14,17 @@ pub enum Event {
}
pub struct Breadcrumbs {
pane_focused: bool,
active_item: Option<Box<dyn ItemHandle>>,
project_search: Option<ViewHandle<ProjectSearchView>>,
subscription: Option<Subscription>,
workspace: WeakViewHandle<Workspace>,
}
impl Breadcrumbs {
pub fn new(workspace: &Workspace) -> Self {
pub fn new() -> Self {
Self {
pane_focused: false,
active_item: Default::default(),
subscription: Default::default(),
project_search: Default::default(),
workspace: workspace.weak_handle(),
}
}
}
@@ -42,67 +38,25 @@ impl View for Breadcrumbs {
"Breadcrumbs"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let active_item = match &self.active_item {
Some(active_item) => active_item,
None => return Empty::new().into_any(),
};
let not_editor = active_item.downcast::<editor::Editor>().is_none();
let theme = theme::current(cx).clone();
let style = &theme.workspace.breadcrumbs;
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
Some(breadcrumbs) => breadcrumbs,
None => return Empty::new().into_any(),
}
.into_iter()
.map(|breadcrumb| {
Text::new(
breadcrumb.text,
theme.workspace.breadcrumbs.default.text.clone(),
)
.with_highlights(breadcrumb.highlights.unwrap_or_default())
.into_any()
});
let crumbs = Flex::row()
.with_children(Itertools::intersperse_with(breadcrumbs, || {
Label::new("", style.default.text.clone()).into_any()
}))
.constrained()
.with_height(theme.workspace.breadcrumb_height)
.contained();
if not_editor || !self.pane_focused {
return crumbs
.with_style(style.default.container)
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
if let Some(breadcrumbs) = self
.active_item
.as_ref()
.and_then(|item| item.breadcrumbs(&theme, cx))
{
Flex::row()
.with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
Label::new("", theme.breadcrumbs.text.clone()).boxed()
}))
.contained()
.with_style(theme.breadcrumbs.container)
.aligned()
.left()
.into_any();
.boxed()
} else {
Empty::new().boxed()
}
MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
let style = style.style_for(state, false);
crumbs.with_style(style.container)
})
.on_click(MouseButton::Left, |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
outline::toggle(workspace, &Default::default(), cx)
})
}
})
.with_tooltip::<Breadcrumbs>(
0,
"Show symbol outline".to_owned(),
Some(Box::new(outline::Toggle)),
theme.tooltip.clone(),
cx,
)
.aligned()
.left()
.into_any()
}
}
@@ -149,8 +103,4 @@ impl ToolbarItemView for Breadcrumbs {
current_location
}
}
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
self.pane_focused = pane_focused;
}
}

View File

@@ -22,7 +22,7 @@ test-support = [
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
log.workspace = true
log = "0.4"
live_kit_client = { path = "../live_kit_client" }
fs = { path = "../fs" }
language = { path = "../language" }
@@ -31,10 +31,10 @@ project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" }
anyhow.workspace = true
anyhow = "1.0.38"
async-broadcast = "0.4"
futures.workspace = true
postage.workspace = true
futures = "0.3"
postage = { version = "0.4.1", features = ["futures-traits"] }
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }

View File

@@ -10,15 +10,15 @@ use futures::{future::Shared, FutureExt};
use postage::watch;
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
WeakModelHandle,
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Subscription, Task, WeakModelHandle,
};
use project::Project;
pub use participant::ParticipantLocation;
pub use room::Room;
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(active_call);
}

View File

@@ -10,7 +10,9 @@ use client::{
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use gpui::{
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
};
use language::LanguageRegistry;
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
use postage::stream::Stream;
@@ -62,13 +64,16 @@ pub struct Room {
impl Entity for Room {
type Event = Event;
fn release(&mut self, cx: &mut AppContext) {
fn release(&mut self, cx: &mut MutableAppContext) {
if self.status.is_online() {
self.leave_internal(cx).detach_and_log_err(cx);
}
}
fn app_will_quit(&mut self, cx: &mut AppContext) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
fn app_will_quit(
&mut self,
cx: &mut MutableAppContext,
) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
if self.status.is_online() {
let leave = self.leave_internal(cx);
Some(
@@ -171,7 +176,7 @@ impl Room {
initial_project: Option<ModelHandle<Project>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut AppContext,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
@@ -214,7 +219,7 @@ impl Room {
call: &IncomingCall,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut AppContext,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
let room_id = call.room_id;
cx.spawn(|mut cx| async move {
@@ -252,7 +257,7 @@ impl Room {
self.leave_internal(cx)
}
fn leave_internal(&mut self, cx: &mut AppContext) -> Task<Result<()>> {
fn leave_internal(&mut self, cx: &mut MutableAppContext) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
@@ -270,7 +275,6 @@ impl Room {
if let Some(project) = project.upgrade(cx) {
project.update(cx, |project, cx| {
project.disconnected_from_host(cx);
project.close(cx);
});
}
}
@@ -419,7 +423,7 @@ impl Room {
false
});
let response = self.client.request_envelope(proto::RejoinRoom {
let response = self.client.request(proto::RejoinRoom {
id: self.id,
reshared_projects,
rejoined_projects,
@@ -427,8 +431,6 @@ impl Room {
cx.spawn(|this, mut cx| async move {
let response = response.await?;
let message_id = response.message_id;
let response = response.payload;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| {
this.status = RoomStatus::Online;
@@ -445,7 +447,7 @@ impl Room {
for rejoined_project in response.rejoined_projects {
if let Some(project) = projects.get(&rejoined_project.id) {
project.update(cx, |project, cx| {
project.rejoined(rejoined_project, message_id, cx).log_err();
project.rejoined(rejoined_project, cx).log_err();
});
}
}

View File

@@ -13,13 +13,11 @@ name = "cli"
path = "src/main.rs"
[dependencies]
anyhow.workspace = true
anyhow = "1.0"
clap = { version = "3.1", features = ["derive"] }
dirs = "3.0"
ipc-channel = "0.16"
serde.workspace = true
serde_derive.workspace = true
util = { path = "../util" }
serde = { version = "1.0", features = ["derive", "rc"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"

View File

@@ -1,5 +1,6 @@
pub use ipc_channel::ipc;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize, Deserialize)]
pub struct IpcHandshake {
@@ -9,12 +10,7 @@ pub struct IpcHandshake {
#[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest {
// The filed is named `path` for compatibility, but now CLI can request
// opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`.
//
// Since Zed CLI has to be installed separately, there can be situations when old CLI is
// querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later.
Open { paths: Vec<String>, wait: bool },
Open { paths: Vec<PathBuf>, wait: bool },
}
#[derive(Debug, Serialize, Deserialize)]
@@ -24,7 +20,3 @@ pub enum CliResponse {
Stderr { message: String },
Exit { status: i32 },
}
/// When Zed started not as an *.app but as a binary (e.g. local development),
/// there's a possibility to tell it to behave "regularly".
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";

View File

@@ -1,6 +1,6 @@
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, Result};
use clap::Parser;
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use cli::{CliRequest, CliResponse, IpcHandshake};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
@@ -16,20 +16,16 @@ use std::{
path::{Path, PathBuf},
ptr,
};
use util::paths::PathLikeWithPosition;
#[derive(Parser)]
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
struct Args {
/// Wait for all of the given paths to be opened/closed before exiting.
/// Wait for all of the given paths to be closed before exiting.
#[clap(short, long)]
wait: bool,
/// A sequence of space-separated paths that you want to open.
///
/// Use `path:line:row` syntax to open a file at a specific location.
/// Non-existing paths and directories will ignore `:line:row` suffix.
#[clap(value_parser = parse_path_with_position)]
paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
#[clap()]
paths: Vec<PathBuf>,
/// Print Zed's version and the app path.
#[clap(short, long)]
version: bool,
@@ -38,14 +34,6 @@ struct Args {
bundle_path: Option<PathBuf>,
}
fn parse_path_with_position(
argument_str: &str,
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
PathLikeWithPosition::parse_str(argument_str, |path_str| {
Ok(Path::new(path_str).to_path_buf())
})
}
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
@@ -55,37 +43,37 @@ struct InfoPlist {
fn main() -> Result<()> {
let args = Args::parse();
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
let bundle_path = if let Some(bundle_path) = args.bundle_path {
bundle_path.canonicalize()?
} else {
locate_bundle()?
};
if args.version {
println!("{}", bundle.zed_version_string());
let plist_path = bundle_path.join("Contents/Info.plist");
let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
println!(
"Zed {} {}",
plist.bundle_short_version_string,
bundle_path.to_string_lossy()
);
return Ok(());
}
for path in args
.paths_with_position
.iter()
.map(|path_with_position| &path_with_position.path_like)
{
for path in args.paths.iter() {
if !path.exists() {
touch(path.as_path())?;
}
}
let (tx, rx) = bundle.launch()?;
let (tx, rx) = launch_app(bundle_path)?;
tx.send(CliRequest::Open {
paths: args
.paths_with_position
.paths
.into_iter()
.map(|path_with_position| {
let path_with_position = path_with_position.map_path_like(|path| {
fs::canonicalize(&path)
.with_context(|| format!("path {path:?} canonicalization"))
})?;
Ok(path_with_position.to_string(|path| path.display().to_string()))
})
.collect::<Result<_>>()?,
.map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
.collect::<Result<Vec<PathBuf>>>()?,
wait: args.wait,
})?;
@@ -101,148 +89,6 @@ fn main() -> Result<()> {
Ok(())
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
impl Bundle {
fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
let bundle_path = if let Some(bundle_path) = args_bundle_path {
bundle_path
.canonicalize()
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
} else {
locate_bundle().context("bundle autodiscovery")?
};
match bundle_path.extension().and_then(|ext| ext.to_str()) {
Some("app") => {
let plist_path = bundle_path.join("Contents/Info.plist");
let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading *.app bundle plist file at {plist_path:?}")
})?;
Ok(Self::App {
app_bundle: bundle_path,
plist,
})
}
_ => {
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
let plist_path = bundle_path
.parent()
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
.join("WebRTC.framework/Resources/Info.plist");
let plist = plist::from_file::<_, InfoPlist>(&plist_path)
.with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
Ok(Self::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath {
executable: excutable,
..
} => excutable,
}
}
fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
match self {
Self::App { app_bundle, .. } => {
let app_path = app_bundle;
let status = unsafe {
let app_url = CFURL::from_path(app_path, true)
.with_context(|| format!("invalid app path {app_path:?}"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
anyhow::ensure!(
status == 0,
"cannot start app bundle {}",
self.zed_version_string()
);
}
Self::LocalPath { executable, .. } => {
let executable_parent = executable
.parent()
.with_context(|| format!("Executable {executable:?} path has no parent"))?;
let subprocess_stdout_file =
fs::File::create(executable_parent.join("zed_dev.log"))
.with_context(|| format!("Log file creation in {executable_parent:?}"))?;
let subprocess_stdin_file =
subprocess_stdout_file.try_clone().with_context(|| {
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
})?;
let mut command = std::process::Command::new(executable);
let command = command
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
.stderr(subprocess_stdout_file)
.stdout(subprocess_stdin_file)
.arg(url);
command
.spawn()
.with_context(|| format!("Spawning {command:?}"))?;
}
}
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
Ok((handshake.requests, handshake.responses))
}
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
}
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
@@ -260,3 +106,38 @@ fn locate_bundle() -> Result<PathBuf> {
}
Ok(app_path)
}
fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
let url = format!("zed-cli://{server_name}");
let status = unsafe {
let app_url =
CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
if status == 0 {
let (_, handshake) = server.accept()?;
Ok((handshake.requests, handshake.responses))
} else {
Err(anyhow!("cannot start {:?}", app_path))
}
}

View File

@@ -17,29 +17,26 @@ db = { path = "../db" }
gpui = { path = "../gpui" }
util = { path = "../util" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
staff_mode = { path = "../staff_mode" }
sum_tree = { path = "../sum_tree" }
anyhow.workspace = true
anyhow = "1.0.38"
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-tls"] }
futures.workspace = true
futures = "0.3"
image = "0.23"
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
schemars.workspace = true
smol.workspace = true
thiserror.workspace = true
time.workspace = true
isahc = "1.7"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
smol = "1.2.5"
thiserror = "1.0.29"
time = { version = "0.3", features = ["serde", "serde-well-known"] }
tiny_http = "0.8"
uuid = { version = "1.1.2", features = ["v4"] }
url = "2.2"
serde.workspace = true
serde_derive.workspace = true
serde = { version = "*", features = ["derive"] }
settings = { path = "../settings" }
tempfile = "3"
[dev-dependencies]
@@ -47,4 +44,3 @@ collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View File

@@ -1,6 +1,7 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub mod http;
pub mod telemetry;
pub mod user;
@@ -10,22 +11,21 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{Request, StatusCode},
};
use futures::{
future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _,
TryStreamExt,
};
use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
use gpui::{
actions, platform::AppVersion, serde_json, AnyModelHandle, AnyWeakModelHandle,
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext,
WeakViewHandle,
actions,
serde_json::{self, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion,
AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
};
use http::HttpClient;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use settings::{Settings, TelemetrySettings};
use std::{
any::TypeId,
collections::HashMap,
@@ -41,11 +41,9 @@ use telemetry::Telemetry;
use thiserror::Error;
use url::Url;
use util::channel::ReleaseChannel;
use util::http::HttpClient;
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
pub use telemetry::ClickhouseEvent;
pub use user::*;
lazy_static! {
@@ -70,34 +68,25 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [SignIn, SignOut]);
pub fn init_settings(cx: &mut AppContext) {
settings::register::<TelemetrySettings>(cx);
}
pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
init_settings(cx);
let client = Arc::downgrade(client);
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action({
let client = client.clone();
move |_: &SignIn, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
)
.detach();
}
let client = client.clone();
cx.spawn(
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
)
.detach();
}
});
cx.add_global_action({
let client = client.clone();
move |_: &SignOut, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(|cx| async move {
client.disconnect(&cx);
})
.detach();
}
let client = client.clone();
cx.spawn(|cx| async move {
client.disconnect(&cx);
})
.detach();
}
});
}
@@ -141,7 +130,7 @@ pub enum EstablishConnectionError {
#[error("{0}")]
Other(#[from] anyhow::Error),
#[error("{0}")]
Http(#[from] util::http::Error),
Http(#[from] http::Error),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
@@ -229,7 +218,7 @@ enum WeakSubscriber {
enum Subscriber {
Model(AnyModelHandle),
View(AnyWeakViewHandle),
View(AnyViewHandle),
}
#[derive(Clone, Debug)]
@@ -305,7 +294,7 @@ impl<T: Entity> PendingEntitySubscription<T> {
state
.entities_by_type_and_remote_id
.insert(id, WeakSubscriber::Model(model.downgrade().into_any()));
.insert(id, WeakSubscriber::Model(model.downgrade().into()));
drop(state);
for message in messages {
self.client.handle_message(message, cx);
@@ -333,42 +322,6 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
}
}
#[derive(Copy, Clone)]
pub struct TelemetrySettings {
pub diagnostics: bool,
pub metrics: bool,
}
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TelemetrySettingsContent {
pub diagnostics: Option<bool>,
pub metrics: Option<bool>,
}
impl settings::Setting for TelemetrySettings {
const KEY: Option<&'static str> = Some("telemetry");
type FileContent = TelemetrySettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &AppContext,
) -> Result<Self> {
Ok(Self {
diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
default_value
.diagnostics
.ok_or_else(Self::missing_default)?,
),
metrics: user_values
.first()
.and_then(|v| v.metrics)
.unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
})
}
}
impl Client {
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
Arc::new(Self {
@@ -490,7 +443,9 @@ impl Client {
}));
}
Status::SignedOut | Status::UpgradeRequired => {
cx.read(|cx| self.telemetry.set_authenticated_user_info(None, false, cx));
let telemetry_settings = cx.read(|cx| cx.global::<Settings>().telemetry());
self.telemetry
.set_authenticated_user_info(None, false, telemetry_settings);
state._reconnect_task.take();
}
_ => {}
@@ -506,7 +461,7 @@ impl Client {
self.state
.write()
.entities_by_type_and_remote_id
.insert(id, WeakSubscriber::View(cx.weak_handle().into_any()));
.insert(id, WeakSubscriber::View(cx.weak_handle().into()));
Subscription::Entity {
client: Arc::downgrade(self),
id,
@@ -516,22 +471,18 @@ impl Client {
pub fn subscribe_to_entity<T: Entity>(
self: &Arc<Self>,
remote_id: u64,
) -> Result<PendingEntitySubscription<T>> {
) -> PendingEntitySubscription<T> {
let id = (TypeId::of::<T>(), remote_id);
self.state
.write()
.entities_by_type_and_remote_id
.insert(id, WeakSubscriber::Pending(Default::default()));
let mut state = self.state.write();
if state.entities_by_type_and_remote_id.contains_key(&id) {
return Err(anyhow!("already subscribed to entity"));
} else {
state
.entities_by_type_and_remote_id
.insert(id, WeakSubscriber::Pending(Default::default()));
Ok(PendingEntitySubscription {
client: self.clone(),
remote_id,
consumed: false,
_entity_type: PhantomData,
})
PendingEntitySubscription {
client: self.clone(),
remote_id,
consumed: false,
_entity_type: PhantomData,
}
}
@@ -554,7 +505,7 @@ impl Client {
let mut state = self.state.write();
state
.models_by_message_type
.insert(message_type_id, model.downgrade().into_any());
.insert(message_type_id, model.downgrade().into());
let prev_handler = state.message_handlers.insert(
message_type_id,
@@ -609,7 +560,7 @@ impl Client {
H: 'static
+ Send
+ Sync
+ Fn(WeakViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
@@ -708,7 +659,7 @@ impl Client {
H: 'static
+ Send
+ Sync
+ Fn(WeakViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_view_message_handler(move |entity, envelope, client, cx| {
@@ -778,10 +729,10 @@ impl Client {
read_from_keychain = credentials.is_some();
if read_from_keychain {
cx.read(|cx| {
self.telemetry().report_mixpanel_event(
self.report_event(
"read credentials from keychain",
Default::default(),
*settings::get::<TelemetrySettings>(cx),
cx.global::<Settings>().telemetry(),
);
});
}
@@ -1074,8 +1025,7 @@ impl Client {
let executor = cx.background();
let telemetry = self.telemetry.clone();
let http = self.http.clone();
let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
let metrics_enabled = cx.read(|cx| cx.global::<Settings>().telemetry());
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
@@ -1159,10 +1109,10 @@ impl Client {
.context("failed to decrypt access token")?;
platform.activate(true);
telemetry.report_mixpanel_event(
telemetry.report_event(
"authenticate with browser",
Default::default(),
telemetry_settings,
metrics_enabled,
);
Ok(Credentials {
@@ -1238,14 +1188,6 @@ impl Client {
&self,
request: T,
) -> impl Future<Output = Result<T::Response>> {
self.request_envelope(request)
.map_ok(|envelope| envelope.payload)
}
pub fn request_envelope<T: RequestMessage>(
&self,
request: T,
) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
let client_id = self.id;
log::debug!(
"rpc request start. client_id:{}. name:{}",
@@ -1254,7 +1196,7 @@ impl Client {
);
let response = self
.connection_id()
.map(|conn_id| self.peer.request_envelope(conn_id, request));
.map(|conn_id| self.peer.request(conn_id, request));
async move {
let response = response?.await;
log::debug!(
@@ -1316,15 +1258,7 @@ impl Client {
pending.push(message);
return;
}
Some(weak_subscriber @ _) => match weak_subscriber {
WeakSubscriber::Model(handle) => {
subscriber = handle.upgrade(cx).map(Subscriber::Model);
}
WeakSubscriber::View(handle) => {
subscriber = Some(Subscriber::View(handle.clone()));
}
WeakSubscriber::Pending(_) => {}
},
Some(weak_subscriber @ _) => subscriber = weak_subscriber.upgrade(cx),
_ => {}
}
}
@@ -1381,8 +1315,40 @@ impl Client {
}
}
pub fn telemetry(&self) -> &Arc<Telemetry> {
&self.telemetry
pub fn start_telemetry(&self) {
self.telemetry.start();
}
pub fn report_event(
&self,
kind: &str,
properties: Value,
telemetry_settings: TelemetrySettings,
) {
self.telemetry
.report_event(kind, properties.clone(), telemetry_settings);
}
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
self.telemetry.log_file_path()
}
pub fn metrics_id(&self) -> Option<Arc<str>> {
self.telemetry.metrics_id()
}
pub fn is_staff(&self) -> Option<bool> {
self.telemetry.is_staff()
}
}
impl WeakSubscriber {
fn upgrade(&self, cx: &AsyncAppContext) -> Option<Subscriber> {
match self {
WeakSubscriber::Model(handle) => handle.upgrade(cx).map(Subscriber::Model),
WeakSubscriber::View(handle) => handle.upgrade(cx).map(Subscriber::View),
WeakSubscriber::Pending(_) => None,
}
}
}
@@ -1430,11 +1396,10 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test::FakeServer;
use crate::test::{FakeHttpClient, FakeServer};
use gpui::{executor::Deterministic, TestAppContext};
use parking_lot::Mutex;
use std::future;
use util::http::FakeHttpClient;
#[gpui::test(iterations = 10)]
async fn test_reconnection(cx: &mut TestAppContext) {
@@ -1629,17 +1594,14 @@ mod tests {
let _subscription1 = client
.subscribe_to_entity(1)
.unwrap()
.set_model(&model1, &mut cx.to_async());
let _subscription2 = client
.subscribe_to_entity(2)
.unwrap()
.set_model(&model2, &mut cx.to_async());
// Ensure dropping a subscription for the same entity type still allows receiving of
// messages for other entity IDs of the same type.
let subscription3 = client
.subscribe_to_entity(3)
.unwrap()
.set_model(&model3, &mut cx.to_async());
drop(subscription3);
@@ -1668,13 +1630,11 @@ mod tests {
},
);
drop(subscription1);
let _subscription2 = client.add_message_handler(
model.clone(),
move |_, _: TypedEnvelope<proto::Ping>, _, _| {
let _subscription2 =
client.add_message_handler(model, move |_, _: TypedEnvelope<proto::Ping>, _, _| {
done_tx2.try_send(()).unwrap();
async { Ok(()) }
},
);
});
server.send(proto::Ping {});
done_rx2.next().await.unwrap();
}

57
crates/client/src/http.rs Normal file
View File

@@ -0,0 +1,57 @@
pub use anyhow::{anyhow, Result};
use futures::future::BoxFuture;
use isahc::{
config::{Configurable, RedirectPolicy},
AsyncBody,
};
pub use isahc::{
http::{Method, Uri},
Error,
};
use smol::future::FutureExt;
use std::{sync::Arc, time::Duration};
pub use url::Url;
pub type Request = isahc::Request<AsyncBody>;
pub type Response = isahc::Response<AsyncBody>;
pub trait HttpClient: Send + Sync {
fn send(&self, req: Request) -> BoxFuture<Result<Response, Error>>;
fn get<'a>(
&'a self,
uri: &str,
body: AsyncBody,
follow_redirects: bool,
) -> BoxFuture<'a, Result<Response, Error>> {
let request = isahc::Request::builder()
.redirect_policy(if follow_redirects {
RedirectPolicy::Follow
} else {
RedirectPolicy::None
})
.method(Method::GET)
.uri(uri)
.body(body);
match request {
Ok(request) => self.send(request),
Err(error) => async move { Err(error.into()) }.boxed(),
}
}
}
pub fn client() -> Arc<dyn HttpClient> {
Arc::new(
isahc::HttpClient::builder()
.connect_timeout(Duration::from_secs(5))
.low_speed_timeout(100, Duration::from_secs(5))
.build()
.unwrap(),
)
}
impl HttpClient for isahc::HttpClient {
fn send(&self, req: Request) -> BoxFuture<Result<Response, Error>> {
Box::pin(async move { self.send_async(req).await })
}
}

View File

@@ -1,16 +1,17 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use crate::http::HttpClient;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
executor::Background,
serde_json::{self, value::Map, Value},
AppContext, Task,
};
use isahc::Request;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use settings::TelemetrySettings;
use std::{
env,
io::Write,
mem,
path::PathBuf,
@@ -18,7 +19,6 @@ use std::{
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -30,69 +30,26 @@ pub struct Telemetry {
#[derive(Default)]
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation
metrics_id: Option<Arc<str>>,
device_id: Option<Arc<str>>,
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
os_name: &'static str,
os_version: Option<Arc<str>>,
architecture: &'static str,
mixpanel_events_queue: Vec<MixpanelEvent>,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
next_mixpanel_event_id: usize,
flush_mixpanel_events_task: Option<Task<()>>,
flush_clickhouse_events_task: Option<Task<()>>,
os_name: &'static str,
queue: Vec<MixpanelEvent>,
next_event_id: usize,
flush_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
}
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
lazy_static! {
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
.ok()
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
static ref CLICKHOUSE_EVENTS_URL: String =
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
}
#[derive(Serialize, Debug)]
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
app_version: Option<Arc<str>>,
os_name: &'static str,
os_version: Option<Arc<str>>,
architecture: &'static str,
release_channel: Option<&'static str>,
events: Vec<ClickhouseEventWrapper>,
}
#[derive(Serialize, Debug)]
struct ClickhouseEventWrapper {
time: u128,
signed_in: bool,
#[serde(flatten)]
event: ClickhouseEvent,
}
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
pub enum ClickhouseEvent {
Editor {
operation: &'static str,
file_extension: Option<String>,
vim_mode: bool,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
},
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
},
}
#[derive(Serialize, Debug)]
@@ -107,8 +64,7 @@ struct MixpanelEventProperties {
#[serde(skip_serializing_if = "str::is_empty")]
token: &'static str,
time: u128,
#[serde(rename = "distinct_id")]
installation_id: Option<Arc<str>>,
distinct_id: Option<Arc<str>>,
#[serde(rename = "$insert_id")]
insert_id: usize,
// Custom fields
@@ -131,7 +87,7 @@ struct MixpanelEngageRequest {
#[serde(rename = "$token")]
token: &'static str,
#[serde(rename = "$distinct_id")]
installation_id: Arc<str>,
distinct_id: Arc<str>,
#[serde(rename = "$set")]
set: Value,
}
@@ -156,23 +112,19 @@ impl Telemetry {
} else {
None
};
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
http_client: client,
executor: cx.background().clone(),
state: Mutex::new(TelemetryState {
os_name: platform.os_name().into(),
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
architecture: env::consts::ARCH,
os_name: platform.os_name().into(),
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
release_channel,
installation_id: None,
device_id: None,
metrics_id: None,
mixpanel_events_queue: Default::default(),
clickhouse_events_queue: Default::default(),
flush_mixpanel_events_task: Default::default(),
flush_clickhouse_events_task: Default::default(),
next_mixpanel_event_id: 0,
queue: Default::default(),
flush_task: Default::default(),
next_event_id: 0,
log_file: None,
is_staff: None,
}),
@@ -203,38 +155,29 @@ impl Telemetry {
self.executor
.spawn(
async move {
let installation_id =
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
installation_id
let device_id =
if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
device_id
} else {
let installation_id = Uuid::new_v4().to_string();
let device_id = Uuid::new_v4().to_string();
KEY_VALUE_STORE
.write_kvp("device_id".to_string(), installation_id.clone())
.write_kvp("device_id".to_string(), device_id.clone())
.await?;
installation_id
device_id
};
let installation_id: Arc<str> = installation_id.into();
let device_id: Arc<str> = device_id.into();
let mut state = this.state.lock();
state.installation_id = Some(installation_id.clone());
for event in &mut state.mixpanel_events_queue {
state.device_id = Some(device_id.clone());
for event in &mut state.queue {
event
.properties
.installation_id
.get_or_insert_with(|| installation_id.clone());
.distinct_id
.get_or_insert_with(|| device_id.clone());
}
let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
if has_mixpanel_events {
this.flush_mixpanel_events();
}
if has_clickhouse_events {
this.flush_clickhouse_events();
if !state.queue.is_empty() {
drop(state);
this.flush();
}
anyhow::Ok(())
@@ -250,37 +193,37 @@ impl Telemetry {
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
cx: &AppContext,
telemetry_settings: TelemetrySettings,
) {
if !settings::get::<TelemetrySettings>(cx).metrics {
if !telemetry_settings.metrics() {
return;
}
let this = self.clone();
let mut state = self.state.lock();
let installation_id = state.installation_id.clone();
let device_id = state.device_id.clone();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
drop(state);
if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
self.executor
.spawn(
async move {
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
token,
installation_id,
distinct_id: device_id,
set: json!({
"Staff": is_staff,
"ID": metrics_id,
"App": true
}),
}])?;
this.http_client
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
.await?;
let request = Request::post(MIXPANEL_ENGAGE_URL)
.header("Content-Type", "application/json")
.body(json_bytes.into())?;
this.http_client.send(request).await?;
anyhow::Ok(())
}
.log_err(),
@@ -289,62 +232,27 @@ impl Telemetry {
}
}
pub fn report_clickhouse_event(
self: &Arc<Self>,
event: ClickhouseEvent,
telemetry_settings: TelemetrySettings,
) {
if !telemetry_settings.metrics {
return;
}
let mut state = self.state.lock();
let signed_in = state.metrics_id.is_some();
state.clickhouse_events_queue.push(ClickhouseEventWrapper {
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
signed_in,
event,
});
if state.installation_id.is_some() {
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_clickhouse_events();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush_clickhouse_events();
}));
}
}
}
pub fn report_mixpanel_event(
pub fn report_event(
self: &Arc<Self>,
kind: &str,
properties: Value,
telemetry_settings: TelemetrySettings,
) {
if !telemetry_settings.metrics {
if !telemetry_settings.metrics() {
return;
}
let mut state = self.state.lock();
let event = MixpanelEvent {
event: kind.into(),
event: kind.to_string(),
properties: MixpanelEventProperties {
token: "",
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
installation_id: state.installation_id.clone(),
insert_id: post_inc(&mut state.next_mixpanel_event_id),
distinct_id: state.device_id.clone(),
insert_id: post_inc(&mut state.next_event_id),
event_properties: if let Value::Object(properties) = properties {
Some(properties)
} else {
@@ -357,17 +265,17 @@ impl Telemetry {
signed_in: state.metrics_id.is_some(),
},
};
state.mixpanel_events_queue.push(event);
if state.installation_id.is_some() {
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
state.queue.push(event);
if state.device_id.is_some() {
if state.queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_mixpanel_events();
self.flush();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
state.flush_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush_mixpanel_events();
this.flush();
}));
}
}
@@ -377,18 +285,14 @@ impl Telemetry {
self.state.lock().metrics_id.clone()
}
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().installation_id.clone()
}
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
self.state.lock().is_staff
}
fn flush_mixpanel_events(self: &Arc<Self>) {
fn flush(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.mixpanel_events_queue);
state.flush_mixpanel_events_task.take();
let mut events = mem::take(&mut state.queue);
state.flush_task.take();
drop(state);
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
@@ -412,9 +316,10 @@ impl Telemetry {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &events)?;
this.http_client
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
.await?;
let request = Request::post(MIXPANEL_EVENTS_URL)
.header("Content-Type", "application/json")
.body(json_bytes.into())?;
this.http_client.send(request).await?;
anyhow::Ok(())
}
.log_err(),
@@ -422,55 +327,4 @@ impl Telemetry {
.detach();
}
}
fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.clickhouse_events_queue);
state.flush_clickhouse_events_task.take();
drop(state);
let this = self.clone();
self.executor
.spawn(
async move {
let mut json_bytes = Vec::new();
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
for event in &mut events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
file.write(b"\n")?;
}
}
{
let state = this.state.lock();
json_bytes.clear();
serde_json::to_writer(
&mut json_bytes,
&ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),
architecture: state.architecture,
release_channel: state.release_channel,
events,
},
)?;
}
this.http_client
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
}

View File

@@ -1,14 +1,16 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use crate::{
http::{self, HttpClient, Request, Response},
Client, Connection, Credentials, EstablishConnectionError, UserStore,
};
use anyhow::{anyhow, Result};
use futures::{stream::BoxStream, StreamExt};
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
use gpui::{executor, ModelHandle, TestAppContext};
use parking_lot::Mutex;
use rpc::{
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
ConnectionId, Peer, Receipt, TypedEnvelope,
};
use std::{rc::Rc, sync::Arc};
use util::http::FakeHttpClient;
use std::{fmt, rc::Rc, sync::Arc};
pub struct FakeServer {
peer: Arc<Peer>,
@@ -217,3 +219,46 @@ impl Drop for FakeServer {
self.disconnect();
}
}
pub struct FakeHttpClient {
handler: Box<
dyn 'static
+ Send
+ Sync
+ Fn(Request) -> BoxFuture<'static, Result<Response, http::Error>>,
>,
}
impl FakeHttpClient {
pub fn create<Fut, F>(handler: F) -> Arc<dyn HttpClient>
where
Fut: 'static + Send + Future<Output = Result<Response, http::Error>>,
F: 'static + Send + Sync + Fn(Request) -> Fut,
{
Arc::new(Self {
handler: Box::new(move |req| Box::pin(handler(req))),
})
}
pub fn with_404_response() -> Arc<dyn HttpClient> {
Self::create(|_| async move {
Ok(isahc::Response::builder()
.status(404)
.body(Default::default())
.unwrap())
})
}
}
impl fmt::Debug for FakeHttpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FakeHttpClient").finish()
}
}
impl HttpClient for FakeHttpClient {
fn send(&self, req: Request) -> BoxFuture<Result<Response, crate::http::Error>> {
let future = (self.handler)(req);
Box::pin(async move { future.await.map(Into::into) })
}
}

View File

@@ -1,14 +1,13 @@
use super::{proto, Client, Status, TypedEnvelope};
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, HashMap, HashSet};
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use staff_mode::StaffMode;
use settings::Settings;
use std::sync::{Arc, Weak};
use util::http::HttpClient;
use util::TryFutureExt as _;
use util::{StaffMode, TryFutureExt as _};
#[derive(Default, Debug)]
pub struct User {
@@ -143,13 +142,11 @@ impl UserStore {
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
cx.read(|cx| {
client.telemetry.set_authenticated_user_info(
info.as_ref().map(|info| info.metrics_id.clone()),
info.as_ref().map(|info| info.staff).unwrap_or(false),
cx,
)
});
client.telemetry.set_authenticated_user_info(
info.as_ref().map(|info| info.metrics_id.clone()),
info.as_ref().map(|info| info.staff).unwrap_or(false),
cx.read(|cx| cx.global::<Settings>().telemetry()),
);
cx.update(|cx| {
cx.update_default_global(|staff_mode: &mut StaffMode, _| {

View File

@@ -9,4 +9,4 @@ path = "src/clock.rs"
doctest = false
[dependencies]
smallvec.workspace = true
smallvec = { version = "1.6", features = ["union"] }

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.12.4"
version = "0.8.0"
publish = false
[[bin]]
@@ -19,7 +19,7 @@ live_kit_server = { path = "../live_kit_server" }
rpc = { path = "../rpc" }
util = { path = "../util" }
anyhow.workspace = true
anyhow = "1.0.40"
async-tungstenite = "0.16"
axum = { version = "0.5", features = ["json", "headers", "ws"] }
axum-extra = { version = "0.3", features = ["erased-json"] }
@@ -27,31 +27,29 @@ base64 = "0.13"
clap = { version = "3.1", features = ["derive"], optional = true }
dashmap = "5.4"
envy = "0.4.2"
futures.workspace = true
futures = "0.3"
hyper = "0.14"
lazy_static.workspace = true
lazy_static = "1.4"
lipsum = { version = "0.8", optional = true }
log.workspace = true
nanoid = "0.4"
parking_lot.workspace = true
parking_lot = "0.11.1"
prometheus = "0.13"
rand.workspace = true
rand = "0.8"
reqwest = { version = "0.11", features = ["json"], optional = true }
scrypt = "0.7"
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
sea-query = "0.27"
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
sha-1 = "0.9"
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
time.workspace = true
time = { version = "0.3", features = ["serde", "serde-well-known"] }
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.17"
tonic = "0.6"
tower = "0.4"
toml.workspace = true
toml = "0.5.8"
tracing = "0.1.34"
tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
@@ -74,15 +72,15 @@ settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
indoc.workspace = true
ctor = "0.1"
env_logger = "0.9"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
util = { path = "../util" }
lazy_static.workspace = true
lazy_static = "1.4"
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
serde_json.workspace = true
serde_json = { version = "1.0", features = ["preserve_order"] }
sqlx = { version = "0.6", features = ["sqlite"] }
unindent.workspace = true
unindent = "0.1"
[features]
seed-support = ["clap", "lipsum", "reqwest"]

View File

@@ -82,37 +82,6 @@ CREATE TABLE "worktree_entries" (
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"branch" VARCHAR,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INTEGER NOT NULL,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
@@ -184,7 +153,7 @@ CREATE TABLE "followers" (
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"follower_connection_id" INTEGER NOT NULL
);
CREATE UNIQUE INDEX
CREATE UNIQUE INDEX
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");

View File

@@ -1,13 +0,0 @@
CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"scan_id" INT8 NOT NULL,
"branch" VARCHAR,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");

View File

@@ -1,15 +0,0 @@
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");

View File

@@ -1,5 +1,5 @@
use crate::{
db::{self, AccessTokenId, Database, UserId},
db::{self, UserId},
AppState, Error, Result,
};
use anyhow::{anyhow, Context};
@@ -8,24 +8,12 @@ use axum::{
middleware::Next,
response::IntoResponse,
};
use lazy_static::lazy_static;
use prometheus::{exponential_buckets, register_histogram, Histogram};
use rand::thread_rng;
use scrypt::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Scrypt,
};
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
lazy_static! {
static ref METRIC_ACCESS_TOKEN_HASHING_TIME: Histogram = register_histogram!(
"access_token_hashing_time",
"time spent hashing access tokens",
exponential_buckets(10.0, 2.0, 10).unwrap(),
)
.unwrap();
}
use std::sync::Arc;
pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
let mut auth_header = req
@@ -54,14 +42,20 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
)
})?;
let mut credentials_valid = false;
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
if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
if state.config.api_token == admin_token {
credentials_valid = true;
}
} else {
verify_access_token(&access_token, user_id, &state.db)
.await
.unwrap_or(false)
};
for password_hash in state.db.get_access_token_hashes(user_id).await? {
if verify_access_token(access_token, &password_hash)? {
credentials_valid = true;
break;
}
}
}
if credentials_valid {
let user = state
@@ -81,26 +75,13 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
#[derive(Serialize, Deserialize)]
struct AccessTokenJson {
version: usize,
id: AccessTokenId,
token: String,
}
pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result<String> {
const VERSION: usize = 1;
let access_token = rpc::auth::random_token();
let access_token_hash =
hash_access_token(&access_token).context("failed to hash access token")?;
let id = db
.create_access_token(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE)
db.create_access_token_hash(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE)
.await?;
Ok(serde_json::to_string(&AccessTokenJson {
version: VERSION,
id,
token: access_token,
})?)
Ok(access_token)
}
fn hash_access_token(token: &str) -> Result<String> {
@@ -108,7 +89,7 @@ fn hash_access_token(token: &str) -> Result<String> {
let params = if cfg!(debug_assertions) {
scrypt::Params::new(1, 1, 1).unwrap()
} else {
scrypt::Params::new(14, 8, 1).unwrap()
scrypt::Params::recommended()
};
Ok(Scrypt
@@ -131,21 +112,7 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<St
Ok(encrypted_access_token)
}
pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc<Database>) -> Result<bool> {
let token: AccessTokenJson = serde_json::from_str(&token)?;
let db_token = db.get_access_token(token.id).await?;
if db_token.user_id != user_id {
return Err(anyhow!("no such access token"))?;
}
let db_hash = PasswordHash::new(&db_token.hash).map_err(anyhow::Error::new)?;
let t0 = Instant::now();
let is_valid = Scrypt
.verify_password(token.token.as_bytes(), &db_hash)
.is_ok();
let duration = t0.elapsed();
log::info!("hashed access token in {:?}", duration);
METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64);
Ok(is_valid)
pub fn verify_access_token(token: &str, hash: &str) -> Result<bool> {
let hash = PasswordHash::new(hash).map_err(anyhow::Error::new)?;
Ok(Scrypt.verify_password(token.as_bytes(), &hash).is_ok())
}

View File

@@ -1,4 +1,4 @@
use collab::{db, executor::Executor};
use collab::db;
use db::{ConnectOptions, Database};
use serde::{de::DeserializeOwned, Deserialize};
use std::fmt::Write;
@@ -13,7 +13,7 @@ struct GitHubUser {
#[tokio::main]
async fn main() {
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))
.await
.expect("failed to connect to postgres database");
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");

View File

@@ -14,10 +14,7 @@ mod user;
mod worktree;
mod worktree_diagnostic_summary;
mod worktree_entry;
mod worktree_repository;
mod worktree_repository_statuses;
use crate::executor::Executor;
use crate::{Error, Result};
use anyhow::anyhow;
use collections::{BTreeMap, HashMap, HashSet};
@@ -25,8 +22,6 @@ pub use contact::Contact;
use dashmap::DashMap;
use futures::StreamExt;
use hyper::StatusCode;
use rand::prelude::StdRng;
use rand::{Rng, SeedableRng};
use rpc::{proto, ConnectionId};
use sea_orm::Condition;
pub use sea_orm::ConnectOptions;
@@ -51,20 +46,20 @@ pub struct Database {
options: ConnectOptions,
pool: DatabaseConnection,
rooms: DashMap<RoomId, Arc<Mutex<()>>>,
rng: Mutex<StdRng>,
executor: Executor,
#[cfg(test)]
background: Option<std::sync::Arc<gpui::executor::Background>>,
#[cfg(test)]
runtime: Option<tokio::runtime::Runtime>,
}
impl Database {
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
pub async fn new(options: ConnectOptions) -> Result<Self> {
Ok(Self {
options: options.clone(),
pool: sea_orm::Database::connect(options).await?,
rooms: DashMap::with_capacity(16384),
rng: Mutex::new(StdRng::seed_from_u64(0)),
executor,
#[cfg(test)]
background: None,
#[cfg(test)]
runtime: None,
})
@@ -177,39 +172,25 @@ impl Database {
.map(|participant| participant.user_id)
.collect::<Vec<_>>();
// Delete participants who failed to reconnect and cancel their calls.
let mut canceled_calls_to_user_ids = Vec::new();
// Delete participants who failed to reconnect.
room_participant::Entity::delete_many()
.filter(stale_participant_filter)
.exec(&*tx)
.await?;
let called_participants = room_participant::Entity::find()
.filter(
Condition::all()
.add(
room_participant::Column::CallingUserId
.is_in(stale_participant_user_ids.iter().copied()),
)
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)
.all(&*tx)
.await?;
room_participant::Entity::delete_many()
.filter(
room_participant::Column::Id
.is_in(called_participants.iter().map(|participant| participant.id)),
)
.exec(&*tx)
.await?;
canceled_calls_to_user_ids.extend(
called_participants
.into_iter()
.map(|participant| participant.user_id),
);
let room = self.get_room(room_id, &tx).await?;
// Delete the room if it becomes empty.
let mut canceled_calls_to_user_ids = Vec::new();
// Delete the room if it becomes empty and cancel pending calls.
if room.participants.is_empty() {
canceled_calls_to_user_ids.extend(
room.pending_participants
.iter()
.map(|pending_participant| UserId::from_proto(pending_participant.user_id)),
);
room_participant::Entity::delete_many()
.filter(room_participant::Column::RoomId.eq(room_id))
.exec(&*tx)
.await?;
project::Entity::delete_many()
.filter(project::Column::RoomId.eq(room_id))
.exec(&*tx)
@@ -1491,8 +1472,6 @@ impl Database {
visible: db_worktree.visible,
updated_entries: Default::default(),
removed_entries: Default::default(),
updated_repositories: Default::default(),
removed_repositories: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
@@ -1502,125 +1481,38 @@ impl Database {
.worktrees
.iter()
.find(|worktree| worktree.id == db_worktree.id as u64);
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_entry::Column::IsDeleted.eq(false)
};
// File entries
{
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
} else {
worktree_entry::Column::IsDeleted.eq(false)
};
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::ProjectId.eq(project.id))
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
} else {
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
});
}
}
}
// Repository Entries
{
let repository_entry_filter =
if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_repository::Column::IsDeleted.eq(false)
};
let mut db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_repository) = db_repositories.next().await {
let db_repository = db_repository?;
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
} else {
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
removed_repo_paths: Default::default(),
updated_statuses: Default::default(),
});
}
}
}
// Repository Status Entries
for repository in worktree.updated_repositories.iter_mut() {
let repository_status_entry_filter =
if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository_statuses::Column::ScanId
.gt(rejoined_worktree.scan_id)
} else {
worktree_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_repository_statuses =
worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(
worktree_repository_statuses::Column::ProjectId
.eq(project.id),
)
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id),
)
.add(repository_status_entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_status_entry) = db_repository_statuses.next().await {
let db_status_entry = db_status_entry?;
if db_status_entry.is_deleted {
repository
.removed_repo_paths
.push(db_status_entry.repo_path);
} else {
repository.updated_statuses.push(proto::StatusEntry {
repo_path: db_status_entry.repo_path,
status: db_status_entry.status as i32,
});
}
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
});
}
}
@@ -2421,115 +2313,6 @@ impl Database {
.await?;
}
if !update.updated_repositories.is_empty() {
worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
|repository| worktree_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
branch: ActiveValue::set(repository.branch.clone()),
is_deleted: ActiveValue::set(false),
},
))
.on_conflict(
OnConflict::columns([
worktree_repository::Column::ProjectId,
worktree_repository::Column::WorktreeId,
worktree_repository::Column::WorkDirectoryId,
])
.update_columns([
worktree_repository::Column::ScanId,
worktree_repository::Column::Branch,
])
.to_owned(),
)
.exec(&*tx)
.await?;
for repository in update.updated_repositories.iter() {
if !repository.updated_statuses.is_empty() {
worktree_repository_statuses::Entity::insert_many(
repository.updated_statuses.iter().map(|status_entry| {
worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
status: ActiveValue::set(status_entry.status as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
}
}),
)
.on_conflict(
OnConflict::columns([
worktree_repository_statuses::Column::ProjectId,
worktree_repository_statuses::Column::WorktreeId,
worktree_repository_statuses::Column::WorkDirectoryId,
worktree_repository_statuses::Column::RepoPath,
])
.update_columns([
worktree_repository_statuses::Column::ScanId,
worktree_repository_statuses::Column::Status,
worktree_repository_statuses::Column::IsDeleted,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
if !repository.removed_repo_paths.is_empty() {
worktree_repository_statuses::Entity::update_many()
.filter(
worktree_repository_statuses::Column::ProjectId
.eq(project_id)
.and(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree_id),
)
.and(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id as i64),
)
.and(worktree_repository_statuses::Column::RepoPath.is_in(
repository.removed_repo_paths.iter().map(String::as_str),
)),
)
.set(worktree_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
}
}
if !update.removed_repositories.is_empty() {
worktree_repository::Entity::update_many()
.filter(
worktree_repository::Column::ProjectId
.eq(project_id)
.and(worktree_repository::Column::WorktreeId.eq(worktree_id))
.and(
worktree_repository::Column::WorkDirectoryId
.is_in(update.removed_repositories.iter().map(|id| *id as i64)),
),
)
.set(worktree_repository::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
@@ -2705,7 +2488,6 @@ impl Database {
root_name: db_worktree.root_name,
visible: db_worktree.visible,
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
@@ -2743,61 +2525,6 @@ impl Database {
}
}
// Populate repository entries.
{
let mut db_repository_entries = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project_id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_repository_entry) = db_repository_entries.next().await {
let db_repository_entry = db_repository_entry?;
if let Some(worktree) =
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
worktree.repository_entries.insert(
db_repository_entry.work_directory_id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
removed_repo_paths: Default::default(),
updated_statuses: Default::default(),
},
);
}
}
}
{
let mut db_status_entries = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_status_entry) = db_status_entries.next().await {
let db_status_entry = db_status_entry?;
if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
{
if let Some(repository_entry) = worktree
.repository_entries
.get_mut(&(db_status_entry.work_directory_id as u64))
{
repository_entry.updated_statuses.push(proto::StatusEntry {
repo_path: db_status_entry.repo_path,
status: db_status_entry.status as i32,
});
}
}
}
}
// Populate worktree diagnostic summaries.
{
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
@@ -3016,16 +2743,16 @@ impl Database {
// access tokens
pub async fn create_access_token(
pub async fn create_access_token_hash(
&self,
user_id: UserId,
access_token_hash: &str,
max_access_token_count: usize,
) -> Result<AccessTokenId> {
) -> Result<()> {
self.transaction(|tx| async {
let tx = tx;
let token = access_token::ActiveModel {
access_token::ActiveModel {
user_id: ActiveValue::set(user_id),
hash: ActiveValue::set(access_token_hash.into()),
..Default::default()
@@ -3048,20 +2775,26 @@ impl Database {
)
.exec(&*tx)
.await?;
Ok(token.id)
Ok(())
})
.await
}
pub async fn get_access_token(
&self,
access_token_id: AccessTokenId,
) -> Result<access_token::Model> {
pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result<Vec<String>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
Hash,
}
self.transaction(|tx| async move {
Ok(access_token::Entity::find_by_id(access_token_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such access token"))?)
Ok(access_token::Entity::find()
.select_only()
.column(access_token::Column::Hash)
.filter(access_token::Column::UserId.eq(user_id))
.order_by_desc(access_token::Column::Id)
.into_values::<_, QueryAs>()
.all(&*tx)
.await?)
})
.await
}
@@ -3072,26 +2805,30 @@ impl Database {
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let mut i = 0;
loop {
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(result) => match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(result),
Err(error) => {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
Ok(result) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(result),
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
},
}
Err(error) => {
tx.rollback().await?;
if !self.retry_on_serialization_error(&error, i).await {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
i += 1;
}
};
@@ -3104,7 +2841,6 @@ impl Database {
Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
{
let body = async {
let mut i = 0;
loop {
let (tx, result) = self.with_transaction(&f).await?;
match result {
@@ -3120,28 +2856,35 @@ impl Database {
}));
}
Err(error) => {
if !self.retry_on_serialization_error(&error, i).await {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
}
Ok(None) => match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(None),
Err(error) => {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
Ok(None) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(None),
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
},
}
Err(error) => {
tx.rollback().await?;
if !self.retry_on_serialization_error(&error, i).await {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
i += 1;
}
};
@@ -3154,34 +2897,38 @@ impl Database {
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let mut i = 0;
loop {
let lock = self.rooms.entry(room_id).or_default().clone();
let _guard = lock.lock_owned().await;
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(data) => match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(RoomGuard {
data,
_guard,
_not_send: PhantomData,
});
}
Err(error) => {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
Ok(data) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(RoomGuard {
data,
_guard,
_not_send: PhantomData,
});
}
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
},
}
Err(error) => {
tx.rollback().await?;
if !self.retry_on_serialization_error(&error, i).await {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
i += 1;
}
};
@@ -3207,14 +2954,14 @@ impl Database {
Ok((tx, result))
}
async fn run<F, T>(&self, future: F) -> Result<T>
async fn run<F, T>(&self, future: F) -> T
where
F: Future<Output = Result<T>>,
F: Future<Output = T>,
{
#[cfg(test)]
{
if let Executor::Deterministic(executor) = &self.executor {
executor.simulate_random_delay().await;
if let Some(background) = self.background.as_ref() {
background.simulate_random_delay().await;
}
self.runtime.as_ref().unwrap().block_on(future)
@@ -3225,27 +2972,6 @@ impl Database {
future.await
}
}
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: u32) -> bool {
// If the error is due to a failure to serialize concurrent transactions, then retry
// this transaction after a delay. With each subsequent retry, double the delay duration.
// Also vary the delay randomly in order to ensure different database connections retry
// at different times.
if is_serialization_error(error) {
let base_delay = 4_u64 << prev_attempt_count.min(16);
let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0);
log::info!(
"retrying transaction after serialization error. delay: {} ms.",
randomized_delay
);
self.executor
.sleep(Duration::from_millis(randomized_delay as u64))
.await;
true
} else {
false
}
}
}
fn is_serialization_error(error: &Error) -> bool {
@@ -3479,8 +3205,6 @@ pub struct RejoinedWorktree {
pub visible: bool,
pub updated_entries: Vec<proto::Entry>,
pub removed_entries: Vec<u64>,
pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub completed_scan_id: u64,
@@ -3535,7 +3259,6 @@ pub struct Worktree {
pub root_name: String,
pub visible: bool,
pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub completed_scan_id: u64,
@@ -3550,6 +3273,7 @@ mod test {
use gpui::executor::Background;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use rand::prelude::*;
use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
use std::sync::Arc;
@@ -3571,9 +3295,7 @@ mod test {
let mut db = runtime.block_on(async {
let mut options = ConnectOptions::new(url);
options.max_connections(5);
let db = Database::new(options, Executor::Deterministic(background))
.await
.unwrap();
let db = Database::new(options).await.unwrap();
let sql = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/migrations.sqlite/20221109000000_test_schema.sql"
@@ -3588,6 +3310,7 @@ mod test {
db
});
db.background = Some(background);
db.runtime = Some(runtime);
Self {
@@ -3621,14 +3344,13 @@ mod test {
options
.max_connections(5)
.idle_timeout(Duration::from_secs(0));
let db = Database::new(options, Executor::Deterministic(background))
.await
.unwrap();
let db = Database::new(options).await.unwrap();
let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
db.migrate(Path::new(migrations_path), false).await.unwrap();
db
});
db.background = Some(background);
db.runtime = Some(runtime);
Self {

View File

@@ -177,63 +177,30 @@ test_both_dbs!(
.unwrap()
.user_id;
let token_1 = db.create_access_token(user, "h1", 2).await.unwrap();
let token_2 = db.create_access_token(user, "h2", 2).await.unwrap();
db.create_access_token_hash(user, "h1", 3).await.unwrap();
db.create_access_token_hash(user, "h2", 3).await.unwrap();
assert_eq!(
db.get_access_token(token_1).await.unwrap(),
access_token::Model {
id: token_1,
user_id: user,
hash: "h1".into(),
}
);
assert_eq!(
db.get_access_token(token_2).await.unwrap(),
access_token::Model {
id: token_2,
user_id: user,
hash: "h2".into()
}
db.get_access_token_hashes(user).await.unwrap(),
&["h2".to_string(), "h1".to_string()]
);
let token_3 = db.create_access_token(user, "h3", 2).await.unwrap();
db.create_access_token_hash(user, "h3", 3).await.unwrap();
assert_eq!(
db.get_access_token(token_3).await.unwrap(),
access_token::Model {
id: token_3,
user_id: user,
hash: "h3".into()
}
db.get_access_token_hashes(user).await.unwrap(),
&["h3".to_string(), "h2".to_string(), "h1".to_string(),]
);
assert_eq!(
db.get_access_token(token_2).await.unwrap(),
access_token::Model {
id: token_2,
user_id: user,
hash: "h2".into()
}
);
assert!(db.get_access_token(token_1).await.is_err());
let token_4 = db.create_access_token(user, "h4", 2).await.unwrap();
db.create_access_token_hash(user, "h4", 3).await.unwrap();
assert_eq!(
db.get_access_token(token_4).await.unwrap(),
access_token::Model {
id: token_4,
user_id: user,
hash: "h4".into()
}
db.get_access_token_hashes(user).await.unwrap(),
&["h4".to_string(), "h3".to_string(), "h2".to_string(),]
);
db.create_access_token_hash(user, "h5", 3).await.unwrap();
assert_eq!(
db.get_access_token(token_3).await.unwrap(),
access_token::Model {
id: token_3,
user_id: user,
hash: "h3".into()
}
db.get_access_token_hashes(user).await.unwrap(),
&["h5".to_string(), "h4".to_string(), "h3".to_string()]
);
assert!(db.get_access_token(token_2).await.is_err());
assert!(db.get_access_token(token_1).await.is_err());
}
);

View File

@@ -1,21 +0,0 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_repositories")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub work_directory_id: i64,
pub scan_id: i64,
pub branch: Option<String>,
pub is_deleted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1,23 +0,0 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_repository_statuses")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub work_directory_id: i64,
#[sea_orm(primary_key)]
pub repo_path: String,
pub status: i64,
pub scan_id: i64,
pub is_deleted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -10,7 +10,6 @@ mod tests;
use axum::{http::StatusCode, response::IntoResponse};
use db::Database;
use executor::Executor;
use serde::Deserialize;
use std::{path::PathBuf, sync::Arc};
@@ -119,7 +118,7 @@ impl AppState {
pub async fn new(config: Config) -> Result<Arc<Self>> {
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
db_options.max_connections(config.database_max_connections);
let db = Database::new(db_options, Executor::Production).await?;
let db = Database::new(db_options).await?;
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
.as_ref()

View File

@@ -32,7 +32,7 @@ async fn main() -> Result<()> {
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
db_options.max_connections(5);
let db = Database::new(db_options, Executor::Production).await?;
let db = Database::new(db_options).await?;
let migrations_path = config
.migrations_path

View File

@@ -51,7 +51,7 @@ use std::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
time::{Duration, Instant},
time::Duration,
};
use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder;
@@ -228,7 +228,7 @@ impl Server {
.add_message_handler(update_buffer_file)
.add_message_handler(buffer_reloaded)
.add_message_handler(buffer_saved)
.add_request_handler(forward_project_request::<proto::SaveBuffer>)
.add_request_handler(save_buffer)
.add_request_handler(get_users)
.add_request_handler(fuzzy_search_users)
.add_request_handler(request_contact)
@@ -397,16 +397,10 @@ impl Server {
"message received"
);
});
let start_time = Instant::now();
let future = (handler)(*envelope, session);
async move {
let result = future.await;
let duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0;
match result {
Err(error) => {
tracing::error!(%error, ?duration_ms, "error handling message")
}
Ok(()) => tracing::info!(?duration_ms, "finished handling message"),
if let Err(error) = future.await {
tracing::error!(%error, "error handling message");
}
}
.instrument(span)
@@ -1069,8 +1063,6 @@ async fn rejoin_room(
removed_entries: worktree.removed_entries,
scan_id: worktree.scan_id,
is_last_update: worktree.completed_scan_id == worktree.scan_id,
updated_repositories: worktree.updated_repositories,
removed_repositories: worktree.removed_repositories,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
session.peer.send(session.connection_id, update.clone())?;
@@ -1391,8 +1383,6 @@ async fn join_project(
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.scan_id == worktree.completed_scan_id,
updated_repositories: worktree.repository_entries.into_values().collect(),
removed_repositories: Default::default(),
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
session.peer.send(session.connection_id, update.clone())?;
@@ -1601,6 +1591,51 @@ where
Ok(())
}
async fn save_buffer(
request: proto::SaveBuffer,
response: Response<proto::SaveBuffer>,
session: Session,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let host_connection_id = {
let collaborators = session
.db()
.await
.project_collaborators(project_id, session.connection_id)
.await?;
collaborators
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?
.connection_id
};
let response_payload = session
.peer
.forward_request(session.connection_id, host_connection_id, request.clone())
.await?;
let mut collaborators = session
.db()
.await
.project_collaborators(project_id, session.connection_id)
.await?;
collaborators.retain(|collaborator| collaborator.connection_id != session.connection_id);
let project_connection_ids = collaborators
.iter()
.map(|collaborator| collaborator.connection_id);
broadcast(
Some(host_connection_id),
project_connection_ids,
|conn_id| {
session
.peer
.forward_send(host_connection_id, conn_id, response_payload.clone())
},
);
response.send(response_payload)?;
Ok(())
}
async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer,
session: Session,
@@ -1620,42 +1655,23 @@ async fn update_buffer(
) -> Result<()> {
session.executor.record_backtrace();
let project_id = ProjectId::from_proto(request.project_id);
let mut guest_connection_ids;
let mut host_connection_id = None;
{
let collaborators = session
.db()
.await
.project_collaborators(project_id, session.connection_id)
.await?;
guest_connection_ids = Vec::with_capacity(collaborators.len() - 1);
for collaborator in collaborators.iter() {
if collaborator.is_host {
host_connection_id = Some(collaborator.connection_id);
} else {
guest_connection_ids.push(collaborator.connection_id);
}
}
}
let host_connection_id = host_connection_id.ok_or_else(|| anyhow!("host not found"))?;
let project_connection_ids = session
.db()
.await
.project_connection_ids(project_id, session.connection_id)
.await?;
session.executor.record_backtrace();
broadcast(
Some(session.connection_id),
guest_connection_ids,
project_connection_ids.iter().copied(),
|connection_id| {
session
.peer
.forward_send(session.connection_id, connection_id, request.clone())
},
);
if host_connection_id != session.connection_id {
session
.peer
.forward_request(session.connection_id, host_connection_id, request.clone())
.await?;
}
response.send(proto::Ack {})?;
Ok(())
}

View File

@@ -7,30 +7,29 @@ use crate::{
use anyhow::anyhow;
use call::ActiveCall;
use client::{
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
self, proto::PeerId, test::FakeHttpClient, Client, Connection, Credentials,
EstablishConnectionError, UserStore,
};
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{
elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View,
ViewContext, ViewHandle, WeakViewHandle,
executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle,
};
use language::LanguageRegistry;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
use settings::SettingsStore;
use settings::Settings;
use std::{
cell::{Ref, RefCell, RefMut},
env,
ops::{Deref, DerefMut},
path::Path,
ops::Deref,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
};
use util::http::FakeHttpClient;
use theme::ThemeRegistry;
use workspace::Workspace;
mod integration_tests;
@@ -101,7 +100,7 @@ impl TestServer {
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
cx.set_global(Settings::test(cx));
});
let http = FakeHttpClient::with_404_response();
@@ -189,19 +188,17 @@ impl TestServer {
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
languages: Arc::new(LanguageRegistry::test()),
languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
themes: ThemeRegistry::new((), cx.font_cache()),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| unimplemented!(),
initialize_workspace: |_, _, _| unimplemented!(),
dock_default_item_factory: |_, _| None,
background_actions: || &[],
});
Project::init(&client);
cx.update(|cx| {
theme::init((), cx);
Project::init(&client, cx);
client::init(&client, cx);
language::init(cx);
editor::init_settings(cx);
workspace::init(app_state.clone(), cx);
call::init(client.clone(), user_store.clone(), cx);
});
@@ -214,10 +211,13 @@ impl TestServer {
let client = TestClient {
client,
username: name.to_string(),
state: Default::default(),
local_projects: Default::default(),
remote_projects: Default::default(),
next_root_dir_id: 0,
user_store,
fs,
language_registry: Arc::new(LanguageRegistry::test()),
buffers: Default::default(),
};
client.wait_for_current_user(cx).await;
client
@@ -316,16 +316,12 @@ impl Drop for TestServer {
struct TestClient {
client: Arc<Client>,
username: String,
state: RefCell<TestClientState>,
local_projects: Vec<ModelHandle<Project>>,
remote_projects: Vec<ModelHandle<Project>>,
next_root_dir_id: usize,
pub user_store: ModelHandle<UserStore>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<FakeFs>,
}
#[derive(Default)]
struct TestClientState {
local_projects: Vec<ModelHandle<Project>>,
remote_projects: Vec<ModelHandle<Project>>,
buffers: HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>,
}
@@ -364,38 +360,6 @@ impl TestClient {
.await;
}
fn local_projects<'a>(&'a self) -> impl Deref<Target = Vec<ModelHandle<Project>>> + 'a {
Ref::map(self.state.borrow(), |state| &state.local_projects)
}
fn remote_projects<'a>(&'a self) -> impl Deref<Target = Vec<ModelHandle<Project>>> + 'a {
Ref::map(self.state.borrow(), |state| &state.remote_projects)
}
fn local_projects_mut<'a>(&'a self) -> impl DerefMut<Target = Vec<ModelHandle<Project>>> + 'a {
RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects)
}
fn remote_projects_mut<'a>(&'a self) -> impl DerefMut<Target = Vec<ModelHandle<Project>>> + 'a {
RefMut::map(self.state.borrow_mut(), |state| &mut state.remote_projects)
}
fn buffers_for_project<'a>(
&'a self,
project: &ModelHandle<Project>,
) -> impl DerefMut<Target = HashSet<ModelHandle<language::Buffer>>> + 'a {
RefMut::map(self.state.borrow_mut(), |state| {
state.buffers.entry(project.clone()).or_default()
})
}
fn buffers<'a>(
&'a self,
) -> impl DerefMut<Target = HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>> + 'a
{
RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers)
}
fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
self.user_store.read_with(cx, |store, _| ContactsSummary {
current: store
@@ -466,41 +430,17 @@ impl TestClient {
project: &ModelHandle<Project>,
cx: &mut TestAppContext,
) -> ViewHandle<Workspace> {
struct WorkspaceContainer {
workspace: Option<WeakViewHandle<Workspace>>,
}
let (_, root_view) = cx.add_window(|_| EmptyView);
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
}
impl Entity for WorkspaceContainer {
type Event = ();
}
impl View for WorkspaceContainer {
fn ui_name() -> &'static str {
"WorkspaceContainer"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(workspace) = self
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade(cx))
{
ChildView::new(&workspace, cx).into_any()
} else {
Empty::new().into_any()
}
}
}
// We use a workspace container so that we don't need to remove the window in order to
// drop the workspace and we can use a ViewHandle instead.
let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None });
let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx));
container.update(cx, |container, cx| {
container.workspace = Some(workspace.downgrade());
cx.notify();
});
workspace
fn create_new_root_dir(&mut self) -> PathBuf {
format!(
"/{}-root-{}",
self.username,
util::post_inc(&mut self.next_root_dir_id)
)
.into()
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -39,13 +39,11 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow.workspace = true
futures.workspace = true
log.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
mod collab_titlebar_item;
mod collaborator_list_popover;
mod contact_finder;
mod contact_list;
mod contact_notification;
@@ -9,27 +10,32 @@ mod notifications;
mod project_shared_notification;
mod sharing_status_indicator;
use anyhow::anyhow;
use call::ActiveCall;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
use gpui::{actions, AppContext, Task};
use gpui::{actions, MutableAppContext, Task};
use std::sync::Arc;
use workspace::AppState;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
actions!(collab, [ToggleScreenSharing]);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
collab_titlebar_item::init(cx);
contact_notification::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
contacts_popover::init(cx);
incoming_call_notification::init(&app_state, cx);
project_shared_notification::init(&app_state, cx);
incoming_call_notification::init(cx);
project_shared_notification::init(cx);
sharing_status_indicator::init(cx);
cx.add_global_action(toggle_screen_sharing);
cx.add_global_action(move |action: &JoinProject, cx| {
join_project(action, app_state.clone(), cx);
});
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut MutableAppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
@@ -41,3 +47,88 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
toggle_screen_sharing.detach_and_log_err(cx);
}
}
fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
let project_id = action.project_id;
let follow_user_id = action.follow_user_id;
cx.spawn(|mut cx| async move {
let existing_workspace = cx.update(|cx| {
cx.window_ids()
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace
} else {
let active_call = cx.read(ActiveCall::global);
let room = active_call
.read_with(&cx, |call, _| call.room().cloned())
.ok_or_else(|| anyhow!("not in a call"))?;
let project = room
.update(&mut cx, |room, cx| {
room.join_project(
project_id,
app_state.languages.clone(),
app_state.fs.clone(),
cx,
)
})
.await?;
let (_, workspace) = cx.add_window(
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|cx| {
let mut workspace = Workspace::new(
Default::default(),
0,
project,
app_state.dock_default_item_factory,
app_state.background_actions,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
},
);
workspace
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_being_followed(follow_peer_id) {
workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
}

View File

@@ -0,0 +1,165 @@
use call::ActiveCall;
use client::UserStore;
use gpui::Action;
use gpui::{
actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext,
};
use settings::Settings;
use crate::collab_titlebar_item::ToggleCollaboratorList;
pub(crate) enum Event {
Dismissed,
}
enum Collaborator {
SelfUser { username: String },
RemoteUser { username: String },
}
actions!(collaborator_list_popover, [NoOp]);
pub(crate) struct CollaboratorListPopover {
list_state: ListState,
}
impl Entity for CollaboratorListPopover {
type Event = Event;
}
impl View for CollaboratorListPopover {
fn ui_name() -> &'static str {
"CollaboratorListPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
MouseEventHandler::<Self>::new(0, cx, |_, _| {
List::new(self.list_state.clone())
.contained()
.with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
.boxed()
})
.on_down_out(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleCollaboratorList);
})
.boxed()
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
}
impl CollaboratorListPopover {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let active_call = ActiveCall::global(cx);
let mut collaborators = user_store
.read(cx)
.current_user()
.map(|u| Collaborator::SelfUser {
username: u.github_login.clone(),
})
.into_iter()
.collect::<Vec<_>>();
//TODO: What should the canonical sort here look like, consult contacts list implementation
if let Some(room) = active_call.read(cx).room() {
for participant in room.read(cx).remote_participants() {
collaborators.push(Collaborator::RemoteUser {
username: participant.1.user.github_login.clone(),
});
}
}
Self {
list_state: ListState::new(
collaborators.len(),
Orientation::Top,
0.,
cx,
move |_, index, cx| match &collaborators[index] {
Collaborator::SelfUser { username } => render_collaborator_list_entry(
index,
username,
None::<NoOp>,
None,
Svg::new("icons/chevron_right_12.svg"),
NoOp,
"Leave call".to_owned(),
cx,
),
Collaborator::RemoteUser { username } => render_collaborator_list_entry(
index,
username,
Some(NoOp),
Some(format!("Follow {username}")),
Svg::new("icons/x_mark_12.svg"),
NoOp,
format!("Remove {username} from call"),
cx,
),
},
),
}
}
}
fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
index: usize,
username: &str,
username_action: Option<UA>,
username_tooltip: Option<String>,
icon: Svg,
icon_action: IA,
icon_tooltip: String,
cx: &mut RenderContext<CollaboratorListPopover>,
) -> ElementBox {
enum Username {}
enum UsernameTooltip {}
enum Icon {}
enum IconTooltip {}
let theme = &cx.global::<Settings>().theme;
let username_theme = theme.contact_list.contact_username.text.clone();
let tooltip_theme = theme.tooltip.clone();
let username = MouseEventHandler::<Username>::new(index, cx, |_, _| {
Label::new(username.to_owned(), username_theme.clone()).boxed()
})
.on_click(MouseButton::Left, move |_, cx| {
if let Some(username_action) = username_action.clone() {
cx.dispatch_action(username_action);
}
});
Flex::row()
.with_child(if let Some(username_tooltip) = username_tooltip {
username
.with_tooltip::<UsernameTooltip, _>(
index,
username_tooltip,
None,
tooltip_theme.clone(),
cx,
)
.boxed()
} else {
username.boxed()
})
.with_child(
MouseEventHandler::<Icon>::new(index, cx, |_, _| icon.boxed())
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(icon_action.clone())
})
.with_tooltip::<IconTooltip, _>(index, icon_tooltip, None, tooltip_theme, cx)
.boxed(),
)
.boxed()
}

View File

@@ -1,41 +1,49 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
use picker::{Picker, PickerDelegate, PickerEvent};
use gpui::{
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, MutableAppContext,
RenderContext, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::sync::Arc;
use util::TryFutureExt;
pub fn init(cx: &mut AppContext) {
Picker::<ContactFinderDelegate>::init(cx);
pub fn init(cx: &mut MutableAppContext) {
Picker::<ContactFinder>::init(cx);
}
pub type ContactFinder = Picker<ContactFinderDelegate>;
pub fn build_contact_finder(
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<ContactFinder>,
) -> ContactFinder {
Picker::new(
ContactFinderDelegate {
user_store,
potential_contacts: Arc::from([]),
selected_index: 0,
},
cx,
)
.with_theme(|theme| theme.contact_finder.picker.clone())
}
pub struct ContactFinderDelegate {
pub struct ContactFinder {
picker: ViewHandle<Picker<Self>>,
potential_contacts: Arc<[Arc<User>]>,
user_store: ModelHandle<UserStore>,
selected_index: usize,
}
impl PickerDelegate for ContactFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
pub enum Event {
Dismissed,
}
impl Entity for ContactFinder {
type Event = Event;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for ContactFinder {
fn match_count(&self) -> usize {
self.potential_contacts.len()
}
@@ -44,22 +52,22 @@ impl PickerDelegate for ContactFinderDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
cx.spawn(|this, mut cx| async move {
async {
let potential_contacts = search_users.await?;
picker.update(&mut cx, |picker, cx| {
picker.delegate_mut().potential_contacts = potential_contacts.into();
this.update(&mut cx, |this, cx| {
this.potential_contacts = potential_contacts.into();
cx.notify();
})?;
});
anyhow::Ok(())
}
.log_err()
@@ -67,7 +75,7 @@ impl PickerDelegate for ContactFinderDelegate {
})
}
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
if let Some(user) = self.potential_contacts.get(self.selected_index) {
let user_store = self.user_store.read(cx);
match user_store.contact_request_status(user) {
@@ -86,8 +94,8 @@ impl PickerDelegate for ContactFinderDelegate {
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
fn render_match(
@@ -96,8 +104,8 @@ impl PickerDelegate for ContactFinderDelegate {
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let theme = &theme::current(cx);
) -> ElementBox {
let theme = &cx.global::<Settings>().theme;
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);
@@ -124,13 +132,15 @@ impl PickerDelegate for ContactFinderDelegate {
.with_style(theme.contact_finder.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(user.github_login.clone(), style.label.clone())
.contained()
.with_style(theme.contact_finder.contact_username)
.aligned()
.left(),
.left()
.boxed(),
)
.with_children(icon_path.map(|icon_path| {
Svg::new(icon_path)
@@ -145,11 +155,37 @@ impl PickerDelegate for ContactFinderDelegate {
.with_height(button_style.button_width)
.aligned()
.flex_float()
.boxed()
}))
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.contact_finder.row_height)
.into_any()
.boxed()
}
}
impl ContactFinder {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
Self {
picker: cx.add_view(|cx| {
Picker::new("Search collaborator by username...", this, cx)
.with_theme(|theme| theme.contact_finder.picker.clone())
}),
potential_contacts: Arc::from([]),
user_store,
selected_index: 0,
}
}
pub fn editor_text(&self, cx: &AppContext) -> String {
self.picker.read(cx).query(cx)
}
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
self.picker
.update(cx, |picker, cx| picker.set_query(editor_text, cx));
self
}
}

View File

@@ -1,3 +1,5 @@
use super::collab_titlebar_item::LeaveCall;
use crate::contacts_popover;
use call::ActiveCall;
use client::{proto::PeerId, Contact, User, UserStore};
use editor::{Cancel, Editor};
@@ -6,27 +8,40 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_actions,
impl_actions, impl_internal_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel},
AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, PromptLevel,
RenderContext, Subscription, View, ViewContext, ViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
use serde::Deserialize;
use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton;
use workspace::Workspace;
use workspace::{JoinProject, OpenSharedScreen};
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
pub fn init(cx: &mut AppContext) {
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactList::remove_contact);
cx.add_action(ContactList::respond_to_contact_request);
cx.add_action(ContactList::cancel);
cx.add_action(ContactList::select_next);
cx.add_action(ContactList::select_prev);
cx.add_action(ContactList::confirm);
cx.add_action(ContactList::toggle_expanded);
cx.add_action(ContactList::call);
}
#[derive(Clone, PartialEq)]
struct ToggleExpanded(Section);
#[derive(Clone, PartialEq)]
struct Call {
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
@@ -138,16 +153,14 @@ pub struct RespondToContactRequest {
}
pub enum Event {
ToggleContactFinder,
Dismissed,
}
pub struct ContactList {
entries: Vec<ContactEntry>,
match_candidates: Vec<StringMatchCandidate>,
list_state: ListState<Self>,
list_state: ListState,
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
filter_editor: ViewHandle<Editor>,
collapsed_sections: Vec<Section>,
@@ -159,7 +172,6 @@ impl ContactList {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let filter_editor = cx.add_view(|cx| {
@@ -190,8 +202,8 @@ impl ContactList {
})
.detach();
let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
let theme = theme::current(cx).clone();
let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
let theme = cx.global::<Settings>().theme.clone();
let is_selected = this.selection == Some(ix);
let current_project_id = this.project.read(cx).remote_id();
@@ -279,7 +291,6 @@ impl ContactList {
filter_editor,
_subscriptions: subscriptions,
project,
workspace,
user_store,
};
this.update_entries(cx);
@@ -305,20 +316,12 @@ impl ContactList {
github_login
);
let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
let window_id = cx.window_id();
cx.spawn(|_, mut cx| async move {
if answer.next().await == Some(0) {
if let Err(e) = user_store
user_store
.update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
.await
{
cx.prompt(
window_id,
PromptLevel::Info,
&format!("Failed to remove contact: {}", e),
&["Ok"],
);
}
.unwrap();
}
})
.detach();
@@ -392,11 +395,18 @@ impl ContactList {
if let Some(entry) = self.entries.get(selection) {
match entry {
ContactEntry::Header(section) => {
self.toggle_expanded(*section, cx);
let section = *section;
self.toggle_expanded(&ToggleExpanded(section), cx);
}
ContactEntry::Contact { contact, calling } => {
if contact.online && !contact.busy && !calling {
self.call(contact.user.id, Some(self.project.clone()), cx);
self.call(
&Call {
recipient_user_id: contact.user.id,
initial_project: Some(self.project.clone()),
},
cx,
);
}
}
ContactEntry::ParticipantProject {
@@ -404,23 +414,13 @@ impl ContactList {
host_user_id,
..
} => {
if let Some(workspace) = self.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(
*project_id,
*host_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
cx.dispatch_global_action(JoinProject {
project_id: *project_id,
follow_user_id: *host_user_id,
});
}
ContactEntry::ParticipantScreen { peer_id, .. } => {
if let Some(workspace) = self.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(*peer_id, cx)
});
}
cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
}
_ => {}
}
@@ -428,7 +428,8 @@ impl ContactList {
}
}
fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
let section = action.0;
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
self.collapsed_sections.remove(ix);
} else {
@@ -739,13 +740,14 @@ impl ContactList {
is_pending: bool,
is_selected: bool,
theme: &theme::ContactList,
) -> AnyElement<Self> {
) -> ElementBox {
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(
@@ -756,14 +758,16 @@ impl ContactList {
.with_style(theme.contact_username.container)
.aligned()
.left()
.flex(1., true),
.flex(1., true)
.boxed(),
)
.with_children(if is_pending {
Some(
Label::new("Calling", theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned(),
.aligned()
.boxed(),
)
} else {
None
@@ -776,7 +780,7 @@ impl ContactList {
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.into_any()
.boxed()
}
fn render_participant_project(
@@ -787,10 +791,8 @@ impl ContactList {
is_last: bool,
is_selected: bool,
theme: &theme::ContactList,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum JoinProject {}
cx: &mut RenderContext<Self>,
) -> ElementBox {
let font_cache = cx.font_cache();
let host_avatar_height = theme
.contact_avatar
@@ -809,44 +811,48 @@ impl ContactList {
worktree_root_names.join(", ")
};
MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
MouseEventHandler::<JoinProject>::new(project_id as usize, cx, |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
Flex::row()
.with_child(
Stack::new()
.with_child(Canvas::new(move |scene, bounds, _, _, _| {
let start_x =
bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
}))
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.boxed(),
)
.constrained()
.with_width(host_avatar_height),
.with_width(host_avatar_height)
.boxed(),
)
.with_child(
Label::new(project_name, row.name.text.clone())
@@ -854,28 +860,29 @@ impl ContactList {
.left()
.contained()
.with_style(row.name.container)
.flex(1., false),
.flex(1., false)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
})
.with_cursor_style(if !is_current {
CursorStyle::PointingHand
} else {
CursorStyle::Arrow
})
.on_click(MouseButton::Left, move |_, this, cx| {
.on_click(MouseButton::Left, move |_, cx| {
if !is_current {
if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx);
}
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: host_user_id,
});
}
})
.into_any()
.boxed()
}
fn render_participant_screen(
@@ -883,10 +890,8 @@ impl ContactList {
is_last: bool,
is_selected: bool,
theme: &theme::ContactList,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum OpenSharedScreen {}
cx: &mut RenderContext<Self>,
) -> ElementBox {
let font_cache = cx.font_cache();
let host_avatar_height = theme
.contact_avatar
@@ -900,7 +905,7 @@ impl ContactList {
let baseline_offset =
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
MouseEventHandler::<OpenSharedScreen, Self>::new(
MouseEventHandler::<OpenSharedScreen>::new(
peer_id.as_u64() as usize,
cx,
|mouse_state, _| {
@@ -910,37 +915,42 @@ impl ContactList {
Flex::row()
.with_child(
Stack::new()
.with_child(Canvas::new(move |scene, bounds, _, _, _| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y =
bounds.min_y() + baseline_offset - (cap_height / 2.);
scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
}))
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.boxed(),
)
.constrained()
.with_width(host_avatar_height),
.with_width(host_avatar_height)
.boxed(),
)
.with_child(
Svg::new("icons/disable_screen_sharing_12.svg")
@@ -950,7 +960,8 @@ impl ContactList {
.aligned()
.left()
.contained()
.with_style(row.icon.container),
.with_style(row.icon.container)
.boxed(),
)
.with_child(
Label::new("Screen", row.name.text.clone())
@@ -958,23 +969,21 @@ impl ContactList {
.left()
.contained()
.with_style(row.name.container)
.flex(1., false),
.flex(1., false)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(peer_id, cx)
});
}
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(OpenSharedScreen { peer_id });
})
.into_any()
.boxed()
}
fn render_header(
@@ -982,8 +991,8 @@ impl ContactList {
theme: &theme::ContactList,
is_selected: bool,
is_collapsed: bool,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
cx: &mut RenderContext<Self>,
) -> ElementBox {
enum Header {}
enum LeaveCallContactList {}
@@ -998,25 +1007,23 @@ impl ContactList {
};
let leave_call = if section == Section::ActiveCall {
Some(
MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
let style = theme.leave_call.style_for(state, false);
Label::new("Leave Call", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, |_, _, cx| {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
})
.aligned(),
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(LeaveCall))
.aligned()
.boxed(),
)
} else {
None
};
let icon_size = theme.section_icon_size;
MouseEventHandler::<Header, Self>::new(section as usize, cx, |_, _| {
MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
Flex::row()
.with_child(
Svg::new(if is_collapsed {
@@ -1030,7 +1037,8 @@ impl ContactList {
.with_max_height(icon_size)
.aligned()
.constrained()
.with_width(icon_size),
.with_width(icon_size)
.boxed(),
)
.with_child(
Label::new(text, header_style.text.clone())
@@ -1038,19 +1046,21 @@ impl ContactList {
.left()
.contained()
.with_margin_left(theme.contact_username.container.margin.left)
.flex(1., true),
.flex(1., true)
.boxed(),
)
.with_children(leave_call)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(header_style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_expanded(section, cx);
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleExpanded(section))
})
.into_any()
.boxed()
}
fn render_contact(
@@ -1059,15 +1069,15 @@ impl ContactList {
project: &ModelHandle<Project>,
theme: &theme::ContactList,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
cx: &mut RenderContext<Self>,
) -> ElementBox {
let online = contact.online;
let busy = contact.busy || calling;
let user_id = contact.user.id;
let github_login = contact.user.github_login.clone();
let initial_project = project.clone();
let mut event_handler =
MouseEventHandler::<Contact, Self>::new(contact.user.id as usize, cx, |_, cx| {
let mut element =
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| {
let status_badge = if contact.online {
@@ -1080,7 +1090,8 @@ impl ContactList {
} else {
theme.contact_status_free
})
.aligned(),
.aligned()
.boxed(),
)
} else {
None
@@ -1090,9 +1101,11 @@ impl ContactList {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left(),
.left()
.boxed(),
)
.with_children(status_badge)
.boxed()
}))
.with_child(
Label::new(
@@ -1103,10 +1116,11 @@ impl ContactList {
.with_style(theme.contact_username.container)
.aligned()
.left()
.flex(1., true),
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Cancel, Self>::new(
MouseEventHandler::<Cancel>::new(
contact.user.id as usize,
cx,
|mouse_state, _| {
@@ -1115,27 +1129,27 @@ impl ContactList {
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.flex_float()
.boxed()
},
)
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.remove_contact(
&RemoveContact {
user_id,
github_login: github_login.clone(),
},
cx,
);
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RemoveContact {
user_id,
github_login: github_login.clone(),
})
})
.flex_float(),
.flex_float()
.boxed(),
)
.with_children(if calling {
Some(
Label::new("Calling", theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned(),
.aligned()
.boxed(),
)
} else {
None
@@ -1148,18 +1162,22 @@ impl ContactList {
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.boxed()
})
.on_click(MouseButton::Left, move |_, this, cx| {
.on_click(MouseButton::Left, move |_, cx| {
if online && !busy {
this.call(user_id, Some(initial_project.clone()), cx);
cx.dispatch_action(Call {
recipient_user_id: user_id,
initial_project: Some(initial_project.clone()),
});
}
});
if online {
event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
element = element.with_cursor_style(CursorStyle::PointingHand);
}
event_handler.into_any()
element.boxed()
}
fn render_contact_request(
@@ -1168,8 +1186,8 @@ impl ContactList {
theme: &theme::ContactList,
is_incoming: bool,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
cx: &mut RenderContext<Self>,
) -> ElementBox {
enum Decline {}
enum Accept {}
enum Cancel {}
@@ -1180,6 +1198,7 @@ impl ContactList {
.with_style(theme.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(
@@ -1190,7 +1209,8 @@ impl ContactList {
.with_style(theme.contact_username.container)
.aligned()
.left()
.flex(1., true),
.flex(1., true)
.boxed(),
);
let user_id = user.id;
@@ -1199,31 +1219,28 @@ impl ContactList {
let button_spacing = theme.contact_button_spacing;
if is_incoming {
row.add_child(
MouseEventHandler::<Decline, Self>::new(user.id as usize, cx, |mouse_state, _| {
row.add_children([
MouseEventHandler::<Decline>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
theme.contact_button.style_for(mouse_state, false)
};
render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.respond_to_contact_request(
&RespondToContactRequest {
user_id,
accept: false,
},
cx,
);
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RespondToContactRequest {
user_id,
accept: false,
})
})
.contained()
.with_margin_right(button_spacing),
);
row.add_child(
MouseEventHandler::<Accept, Self>::new(user.id as usize, cx, |mouse_state, _| {
.with_margin_right(button_spacing)
.boxed(),
MouseEventHandler::<Accept>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
@@ -1232,21 +1249,20 @@ impl ContactList {
render_icon_button(button_style, "icons/check_8.svg")
.aligned()
.flex_float()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.respond_to_contact_request(
&RespondToContactRequest {
user_id,
accept: true,
},
cx,
);
}),
);
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RespondToContactRequest {
user_id,
accept: true,
})
})
.boxed(),
]);
} else {
row.add_child(
MouseEventHandler::<Cancel, Self>::new(user.id as usize, cx, |mouse_state, _| {
MouseEventHandler::<Cancel>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
@@ -1255,19 +1271,18 @@ impl ContactList {
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.flex_float()
.boxed()
})
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.remove_contact(
&RemoveContact {
user_id,
github_login: github_login.clone(),
},
cx,
);
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RemoveContact {
user_id,
github_login: github_login.clone(),
})
})
.flex_float(),
.flex_float()
.boxed(),
);
}
@@ -1279,15 +1294,12 @@ impl ContactList {
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.into_any()
.boxed()
}
fn call(
&mut self,
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
cx: &mut ViewContext<Self>,
) {
fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
let recipient_user_id = action.recipient_user_id;
let initial_project = action.initial_project.clone();
ActiveCall::global(cx)
.update(cx, |call, cx| {
call.invite(recipient_user_id, initial_project, cx)
@@ -1305,48 +1317,53 @@ impl View for ContactList {
"ContactList"
}
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.add_identifier("menu");
cx
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
enum AddContact {}
let theme = theme::current(cx).clone();
let theme = cx.global::<Settings>().theme.clone();
Flex::column()
.with_child(
Flex::row()
.with_child(
ChildView::new(&self.filter_editor, cx)
ChildView::new(self.filter_editor.clone(), cx)
.contained()
.with_style(theme.contact_list.user_query_editor.container)
.flex(1., true),
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<AddContact, Self>::new(0, cx, |_, _| {
MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
render_icon_button(
&theme.contact_list.add_contact_button,
"icons/user_plus_16.svg",
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
cx.emit(Event::ToggleContactFinder)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(contacts_popover::ToggleContactFinder)
})
.with_tooltip::<AddContact>(
.with_tooltip::<AddContact, _>(
0,
"Search for new contact".into(),
None,
theme.tooltip.clone(),
cx,
),
)
.boxed(),
)
.constrained()
.with_height(theme.contact_list.user_query_editor_height),
.with_height(theme.contact_list.user_query_editor_height)
.boxed(),
)
.with_child(List::new(self.list_state.clone()).flex(1., false))
.into_any()
.with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
.boxed()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -1362,7 +1379,7 @@ impl View for ContactList {
}
}
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<ContactList> {
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
Svg::new(svg_path)
.with_color(style.color)
.constrained()

View File

@@ -2,9 +2,19 @@ use std::sync::Arc;
use crate::notifications::render_user_notification;
use client::{ContactEventKind, User, UserStore};
use gpui::{elements::*, Entity, ModelHandle, View, ViewContext};
use gpui::{
elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext,
View, ViewContext,
};
use workspace::notifications::Notification;
impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactNotification::dismiss);
cx.add_action(ContactNotification::respond_to_contact_request);
}
pub struct ContactNotification {
user_store: ModelHandle<UserStore>,
user: Arc<User>,
@@ -33,24 +43,26 @@ impl View for ContactNotification {
"ContactNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
match self.kind {
ContactEventKind::Requested => render_user_notification(
self.user.clone(),
"wants to add you as a contact",
Some("They won't be alerted if you decline."),
|notification, cx| notification.dismiss(cx),
Dismiss(self.user.id),
vec![
(
"Decline",
Box::new(|notification, cx| {
notification.respond_to_contact_request(false, cx)
Box::new(RespondToContactRequest {
user_id: self.user.id,
accept: false,
}),
),
(
"Accept",
Box::new(|notification, cx| {
notification.respond_to_contact_request(true, cx)
Box::new(RespondToContactRequest {
user_id: self.user.id,
accept: true,
}),
),
],
@@ -60,7 +72,7 @@ impl View for ContactNotification {
self.user.clone(),
"accepted your contact request",
None,
|notification, cx| notification.dismiss(cx),
Dismiss(self.user.id),
vec![],
cx,
),
@@ -102,7 +114,7 @@ impl ContactNotification {
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
self.user_store.update(cx, |store, cx| {
store
.dismiss_contact_request(self.user.id, cx)
@@ -111,10 +123,14 @@ impl ContactNotification {
cx.emit(Event::Dismiss);
}
fn respond_to_contact_request(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
fn respond_to_contact_request(
&mut self,
action: &RespondToContactRequest,
cx: &mut ViewContext<Self>,
) {
self.user_store
.update(cx, |store, cx| {
store.respond_to_contact_request(self.user.id, accept, cx)
store.respond_to_contact_request(action.user_id, action.accept, cx)
})
.detach();
}

View File

@@ -1,19 +1,15 @@
use crate::{
contact_finder::{build_contact_finder, ContactFinder},
contact_list::ContactList,
};
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
use client::UserStore;
use gpui::{
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
ViewContext, ViewHandle, WeakViewHandle,
actions, elements::*, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
use picker::PickerEvent;
use project::Project;
use workspace::Workspace;
use settings::Settings;
actions!(contacts_popover, [ToggleContactFinder]);
pub fn init(cx: &mut AppContext) {
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPopover::toggle_contact_finder);
}
@@ -30,7 +26,6 @@ pub struct ContactsPopover {
child: Child,
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
_subscription: Option<gpui::Subscription>,
}
@@ -38,16 +33,14 @@ impl ContactsPopover {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
child: Child::ContactList(cx.add_view(|cx| {
ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx)
})),
child: Child::ContactList(
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
),
project,
user_store,
workspace,
_subscription: None,
};
this.show_contact_list(String::new(), cx);
@@ -57,19 +50,19 @@ impl ContactsPopover {
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child {
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), cx),
Child::ContactFinder(finder) => {
self.show_contact_list(finder.read(cx).editor_text(cx), cx)
}
}
}
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| {
let finder = build_contact_finder(self.user_store.clone(), cx);
finder.set_query(editor_text, cx);
finder
ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx)
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
PickerEvent::Dismiss => cx.emit(Event::Dismissed),
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactFinder(child);
cx.notify();
@@ -77,20 +70,12 @@ impl ContactsPopover {
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| {
ContactList::new(
self.project.clone(),
self.user_store.clone(),
self.workspace.clone(),
cx,
)
.with_editor_text(editor_text, cx)
ContactList::new(self.project.clone(), self.user_store.clone(), cx)
.with_editor_text(editor_text, cx)
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
crate::contact_list::Event::ToggleContactFinder => {
this.toggle_contact_finder(&Default::default(), cx)
}
}));
self.child = Child::ContactList(child);
cx.notify();
@@ -106,24 +91,27 @@ impl View for ContactsPopover {
"ContactsPopover"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = theme::current(cx).clone();
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let child = match &self.child {
Child::ContactList(child) => ChildView::new(child, cx),
Child::ContactFinder(child) => ChildView::new(child, cx),
};
MouseEventHandler::<ContactsPopover, Self>::new(0, cx, |_, _| {
MouseEventHandler::<ContactsPopover>::new(0, cx, |_, _| {
Flex::column()
.with_child(child.flex(1., true))
.with_child(child.flex(1., true).boxed())
.contained()
.with_style(theme.contacts_popover.container)
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
.boxed()
})
.on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
.into_any()
.on_down_out(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleContactsMenu);
})
.boxed()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {

View File

@@ -7,14 +7,12 @@ use gpui::{
},
json::ToJson,
serde_json::{self, json},
AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext,
Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
};
use crate::CollabTitlebarItem;
pub(crate) struct FacePile {
overlap: f32,
faces: Vec<AnyElement<CollabTitlebarItem>>,
faces: Vec<ElementBox>,
}
impl FacePile {
@@ -26,21 +24,20 @@ impl FacePile {
}
}
impl Element<CollabTitlebarItem> for FacePile {
impl Element for FacePile {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
view: &mut CollabTitlebarItem,
cx: &mut LayoutContext<CollabTitlebarItem>,
cx: &mut gpui::LayoutContext,
) -> (Vector2F, Self::LayoutState) {
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
let mut width = 0.;
for face in &mut self.faces {
width += face.layout(constraint, view, cx).x();
width += face.layout(constraint, cx).x();
}
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
@@ -49,12 +46,10 @@ impl Element<CollabTitlebarItem> for FacePile {
fn paint(
&mut self,
scene: &mut SceneBuilder,
bounds: RectF,
visible_bounds: RectF,
_layout: &mut Self::LayoutState,
view: &mut CollabTitlebarItem,
cx: &mut ViewContext<CollabTitlebarItem>,
cx: &mut PaintContext,
) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
@@ -64,8 +59,8 @@ impl Element<CollabTitlebarItem> for FacePile {
for face in self.faces.iter_mut().rev() {
let size = face.size();
origin_x -= size.x();
scene.paint_layer(None, |scene| {
face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
cx.paint_layer(None, |cx| {
face.paint(vec2f(origin_x, origin_y), visible_bounds, cx);
});
origin_x += self.overlap;
}
@@ -80,8 +75,7 @@ impl Element<CollabTitlebarItem> for FacePile {
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &CollabTitlebarItem,
_: &ViewContext<CollabTitlebarItem>,
_: &MeasurementContext,
) -> Option<RectF> {
None
}
@@ -91,8 +85,7 @@ impl Element<CollabTitlebarItem> for FacePile {
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &CollabTitlebarItem,
_: &ViewContext<CollabTitlebarItem>,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "FacePile",
@@ -101,8 +94,8 @@ impl Element<CollabTitlebarItem> for FacePile {
}
}
impl Extend<AnyElement<CollabTitlebarItem>> for FacePile {
fn extend<T: IntoIterator<Item = AnyElement<CollabTitlebarItem>>>(&mut self, children: T) {
impl Extend<ElementBox> for FacePile {
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
self.faces.extend(children);
}
}

View File

@@ -1,19 +1,21 @@
use std::sync::{Arc, Weak};
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AnyElement, AppContext, Entity, View, ViewContext,
impl_internal_actions, CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext,
View, ViewContext, WindowBounds, WindowKind, WindowOptions,
};
use settings::Settings;
use util::ResultExt;
use workspace::AppState;
use workspace::JoinProject;
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(IncomingCallNotification::respond_to_call);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_windows = Vec::new();
@@ -25,7 +27,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
if let Some(incoming_call) = incoming_call {
const PADDING: f32 = 16.;
let window_size = cx.read(|cx| {
let theme = &theme::current(cx).incoming_call_notification;
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
@@ -41,12 +43,11 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
titlebar: None,
center: false,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
screen: Some(screen),
},
|_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
|_| IncomingCallNotification::new(incoming_call.clone()),
);
notification_windows.push(window_id);
@@ -64,40 +65,32 @@ struct RespondToCall {
pub struct IncomingCallNotification {
call: IncomingCall,
app_state: Weak<AppState>,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state }
pub fn new(call: IncomingCall) -> Self {
Self { call }
}
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if accept {
if action.accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
let app_state = self.app_state.clone();
cx.app_context()
.spawn(|mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
if let Some(app_state) = app_state.upgrade() {
workspace::join_remote_project(
project_id,
caller_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
cx.spawn_weak(|_, mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: caller_user_id,
})
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, _| {
active_call.decline_incoming().log_err();
@@ -105,8 +98,8 @@ impl IncomingCallNotification {
}
}
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).incoming_call_notification;
fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
.call
@@ -118,6 +111,7 @@ impl IncomingCallNotification {
Image::from_data(avatar)
.with_style(theme.caller_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
@@ -127,7 +121,8 @@ impl IncomingCallNotification {
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container),
.with_style(theme.caller_username.container)
.boxed(),
)
.with_child(
Label::new(
@@ -142,7 +137,8 @@ impl IncomingCallNotification {
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container),
.with_style(theme.caller_message.container)
.boxed(),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
@@ -153,56 +149,66 @@ impl IncomingCallNotification {
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
.with_style(theme.worktree_roots.container)
.boxed(),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned(),
.aligned()
.boxed(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.into_any()
.boxed()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Accept {}
enum Decline {}
let theme = theme::current(cx);
Flex::column()
.with_child(
MouseEventHandler::<Accept, Self>::new(0, cx, |_, _| {
let theme = &theme.incoming_call_notification;
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Accept", theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(true, cx);
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: true });
})
.flex(1., true),
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Decline, Self>::new(0, cx, |_, _| {
let theme = &theme.incoming_call_notification;
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Decline", theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(false, cx);
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: false });
})
.flex(1., true),
.flex(1., true)
.boxed(),
)
.constrained()
.with_width(theme.incoming_call_notification.button_width)
.into_any()
.with_width(
cx.global::<Settings>()
.theme
.incoming_call_notification
.button_width,
)
.boxed()
}
}
@@ -215,14 +221,19 @@ impl View for IncomingCallNotification {
"IncomingCallNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let background = theme::current(cx).incoming_call_notification.background;
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let background = cx
.global::<Settings>()
.theme
.incoming_call_notification
.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
.boxed()
}
}

View File

@@ -1,27 +1,23 @@
use client::User;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
AnyElement, Element, View, ViewContext,
elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
View,
};
use settings::Settings;
use std::sync::Arc;
enum Dismiss {}
enum Button {}
pub fn render_user_notification<F, V>(
pub fn render_user_notification<V: View, A: Action + Clone>(
user: Arc<User>,
title: &'static str,
body: Option<&'static str>,
on_dismiss: F,
buttons: Vec<(&'static str, Box<dyn Fn(&mut V, &mut ViewContext<V>)>)>,
cx: &mut ViewContext<V>,
) -> AnyElement<V>
where
F: 'static + Fn(&mut V, &mut ViewContext<V>),
V: View,
{
let theme = theme::current(cx).clone();
dismiss_action: A,
buttons: Vec<(&'static str, Box<dyn Action>)>,
cx: &mut RenderContext<V>,
) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.contact_notification;
Flex::column()
@@ -38,6 +34,7 @@ where
)
.aligned()
.top()
.boxed()
}))
.with_child(
Text::new(
@@ -49,10 +46,11 @@ where
.aligned()
.top()
.left()
.flex(1., true),
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
@@ -64,10 +62,13 @@ where
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, view, cx| on_dismiss(view, cx))
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_any_action(dismiss_action.boxed_clone())
})
.aligned()
.constrained()
.with_height(
@@ -76,14 +77,16 @@ where
)
.aligned()
.top()
.flex_float(),
.flex_float()
.boxed(),
)
.into_any_named("contact notification header"),
.named("contact notification header"),
)
.with_children(body.map(|body| {
Label::new(body, theme.body_message.text.clone())
.contained()
.with_style(theme.body_message.container)
.boxed()
}))
.with_children(if buttons.is_empty() {
None
@@ -91,21 +94,26 @@ where
Some(
Flex::row()
.with_children(buttons.into_iter().enumerate().map(
|(ix, (message, handler))| {
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
|(ix, (message, action))| {
MouseEventHandler::<Button>::new(ix, cx, |state, _| {
let button = theme.button.style_for(state, false);
Label::new(message, button.text.clone())
.contained()
.with_style(button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| handler(view, cx))
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_any_action(action.boxed_clone())
})
.boxed()
},
))
.aligned()
.right(),
.right()
.boxed(),
)
})
.contained()
.into_any()
.boxed()
}

View File

@@ -2,16 +2,22 @@ use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AppContext, Entity, View, ViewContext,
CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
WindowBounds, WindowKind, WindowOptions,
};
use std::sync::{Arc, Weak};
use workspace::AppState;
use settings::Settings;
use std::sync::Arc;
use workspace::JoinProject;
actions!(project_shared_notification, [DismissProject]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectSharedNotification::join);
cx.add_action(ProjectSharedNotification::dismiss);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
@@ -21,7 +27,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
worktree_root_names,
} => {
const PADDING: f32 = 16.;
let theme = &theme::current(cx).project_shared_notification;
let theme = &cx.global::<Settings>().theme.project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() {
@@ -35,7 +41,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
titlebar: None,
center: false,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
screen: Some(screen),
@@ -45,7 +50,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
},
);
@@ -58,14 +62,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
room::Event::RemoteProjectUnshared { project_id } => {
if let Some(window_ids) = notification_windows.remove(&project_id) {
for window_id in window_ids {
cx.update_window(window_id, |cx| cx.remove_window());
cx.remove_window(window_id);
}
}
}
room::Event::Left => {
for (_, window_ids) in notification_windows.drain() {
for window_id in window_ids {
cx.update_window(window_id, |cx| cx.remove_window());
cx.remove_window(window_id);
}
}
}
@@ -78,43 +82,36 @@ pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
app_state: Weak<AppState>,
}
impl ProjectSharedNotification {
fn new(
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
app_state: Weak<AppState>,
) -> Self {
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
Self {
project_id,
worktree_root_names,
owner,
app_state,
}
}
fn join(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
}
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
cx.propagate_action();
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
}
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).project_shared_notification;
fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.owner_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
@@ -124,7 +121,8 @@ impl ProjectSharedNotification {
theme.owner_username.text.clone(),
)
.contained()
.with_style(theme.owner_username.container),
.with_style(theme.owner_username.container)
.boxed(),
)
.with_child(
Label::new(
@@ -139,7 +137,8 @@ impl ProjectSharedNotification {
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container),
.with_style(theme.message.container)
.boxed(),
)
.with_children(if self.worktree_root_names.is_empty() {
None
@@ -150,54 +149,72 @@ impl ProjectSharedNotification {
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
.with_style(theme.worktree_roots.container)
.boxed(),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned(),
.aligned()
.boxed(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.into_any()
.boxed()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Open {}
enum Dismiss {}
let theme = theme::current(cx);
let project_id = self.project_id;
let owner_user_id = self.owner.id;
Flex::column()
.with_child(
MouseEventHandler::<Open, Self>::new(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
.flex(1., true),
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: owner_user_id,
});
})
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.dismiss(cx);
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(DismissProject);
})
.flex(1., true),
.flex(1., true)
.boxed(),
)
.constrained()
.with_width(theme.project_shared_notification.button_width)
.into_any()
.with_width(
cx.global::<Settings>()
.theme
.project_shared_notification
.button_width,
)
.boxed()
}
}
@@ -210,14 +227,18 @@ impl View for ProjectSharedNotification {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
let background = theme::current(cx).project_shared_notification.background;
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let background = cx
.global::<Settings>()
.theme
.project_shared_notification
.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
.boxed()
}
}

View File

@@ -1,30 +1,26 @@
use crate::toggle_screen_sharing;
use call::ActiveCall;
use gpui::{
color::Color,
elements::{MouseEventHandler, Svg},
platform::{Appearance, MouseButton},
AnyElement, AppContext, Element, Entity, View, ViewContext,
Appearance, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View,
};
use workspace::WorkspaceSettings;
use settings::Settings;
pub fn init(cx: &mut AppContext) {
use crate::ToggleScreenSharing;
pub fn init(cx: &mut MutableAppContext) {
let active_call = ActiveCall::global(cx);
let mut status_indicator = None;
cx.observe(&active_call, move |call, cx| {
if let Some(room) = call.read(cx).room() {
if room.read(cx).is_screen_sharing() {
if status_indicator.is_none()
&& settings::get::<WorkspaceSettings>(cx).show_call_status_icon
{
if status_indicator.is_none() && cx.global::<Settings>().show_call_status_icon {
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
}
} else if let Some((window_id, _)) = status_indicator.take() {
cx.update_window(window_id, |cx| cx.remove_window());
cx.remove_status_bar_item(window_id);
}
} else if let Some((window_id, _)) = status_indicator.take() {
cx.update_window(window_id, |cx| cx.remove_window());
}
})
.detach();
@@ -41,22 +37,23 @@ impl View for SharingStatusIndicator {
"SharingStatusIndicator"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let color = match cx.window_appearance() {
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let color = match cx.appearance {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
MouseEventHandler::<Self, Self>::new(0, cx, |_, _| {
MouseEventHandler::<Self>::new(0, cx, |_, _| {
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(color)
.constrained()
.with_width(18.)
.aligned()
.boxed()
})
.on_click(MouseButton::Left, |_, _, cx| {
toggle_screen_sharing(&Default::default(), cx)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(ToggleScreenSharing);
})
.into_any()
.boxed()
}
}

View File

@@ -24,10 +24,3 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
pub type HashSet<T> = std::collections::HashSet<T>;
pub use std::collections::*;
// NEW TYPES
#[derive(Default)]
pub struct CommandPaletteFilter {
pub filtered_namespaces: HashSet<&'static str>,
}

View File

@@ -23,9 +23,8 @@ workspace = { path = "../workspace" }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
serde_json.workspace = true
serde_json = { version = "1.0", features = ["preserve_order"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
ctor = "0.1"
env_logger = "0.9"

View File

@@ -1,24 +1,31 @@
use collections::CommandPaletteFilter;
use collections::HashSet;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState,
ViewContext,
actions,
elements::{ChildView, Flex, Label, ParentElement},
keymap_matcher::Keystroke,
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::cmp;
use util::ResultExt;
use workspace::Workspace;
pub fn init(cx: &mut AppContext) {
cx.add_action(toggle_command_palette);
CommandPalette::init(cx);
#[derive(Default)]
pub struct CommandPaletteFilter {
pub filtered_namespaces: HashSet<&'static str>,
}
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CommandPalette::toggle);
Picker::<CommandPalette>::init(cx);
}
actions!(command_palette, [Toggle]);
pub type CommandPalette = Picker<CommandPaletteDelegate>;
pub struct CommandPaletteDelegate {
pub struct CommandPalette {
picker: ViewHandle<Picker<Self>>,
actions: Vec<Command>,
matches: Vec<StringMatch>,
selected_ix: usize,
@@ -40,29 +47,104 @@ struct Command {
keystrokes: Vec<Keystroke>,
}
fn toggle_command_palette(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| cx.view_id());
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id), cx))
});
}
impl CommandPalette {
pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
let actions = cx
.available_actions(cx.window_id(), focused_view_id)
.filter_map(|(name, action, bindings)| {
if cx.has_global::<CommandPaletteFilter>() {
let filter = cx.global::<CommandPaletteFilter>();
if filter.filtered_namespaces.contains(action.namespace()) {
return None;
}
}
impl CommandPaletteDelegate {
pub fn new(focused_view_id: usize) -> Self {
Some(Command {
name: humanize_action_name(name),
action,
keystrokes: bindings
.iter()
.map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
})
.collect();
let picker = cx.add_view(|cx| Picker::new("Execute a command...", this, cx));
Self {
actions: Default::default(),
picker,
actions,
matches: vec![],
selected_ix: 0,
focused_view_id,
}
}
}
impl PickerDelegate for CommandPaletteDelegate {
fn placeholder_text(&self) -> std::sync::Arc<str> {
"Execute a command...".into()
fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let workspace = cx.handle();
let window_id = cx.window_id();
let focused_view_id = cx
.focused_view_id(window_id)
.unwrap_or_else(|| workspace.id());
cx.as_mut().defer(move |cx| {
let this = cx.add_view(workspace.clone(), |cx| Self::new(focused_view_id, cx));
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| {
cx.subscribe(&this, Self::on_event).detach();
this
});
});
});
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => workspace.dismiss_modal(cx),
Event::Confirmed {
window_id,
focused_view_id,
action,
} => {
let window_id = *window_id;
let focused_view_id = *focused_view_id;
let action = action.boxed_clone();
workspace.dismiss_modal(cx);
cx.as_mut()
.defer(move |cx| cx.dispatch_any_action_at(window_id, focused_view_id, action))
}
}
}
}
impl Entity for CommandPalette {
type Event = Event;
}
impl View for CommandPalette {
fn ui_name() -> &'static str {
"CommandPalette"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for CommandPalette {
fn match_count(&self) -> usize {
self.matches.len()
}
@@ -71,55 +153,26 @@ impl PickerDelegate for CommandPaletteDelegate {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
self.selected_ix = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<Picker<Self>>,
cx: &mut gpui::ViewContext<Self>,
) -> gpui::Task<()> {
let window_id = cx.window_id();
let view_id = self.focused_view_id;
cx.spawn(move |picker, mut cx| async move {
let actions = cx
.available_actions(window_id, view_id)
.into_iter()
.filter_map(|(name, action, bindings)| {
let filtered = cx.read(|cx| {
if cx.has_global::<CommandPaletteFilter>() {
let filter = cx.global::<CommandPaletteFilter>();
filter.filtered_namespaces.contains(action.namespace())
} else {
false
}
});
if filtered {
None
} else {
Some(Command {
name: humanize_action_name(name),
action,
keystrokes: bindings
.iter()
.map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
}
})
.collect::<Vec<_>>();
let candidates = actions
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
let candidates = self
.actions
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
let matches = if query.is_empty() {
candidates
.into_iter()
@@ -142,37 +195,32 @@ impl PickerDelegate for CommandPaletteDelegate {
)
.await
};
picker
.update(&mut cx, |picker, _| {
let delegate = picker.delegate_mut();
delegate.actions = actions;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_ix = 0;
} else {
delegate.selected_ix =
cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
}
})
.log_err();
this.update(&mut cx, |this, _| {
this.matches = matches;
if this.matches.is_empty() {
this.selected_ix = 0;
} else {
this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1);
}
});
})
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
if !self.matches.is_empty() {
let window_id = cx.window_id();
let focused_view_id = self.focused_view_id;
let action_ix = self.matches[self.selected_ix].candidate_id;
let action = self.actions.remove(action_ix).action;
cx.app_context()
.spawn(move |mut cx| async move {
cx.dispatch_action(window_id, focused_view_id, action.as_ref())
})
.detach_and_log_err(cx);
cx.emit(Event::Confirmed {
window_id: cx.window_id(),
focused_view_id: self.focused_view_id,
action: self.actions.remove(action_ix).action,
});
} else {
cx.emit(Event::Dismissed);
}
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
@@ -181,10 +229,11 @@ impl PickerDelegate for CommandPaletteDelegate {
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
) -> gpui::ElementBox {
let mat = &self.matches[ix];
let command = &self.actions[mat.candidate_id];
let theme = theme::current(cx);
let settings = cx.global::<Settings>();
let theme = &settings.theme;
let style = theme.picker.item.style_for(mouse_state, selected);
let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
let keystroke_spacing = theme.command_palette.keystroke_spacing;
@@ -192,7 +241,8 @@ impl PickerDelegate for CommandPaletteDelegate {
Flex::row()
.with_child(
Label::new(mat.string.clone(), style.label.clone())
.with_highlights(mat.positions.clone()),
.with_highlights(mat.positions.clone())
.boxed(),
)
.with_children(command.keystrokes.iter().map(|keystroke| {
Flex::row()
@@ -209,7 +259,8 @@ impl PickerDelegate for CommandPaletteDelegate {
Some(
Label::new(label, key_style.label.clone())
.contained()
.with_style(key_style.container),
.with_style(key_style.container)
.boxed(),
)
} else {
None
@@ -219,15 +270,17 @@ impl PickerDelegate for CommandPaletteDelegate {
.with_child(
Label::new(keystroke.key.clone(), key_style.label.clone())
.contained()
.with_style(key_style.container),
.with_style(key_style.container)
.boxed(),
)
.contained()
.with_margin_left(keystroke_spacing)
.flex_float()
.boxed()
}))
.contained()
.with_style(style.container)
.into_any()
.boxed()
}
}
@@ -266,11 +319,9 @@ impl std::fmt::Debug for Command {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use editor::Editor;
use gpui::{executor::Deterministic, TestAppContext};
use gpui::TestAppContext;
use project::Project;
use workspace::{AppState, Workspace};
@@ -291,24 +342,30 @@ mod tests {
}
#[gpui::test]
async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
let app_state = init_test(cx);
async fn test_command_palette(cx: &mut TestAppContext) {
let app_state = cx.update(AppState::test);
cx.update(|cx| {
editor::init(cx);
workspace::init(app_state.clone(), cx);
init(cx);
});
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.add_view(window_id, |cx| {
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.add_view(&workspace, |cx| {
let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx);
editor
});
workspace.update(cx, |workspace, cx| {
cx.focus(&editor);
cx.focus(editor.clone());
workspace.add_item(Box::new(editor.clone()), cx)
});
workspace.update(cx, |workspace, cx| {
toggle_command_palette(workspace, &Toggle, cx);
CommandPalette::toggle(workspace, &Toggle, cx)
});
let palette = workspace.read_with(cx, |workspace, _| {
@@ -317,17 +374,15 @@ mod tests {
palette
.update(cx, |palette, cx| {
palette
.delegate_mut()
.update_matches("bcksp".to_string(), cx)
palette.update_matches("bcksp".to_string(), cx)
})
.await;
palette.update(cx, |palette, cx| {
assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
palette.confirm(&Default::default(), cx);
assert_eq!(palette.matches[0].string, "editor: backspace");
palette.confirm(cx);
});
deterministic.run_until_parked();
editor.read_with(cx, |editor, cx| {
assert_eq!(editor.text(cx), "ab");
});
@@ -340,7 +395,7 @@ mod tests {
});
workspace.update(cx, |workspace, cx| {
toggle_command_palette(workspace, &Toggle, cx);
CommandPalette::toggle(workspace, &Toggle, cx);
});
// Assert editor command not present
@@ -350,26 +405,10 @@ mod tests {
palette
.update(cx, |palette, cx| {
palette
.delegate_mut()
.update_matches("bcksp".to_string(), cx)
palette.update_matches("bcksp".to_string(), cx)
})
.await;
palette.update(cx, |palette, _| {
assert!(palette.delegate().matches.is_empty())
});
}
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let app_state = AppState::test(cx);
theme::init((), cx);
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
init(cx);
app_state
})
palette.update(cx, |palette, _| assert!(palette.matches.is_empty()));
}
}

View File

@@ -13,4 +13,4 @@ gpui = { path = "../gpui" }
menu = { path = "../menu" }
settings = { path = "../settings" }
theme = { path = "../theme" }
smallvec.workspace = true
smallvec = "1.6"

View File

@@ -1,99 +1,43 @@
use gpui::{
anyhow,
elements::*,
geometry::vector::Vector2F,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
Action, AnyViewHandle, AppContext, Axis, Entity, MouseState, SizeConstraint, Subscription,
View, ViewContext,
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
};
use menu::*;
use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
use settings::Settings;
use std::{any::TypeId, borrow::Cow, time::Duration};
pub fn init(cx: &mut AppContext) {
pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
#[derive(Copy, Clone, PartialEq)]
struct Clicked;
impl_internal_actions!(context_menu, [Clicked]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContextMenu::select_first);
cx.add_action(ContextMenu::select_last);
cx.add_action(ContextMenu::select_next);
cx.add_action(ContextMenu::select_prev);
cx.add_action(ContextMenu::clicked);
cx.add_action(ContextMenu::confirm);
cx.add_action(ContextMenu::cancel);
}
pub type StaticItem = Box<dyn Fn(&mut AppContext) -> AnyElement<ContextMenu>>;
type ContextMenuItemBuilder =
Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>>;
pub enum ContextMenuItemLabel {
String(Cow<'static, str>),
Element(ContextMenuItemBuilder),
}
impl From<Cow<'static, str>> for ContextMenuItemLabel {
fn from(s: Cow<'static, str>) -> Self {
Self::String(s)
}
}
impl From<&'static str> for ContextMenuItemLabel {
fn from(s: &'static str) -> Self {
Self::String(s.into())
}
}
impl From<String> for ContextMenuItemLabel {
fn from(s: String) -> Self {
Self::String(s.into())
}
}
impl<T> From<T> for ContextMenuItemLabel
where
T: 'static + Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>,
{
fn from(f: T) -> Self {
Self::Element(Box::new(f))
}
}
pub enum ContextMenuItemAction {
Action(Box<dyn Action>),
Handler(Arc<dyn Fn(&mut ViewContext<ContextMenu>)>),
}
impl Clone for ContextMenuItemAction {
fn clone(&self) -> Self {
match self {
Self::Action(action) => Self::Action(action.boxed_clone()),
Self::Handler(handler) => Self::Handler(handler.clone()),
}
}
}
pub enum ContextMenuItem {
Item {
label: ContextMenuItemLabel,
action: ContextMenuItemAction,
label: Cow<'static, str>,
action: Box<dyn Action>,
},
Static(StaticItem),
Separator,
}
impl ContextMenuItem {
pub fn action(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
Self::Item {
label: label.into(),
action: ContextMenuItemAction::Action(Box::new(action)),
}
}
pub fn handler(
label: impl Into<ContextMenuItemLabel>,
handler: impl 'static + Fn(&mut ViewContext<ContextMenu>),
) -> Self {
Self::Item {
label: label.into(),
action: ContextMenuItemAction::Handler(Arc::new(handler)),
action: Box::new(action),
}
}
@@ -107,10 +51,7 @@ impl ContextMenuItem {
fn action_id(&self) -> Option<TypeId> {
match self {
ContextMenuItem::Item { action, .. } => match action {
ContextMenuItemAction::Action(action) => Some(action.id()),
ContextMenuItemAction::Handler(_) => None,
},
ContextMenuItem::Item { action, .. } => Some(action.id()),
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
}
}
@@ -125,6 +66,7 @@ pub struct ContextMenu {
selected_index: Option<usize>,
visible: bool,
previously_focused_view_id: Option<usize>,
clicked: bool,
parent_view_id: usize,
_actions_observation: Subscription,
}
@@ -138,27 +80,29 @@ impl View for ContextMenu {
"ContextMenu"
}
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.add_identifier("menu");
cx
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if !self.visible {
return Empty::new().into_any();
return Empty::new().boxed();
}
// Render the menu once at minimum width.
let mut collapsed_menu = self.render_menu_for_measurement(cx);
let expanded_menu =
self.render_menu(cx)
.constrained()
.dynamically(move |constraint, view, cx| {
SizeConstraint::strict_along(
Axis::Horizontal,
collapsed_menu.layout(constraint, view, cx).0.x(),
)
});
let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed();
let expanded_menu = self
.render_menu(cx)
.constrained()
.dynamically(move |constraint, cx| {
SizeConstraint::strict_along(
Axis::Horizontal,
collapsed_menu.layout(constraint, cx).x(),
)
})
.boxed();
Overlay::new(expanded_menu)
.with_hoverable(true)
@@ -166,7 +110,7 @@ impl View for ContextMenu {
.with_anchor_position(self.anchor_position)
.with_anchor_corner(self.anchor_corner)
.with_position_mode(self.position_mode)
.into_any()
.boxed()
}
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -175,7 +119,9 @@ impl View for ContextMenu {
}
impl ContextMenu {
pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let parent_view_id = cx.parent().unwrap();
Self {
show_count: 0,
anchor_position: Default::default(),
@@ -185,6 +131,7 @@ impl ContextMenu {
selected_index: Default::default(),
visible: Default::default(),
previously_focused_view_id: Default::default(),
clicked: false,
parent_view_id,
_actions_observation: cx.observe_actions(Self::action_dispatched),
}
@@ -200,33 +147,28 @@ impl ContextMenu {
.iter()
.position(|item| item.action_id() == Some(action_id))
{
self.selected_index = Some(ix);
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background().timer(Duration::from_millis(50)).await;
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
if self.clicked {
self.cancel(&Default::default(), cx);
} else {
self.selected_index = Some(ix);
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background().timer(Duration::from_millis(50)).await;
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
})
.detach();
}
}
}
fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
self.clicked = true;
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
match action {
ContextMenuItemAction::Action(action) => {
let window_id = cx.window_id();
let view_id = self.parent_view_id;
let action = action.boxed_clone();
cx.app_context()
.spawn(|mut cx| async move {
cx.dispatch_action(window_id, view_id, action.as_ref())
})
.detach_and_log_err(cx);
}
ContextMenuItemAction::Handler(handler) => handler(cx),
}
cx.dispatch_any_action(action.boxed_clone());
self.reset(cx);
}
}
@@ -247,6 +189,7 @@ impl ContextMenu {
self.items.clear();
self.visible = false;
self.selected_index.take();
self.clicked = false;
cx.notify();
}
@@ -308,7 +251,7 @@ impl ContextMenu {
self.visible = true;
self.show_count += 1;
if !cx.is_self_focused() {
self.previously_focused_view_id = cx.focused_view_id();
self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
}
cx.focus_self();
} else {
@@ -321,42 +264,38 @@ impl ContextMenu {
self.position_mode = mode;
}
fn render_menu_for_measurement(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
let style = theme::current(cx).context_menu.clone();
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
let window_id = cx.window_id();
let style = cx.global::<Settings>().theme.context_menu.clone();
Flex::row()
.with_child(
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, .. } => {
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, .. } => {
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
match label {
ContextMenuItemLabel::String(label) => {
Label::new(label.to_string(), style.label.clone())
.contained()
.with_style(style.container)
.into_any()
}
ContextMenuItemLabel::Element(element) => {
element(&mut Default::default(), style)
}
Label::new(label.to_string(), style.label.clone())
.contained()
.with_style(style.container)
.boxed()
}
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new()
.collapsed()
.contained()
.with_style(style.separator)
.constrained()
.with_height(1.)
.boxed(),
}
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new()
.collapsed()
.contained()
.with_style(style.separator)
.constrained()
.with_height(1.)
.into_any(),
}
})),
}))
.boxed(),
)
.with_child(
Flex::column()
@@ -367,20 +306,17 @@ impl ContextMenu {
&mut Default::default(),
Some(ix) == self.selected_index,
);
match action {
ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.into_any(),
ContextMenuItemAction::Handler(_) => Empty::new().into_any(),
}
KeystrokeLabel::new(
window_id,
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.boxed()
}
ContextMenuItem::Static(_) => Empty::new().into_any(),
ContextMenuItem::Static(_) => Empty::new().boxed(),
ContextMenuItem::Separator => Empty::new()
.collapsed()
@@ -388,84 +324,63 @@ impl ContextMenu {
.with_height(1.)
.contained()
.with_style(style.separator)
.into_any(),
.boxed(),
}
}))
.contained()
.with_margin_left(style.keystroke_margin),
.with_margin_left(style.keystroke_margin)
.boxed(),
)
.contained()
.with_style(style.container)
}
fn render_menu(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
enum Menu {}
enum MenuItem {}
let style = theme::current(cx).context_menu.clone();
let style = cx.global::<Settings>().theme.context_menu.clone();
MouseEventHandler::<Menu, ContextMenu>::new(0, cx, |_, cx| {
let window_id = cx.window_id();
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, action } => {
let action = action.clone();
let view_id = self.parent_view_id;
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
let action = action.boxed_clone();
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
let keystroke = match &action {
ContextMenuItemAction::Action(action) => Some(
Flex::row()
.with_child(
Label::new(label.clone(), style.label.clone())
.contained()
.boxed(),
)
.with_child({
KeystrokeLabel::new(
view_id,
window_id,
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.flex_float(),
),
ContextMenuItemAction::Handler(_) => None,
};
Flex::row()
.with_child(match label {
ContextMenuItemLabel::String(label) => {
Label::new(label.clone(), style.label.clone())
.contained()
.into_any()
}
ContextMenuItemLabel::Element(element) => {
element(state, style)
}
.flex_float()
.boxed()
})
.with_children(keystroke)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_click(MouseButton::Left, move |_, menu, cx| {
menu.cancel(&Default::default(), cx);
let window_id = cx.window_id();
match &action {
ContextMenuItemAction::Action(action) => {
let action = action.boxed_clone();
cx.app_context()
.spawn(|mut cx| async move {
cx.dispatch_action(
window_id,
view_id,
action.as_ref(),
)
})
.detach_and_log_err(cx);
}
ContextMenuItemAction::Handler(handler) => handler(cx),
}
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(Clicked);
cx.dispatch_any_action(action.boxed_clone());
})
.on_drag(MouseButton::Left, |_, _, _| {})
.into_any()
.on_drag(MouseButton::Left, |_, _| {})
.boxed()
}
ContextMenuItem::Static(f) => f(cx),
@@ -475,17 +390,14 @@ impl ContextMenu {
.with_height(1.)
.contained()
.with_style(style.separator)
.into_any(),
.boxed(),
}
}))
.contained()
.with_style(style.container)
.boxed()
})
.on_down_out(MouseButton::Left, |_, this, cx| {
this.cancel(&Default::default(), cx);
})
.on_down_out(MouseButton::Right, |_, this, cx| {
this.cancel(&Default::default(), cx);
})
.on_down_out(MouseButton::Left, |_, cx| cx.dispatch_action(Cancel))
.on_down_out(MouseButton::Right, |_, cx| cx.dispatch_action(Cancel))
}
}

View File

@@ -1,49 +0,0 @@
[package]
name = "copilot"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/copilot.rs"
doctest = false
[features]
test-support = [
"collections/test-support",
"gpui/test-support",
"language/test-support",
"lsp/test-support",
"settings/test-support",
"util/test-support",
]
[dependencies]
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
gpui = { path = "../gpui" }
language = { path = "../language" }
settings = { path = "../settings" }
theme = { path = "../theme" }
lsp = { path = "../lsp" }
node_runtime = { path = "../node_runtime"}
util = { path = "../util" }
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
anyhow.workspace = true
log.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
futures.workspace = true
[dev-dependencies]
clock = { path = "../clock" }
collections = { path = "../collections", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@@ -1,225 +0,0 @@
use serde::{Deserialize, Serialize};
pub enum CheckStatus {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckStatusParams {
pub local_checks_only: bool,
}
impl lsp::request::Request for CheckStatus {
type Params = CheckStatusParams;
type Result = SignInStatus;
const METHOD: &'static str = "checkStatus";
}
pub enum SignInInitiate {}
#[derive(Debug, Serialize, Deserialize)]
pub struct SignInInitiateParams {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum SignInInitiateResult {
AlreadySignedIn { user: String },
PromptUserDeviceFlow(PromptUserDeviceFlow),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptUserDeviceFlow {
pub user_code: String,
pub verification_uri: String,
}
impl lsp::request::Request for SignInInitiate {
type Params = SignInInitiateParams;
type Result = SignInInitiateResult;
const METHOD: &'static str = "signInInitiate";
}
pub enum SignInConfirm {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignInConfirmParams {
pub user_code: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum SignInStatus {
#[serde(rename = "OK")]
Ok {
user: String,
},
MaybeOk {
user: String,
},
AlreadySignedIn {
user: String,
},
NotAuthorized {
user: String,
},
NotSignedIn,
}
impl lsp::request::Request for SignInConfirm {
type Params = SignInConfirmParams;
type Result = SignInStatus;
const METHOD: &'static str = "signInConfirm";
}
pub enum SignOut {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignOutParams {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignOutResult {}
impl lsp::request::Request for SignOut {
type Params = SignOutParams;
type Result = SignOutResult;
const METHOD: &'static str = "signOut";
}
pub enum GetCompletions {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsParams {
pub doc: GetCompletionsDocument,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsDocument {
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Url,
pub relative_path: String,
pub position: lsp::Position,
pub version: usize,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsResult {
pub completions: Vec<Completion>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Completion {
pub text: String,
pub position: lsp::Position,
pub uuid: String,
pub range: lsp::Range,
pub display_text: String,
}
impl lsp::request::Request for GetCompletions {
type Params = GetCompletionsParams;
type Result = GetCompletionsResult;
const METHOD: &'static str = "getCompletions";
}
pub enum GetCompletionsCycling {}
impl lsp::request::Request for GetCompletionsCycling {
type Params = GetCompletionsParams;
type Result = GetCompletionsResult;
const METHOD: &'static str = "getCompletionsCycling";
}
pub enum LogMessage {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogMessageParams {
pub level: u8,
pub message: String,
pub metadata_str: String,
pub extra: Vec<String>,
}
impl lsp::notification::Notification for LogMessage {
type Params = LogMessageParams;
const METHOD: &'static str = "LogMessage";
}
pub enum StatusNotification {}
#[derive(Debug, Serialize, Deserialize)]
pub struct StatusNotificationParams {
pub message: String,
pub status: String, // One of Normal/InProgress
}
impl lsp::notification::Notification for StatusNotification {
type Params = StatusNotificationParams;
const METHOD: &'static str = "statusNotification";
}
pub enum SetEditorInfo {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetEditorInfoParams {
pub editor_info: EditorInfo,
pub editor_plugin_info: EditorPluginInfo,
}
impl lsp::request::Request for SetEditorInfo {
type Params = SetEditorInfoParams;
type Result = String;
const METHOD: &'static str = "setEditorInfo";
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorPluginInfo {
pub name: String,
pub version: String,
}
pub enum NotifyAccepted {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotifyAcceptedParams {
pub uuid: String,
}
impl lsp::request::Request for NotifyAccepted {
type Params = NotifyAcceptedParams;
type Result = String;
const METHOD: &'static str = "notifyAccepted";
}
pub enum NotifyRejected {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotifyRejectedParams {
pub uuids: Vec<String>,
}
impl lsp::request::Request for NotifyRejected {
type Params = NotifyRejectedParams;
type Result = String;
const METHOD: &'static str = "notifyRejected";
}

View File

@@ -1,376 +0,0 @@
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{
elements::*,
geometry::rect::RectF,
platform::{WindowBounds, WindowKind, WindowOptions},
AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
ViewHandle,
};
use theme::ui::modal;
#[derive(PartialEq, Eq, Debug, Clone)]
struct CopyUserCode;
#[derive(PartialEq, Eq, Debug, Clone)]
struct OpenGithub;
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
pub fn init(cx: &mut AppContext) {
if let Some(copilot) = Copilot::global(cx) {
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
cx.observe(&copilot, move |copilot, cx| {
let status = copilot.read(cx).status();
match &status {
crate::Status::SigningIn { prompt } => {
if let Some(code_verification_handle) = code_verification.as_mut() {
let window_id = code_verification_handle.window_id();
let updated = cx.update_window(window_id, |cx| {
code_verification_handle.update(cx, |code_verification, cx| {
code_verification.set_status(status.clone(), cx)
});
cx.activate_window();
});
if updated.is_none() {
code_verification = Some(create_copilot_auth_window(cx, &status));
}
} else if let Some(_prompt) = prompt {
code_verification = Some(create_copilot_auth_window(cx, &status));
}
}
Status::Authorized | Status::Unauthorized => {
if let Some(code_verification) = code_verification.as_ref() {
let window_id = code_verification.window_id();
cx.update_window(window_id, |cx| {
code_verification.update(cx, |code_verification, cx| {
code_verification.set_status(status, cx)
});
cx.platform().activate(true);
cx.activate_window();
});
}
}
_ => {
if let Some(code_verification) = code_verification.take() {
cx.update_window(code_verification.window_id(), |cx| cx.remove_window());
}
}
}
})
.detach();
}
}
fn create_copilot_auth_window(
cx: &mut AppContext,
status: &Status,
) -> ViewHandle<CopilotCodeVerification> {
let window_size = theme::current(cx).copilot.modal.dimensions();
let window_options = WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
titlebar: None,
center: true,
focus: true,
show: true,
kind: WindowKind::Normal,
is_movable: true,
screen: None,
};
let (_, view) = cx.add_window(window_options, |_cx| {
CopilotCodeVerification::new(status.clone())
});
view
}
pub struct CopilotCodeVerification {
status: Status,
connect_clicked: bool,
}
impl CopilotCodeVerification {
pub fn new(status: Status) -> Self {
Self {
status,
connect_clicked: false,
}
}
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
self.status = status;
cx.notify();
}
fn render_device_code(
data: &PromptUserDeviceFlow,
style: &theme::Copilot,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
let copied = cx
.read_from_clipboard()
.map(|item| item.text() == &data.user_code)
.unwrap_or(false);
let device_code_style = &style.auth.prompting.device_code;
MouseEventHandler::<Self, _>::new(0, cx, |state, _cx| {
Flex::row()
.with_child(
Label::new(data.user_code.clone(), device_code_style.text.clone())
.aligned()
.contained()
.with_style(device_code_style.left_container)
.constrained()
.with_width(device_code_style.left),
)
.with_child(
Label::new(
if copied { "Copied!" } else { "Copy" },
device_code_style.cta.style_for(state, false).text.clone(),
)
.aligned()
.contained()
.with_style(*device_code_style.right_container.style_for(state, false))
.constrained()
.with_width(device_code_style.right),
)
.contained()
.with_style(device_code_style.cta.style_for(state, false).container)
})
.on_click(gpui::platform::MouseButton::Left, {
let user_code = data.user_code.clone();
move |_, _, cx| {
cx.platform()
.write_to_clipboard(ClipboardItem::new(user_code.clone()));
cx.notify();
}
})
.with_cursor_style(gpui::platform::CursorStyle::PointingHand)
}
fn render_prompting_modal(
connect_clicked: bool,
data: &PromptUserDeviceFlow,
style: &theme::Copilot,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum ConnectButton {}
Flex::column()
.with_child(
Flex::column()
.with_children([
Label::new(
"Enable Copilot by connecting",
style.auth.prompting.subheading.text.clone(),
)
.aligned(),
Label::new(
"your existing license.",
style.auth.prompting.subheading.text.clone(),
)
.aligned(),
])
.align_children_center()
.contained()
.with_style(style.auth.prompting.subheading.container),
)
.with_child(Self::render_device_code(data, &style, cx))
.with_child(
Flex::column()
.with_children([
Label::new(
"Paste this code into GitHub after",
style.auth.prompting.hint.text.clone(),
)
.aligned(),
Label::new(
"clicking the button below.",
style.auth.prompting.hint.text.clone(),
)
.aligned(),
])
.align_children_center()
.contained()
.with_style(style.auth.prompting.hint.container.clone()),
)
.with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
if connect_clicked {
"Waiting for connection..."
} else {
"Connect to GitHub"
},
style.auth.content_width,
&style.auth.cta_button,
cx,
{
let verification_uri = data.verification_uri.clone();
move |_, verification, cx| {
cx.platform().open_url(&verification_uri);
verification.connect_clicked = true;
}
},
))
.align_children_center()
.into_any()
}
fn render_enabled_modal(
style: &theme::Copilot,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum DoneButton {}
let enabled_style = &style.auth.authorized;
Flex::column()
.with_child(
Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
.contained()
.with_style(enabled_style.subheading.container)
.aligned(),
)
.with_child(
Flex::column()
.with_children([
Label::new(
"You can update your settings or",
enabled_style.hint.text.clone(),
)
.aligned(),
Label::new(
"sign out from the Copilot menu in",
enabled_style.hint.text.clone(),
)
.aligned(),
Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
])
.align_children_center()
.contained()
.with_style(enabled_style.hint.container),
)
.with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
"Done",
style.auth.content_width,
&style.auth.cta_button,
cx,
|_, _, cx| cx.remove_window(),
))
.align_children_center()
.into_any()
}
fn render_unauthorized_modal(
style: &theme::Copilot,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let unauthorized_style = &style.auth.not_authorized;
Flex::column()
.with_child(
Flex::column()
.with_children([
Label::new(
"Enable Copilot by connecting",
unauthorized_style.subheading.text.clone(),
)
.aligned(),
Label::new(
"your existing license.",
unauthorized_style.subheading.text.clone(),
)
.aligned(),
])
.align_children_center()
.contained()
.with_style(unauthorized_style.subheading.container),
)
.with_child(
Flex::column()
.with_children([
Label::new(
"You must have an active copilot",
unauthorized_style.warning.text.clone(),
)
.aligned(),
Label::new(
"license to use it in Zed.",
unauthorized_style.warning.text.clone(),
)
.aligned(),
])
.align_children_center()
.contained()
.with_style(unauthorized_style.warning.container),
)
.with_child(theme::ui::cta_button::<Self, _, _, _>(
"Subscribe on GitHub",
style.auth.content_width,
&style.auth.cta_button,
cx,
|_, _, cx| {
cx.remove_window();
cx.platform().open_url(COPILOT_SIGN_UP_URL)
},
))
.align_children_center()
.into_any()
}
}
impl Entity for CopilotCodeVerification {
type Event = ();
}
impl View for CopilotCodeVerification {
fn ui_name() -> &'static str {
"CopilotCodeVerification"
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.notify()
}
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.notify()
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum ConnectModal {}
let style = theme::current(cx).clone();
modal::<ConnectModal, _, _, _, _>(
"Connect Copilot to Zed",
&style.copilot.modal,
cx,
|cx| {
Flex::column()
.with_children([
theme::ui::icon(&style.copilot.auth.header).into_any(),
match &self.status {
Status::SigningIn {
prompt: Some(prompt),
} => Self::render_prompting_modal(
self.connect_clicked,
&prompt,
&style.copilot,
cx,
),
Status::Unauthorized => {
self.connect_clicked = false;
Self::render_unauthorized_modal(&style.copilot, cx)
}
Status::Authorized => {
self.connect_clicked = false;
Self::render_enabled_modal(&style.copilot, cx)
}
_ => Empty::new().into_any(),
},
])
.align_children_center()
},
)
.into_any()
}
}

View File

@@ -1,28 +0,0 @@
[package]
name = "copilot_button"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/copilot_button.rs"
doctest = false
[dependencies]
assets = { path = "../assets" }
copilot = { path = "../copilot" }
editor = { path = "../editor" }
fs = { path = "../fs" }
context_menu = { path = "../context_menu" }
gpui = { path = "../gpui" }
language = { path = "../language" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow.workspace = true
smol.workspace = true
futures.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View File

@@ -1,434 +0,0 @@
use anyhow::Result;
use context_menu::{ContextMenu, ContextMenuItem};
use copilot::{Copilot, SignOut, Status};
use editor::{scroll::autoscroll::Autoscroll, Editor};
use fs::Fs;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use language::language_settings::{self, all_language_settings, AllLanguageSettings};
use settings::{update_settings_file, SettingsStore};
use std::{path::Path, sync::Arc};
use util::{paths, ResultExt};
use workspace::{
create_and_open_local_file, item::ItemHandle,
notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
};
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
const COPILOT_STARTING_TOAST_ID: usize = 1337;
const COPILOT_ERROR_TOAST_ID: usize = 1338;
pub struct CopilotButton {
popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<str>>,
path: Option<Arc<Path>>,
fs: Arc<dyn Fs>,
}
impl Entity for CopilotButton {
type Event = ();
}
impl View for CopilotButton {
fn ui_name() -> &'static str {
"CopilotButton"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let all_language_settings = &all_language_settings(cx);
if !all_language_settings.copilot.feature_enabled {
return Empty::new().into_any();
}
let theme = theme::current(cx).clone();
let active = self.popup_menu.read(cx).visible();
let Some(copilot) = Copilot::global(cx) else {
return Empty::new().into_any();
};
let status = copilot.read(cx).status();
let enabled = self
.editor_enabled
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
Stack::new()
.with_child(
MouseEventHandler::<Self, _>::new(0, cx, {
let theme = theme.clone();
let status = status.clone();
move |state, _cx| {
let style = theme
.workspace
.status_bar
.panel_buttons
.button
.style_for(state, active);
Flex::row()
.with_child(
Svg::new({
match status {
Status::Error(_) => "icons/copilot_error_16.svg",
Status::Authorized => {
if enabled {
"icons/copilot_16.svg"
} else {
"icons/copilot_disabled_16.svg"
}
}
_ => "icons/copilot_init_16.svg",
}
})
.with_color(style.icon_color)
.constrained()
.with_width(style.icon_size)
.aligned()
.into_any_named("copilot-icon"),
)
.constrained()
.with_height(style.icon_size)
.contained()
.with_style(style.container)
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, {
let status = status.clone();
move |_, this, cx| match status {
Status::Authorized => this.deploy_copilot_menu(cx),
Status::Error(ref e) => {
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>()
{
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
)
.on_click(
"Reinstall Copilot",
|cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
copilot.reinstall(cx)
})
.detach();
}
},
),
cx,
);
});
}
}
_ => this.deploy_copilot_start_menu(cx),
}
})
.with_tooltip::<Self>(
0,
"GitHub Copilot".into(),
None,
theme.tooltip.clone(),
cx,
),
)
.with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
.into_any()
}
}
impl CopilotButton {
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
let button_view_id = cx.view_id();
let menu = cx.add_view(|cx| {
let mut menu = ContextMenu::new(button_view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
});
cx.observe(&menu, |_, _, cx| cx.notify()).detach();
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify())
.detach();
Self {
popup_menu: menu,
editor_subscription: None,
editor_enabled: None,
language: None,
path: None,
fs,
}
}
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut menu_options = Vec::with_capacity(2);
let fs = self.fs.clone();
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
initiate_sign_in(cx)
}));
menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
hide_copilot(fs.clone(), cx)
}));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
cx,
);
});
}
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
let fs = self.fs.clone();
let mut menu_options = Vec::with_capacity(8);
if let Some(language) = self.language.clone() {
let fs = fs.clone();
let language_enabled =
language_settings::language_settings(Some(language.as_ref()), cx)
.show_copilot_suggestions;
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language
),
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
));
}
let settings = settings::get::<AllLanguageSettings>(cx);
if let Some(path) = self.path.as_ref() {
let path_enabled = settings.copilot_enabled_for_path(path);
let path = path.clone();
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for This Path",
if path_enabled { "Hide" } else { "Show" }
),
move |cx| {
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
let workspace = workspace.downgrade();
cx.spawn(|_, cx| {
configure_disabled_globs(
workspace,
path_enabled.then_some(path.clone()),
cx,
)
})
.detach_and_log_err(cx);
}
},
));
}
let globally_enabled = settings.copilot_enabled(None, None);
menu_options.push(ContextMenuItem::handler(
if globally_enabled {
"Hide Suggestions for All Files"
} else {
"Show Suggestions for All Files"
},
move |cx| toggle_copilot_globally(fs.clone(), cx),
));
menu_options.push(ContextMenuItem::Separator);
let icon_style = theme::current(cx).copilot.out_link_icon.clone();
menu_options.push(ContextMenuItem::action(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone()))
.with_child(theme::ui::icon(icon_style.style_for(state, false)))
.align_children_center()
.into_any()
},
OsOpen::new(COPILOT_SETTINGS_URL),
));
menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
cx,
);
});
}
pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let suggestion_anchor = editor.selections.newest_anchor().start;
let language_name = snapshot
.language_at(suggestion_anchor)
.map(|language| language.name());
let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
self.editor_enabled = Some(
all_language_settings(cx)
.copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
);
self.language = language_name;
self.path = path.cloned();
cx.notify()
}
}
impl StatusItemView for CopilotButton {
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
self.editor_subscription =
Some((cx.observe(&editor, Self::update_enabled), editor.id()));
self.update_enabled(editor, cx);
} else {
self.language = None;
self.editor_subscription = None;
self.editor_enabled = None;
}
cx.notify();
}
}
async fn configure_disabled_globs(
workspace: WeakViewHandle<Workspace>,
path_to_disable: Option<Arc<Path>>,
mut cx: AsyncAppContext,
) -> Result<()> {
let settings_editor = workspace
.update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
settings::initial_user_settings_content(&assets::Assets)
.as_ref()
.into()
})
})?
.await?
.downcast::<Editor>()
.unwrap();
settings_editor.downgrade().update(&mut cx, |item, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
let settings = cx.global::<SettingsStore>();
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
let copilot = file.copilot.get_or_insert_with(Default::default);
let globs = copilot.disabled_globs.get_or_insert_with(|| {
settings
.get::<AllLanguageSettings>(None)
.copilot
.disabled_globs
.iter()
.map(|glob| glob.glob().to_string())
.collect()
});
if let Some(path_to_disable) = &path_to_disable {
globs.push(path_to_disable.to_string_lossy().into_owned());
} else {
globs.clear();
}
});
if !edits.is_empty() {
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
});
// When *enabling* a path, don't actually perform an edit, just select the range.
if path_to_disable.is_some() {
item.edit(edits.iter().cloned(), cx);
}
}
})?;
anyhow::Ok(())
}
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
});
}
fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.languages
.entry(language)
.or_default()
.show_copilot_suggestions = Some(!show_copilot_suggestions);
});
}
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.features.get_or_insert(Default::default()).copilot = Some(false);
});
}
fn initiate_sign_in(cx: &mut WindowContext) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
match status {
Status::Starting { task } => {
let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
cx,
)
});
let workspace = workspace.downgrade();
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.read(Copilot::global) {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
cx,
),
_ => {
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
})
.log_err();
}
})
.detach();
}
_ => {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
}
}

View File

@@ -17,17 +17,16 @@ gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" }
sqlez_macros = { path = "../sqlez_macros" }
util = { path = "../util" }
anyhow.workspace = true
indoc.workspace = true
async-trait.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
anyhow = "1.0.57"
indoc = "1.0.4"
async-trait = "0.1"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
serde = { version = "1.0", features = ["derive"] }
smol = "1.2"
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
env_logger.workspace = true
tempdir.workspace = true
env_logger = "0.9.1"
tempdir = { version = "0.3.7" }

View File

@@ -4,7 +4,7 @@ pub mod query;
// Re-export
pub use anyhow;
use anyhow::Context;
use gpui::AppContext;
use gpui::MutableAppContext;
pub use indoc::indoc;
pub use lazy_static;
use parking_lot::{Mutex, RwLock};
@@ -239,7 +239,7 @@ macro_rules! define_connection {
};
}
pub fn write_and_log<F>(cx: &mut AppContext, db_write: impl FnOnce() -> F + Send + 'static)
pub fn write_and_log<F>(cx: &mut MutableAppContext, db_write: impl FnOnce() -> F + Send + 'static)
where
F: Future<Output = anyhow::Result<()>> + Send,
{

View File

@@ -9,29 +9,24 @@ path = "src/diagnostics.rs"
doctest = false
[dependencies]
anyhow = "1.0"
smallvec = { version = "1.6", features = ["union"] }
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
gpui = { path = "../gpui" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow.workspace = true
smallvec.workspace = true
postage.workspace = true
postage = { version = "0.4", features = ["futures-traits"] }
[dev-dependencies]
unindent = "0.1"
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
serde_json.workspace = true
unindent.workspace = true
serde_json = { version = "1", features = ["preserve_order"] }

View File

@@ -1,7 +1,7 @@
pub mod items;
use anyhow::Result;
use collections::{BTreeSet, HashSet};
use collections::{BTreeMap, HashSet};
use editor::{
diagnostic_block_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
@@ -10,37 +10,38 @@ use editor::{
Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use gpui::{
actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use language::{
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
SelectionGoal,
};
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json;
use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
borrow::Cow,
cmp::Ordering,
ops::Range,
path::PathBuf,
sync::Arc,
};
use theme::ThemeSettings;
use util::TryFutureExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
item::{Item, ItemEvent, ItemHandle},
ItemNavHistory, Pane, Workspace,
};
actions!(diagnostics, [Deploy]);
impl_internal_actions!(diagnostics, [Jump]);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut AppContext) {
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectDiagnosticsEditor::deploy);
items::init(cx);
}
@@ -54,7 +55,7 @@ struct ProjectDiagnosticsEditor {
summary: DiagnosticSummary,
excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<PathState>,
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
paths_to_update: BTreeMap<ProjectPath, usize>,
}
struct PathState {
@@ -70,7 +71,6 @@ struct Jump {
}
struct DiagnosticGroupState {
language_server_id: LanguageServerId,
primary_diagnostic: DiagnosticEntry<language::Anchor>,
primary_excerpt_ix: usize,
excerpts: Vec<ExcerptId>,
@@ -87,20 +87,16 @@ impl View for ProjectDiagnosticsEditor {
"ProjectDiagnosticsEditor"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.path_states.is_empty() {
let theme = &theme::current(cx).project_diagnostics;
PaneBackdrop::new(
cx.view_id(),
Label::new("No problems in workspace", theme.empty_message.clone())
.aligned()
.contained()
.with_style(theme.container)
.into_any(),
)
.into_any()
let theme = &cx.global::<Settings>().theme.project_diagnostics;
Label::new("No problems in workspace", theme.empty_message.clone())
.aligned()
.contained()
.with_style(theme.container)
.boxed()
} else {
ChildView::new(&self.editor, cx).into_any()
ChildView::new(&self.editor, cx).boxed()
}
}
@@ -119,7 +115,7 @@ impl View for ProjectDiagnosticsEditor {
}),
"summary": self.summary,
"paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
(path.path.to_string_lossy(), server_id.0)
(path.path.to_string_lossy(), server_id)
).collect::<Vec<_>>(),
"paths_states": self.path_states.iter().map(|state|
json!({
@@ -152,7 +148,7 @@ impl ProjectDiagnosticsEditor {
path,
} => {
this.paths_to_update
.insert((path.clone(), *language_server_id));
.insert(path.clone(), *language_server_id);
}
_ => {}
})
@@ -165,18 +161,13 @@ impl ProjectDiagnosticsEditor {
editor.set_vertical_scroll_margin(5, cx);
editor
});
cx.subscribe(&editor, |this, _, event, cx| {
cx.emit(event.clone());
if event == &editor::Event::Focused && this.path_states.is_empty() {
cx.focus_self()
}
})
.detach();
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
.detach();
let project = project_handle.read(cx);
let paths_to_update = project
.diagnostic_summaries(cx)
.map(|(path, server_id, _)| (path, server_id))
.map(|e| (e.0, e.1.language_server_id))
.collect();
let summary = project.diagnostic_summary(cx);
let mut this = Self {
@@ -204,13 +195,9 @@ impl ProjectDiagnosticsEditor {
}
}
fn update_excerpts(
&mut self,
language_server_id: Option<LanguageServerId>,
cx: &mut ViewContext<Self>,
) {
fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
let mut paths = Vec::new();
self.paths_to_update.retain(|(path, server_id)| {
self.paths_to_update.retain(|path, server_id| {
if language_server_id
.map_or(true, |language_server_id| language_server_id == *server_id)
{
@@ -227,9 +214,7 @@ impl ProjectDiagnosticsEditor {
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
.await?;
this.update(&mut cx, |this, cx| {
this.populate_excerpts(path, language_server_id, buffer, cx)
})?;
this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx))
}
Result::<_, anyhow::Error>::Ok(())
}
@@ -241,7 +226,6 @@ impl ProjectDiagnosticsEditor {
fn populate_excerpts(
&mut self,
path: ProjectPath,
language_server_id: Option<LanguageServerId>,
buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>,
) {
@@ -280,9 +264,9 @@ impl ProjectDiagnosticsEditor {
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
let mut new_groups = snapshot
.diagnostic_groups(language_server_id)
.diagnostic_groups()
.into_iter()
.filter(|(_, group)| {
.filter(|group| {
group.entries[group.primary_ix].diagnostic.severity
<= DiagnosticSeverity::WARNING
})
@@ -294,27 +278,12 @@ impl ProjectDiagnosticsEditor {
match (old_groups.peek(), new_groups.peek()) {
(None, None) => break,
(None, Some(_)) => to_insert = new_groups.next(),
(Some((_, old_group)), None) => {
if language_server_id.map_or(true, |id| id == old_group.language_server_id)
{
to_remove = old_groups.next();
} else {
to_keep = old_groups.next();
}
}
(Some((_, old_group)), Some((_, new_group))) => {
(Some(_), None) => to_remove = old_groups.next(),
(Some((_, old_group)), Some(new_group)) => {
let old_primary = &old_group.primary_diagnostic;
let new_primary = &new_group.entries[new_group.primary_ix];
match compare_diagnostics(old_primary, new_primary, &snapshot) {
Ordering::Less => {
if language_server_id
.map_or(true, |id| id == old_group.language_server_id)
{
to_remove = old_groups.next();
} else {
to_keep = old_groups.next();
}
}
Ordering::Less => to_remove = old_groups.next(),
Ordering::Equal => {
to_keep = old_groups.next();
new_groups.next();
@@ -324,9 +293,8 @@ impl ProjectDiagnosticsEditor {
}
}
if let Some((language_server_id, group)) = to_insert {
if let Some(group) = to_insert {
let mut group_state = DiagnosticGroupState {
language_server_id,
primary_diagnostic: group.entries[group.primary_ix].clone(),
primary_excerpt_ix: 0,
excerpts: Default::default(),
@@ -537,16 +505,16 @@ impl ProjectDiagnosticsEditor {
}
impl Item for ProjectDiagnosticsEditor {
fn tab_content<T: View>(
fn tab_content(
&self,
_detail: Option<usize>,
style: &theme::Tab,
cx: &AppContext,
) -> AnyElement<T> {
) -> ElementBox {
render_summary(
&self.summary,
&style.label.text,
&theme::current(cx).project_diagnostics,
&cx.global::<Settings>().theme.project_diagnostics,
)
}
@@ -558,20 +526,11 @@ impl Item for ProjectDiagnosticsEditor {
false
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, cx))
}
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
Some("Project Diagnostics".into())
}
fn is_dirty(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).is_dirty(cx)
}
@@ -643,16 +602,16 @@ impl Item for ProjectDiagnosticsEditor {
))
}
fn act_as_type<'a>(
&'a self,
fn act_as_type(
&self,
type_id: TypeId,
self_handle: &'a ViewHandle<Self>,
_: &'a AppContext,
) -> Option<&AnyViewHandle> {
self_handle: &ViewHandle<Self>,
_: &AppContext,
) -> Option<AnyViewHandle> {
if type_id == TypeId::of::<Self>() {
Some(self_handle)
Some(self_handle.into())
} else if type_id == TypeId::of::<Editor>() {
Some(&self.editor)
Some((&self.editor).into())
} else {
None
}
@@ -666,14 +625,6 @@ impl Item for ProjectDiagnosticsEditor {
Some("diagnostics")
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft { flex: None }
}
fn deserialize(
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
@@ -686,12 +637,12 @@ impl Item for ProjectDiagnosticsEditor {
}
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
Arc::new(move |cx| {
let settings = settings::get::<ThemeSettings>(cx);
let settings = cx.global::<Settings>();
let theme = &settings.theme.editor;
let style = theme.diagnostic_header.clone();
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
let icon_width = cx.em_width * style.icon_width_factor;
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
Svg::new("icons/circle_x_mark_12.svg")
@@ -707,17 +658,8 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.with_width(icon_width)
.aligned()
.contained()
.with_margin_right(cx.gutter_padding),
.boxed(),
)
.with_children(diagnostic.source.as_ref().map(|source| {
Label::new(
format!("{source}: "),
style.source.label.clone().with_font_size(font_size),
)
.contained()
.with_style(style.message.container)
.aligned()
}))
.with_child(
Label::new(
message.clone(),
@@ -726,45 +668,47 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.with_highlights(highlights.clone())
.contained()
.with_style(style.message.container)
.aligned(),
.with_margin_left(cx.gutter_padding)
.aligned()
.boxed(),
)
.with_children(diagnostic.code.clone().map(|code| {
Label::new(code, style.code.text.clone().with_font_size(font_size))
.contained()
.with_style(style.code.container)
.aligned()
.boxed()
}))
.contained()
.with_style(style.container)
.with_padding_left(cx.gutter_padding)
.with_padding_right(cx.gutter_padding)
.expanded()
.into_any_named("diagnostic header")
.named("diagnostic header")
})
}
pub(crate) fn render_summary<T: View>(
pub(crate) fn render_summary(
summary: &DiagnosticSummary,
text_style: &TextStyle,
theme: &theme::ProjectDiagnostics,
) -> AnyElement<T> {
) -> ElementBox {
if summary.error_count == 0 && summary.warning_count == 0 {
Label::new("No problems", text_style.clone()).into_any()
Label::new("No problems", text_style.clone()).boxed()
} else {
let icon_width = theme.tab_icon_width;
let icon_spacing = theme.tab_icon_spacing;
let summary_spacing = theme.tab_summary_spacing;
Flex::row()
.with_child(
.with_children([
Svg::new("icons/circle_x_mark_12.svg")
.with_color(text_style.color)
.constrained()
.with_width(icon_width)
.aligned()
.contained()
.with_margin_right(icon_spacing),
)
.with_child(
.with_margin_right(icon_spacing)
.named("no-icon"),
Label::new(
summary.error_count.to_string(),
LabelStyle {
@@ -772,9 +716,8 @@ pub(crate) fn render_summary<T: View>(
highlight_text: None,
},
)
.aligned(),
)
.with_child(
.aligned()
.boxed(),
Svg::new("icons/triangle_exclamation_12.svg")
.with_color(text_style.color)
.constrained()
@@ -782,9 +725,8 @@ pub(crate) fn render_summary<T: View>(
.aligned()
.contained()
.with_margin_left(summary_spacing)
.with_margin_right(icon_spacing),
)
.with_child(
.with_margin_right(icon_spacing)
.named("warn-icon"),
Label::new(
summary.warning_count.to_string(),
LabelStyle {
@@ -792,9 +734,10 @@ pub(crate) fn render_summary<T: View>(
highlight_text: None,
},
)
.aligned(),
)
.into_any()
.aligned()
.boxed(),
])
.boxed()
}
}
@@ -823,53 +766,52 @@ mod tests {
display_map::{BlockContext, TransformBlock},
DisplayPoint,
};
use gpui::{TestAppContext, WindowContext};
use gpui::TestAppContext;
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
use unindent::Unindent as _;
use workspace::AppState;
#[gpui::test]
async fn test_diagnostics(cx: &mut TestAppContext) {
init_test(cx);
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
"/test",
json!({
"consts.rs": "
const a: i32 = 'a';
const b: i32 = c;
"
.unindent(),
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/test",
json!({
"consts.rs": "
const a: i32 = 'a';
const b: i32 = c;
"
.unindent(),
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
// comment 1
// comment 2
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
// comment 1
// comment 2
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// Create some diagnostics
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
language_server_id,
0,
PathBuf::from("/test/main.rs"),
None,
vec![
@@ -950,7 +892,7 @@ mod tests {
});
// Open the project diagnostics view while there are already diagnostics.
let view = cx.add_view(window_id, |cx| {
let view = cx.add_view(&workspace, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
@@ -1018,10 +960,10 @@ mod tests {
// Diagnostics are added for another earlier path.
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(language_server_id, cx);
project.disk_based_diagnostics_started(0, cx);
project
.update_diagnostic_entries(
language_server_id,
0,
PathBuf::from("/test/consts.rs"),
None,
vec![DiagnosticEntry {
@@ -1038,7 +980,7 @@ mod tests {
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(language_server_id, cx);
project.disk_based_diagnostics_finished(0, cx);
});
view.next_notification(cx).await;
@@ -1118,10 +1060,10 @@ mod tests {
// Diagnostics are added to the first path
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(language_server_id, cx);
project.disk_based_diagnostics_started(0, cx);
project
.update_diagnostic_entries(
language_server_id,
0,
PathBuf::from("/test/consts.rs"),
None,
vec![
@@ -1154,7 +1096,7 @@ mod tests {
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(language_server_id, cx);
project.disk_based_diagnostics_finished(0, cx);
});
view.next_notification(cx).await;
@@ -1234,285 +1176,13 @@ mod tests {
});
}
#[gpui::test]
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/test",
json!({
"main.js": "
a();
b();
c();
d();
e();
".unindent()
}),
)
.await;
let server_id_1 = LanguageServerId(100);
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let view = cx.add_view(window_id, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
// Two language servers start updating diagnostics
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(server_id_1, cx);
project.disk_based_diagnostics_started(server_id_2, cx);
project
.update_diagnostic_entries(
server_id_1,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
diagnostic: Diagnostic {
message: "error 1".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
diagnostic: Diagnostic {
message: "warning 1".to_string(),
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 2,
..Default::default()
},
}],
cx,
)
.unwrap();
});
// The first language server finishes
project.update(cx, |project, cx| {
project.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are shown.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", //
"b();",
)
);
});
// The second language server finishes
project.update(cx, |project, cx| {
project.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language server's diagnostics are shown.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(6, "collapsed context".into()),
(7, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", // location
"b();\n", //
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"a();\n", // context
"b();\n", //
"c();", // context
)
);
});
// Both language servers start updating diagnostics, and the first server finishes.
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(server_id_1, cx);
project.disk_based_diagnostics_started(server_id_2, cx);
project
.update_diagnostic_entries(
server_id_1,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
diagnostic: Diagnostic {
message: "warning 2".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.rs"),
None,
vec![],
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are updated.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(7, "collapsed context".into()),
(8, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", // location
"b();\n", //
"c();\n", // context
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"b();\n", // context
"c();\n", //
"d();", // context
)
);
});
// The second language server finishes.
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
diagnostic: Diagnostic {
message: "warning 2".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language servers' diagnostics are updated.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(7, "collapsed context".into()),
(8, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"b();\n", // location
"c();\n", //
"d();\n", // context
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"c();\n", // context
"d();\n", //
"e();", // context
)
);
});
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
client::init_settings(cx);
workspace::init_settings(cx);
});
}
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
editor.update(cx, |editor, cx| {
fn editor_blocks(
editor: &ViewHandle<Editor>,
cx: &mut MutableAppContext,
) -> Vec<(u32, String)> {
let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
let mut cx = presenter.build_layout_context(Default::default(), false, cx);
cx.render(editor, |editor, cx| {
let snapshot = editor.snapshot(cx);
snapshot
.blocks_in_range(0..snapshot.max_point().row())
@@ -1520,7 +1190,7 @@ mod tests {
let name = match block {
TransformBlock::Custom(block) => block
.render(&mut BlockContext {
view_context: cx,
cx,
anchor_x: 0.,
scroll_x: 0.,
gutter_padding: 0.,

View File

@@ -1,32 +1,28 @@
use collections::HashSet;
use editor::{Editor, GoToDiagnostic};
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
serde_json, AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MouseButton,
MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::Diagnostic;
use lsp::LanguageServerId;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::ProjectDiagnosticsEditor;
use project::Project;
use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView};
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
active_editor: Option<WeakViewHandle<Editor>>,
workspace: WeakViewHandle<Workspace>,
current_diagnostic: Option<Diagnostic>,
in_progress_checks: HashSet<LanguageServerId>,
in_progress_checks: HashSet<usize>,
_observe_active_editor: Option<Subscription>,
}
pub fn init(cx: &mut AppContext) {
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(DiagnosticIndicator::go_to_next_diagnostic);
}
impl DiagnosticIndicator {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let project = workspace.project();
pub fn new(project: &ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
this.in_progress_checks.insert(*language_server_id);
@@ -47,7 +43,6 @@ impl DiagnosticIndicator {
.language_servers_running_disk_based_diagnostics()
.collect(),
active_editor: None,
workspace: workspace.weak_handle(),
current_diagnostic: None,
_observe_active_editor: None,
}
@@ -87,16 +82,17 @@ impl View for DiagnosticIndicator {
"DiagnosticIndicator"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Summary {}
enum Message {}
let tooltip_style = theme::current(cx).tooltip.clone();
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
let in_progress = !self.in_progress_checks.is_empty();
let mut element = Flex::row().with_child(
MouseEventHandler::<Summary, _>::new(0, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
MouseEventHandler::<Summary>::new(0, cx, |state, cx| {
let style = cx
.global::<Settings>()
.theme
.workspace
.status_bar
.diagnostic_summary
@@ -104,23 +100,23 @@ impl View for DiagnosticIndicator {
let mut summary_row = Flex::row();
if self.summary.error_count > 0 {
summary_row.add_child(
summary_row.add_children([
Svg::new("icons/circle_x_mark_16.svg")
.with_color(style.icon_color_error)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_margin_right(style.icon_spacing),
);
summary_row.add_child(
.with_margin_right(style.icon_spacing)
.named("error-icon"),
Label::new(self.summary.error_count.to_string(), style.text.clone())
.aligned(),
);
.aligned()
.boxed(),
]);
}
if self.summary.warning_count > 0 {
summary_row.add_child(
summary_row.add_children([
Svg::new("icons/triangle_exclamation_16.svg")
.with_color(style.icon_color_warning)
.constrained()
@@ -132,12 +128,12 @@ impl View for DiagnosticIndicator {
style.summary_spacing
} else {
0.
}),
);
summary_row.add_child(
})
.named("warning-icon"),
Label::new(self.summary.warning_count.to_string(), style.text.clone())
.aligned(),
);
.aligned()
.boxed(),
]);
}
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
@@ -147,7 +143,7 @@ impl View for DiagnosticIndicator {
.constrained()
.with_width(style.icon_width)
.aligned()
.into_any_named("ok-icon"),
.named("ok-icon"),
);
}
@@ -162,16 +158,11 @@ impl View for DiagnosticIndicator {
} else {
style.container_ok
})
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
})
}
})
.with_tooltip::<Summary>(
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Deploy))
.with_tooltip::<Summary, _>(
0,
"Project Diagnostics".to_string(),
Some(Box::new(crate::Deploy)),
@@ -179,10 +170,10 @@ impl View for DiagnosticIndicator {
cx,
)
.aligned()
.into_any(),
.boxed(),
);
let style = &theme::current(cx).workspace.status_bar;
let style = &cx.global::<Settings>().theme.workspace.status_bar;
let item_spacing = style.item_spacing;
if in_progress {
@@ -190,12 +181,13 @@ impl View for DiagnosticIndicator {
Label::new("Checking…", style.diagnostic_message.default.text.clone())
.aligned()
.contained()
.with_margin_left(item_spacing),
.with_margin_left(item_spacing)
.boxed(),
);
} else if let Some(diagnostic) = &self.current_diagnostic {
let message_style = style.diagnostic_message.clone();
element.add_child(
MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
MouseEventHandler::<Message>::new(1, cx, |state, _| {
Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(),
message_style.style_for(state, false).text.clone(),
@@ -203,15 +195,17 @@ impl View for DiagnosticIndicator {
.aligned()
.contained()
.with_margin_left(item_spacing)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.go_to_next_diagnostic(&Default::default(), cx)
}),
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(GoToDiagnostic)
})
.boxed(),
);
}
element.into_any_named("diagnostic indicator")
element.named("diagnostic indicator")
}
fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {

View File

@@ -4,9 +4,9 @@ use collections::HashSet;
use gpui::{
elements::{Empty, MouseEventHandler, Overlay},
geometry::{rect::RectF, vector::Vector2F},
platform::{CursorStyle, MouseButton},
scene::{MouseDown, MouseDrag},
AnyElement, Element, View, ViewContext, WeakViewHandle, WindowContext,
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
View, WeakViewHandle,
};
const DEAD_ZONE: f32 = 4.;
@@ -26,7 +26,7 @@ enum State<V: View> {
region_offset: Vector2F,
region: RectF,
payload: Rc<dyn Any + 'static>,
render: Rc<dyn Fn(Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
},
Canceled,
}
@@ -111,7 +111,7 @@ impl<V: View> DragAndDrop<V> {
})
}
pub fn drag_started(event: MouseDown, cx: &mut WindowContext) {
pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
cx.update_global(|this: &mut Self, _| {
this.currently_dragged = Some(State::Down {
region_offset: event.position - event.region.origin(),
@@ -123,8 +123,8 @@ impl<V: View> DragAndDrop<V> {
pub fn dragging<T: Any>(
event: MouseDrag,
payload: Rc<T>,
cx: &mut WindowContext,
render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
cx: &mut EventContext,
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
) {
let window_id = cx.window_id();
cx.update_global(|this: &mut Self, cx| {
@@ -178,7 +178,7 @@ impl<V: View> DragAndDrop<V> {
});
}
pub fn render(cx: &mut ViewContext<V>) -> Option<AnyElement<V>> {
pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
enum DraggedElementHandler {}
cx.global::<Self>()
.currently_dragged
@@ -199,25 +199,23 @@ impl<V: View> DragAndDrop<V> {
return None;
}
let position = (position - region_offset).round();
let position = position - region_offset;
Some(
Overlay::new(
MouseEventHandler::<DraggedElementHandler, V>::new(
0,
cx,
|_, cx| render(payload, cx),
)
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
render(payload, cx)
})
.with_cursor_style(CursorStyle::Arrow)
.on_up(MouseButton::Left, |_, _, cx| {
cx.window_context().defer(|cx| {
.on_up(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| {
this.finish_dragging(cx)
});
});
cx.propagate_event();
})
.on_up_out(MouseButton::Left, |_, _, cx| {
cx.window_context().defer(|cx| {
.on_up_out(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| {
this.finish_dragging(cx)
});
@@ -227,38 +225,43 @@ impl<V: View> DragAndDrop<V> {
.with_hoverable(false)
.constrained()
.with_width(region.width())
.with_height(region.height()),
.with_height(region.height())
.boxed(),
)
.with_anchor_position(position)
.into_any(),
.boxed(),
)
}
State::Canceled => Some(
MouseEventHandler::<DraggedElementHandler, V>::new(0, cx, |_, _| {
Empty::new().constrained().with_width(0.).with_height(0.)
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
Empty::new()
.constrained()
.with_width(0.)
.with_height(0.)
.boxed()
})
.on_up(MouseButton::Left, |_, _, cx| {
cx.window_context().defer(|cx| {
.on_up(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, _| {
this.currently_dragged = None;
});
});
})
.on_up_out(MouseButton::Left, |_, _, cx| {
cx.window_context().defer(|cx| {
.on_up_out(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, _| {
this.currently_dragged = None;
});
});
})
.into_any(),
.boxed(),
),
}
})
}
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut WindowContext) {
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut MutableAppContext) {
if let Some(State::Dragging {
payload, window_id, ..
}) = &self.currently_dragged
@@ -271,13 +274,13 @@ impl<V: View> DragAndDrop<V> {
}
}
fn finish_dragging(&mut self, cx: &mut WindowContext) {
fn finish_dragging(&mut self, cx: &mut MutableAppContext) {
if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
self.notify_containers_for_window(window_id, cx);
}
}
fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut WindowContext) {
fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut MutableAppContext) {
self.containers.retain(|container| {
if let Some(container) = container.upgrade(cx) {
if container.window_id() == window_id {
@@ -291,35 +294,35 @@ impl<V: View> DragAndDrop<V> {
}
}
pub trait Draggable<V: View> {
fn as_draggable<D: View, P: Any>(
pub trait Draggable {
fn as_draggable<V: View, P: Any>(
self,
payload: P,
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
) -> Self
where
Self: Sized;
}
impl<Tag, V: View> Draggable<V> for MouseEventHandler<Tag, V> {
fn as_draggable<D: View, P: Any>(
impl<Tag> Draggable for MouseEventHandler<Tag> {
fn as_draggable<V: View, P: Any>(
self,
payload: P,
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
) -> Self
where
Self: Sized,
{
let payload = Rc::new(payload);
let render = Rc::new(render);
self.on_down(MouseButton::Left, move |e, _, cx| {
self.on_down(MouseButton::Left, move |e, cx| {
cx.propagate_event();
DragAndDrop::<D>::drag_started(e, cx);
DragAndDrop::<V>::drag_started(e, cx);
})
.on_drag(MouseButton::Left, move |e, _, cx| {
.on_drag(MouseButton::Left, move |e, cx| {
let payload = payload.clone();
let render = render.clone();
DragAndDrop::<D>::dragging(e, payload, cx, render)
DragAndDrop::<V>::dragging(e, payload, cx, render)
})
}
}

View File

@@ -11,7 +11,6 @@ doctest = false
[features]
test-support = [
"rand",
"copilot/test-support",
"text/test-support",
"language/test-support",
"gpui/test-support",
@@ -23,11 +22,10 @@ test-support = [
]
[dependencies]
client = { path = "../client" }
clock = { path = "../clock" }
copilot = { path = "../copilot" }
db = { path = "../db" }
drag_and_drop = { path = "../drag_and_drop" }
text = { path = "../text" }
clock = { path = "../clock" }
db = { path = "../db" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
@@ -40,36 +38,30 @@ rpc = { path = "../rpc" }
settings = { path = "../settings" }
snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
theme = { path = "../theme" }
util = { path = "../util" }
sqlez = { path = "../sqlez" }
workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow.workspace = true
futures.workspace = true
anyhow = "1.0"
futures = "0.3"
indoc = "1.0.4"
itertools = "0.10"
lazy_static.workspace = true
log.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
rand = { workspace = true, optional = true }
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
lazy_static = "1.4"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = "2.1.1"
parking_lot = "0.11"
postage = { version = "0.4", features = ["futures-traits"] }
rand = { version = "0.8.3", optional = true }
serde = { workspace = true }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { version = "*", optional = true }
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
[dev-dependencies]
copilot = { path = "../copilot", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
@@ -78,11 +70,10 @@ util = { path = "../util", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
rand.workspace = true
unindent.workspace = true
ctor = "0.1"
env_logger = "0.9"
rand = "0.8"
unindent = "0.1.7"
tree-sitter = "0.20"
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"

View File

@@ -1,9 +1,9 @@
use crate::EditorSettings;
use gpui::{Entity, ModelContext};
use settings::SettingsStore;
use smol::Timer;
use std::time::Duration;
use gpui::{Entity, ModelContext};
use settings::Settings;
use smol::Timer;
pub struct BlinkManager {
blink_interval: Duration,
@@ -15,9 +15,12 @@ pub struct BlinkManager {
impl BlinkManager {
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
// Make sure we blink the cursors if the setting is re-enabled
cx.observe_global::<SettingsStore, _>(move |this, cx| {
this.blink_cursors(this.blink_epoch, cx)
let weak_handle = cx.weak_handle();
cx.observe_global::<Settings, _>(move |_, cx| {
if let Some(this) = weak_handle.upgrade(cx) {
// Make sure we blink the cursors if the setting is re-enabled
this.update(cx, |this, cx| this.blink_cursors(this.blink_epoch, cx));
}
})
.detach();
@@ -64,7 +67,7 @@ impl BlinkManager {
}
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if settings::get::<EditorSettings>(cx).cursor_blink {
if cx.global::<Settings>().cursor_blink {
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
self.visible = !self.visible;
cx.notify();

View File

@@ -1,24 +1,20 @@
mod block_map;
mod fold_map;
mod suggestion_map;
mod tab_map;
mod wrap_map;
use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
pub use block_map::{BlockMap, BlockPoint};
use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::{FoldMap, FoldOffset};
use fold_map::FoldMap;
use gpui::{
color::Color,
fonts::{FontId, HighlightStyle},
Entity, ModelContext, ModelHandle,
};
use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
};
use language::{OffsetUtf16, Point, Subscription as BufferSubscription};
use settings::Settings;
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
pub use suggestion_map::Suggestion;
use suggestion_map::SuggestionMap;
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
use wrap_map::WrapMap;
@@ -44,7 +40,6 @@ pub struct DisplayMap {
buffer: ModelHandle<MultiBuffer>,
buffer_subscription: BufferSubscription,
fold_map: FoldMap,
suggestion_map: SuggestionMap,
tab_map: TabMap,
wrap_map: ModelHandle<WrapMap>,
block_map: BlockMap,
@@ -70,7 +65,6 @@ impl DisplayMap {
let tab_size = Self::tab_size(&buffer, cx);
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (suggestion_map, snapshot) = SuggestionMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
@@ -79,7 +73,6 @@ impl DisplayMap {
buffer,
buffer_subscription,
fold_map,
suggestion_map,
tab_map,
wrap_map,
block_map,
@@ -91,25 +84,21 @@ impl DisplayMap {
pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits);
let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (tab_snapshot, edits) = self
.tab_map
.sync(suggestion_snapshot.clone(), edits, tab_size);
let (wrap_snapshot, edits) = self
let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits, tab_size);
let (wraps_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits);
.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
let blocks_snapshot = self.block_map.read(wraps_snapshot.clone(), edits);
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
fold_snapshot,
suggestion_snapshot,
tab_snapshot,
wrap_snapshot,
block_snapshot,
folds_snapshot,
tabs_snapshot,
wraps_snapshot,
blocks_snapshot,
text_highlights: self.text_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
}
@@ -133,14 +122,12 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -158,14 +145,12 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -182,7 +167,6 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -200,7 +184,6 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -231,32 +214,6 @@ impl DisplayMap {
self.text_highlights.remove(&Some(type_id))
}
pub fn has_suggestion(&self) -> bool {
self.suggestion_map.has_suggestion()
}
pub fn replace_suggestion<T>(
&self,
new_suggestion: Option<Suggestion<T>>,
cx: &mut ModelContext<Self>,
) -> Option<Suggestion<FoldOffset>>
where
T: ToPoint,
{
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits, old_suggestion) =
self.suggestion_map.replace(new_suggestion, snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
old_suggestion
}
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
@@ -277,7 +234,8 @@ impl DisplayMap {
.as_singleton()
.and_then(|buffer| buffer.read(cx).language())
.map(|language| language.name());
language_settings(language_name.as_deref(), cx).tab_size
cx.global::<Settings>().tab_size(language_name.as_deref())
}
#[cfg(test)]
@@ -288,11 +246,10 @@ impl DisplayMap {
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
fold_snapshot: fold_map::FoldSnapshot,
suggestion_snapshot: suggestion_map::SuggestionSnapshot,
tab_snapshot: tab_map::TabSnapshot,
wrap_snapshot: wrap_map::WrapSnapshot,
block_snapshot: block_map::BlockSnapshot,
folds_snapshot: fold_map::FoldSnapshot,
tabs_snapshot: tab_map::TabSnapshot,
wraps_snapshot: wrap_map::WrapSnapshot,
blocks_snapshot: block_map::BlockSnapshot,
text_highlights: TextHighlights,
clip_at_line_ends: bool,
}
@@ -300,7 +257,7 @@ pub struct DisplaySnapshot {
impl DisplaySnapshot {
#[cfg(test)]
pub fn fold_count(&self) -> usize {
self.fold_snapshot.fold_count()
self.folds_snapshot.fold_count()
}
pub fn is_empty(&self) -> bool {
@@ -308,7 +265,7 @@ impl DisplaySnapshot {
}
pub fn buffer_rows(&self, start_row: u32) -> DisplayBufferRows {
self.block_snapshot.buffer_rows(start_row)
self.blocks_snapshot.buffer_rows(start_row)
}
pub fn max_buffer_row(&self) -> u32 {
@@ -317,9 +274,9 @@ impl DisplaySnapshot {
pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop {
let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left);
let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Left);
*fold_point.column_mut() = 0;
point = fold_point.to_buffer_point(&self.fold_snapshot);
point = fold_point.to_buffer_point(&self.folds_snapshot);
let mut display_point = self.point_to_display_point(point, Bias::Left);
*display_point.column_mut() = 0;
@@ -333,9 +290,9 @@ impl DisplaySnapshot {
pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop {
let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right);
*fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row());
point = fold_point.to_buffer_point(&self.fold_snapshot);
let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Right);
*fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row());
point = fold_point.to_buffer_point(&self.folds_snapshot);
let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row());
@@ -365,39 +322,37 @@ impl DisplaySnapshot {
}
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
let fold_point = self.fold_snapshot.to_fold_point(point, bias);
let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
let tab_point = self.tab_snapshot.to_tab_point(suggestion_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
let fold_point = self.folds_snapshot.to_fold_point(point, bias);
let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
let wrap_point = self.wraps_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.blocks_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0;
let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
fold_point.to_buffer_point(&self.fold_snapshot)
let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
let fold_point = self.tabs_snapshot.to_fold_point(tab_point, bias).0;
fold_point.to_buffer_point(&self.folds_snapshot)
}
pub fn max_point(&self) -> DisplayPoint {
DisplayPoint(self.block_snapshot.max_point())
DisplayPoint(self.blocks_snapshot.max_point())
}
/// Returns text chunks starting at the given display row until the end of the file
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
self.block_snapshot
.chunks(display_row..self.max_point().row() + 1, false, None, None)
self.blocks_snapshot
.chunks(display_row..self.max_point().row() + 1, false, None)
.map(|h| h.text)
}
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.block_snapshot
.chunks(row..row + 1, false, None, None)
self.blocks_snapshot
.chunks(row..row + 1, false, None)
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
@@ -405,25 +360,16 @@ impl DisplaySnapshot {
})
}
pub fn chunks(
&self,
display_rows: Range<u32>,
language_aware: bool,
suggestion_highlight: Option<HighlightStyle>,
) -> DisplayChunks<'_> {
self.block_snapshot.chunks(
display_rows,
language_aware,
Some(&self.text_highlights),
suggestion_highlight,
)
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
self.blocks_snapshot
.chunks(display_rows, language_aware, Some(&self.text_highlights))
}
pub fn chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
self.text_chunks(point.row())
.flat_map(str::chars)
.skip_while({
@@ -450,7 +396,7 @@ impl DisplaySnapshot {
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
self.reverse_text_chunks(point.row())
.flat_map(|chunk| chunk.chars().rev())
.skip_while({
@@ -564,7 +510,7 @@ impl DisplaySnapshot {
}
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
let mut clipped = self.block_snapshot.clip_point(point.0, bias);
let mut clipped = self.blocks_snapshot.clip_point(point.0, bias);
if self.clip_at_line_ends {
clipped = self.clip_at_line_end(DisplayPoint(clipped)).0
}
@@ -575,7 +521,7 @@ impl DisplaySnapshot {
let mut point = point.0;
if point.column == self.line_len(point.row) {
point.column = point.column.saturating_sub(1);
point = self.block_snapshot.clip_point(point, Bias::Left);
point = self.blocks_snapshot.clip_point(point, Bias::Left);
}
DisplayPoint(point)
}
@@ -584,34 +530,37 @@ impl DisplaySnapshot {
where
T: ToOffset,
{
self.fold_snapshot.folds_in_range(range)
self.folds_snapshot.folds_in_range(range)
}
pub fn blocks_in_range(
&self,
rows: Range<u32>,
) -> impl Iterator<Item = (u32, &TransformBlock)> {
self.block_snapshot.blocks_in_range(rows)
self.blocks_snapshot.blocks_in_range(rows)
}
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
self.fold_snapshot.intersects_fold(offset)
self.folds_snapshot.intersects_fold(offset)
}
pub fn is_line_folded(&self, buffer_row: u32) -> bool {
self.fold_snapshot.is_line_folded(buffer_row)
pub fn is_line_folded(&self, display_row: u32) -> bool {
let block_point = BlockPoint(Point::new(display_row, 0));
let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
self.folds_snapshot.is_line_folded(tab_point.row())
}
pub fn is_block_line(&self, display_row: u32) -> bool {
self.block_snapshot.is_block_line(display_row)
self.blocks_snapshot.is_block_line(display_row)
}
pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
let wrap_row = self
.block_snapshot
.blocks_snapshot
.to_wrap_point(BlockPoint::new(display_row, 0))
.row();
self.wrap_snapshot.soft_wrap_indent(wrap_row)
self.wraps_snapshot.soft_wrap_indent(wrap_row)
}
pub fn text(&self) -> String {
@@ -645,84 +594,57 @@ impl DisplaySnapshot {
(indent, is_blank)
}
pub fn line_indent_for_buffer_row(&self, buffer_row: u32) -> (u32, bool) {
let (buffer, range) = self
.buffer_snapshot
.buffer_line_for_row(buffer_row)
.unwrap();
let mut indent_size = 0;
let mut is_blank = false;
for c in buffer.chars_at(Point::new(range.start.row, 0)) {
if c == ' ' || c == '\t' {
indent_size += 1;
} else {
if c == '\n' {
is_blank = true;
}
break;
}
}
(indent_size, is_blank)
}
pub fn line_len(&self, row: u32) -> u32 {
self.block_snapshot.line_len(row)
self.blocks_snapshot.line_len(row)
}
pub fn longest_row(&self) -> u32 {
self.block_snapshot.longest_row()
self.blocks_snapshot.longest_row()
}
pub fn fold_for_line(self: &Self, buffer_row: u32) -> Option<FoldStatus> {
if self.is_line_folded(buffer_row) {
Some(FoldStatus::Folded)
} else if self.is_foldable(buffer_row) {
pub fn fold_for_line(self: &Self, display_row: u32) -> Option<FoldStatus> {
if self.is_foldable(display_row) {
Some(FoldStatus::Foldable)
} else if self.is_line_folded(display_row) {
Some(FoldStatus::Folded)
} else {
None
}
}
pub fn is_foldable(self: &Self, buffer_row: u32) -> bool {
let max_row = self.buffer_snapshot.max_buffer_row();
if buffer_row >= max_row {
pub fn is_foldable(self: &Self, row: u32) -> bool {
let max_point = self.max_point();
if row >= max_point.row() {
return false;
}
let (indent_size, is_blank) = self.line_indent_for_buffer_row(buffer_row);
let (start_indent, is_blank) = self.line_indent(row);
if is_blank {
return false;
}
for next_row in (buffer_row + 1)..=max_row {
let (next_indent_size, next_line_is_blank) = self.line_indent_for_buffer_row(next_row);
if next_indent_size > indent_size {
return true;
} else if !next_line_is_blank {
break;
for display_row in next_rows(row, self) {
let (indent, is_blank) = self.line_indent(display_row);
if !is_blank {
return indent > start_indent;
}
}
false
return false;
}
pub fn foldable_range(self: &Self, buffer_row: u32) -> Option<Range<Point>> {
let start = Point::new(buffer_row, self.buffer_snapshot.line_len(buffer_row));
if self.is_foldable(start.row) && !self.is_line_folded(start.row) {
let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
let max_point = self.buffer_snapshot.max_point();
pub fn foldable_range(self: &Self, row: u32) -> Option<Range<DisplayPoint>> {
let start = DisplayPoint::new(row, self.line_len(row));
if self.is_foldable(row) && !self.is_line_folded(start.row()) {
let (start_indent, _) = self.line_indent(row);
let max_point = self.max_point();
let mut end = None;
for row in (buffer_row + 1)..=max_point.row {
let (indent, is_blank) = self.line_indent_for_buffer_row(row);
for row in next_rows(row, self) {
let (indent, is_blank) = self.line_indent(row);
if !is_blank && indent <= start_indent {
let prev_row = row - 1;
end = Some(Point::new(
prev_row,
self.buffer_snapshot.line_len(prev_row),
));
end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1)));
break;
}
}
@@ -789,11 +711,10 @@ impl DisplayPoint {
}
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0;
let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point);
fold_point.to_buffer_offset(&map.fold_snapshot)
let unblocked_point = map.blocks_snapshot.to_wrap_point(self.0);
let unwrapped_point = map.wraps_snapshot.to_tab_point(unblocked_point);
let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
unexpanded_point.to_buffer_offset(&map.folds_snapshot)
}
}
@@ -843,13 +764,9 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
pub mod tests {
use super::*;
use crate::{movement, test::marked_display_snapshot};
use gpui::{color::Color, elements::*, test::observe, AppContext};
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, SelectionGoal,
};
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
use language::{Buffer, Language, LanguageConfig, SelectionGoal};
use rand::{prelude::*, Rng};
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use theme::SyntaxTheme;
@@ -868,9 +785,7 @@ pub mod tests {
let mut tab_size = rng.gen_range(1..=4);
let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
let excerpt_header_height = rng.gen_range(1..=5);
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -886,7 +801,9 @@ pub mod tests {
log::info!("wrap width: {:?}", wrap_width);
cx.update(|cx| {
init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
let mut settings = Settings::test(cx);
settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
cx.set_global(settings)
});
let buffer = cx.update(|cx| {
@@ -918,10 +835,10 @@ pub mod tests {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
for _i in 0..operations {
@@ -941,11 +858,9 @@ pub mod tests {
tab_size = *tab_sizes.choose(&mut rng).unwrap();
log::info!("setting tab size to {:?}", tab_size);
cx.update(|cx| {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |s| {
s.defaults.tab_size = NonZeroU32::new(tab_size);
});
});
let mut settings = Settings::test(cx);
settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
cx.set_global(settings)
});
}
30..=44 => {
@@ -977,7 +892,7 @@ pub mod tests {
position,
height,
disposition,
render: Arc::new(|_| Empty::new().into_any()),
render: Arc::new(|_| Empty::new().boxed()),
}
})
.collect::<Vec<_>>();
@@ -1028,10 +943,10 @@ pub mod tests {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
fold_count = snapshot.fold_count();
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
// Line boundaries
@@ -1121,20 +1036,19 @@ pub mod tests {
}
#[gpui::test(retries = 5)]
fn test_soft_wraps(cx: &mut AppContext) {
fn test_soft_wraps(cx: &mut MutableAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
init_test(cx, |_| {});
cx.foreground().forbid_parking();
let font_cache = cx.font_cache();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 12.0;
let wrap_width = Some(64.);
cx.set_global(Settings::test(cx));
let text = "one two three four five\nsix seven eight";
let buffer = MultiBuffer::build_simple(text, cx);
@@ -1213,15 +1127,11 @@ pub mod tests {
}
#[gpui::test]
fn test_text_chunks(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});
fn test_text_chunks(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let text = sample_text(6, 6, 'a');
let buffer = MultiBuffer::build_simple(&text, cx);
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
@@ -1229,7 +1139,6 @@ pub mod tests {
let font_size = 14.0;
let map =
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
buffer.update(cx, |buffer, cx| {
buffer.edit(
vec![
@@ -1294,17 +1203,18 @@ pub mod tests {
.unwrap(),
);
language.set_theme(&theme);
cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
cx.update(|cx| {
let mut settings = Settings::test(cx);
settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
cx.set_global(settings);
});
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let font_cache = cx.font_cache();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1384,7 +1294,7 @@ pub mod tests {
);
language.set_theme(&theme);
cx.update(|cx| init_test(cx, |_| {}));
cx.update(|cx| cx.set_global(Settings::test(cx)));
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
@@ -1392,9 +1302,7 @@ pub mod tests {
let font_cache = cx.font_cache();
let family_id = font_cache
.load_family(&["Courier"], &Default::default())
.unwrap();
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1431,8 +1339,9 @@ pub mod tests {
#[gpui::test]
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
cx.update(|cx| init_test(cx, |_| {}));
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
cx.update(|cx| cx.set_global(Settings::test(cx)));
let theme = SyntaxTheme::new(vec![
("operator".to_string(), Color::red().into()),
("string".to_string(), Color::green().into()),
@@ -1465,9 +1374,7 @@ pub mod tests {
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let font_cache = cx.font_cache();
let family_id = font_cache
.load_family(&["Courier"], &Default::default())
.unwrap();
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1510,10 +1417,9 @@ pub mod tests {
}
#[gpui::test]
fn test_clip_point(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
match bias {
@@ -1560,10 +1466,10 @@ pub mod tests {
}
#[gpui::test]
fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});
fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(text: &str, cx: &mut gpui::AppContext) {
fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
unmarked_snapshot.clip_at_line_ends = true;
assert_eq!(
@@ -1579,15 +1485,12 @@ pub mod tests {
}
#[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});
fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let text = "\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = MultiBuffer::build_simple(text, cx);
let font_cache = cx.font_cache();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1641,14 +1544,11 @@ pub mod tests {
}
#[gpui::test]
fn test_max_point(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});
fn test_max_point(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let font_cache = cx.font_cache();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1691,7 +1591,7 @@ pub mod tests {
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,
theme: &'a SyntaxTheme,
cx: &mut AppContext,
cx: &mut MutableAppContext,
) -> Vec<(String, Option<Color>)> {
chunks(rows, map, theme, cx)
.into_iter()
@@ -1703,11 +1603,11 @@ pub mod tests {
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,
theme: &'a SyntaxTheme,
cx: &mut AppContext,
cx: &mut MutableAppContext,
) -> Vec<(String, Option<Color>, Option<Color>)> {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
for chunk in snapshot.chunks(rows, true, None) {
for chunk in snapshot.chunks(rows, true) {
let syntax_color = chunk
.syntax_highlight_id
.and_then(|id| id.style(theme)?.color);
@@ -1722,13 +1622,4 @@ pub mod tests {
}
chunks
}
fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
language::init(cx);
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
}
}

View File

@@ -2,9 +2,9 @@ use super::{
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
TextHighlights,
};
use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
use gpui::{fonts::HighlightStyle, AnyElement, ViewContext};
use gpui::{ElementBox, RenderContext};
use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex;
use std::{
@@ -50,7 +50,7 @@ struct BlockRow(u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32);
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> ElementBox>;
pub struct Block {
id: BlockId,
@@ -69,7 +69,7 @@ where
pub position: P,
pub height: u8,
pub style: BlockStyle,
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
pub render: Arc<dyn Fn(&mut BlockContext) -> ElementBox>,
pub disposition: BlockDisposition,
}
@@ -80,8 +80,8 @@ pub enum BlockStyle {
Sticky,
}
pub struct BlockContext<'a, 'b, 'c> {
pub view_context: &'c mut ViewContext<'a, 'b, Editor>,
pub struct BlockContext<'a, 'b> {
pub cx: &'b mut RenderContext<'a, crate::Editor>,
pub anchor_x: f32,
pub scroll_x: f32,
pub gutter_width: f32,
@@ -572,7 +572,7 @@ impl<'a> BlockMapWriter<'a> {
impl BlockSnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(0..self.transforms.summary().output_rows, false, None, None)
self.chunks(0..self.transforms.summary().output_rows, false, None)
.map(|chunk| chunk.text)
.collect()
}
@@ -582,7 +582,6 @@ impl BlockSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
suggestion_highlight: Option<HighlightStyle>,
) -> BlockChunks<'a> {
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -615,7 +614,6 @@ impl BlockSnapshot {
input_start..input_end,
language_aware,
text_highlights,
suggestion_highlight,
),
input_chunk: Default::default(),
transforms: cursor,
@@ -833,7 +831,10 @@ impl<'a> Iterator for BlockChunks<'a> {
return Some(Chunk {
text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
..Default::default()
syntax_highlight_id: None,
highlight_style: None,
diagnostic_severity: None,
is_unnecessary: false,
});
}
@@ -929,22 +930,22 @@ impl BlockDisposition {
}
}
impl<'a, 'b, 'c> Deref for BlockContext<'a, 'b, 'c> {
type Target = ViewContext<'a, 'b, Editor>;
impl<'a, 'b> Deref for BlockContext<'a, 'b> {
type Target = RenderContext<'a, crate::Editor>;
fn deref(&self) -> &Self::Target {
self.view_context
self.cx
}
}
impl DerefMut for BlockContext<'_, '_, '_> {
impl<'a, 'b> DerefMut for BlockContext<'a, 'b> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.view_context
self.cx
}
}
impl Block {
pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
pub fn render(&self, cx: &mut BlockContext) -> ElementBox {
self.render.lock()(cx)
}
@@ -988,12 +989,11 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
#[cfg(test)]
mod tests {
use super::*;
use crate::display_map::suggestion_map::SuggestionMap;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
use crate::multi_buffer::MultiBuffer;
use gpui::{elements::Empty, Element};
use rand::prelude::*;
use settings::SettingsStore;
use settings::Settings;
use std::env;
use util::RandomCharIter;
@@ -1012,13 +1012,10 @@ mod tests {
}
#[gpui::test]
fn test_basic_blocks(cx: &mut gpui::AppContext) {
init_test(cx);
fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
@@ -1029,10 +1026,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1042,21 +1038,21 @@ mod tests {
position: buffer_snapshot.anchor_after(Point::new(1, 0)),
height: 1,
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().into_any_named("block 1")),
render: Arc::new(|_| Empty::new().named("block 1")),
},
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 2)),
height: 2,
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().into_any_named("block 2")),
render: Arc::new(|_| Empty::new().named("block 2")),
},
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(3, 3)),
height: 3,
disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().into_any_named("block 3")),
render: Arc::new(|_| Empty::new().named("block 3")),
},
]);
@@ -1174,27 +1170,22 @@ mod tests {
buffer.snapshot(cx)
});
let (fold_snapshot, fold_edits) =
let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot, subscription.consume().into_inner());
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap());
let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, 4.try_into().unwrap());
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
let snapshot = block_map.read(wraps_snapshot, wrap_edits);
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
}
#[gpui::test]
fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
init_test(cx);
fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
@@ -1204,10 +1195,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
let (_, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, tabs_snapshot) = TabMap::new(folds_snapshot, 1.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, Some(60.), cx);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1216,14 +1206,14 @@ mod tests {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 12)),
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().into_any_named("block 1")),
render: Arc::new(|_| Empty::new().named("block 1")),
height: 1,
},
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 1)),
disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().into_any_named("block 2")),
render: Arc::new(|_| Empty::new().named("block 2")),
height: 1,
},
]);
@@ -1238,8 +1228,8 @@ mod tests {
}
#[gpui::test(iterations = 100)]
fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
init_test(cx);
fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
cx.set_global(Settings::test(cx));
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
@@ -1251,10 +1241,7 @@ mod tests {
Some(rng.gen_range(0.0..=100.0))
};
let tab_size = 1.try_into().unwrap();
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
@@ -1276,11 +1263,10 @@ mod tests {
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size);
let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot, tab_size);
let (wrap_map, wraps_snapshot) =
WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
let mut block_map = BlockMap::new(
wraps_snapshot,
buffer_start_header_height,
@@ -1326,19 +1312,17 @@ mod tests {
position,
height,
disposition,
render: Arc::new(|_| Empty::new().into_any()),
render: Arc::new(|_| Empty::new().boxed()),
}
})
.collect::<Vec<_>>();
let (fold_snapshot, fold_edits) =
let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), vec![]);
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
let block_ids = block_map.insert(block_properties.clone());
@@ -1356,14 +1340,12 @@ mod tests {
})
.collect();
let (fold_snapshot, fold_edits) =
let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), vec![]);
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.remove(block_ids_to_remove);
@@ -1380,13 +1362,10 @@ mod tests {
}
}
let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
assert_eq!(
@@ -1497,7 +1476,6 @@ mod tests {
start_row as u32..blocks_snapshot.max_point().row + 1,
false,
None,
None,
)
.map(|chunk| chunk.text)
.collect::<String>();
@@ -1647,11 +1625,6 @@ mod tests {
}
}
fn init_test(cx: &mut gpui::AppContext) {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
}
impl TransformBlock {
fn as_custom(&self) -> Option<&Block> {
match self {

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