Compare commits

..

77 Commits

Author SHA1 Message Date
Conrad Irwin
dd25902aeb collab 0.41.0 2024-01-25 11:17:09 -07:00
Conrad Irwin
fc2f5d86c7 collab fixes (#6720)
- Fail faster on serialization failure
- Move expensive participant update out of transaction

Release Notes:

- Fixed creating/moving channels in busy workspaces
2024-01-25 11:16:19 -07:00
Conrad Irwin
fbdca993ff Format 2024-01-25 11:12:02 -07:00
Conrad Irwin
adb6f3e9f7 Move expensive participant update out of transaction
Co-Authored-By: Marshall <marshall@zed.dev>
2024-01-25 11:05:19 -07:00
Conrad Irwin
ca27ac21c2 Fail faster on serialization failure
Co-Authored-By: Thorsten <thorsten@zed.dev>
2024-01-25 09:59:57 -07:00
Marshall Bowers
7068161bd7 Sort .mailmap alphabetically (#6711)
This PR sorts the entries in the `.mailmap` file to keep them in
alphabetical order.

Release Notes:

- N/A
2024-01-25 09:51:47 -05:00
Kirill Bulatov
2b844f5cb5 Sort file finder matched entries better (#6704)
* applicable history items were sorted by latest opened order, now
sorted by match score as the search matches
* adjust the match sorting to show paths in the alphanumerical order (in
case of a tie on other params)

Release Notes:

- Improved file finder entries ordering

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-01-25 15:25:33 +02:00
Thorsten Ball
50c3ad963e Improve performance of select-all-matches (#6700)
This fixes #6440.

The previous approach was calling select-next-match in a loop, which
leaves optimizations on the table when you already know that you want to
select all of the matches.

So what we did here is to optimize the code for the "give me all
matches" case:

1. Find all results in the current buffer
2. Build up *all* selections
3. Sort selections & throw away overlapping ones (keep oldest)
4. Unfold if necessary
5. Render selections

On my M3 Max searching for `<span` in the test file [1] from the ticket,
it

previously took: ~1.07s
now takes: ~4ms

[1]:
https://github.com/standardebooks/edgar-allan-poe_poetry/blob/master/src/epub/text/poetry.xhtml

![screenshot-2024-01-25-12 49
32@2x](https://github.com/zed-industries/zed/assets/1185253/9f8ef0fa-a3a7-461c-9ed6-263e48835806)

### Release Notes:

- Improved performance of select-all-matches by factor of ~250
([#6440](https://github.com/zed-industries/zed/issues/6440)).
2024-01-25 13:59:46 +01:00
Thomas Coratger
e13fb31287 small refactoring in color crate (#6695)
Make the code more readable
2024-01-25 14:36:52 +02:00
Thorsten Ball
0a78c67647 Improve performance of select-all-matches
This fixes #6440.

The previous approach was calling select-next-match in a loop, which
leaves optimizations on the table when you already know that you want to
select all of the matches.

So what we did here is to optimize the code for the "give me all
matches" case:

1. Find all results in the current buffer
2. Build up *all* selections
3. Sort selections & throw away overlapping ones (keep oldest)
4. Unfold if necessary
5. Render selections

On my M3 Max searching for `<span` in the test file [1] from the ticket,
it

previously took: ~1.07s
now takes: ~4ms

[1]: https://github.com/standardebooks/edgar-allan-poe_poetry/blob/master/src/epub/text/poetry.xhtml

Co-authored-by: Antonio <antonio@zed.dev>
2024-01-25 13:35:22 +01:00
Piotr Osiewicz
062288dea5 Search woes (#6698)
Fixes #6441 
Release Notes:
- Fixed "SelectNextMatch", "SelectPrevMatch" and "SelectAllMatches"
actions not working when search bar is not visible.
- Fixed "SelectPrevMatch" not being bound in project search.

---------

Co-authored-by: Kirill <kirill@zed.dev>
2024-01-25 13:28:21 +01:00
Thorsten Ball
dd3ec15acc Log error if worktree fails to relativize git repo path (#6693)
This is a follow-up to #6459. It logs the error instead of silently
skipping it.

Release Notes:

- N/A
2024-01-25 11:27:50 +01:00
Thorsten Ball
d17d37ff61 Log error if worktree fails to relativize git repo path
We saw a panic that was caused by the previous `Option.unwrap()`, so
this changes the method to return a `Result` and logs the error if
possible.

Co-authored-by: Antonio <antonio@zed.dev>
2024-01-25 11:10:00 +01:00
Thorsten Ball
710e47977d Add Conrad and Thorsten to .mailmap file (#6691)
Follow-up to #4138.

Release Notes:

- N/A
2024-01-25 10:08:01 +01:00
Thorsten Ball
c6e7cf1cbc bugfix: fix LineEnding for windows (#6688)
fixes a bug when compiling on windows


![image](https://github.com/zed-industries/zed/assets/715417/5a35b1b2-29f4-4987-9410-730c9b287f82)

Release Notes:

- Fixed: compilation error related to `LineEnding` on Windows
2024-01-25 09:58:53 +01:00
Thorsten Ball
dcf05812c2 Add Conrad and Thorsten to .mailmap file 2024-01-25 09:56:14 +01:00
Sharun
0c4679f892 bugfix: fix LineEnding for windows 2024-01-25 03:16:42 -05:00
gmorenz
dd07d2f8a2 Update cocoa to crates.io version (#6452)
https://github.com/servo/core-foundation-rs/pull/457 was released in
cocoa 0.25, so this patch dependency on github can be removed.

Release Notes:

- N/A
2024-01-25 10:04:29 +02:00
Conrad Irwin
1c2859d72b collab errors (#4152)
One of the complaints of users on our first Hack call was that the error
messages you got when channel joining failed were not great.

This aims to fix that specific case, and lay the groundwork for future
improvements.

It adds two new methods to anyhow::Error

* `.error_code()` which returns a value from zed.proto (or
ErrorCode::Internal if the error has no specific tag)
* `.error_tag("key")` which returns the value of the tag (or None).

To construct errors with these fields set, you can use a builder API
based on the ErrorCode type:

* `Err(ErrorCode::Forbidden.anyhow())`
* `Err(ErrorCode::Forbidden.message("cannot join channel").into())` - to
add any context you want in the logs
* `Err(ErrorCode::WrongReleaseChannel.tag("required", "stable").into())`
- to add structured metadata to help the client handle the error better.


Release Notes:

- Improved error messaging when channel joining fails.
2024-01-24 23:23:58 -07:00
Conrad Irwin
01424a62ea Allow prompts to have detail, and use for good
Make channel panel errors louder
2024-01-24 23:15:37 -07:00
Sean Aye
5fcc75be1f gpui: Add hello world example (#6465)
Added hello world example to the gpui crate copied from https://gpui.rs

Release Notes:

- N/A
2024-01-25 01:03:53 -05:00
Conrad Irwin
cf3b2ba146 Fix panic from unwrapping a relativize() (#6459)
Release Notes:

- Fixed a panic that could occur finding a git repository.
2024-01-24 23:03:41 -07:00
Joseph T. Lyons
e77db87bad Remove references to community repo (#6466)
Release Notes:

- N/A
2024-01-25 00:38:01 -05:00
Joseph T. Lyons
f9170cb239 Remove references to community repo 2024-01-25 00:30:26 -05:00
Marshall Bowers
bf7489269e Clean up docs (#6464)
This PR cleans up some of the Zed meta docs.

Namely:
- Removed references to the `zed-industries/community` repository
- Removed old docs directory (it'll be in Git history if we need it)

Release Notes:

- N/A
2024-01-25 00:05:15 -05:00
Jeff Li
ba97661e1c Fix backend dependencies link in local collaboration docs (#6461)
Release Notes:

- N/A
2024-01-24 23:53:41 -05:00
Conrad Irwin
320088f1fa Fix panic from unwrapping a relativize() 2024-01-24 20:56:39 -07:00
Conrad Irwin
df420c3767 Better naming 2024-01-24 20:48:29 -07:00
Conrad Irwin
4bcd3494b7 Try to send typed errors back and forth
TEMP

TEMP

First pass of structured errors

Improved error handling for channel joining failures
2024-01-24 20:32:39 -07:00
Conrad Irwin
865369882e Fix circular locking in prompts (#6456)
Sometimes Cocoa calls app delegate methods (notably the display link)
while we're calling Cocoa methods. This causes a deadlock unless we
are careful to run cocao methods while we're not holding our internal
locks

Release Notes:

- Fixed a crash when opening the MacOS Save As dialogue.
2024-01-24 20:10:32 -07:00
Conrad Irwin
a181dc8d58 Use the correct snapshot when calculating mouse positions (#6453)
Release Notes:

- Fixed a panic in calculating remote cursor positions
2024-01-24 19:44:02 -07:00
Conrad Irwin
249a6da54a Fix circular locking in prompts
Sometimes Cocoa calls app delegate methods (notably the display link)
while we're calling Cocoa methods. This causes a deadlock unless we
are careful to run cocao methods while we're not holding our internal
locks
2024-01-24 19:40:01 -07:00
Conrad Irwin
f185aca25a Use the correct snapshot when calculating mouse positions 2024-01-24 19:10:53 -07:00
Max Brunsfeld
6ed7cc7833 Simplify language server startup (#6449)
These are just some small refactorings of our language-server-starting
code, motivated by another change that I decided to bail on:
https://github.com/zed-industries/zed/pull/6448.
2024-01-24 17:36:50 -08:00
Marshall Bowers
90c1d8f734 Update Cargo.lock (#6445)
This PR updates `Cargo.lock` to reflect the changes made in #6435.

Release Notes:

- N/A
2024-01-24 17:46:40 -05:00
Dragan Okanovic
416696a686 Fix typo in build instructions (#6444)
Typo fix.

Release Notes:

- N/A
2024-01-24 17:36:21 -05:00
Joseph T. Lyons
10437794e4 Remove unused dependency (#6435)
[[PR Description]]

The `color` crate is not actually used by the `zed` crate, despite being
listed as a dependency. This PR removes it.
2024-01-24 16:19:04 -05:00
Julia
569bb687be Prevent channel from vertically growing when containing facepile (#6439)
Previously at UI font size of 20
 - User-less channel: 28 px tall
 - Occupied channel: 30 px tall
 
Now, still at UI font size 20:
 - User-less channel: 30 px tall
 - Occupied channel: 30 px tall

Release Notes:

- Fixed an issue where a channel would grow in height while showing
participant avatars.
2024-01-24 16:17:25 -05:00
Julia
9bc968eabb Prevent channel from vertically growing when containing facepile 2024-01-24 15:57:12 -05:00
Conrad Irwin
4ac3095a15 chat mentions for newly joined users (#5099)
Release Notes:

- Added chat mentions for everyone in the call
2024-01-24 13:33:23 -07:00
Julian Braha
5907bb5b55 Remove unused dependency 2024-01-24 20:29:29 +00:00
Conrad Irwin
28b2c89254 Fix crash in feedback modal (#6431)
After the general release we saw a number of crashes due to a SEGFAULT
inside the
System::new() method apparently relating to refreshing the user list.

As we do not need the user list, and the similar code in the telemtry
create is not crashing,
do less work for now.

Release Notes:

- Fixed a crash when opening the feedback modal
2024-01-24 13:25:22 -07:00
Chase Weaver
cf3b4b0ba7 Fix: Comment shortcut in Svelte files now respects if the line(s) are HTML vs JS/TS (#5860)
Release Notes:

- Fixed: When using the comment shortcut, previously HTML or JS/TS would
all prefix lines with `// `. This PR brings the comments inline with
what is expected (`// ` for JS/TS, `<!-- ... -->` for HTML). Fixes
[#4578](https://github.com/zed-industries/zed/issues/4578).
2024-01-24 12:24:49 -08:00
Mikayla Maki
71ec781215 Fix RUSTSEC-2018-0017 (#5338)
[[PR Description]]

This replaces the `tempdir` crate with `tempfile`, which resolves
[RUSTSEC-2018-0017](https://rustsec.org/advisories/RUSTSEC-2018-0017).
2024-01-24 12:19:05 -08:00
Joseph T. Lyons
e1b7b5eaa6 Update all links pointing to community (#6434)
Release Notes:

- N/A
2024-01-24 15:17:47 -05:00
Joseph T. Lyons
40dbe15b2a Update all links point to community 2024-01-24 15:11:17 -05:00
Joseph T. Lyons
6c555fe13c Fix typos (#6429)
Release Notes:

- N/A
2024-01-24 15:02:03 -05:00
Conrad Irwin
334dc620ea Fix crash in feedback modal 2024-01-24 13:00:12 -07:00
Joseph T. Lyons
1a11da916b Fix typos 2024-01-24 14:58:47 -05:00
Joseph T. Lyons
fc01eeebbc Update links to script 2024-01-24 14:48:44 -05:00
Joseph T. Lyons
fde4c09906 Remove open source checklist from issue config 2024-01-24 14:45:17 -05:00
Conrad Irwin
482c01aaf3 Ensure chat opens when guests join shared projects (#6425)
This was broken because the panel was created before being added to a
dock. Invert the control order and add `starts_open()` to the Panel
trait (which lets us simplify how this worked for the ProjectPanel too)

Release Notes:

- Improved Chat visibility when joining a call as a guest
2024-01-24 12:45:04 -07:00
Joseph T. Lyons
db33eafdb1 Delete 0_feature_parity_report.yml 2024-01-24 14:44:00 -05:00
Joseph T. Lyons
855e0f6f36 Add top-ranking issues script 2024-01-24 14:40:55 -05:00
Conrad Irwin
c56debc705 clippy 2024-01-24 12:40:24 -07:00
Conrad Irwin
e7db5d0638 Use run_until_parked instead of condition
Avoids spurious failures when the CI server is going slow
2024-01-24 12:28:45 -07:00
Conrad Irwin
a860ca6a3c Fix tests that were toggling the panel shut 2024-01-24 12:22:12 -07:00
Conrad Irwin
4427e7968b (probably) install rosetta if it's not there (#5817)
We set up a new CI server recently, and this caused a build to fail

Release Notes:

- N/A
2024-01-24 12:17:02 -07:00
Conrad Irwin
2a11c22760 Ensure chat opens when guests join shared projects
This was broken because the panel was created before being added to a
dock. Invert the control order and add `starts_open()` to the Panel
trait.
2024-01-24 12:06:03 -07:00
charlotte
6285decfa2 Add Git submodule initialization to repository setup steps (#5818)
Was running into an issue building live_kit_server after a fresh clone
due to missing dependencies for build.rs. The use of git submodules
wasn't currently documented.
2024-01-24 14:02:26 -05:00
Mikayla Maki
4599fa840d Revert "Ensure that notify observations are sent during Window::draw()" (#6152)
Reverts zed-industries/zed#4236

This causes an infinite loop when opening the language server logs
2024-01-24 10:56:45 -08:00
Pseudomata
da01c1a83b Open bug reports in zed-industries/zed (#5862)
It looks like bug reports are being moved from
`zed-industries/community` to `zed-industries/zed`. This PR updates the
`feedback` crate to update the GitHub URL to reflect this change.
2024-01-24 13:51:43 -05:00
Mikayla Maki
a7368904f3 Revert "Ensure that notify observations are sent during Window::draw()" 2024-01-24 10:50:52 -08:00
Conrad Irwin
ad537f638c (probably) install rosetta if it's not there 2024-01-24 11:35:59 -07:00
Conrad Irwin
e072c96003 Fix tests 2024-01-24 11:35:10 -07:00
Conrad Irwin
9693e14809 Merge branch 'main' into chat-mentions-for-newly-joined-users 2024-01-24 11:31:27 -07:00
Patrick Dubroy
291f353085 Fix registers link in README.md (#5389)
It looked to me that `registers` was intended to be a link here...if I'm
wrong, feel free to ignore :-)

Release Notes:

- N/A
2024-01-24 13:21:08 -05:00
Julian Braha
08d2ba72d6 Add missing .unwrap() 2024-01-24 18:19:03 +00:00
Conrad Irwin
c5ad1728f9 Clippy 2024-01-24 11:18:17 -07:00
Julian Braha
85f5e7d0bb Fix formatting with cargo-fmt 2024-01-24 18:10:11 +00:00
Julian Braha
fd6f71d287 Replace tempdir crate with tempfile 2024-01-24 17:58:09 +00:00
Conrad Irwin
c1df166700 Allow completions of everyone in the call 2024-01-24 10:51:46 -07:00
Max Brunsfeld
c81d318098 Start work on allowing mentions for all users in call 2024-01-24 09:45:26 -08:00
Joseph T. Lyons
f8604e88ef Add GitHub items 2024-01-24 12:26:15 -05:00
Conrad Irwin
489ef23b76 Show hovered cursors less flickerily (#4242)
Now when hovering on a cursor it'll stay around for 2 seconds

Release Notes:

- Improved hovering over collaborators' cursors.
2024-01-24 10:20:34 -07:00
Joseph T. Lyons
a4897e00b4 v0.121.x dev 2024-01-24 10:54:33 -05:00
Conrad Irwin
4e085b2052 Show hovered cursors less flickerily 2024-01-23 22:32:22 -07:00
112 changed files with 2093 additions and 1512 deletions

View File

@@ -0,0 +1,24 @@
name: Feature Request
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
labels: ["admin read", "triage", "enhancement"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the feature
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: |
If applicable, add mockups / screenshots to help present your vision of the feature
description: Drag images into the text input below
validations:
required: false

View File

@@ -0,0 +1,47 @@
name: Language Support
description: Request language support
title: "<name_of_language> support"
labels:
[
"admin read",
"triage",
"enhancement",
"language",
"unsupported language",
"potential plugin",
]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: input
attributes:
label: Language
description: What language do you want support for?
placeholder: HTML
validations:
required: true
- type: input
attributes:
label: Tree Sitter parser link
description: If applicable, provide a link to the appropriate tree sitter parser. Look here first - https://tree-sitter.github.io/tree-sitter/#available-parsers
placeholder: https://github.com/tree-sitter/tree-sitter-html
validations:
required: false
- type: input
attributes:
label: Language server link
description: If applicable, provide a link to the appropriate language server. Look here first - https://microsoft.github.io/language-server-protocol/implementors/servers/
placeholder: https://github.com/Microsoft/vscode/tree/main/extensions/html-language-features/server
validations:
required: false
- type: textarea
attributes:
label: Misc notes
description: Provide any additional things the team should consider when adding support for this language
validations:
required: false

38
.github/ISSUE_TEMPLATE/2_bug_report.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Bug Report
description: "Tip: open this issue template from within Zed with the `file bug report` command palette action"
labels: ["admin read", "triage", "defect"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
validations:
required: false
- type: textarea
attributes:
label: |
If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
description: Drag Zed.log into the text input below
validations:
required: false

10
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
contact_links:
- name: Top-Ranking Issues
url: https://github.com/zed-industries/zed/issues/5393
about: See an overview of the most popular Zed issues
- name: Platform Support
url: https://github.com/zed-industries/zed/issues/5391
about: A quick note on platform support
- name: Postive Feedback
url: https://github.com/zed-industries/zed/discussions/5397
about: A central location for kind words about Zed

View File

@@ -2,4 +2,4 @@
Release Notes:
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).

View File

@@ -0,0 +1,17 @@
on:
schedule:
- cron: "0 */12 * * *"
workflow_dispatch:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10.5"
architecture: "x64"
cache: "pip"
- run: pip install -r script/update_top_ranking_issues/requirements.txt
- run: python script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --prod

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ DerivedData/
.netrc
.swiftpm
**/*.db
.pytest_cache
.venv

View File

@@ -11,6 +11,8 @@
Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
Julia <floc@unpromptedtirade.com>
@@ -37,3 +39,6 @@ Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
Piotr Osiewicz <piotr@zed.dev>
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
Thorsten Ball <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>

View File

@@ -4,14 +4,14 @@ Thanks for your interest in contributing to Zed, the collaborative platform that
We want to avoid anyone spending time on a pull request that may not be accepted, so we suggest you discuss your ideas with the team and community before starting on major changes. Bug fixes, however, are almost always welcome.
All activity in Zed forums is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
## Contribution ideas
If you're looking for ideas about what to work on, check out:
- Our public roadmap (link coming soon!) contains a rough outline of our near-term priorities for Zed.
- Our [top-ranking issues](https://github.com/zed-industries/community/issues/52) based on votes by the community.
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
Outside of a handful of extremely popular languages and themes, we are generally not looking to extend Zed's language or theme support by directly building them into Zed. We really want to build a plugin system to handle making the editor extensible going forward. If you are passionate about shipping new languages or themes we suggest contributing to the extension system to help us get there faster.

241
Cargo.lock generated
View File

@@ -692,7 +692,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"tempdir",
"tempfile",
"theme",
"util",
"workspace",
@@ -1423,36 +1423,37 @@ dependencies = [
[[package]]
name = "cocoa"
version = "0.24.0"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation",
"core-foundation",
"core-graphics",
"foreign-types",
"core-graphics 0.23.1",
"foreign-types 0.5.0",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.1.1"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation",
"core-graphics-types",
"foreign-types",
"libc",
"objc",
]
[[package]]
name = "collab"
version = "0.40.1"
version = "0.41.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1750,10 +1751,11 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys 0.8.3",
"core-foundation-sys 0.8.6",
"libc",
"uuid 0.5.1",
]
@@ -1766,29 +1768,44 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "core-graphics"
version = "0.22.3"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"foreign-types",
"foreign-types 0.3.2",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.1.1"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"foreign-types",
"libc",
]
@@ -1808,8 +1825,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
dependencies = [
"core-foundation",
"core-graphics",
"foreign-types",
"core-graphics 0.22.3",
"foreign-types 0.3.2",
"libc",
]
@@ -1840,7 +1857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c"
dependencies = [
"alsa",
"core-foundation-sys 0.8.3",
"core-foundation-sys 0.8.6",
"coreaudio-rs",
"dasp_sample",
"jni 0.19.0",
@@ -2060,7 +2077,7 @@ dependencies = [
"smol",
"sqlez",
"sqlez_macros",
"tempdir",
"tempfile",
"util",
]
@@ -2412,23 +2429,12 @@ dependencies = [
[[package]]
name = "errno"
version = "0.3.3"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
"windows-sys 0.52.0",
]
[[package]]
@@ -2650,7 +2656,7 @@ dependencies = [
"bitflags 1.3.2",
"byteorder",
"core-foundation",
"core-graphics",
"core-graphics 0.22.3",
"core-text",
"dirs-next",
"dwrote",
@@ -2683,7 +2689,28 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared 0.3.1",
]
[[package]]
name = "foreign-types-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
@@ -2692,6 +2719,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
@@ -2757,7 +2790,7 @@ dependencies = [
"bitflags 1.3.2",
"fsevent-sys",
"parking_lot 0.11.2",
"tempdir",
"tempfile",
]
[[package]]
@@ -2769,12 +2802,6 @@ dependencies = [
"libc",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
@@ -3080,7 +3107,7 @@ dependencies = [
"cocoa",
"collections",
"core-foundation",
"core-graphics",
"core-graphics 0.22.3",
"core-text",
"ctor",
"derive_more",
@@ -3088,7 +3115,7 @@ dependencies = [
"env_logger",
"etagere",
"font-kit",
"foreign-types",
"foreign-types 0.3.2",
"futures 0.3.28",
"gpui_macros",
"image",
@@ -3414,7 +3441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
dependencies = [
"android_system_properties",
"core-foundation-sys 0.8.3",
"core-foundation-sys 0.8.6",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
@@ -3863,9 +3890,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.148"
version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
name = "libgit2-sys"
@@ -3998,8 +4025,8 @@ dependencies = [
"cocoa",
"collections",
"core-foundation",
"core-graphics",
"foreign-types",
"core-graphics 0.22.3",
"foreign-types 0.3.2",
"futures 0.3.28",
"gpui",
"hmac 0.12.1",
@@ -4159,7 +4186,7 @@ dependencies = [
"block",
"bytes 1.5.0",
"core-foundation",
"foreign-types",
"foreign-types 0.3.2",
"metal",
"objc",
]
@@ -4214,7 +4241,7 @@ dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation",
"foreign-types",
"foreign-types 0.3.2",
"log",
"objc",
]
@@ -4882,7 +4909,7 @@ checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [
"bitflags 2.4.1",
"cfg-if 1.0.0",
"foreign-types",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
@@ -5564,7 +5591,7 @@ dependencies = [
"similar",
"smol",
"sum_tree",
"tempdir",
"tempfile",
"terminal",
"text",
"thiserror",
@@ -5801,19 +5828,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi 0.3.9",
]
[[package]]
name = "rand"
version = "0.7.3"
@@ -5858,21 +5872,6 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rand_core"
version = "0.5.1"
@@ -5938,15 +5937,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8"
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "recent_projects"
version = "0.1.0"
@@ -5986,6 +5976,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_users"
version = "0.4.3"
@@ -6051,15 +6050,6 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "rend"
version = "0.4.0"
@@ -6272,7 +6262,7 @@ dependencies = [
"smol",
"smol-timeout",
"strum",
"tempdir",
"tempfile",
"tracing",
"util",
"zstd",
@@ -6422,15 +6412,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.21"
version = "0.38.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys 0.4.12",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -6748,7 +6738,7 @@ checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys 0.8.3",
"core-foundation-sys 0.8.6",
"libc",
"security-framework-sys",
]
@@ -6759,7 +6749,7 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys 0.8.3",
"core-foundation-sys 0.8.6",
"libc",
]
@@ -6797,7 +6787,7 @@ dependencies = [
"settings",
"sha1",
"smol",
"tempdir",
"tempfile",
"tiktoken-rs",
"tree-sitter",
"tree-sitter-cpp",
@@ -7766,7 +7756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys 0.8.3",
"core-foundation-sys 0.8.6",
"libc",
"ntapi 0.4.1",
"once_cell",
@@ -7797,27 +7787,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand 0.4.6",
"remove_dir_all",
]
[[package]]
name = "tempfile"
version = "3.8.0"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
dependencies = [
"cfg-if 1.0.0",
"fastrand 2.0.0",
"redox_syscall 0.3.5",
"rustix 0.38.21",
"windows-sys 0.48.0",
"redox_syscall 0.4.1",
"rustix 0.38.30",
"windows-sys 0.52.0",
]
[[package]]
@@ -8973,7 +8953,7 @@ dependencies = [
"serde_json",
"smol",
"take-until",
"tempdir",
"tempfile",
"url",
]
@@ -9300,7 +9280,7 @@ dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.21",
"rustix 0.38.30",
]
[[package]]
@@ -9704,7 +9684,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.119.18"
version = "0.121.0"
dependencies = [
"activity_indicator",
"ai",
@@ -9726,7 +9706,6 @@ dependencies = [
"client",
"collab_ui",
"collections",
"color",
"command_palette",
"copilot",
"copilot_ui",
@@ -9786,7 +9765,7 @@ dependencies = [
"smallvec",
"smol",
"sum_tree",
"tempdir",
"tempfile",
"terminal_view",
"text",
"theme",

View File

@@ -121,7 +121,7 @@ smallvec = { version = "1.6", features = ["union"] }
smol = { version = "1.2" }
strum = { version = "0.25.0", features = ["derive"] }
sysinfo = "0.29.10"
tempdir = { version = "0.3.7" }
tempfile = { version = "3.9.0" }
thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" }
@@ -165,13 +165,6 @@ tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev =
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "31c40449749c4263a91a43593831b82229049a4c" }
# wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-foundation-sys = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
[profile.dev]
split-debuginfo = "unpacked"
debug = "limited"

View File

@@ -2959,6 +2959,7 @@ impl InlineAssistant {
cx.prompt(
PromptLevel::Info,
prompt_text.as_str(),
None,
&["Continue", "Cancel"],
)
})?;

View File

@@ -29,4 +29,4 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
tempdir.workspace = true
tempfile.workspace = true

View File

@@ -130,7 +130,8 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
"Auto-updates disabled for non-bundled app.",
"Could not check for updates",
Some("Auto-updates disabled for non-bundled app."),
&["Ok"],
));
}
@@ -291,7 +292,9 @@ impl AutoUpdater {
cx.notify();
})?;
let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
let temp_dir = tempfile::Builder::new()
.prefix("zed-auto-update")
.tempdir()?;
let dmg_path = temp_dir.path().join("Zed.dmg");
let mount_path = temp_dir.path().join("Zed");
let running_app_path = ZED_APP_PATH

View File

@@ -689,12 +689,7 @@ impl Client {
Ok(())
}
Err(error) => {
client.respond_with_error(
receipt,
proto::Error {
message: format!("{:?}", error),
},
)?;
client.respond_with_error(receipt, error.to_proto())?;
Err(error)
}
}

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.40.1"
version = "0.41.0"
publish = false
license = "AGPL-3.0-only"

View File

@@ -169,6 +169,30 @@ impl Database {
self.run(body).await
}
pub async fn weak_transaction<F, Fut, T>(&self, f: F) -> Result<T>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let (tx, result) = self.with_weak_transaction(&f).await?;
match result {
Ok(result) => match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(result),
Err(error) => {
return Err(error);
}
},
Err(error) => {
tx.rollback().await?;
return Err(error);
}
}
};
self.run(body).await
}
/// The same as room_transaction, but if you need to only optionally return a Room.
async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
where
@@ -284,6 +308,30 @@ impl Database {
Ok((tx, result))
}
async fn with_weak_transaction<F, Fut, T>(
&self,
f: &F,
) -> Result<(DatabaseTransaction, Result<T>)>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let tx = self
.pool
.begin_with_config(Some(IsolationLevel::ReadCommitted), None)
.await?;
let mut tx = Arc::new(Some(tx));
let result = f(TransactionHandle(tx.clone())).await;
let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
return Err(anyhow!(
"couldn't complete transaction because it's still in use"
))?;
};
Ok((tx, result))
}
async fn run<F, T>(&self, future: F) -> Result<T>
where
F: Future<Output = Result<T>>,
@@ -303,13 +351,14 @@ impl Database {
}
}
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: u32) -> bool {
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: usize) -> 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);
const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.];
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
let base_delay = SLEEPS[prev_attempt_count];
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.",
@@ -456,9 +505,8 @@ pub struct NewUserResult {
/// The result of moving a channel.
#[derive(Debug)]
pub struct MoveChannelResult {
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
pub participants_to_remove: HashSet<UserId>,
pub moved_channels: HashSet<ChannelId>,
pub previous_participants: Vec<ChannelMember>,
pub descendent_ids: Vec<ChannelId>,
}
/// The result of renaming a channel.

View File

@@ -1,5 +1,5 @@
use super::*;
use rpc::proto::channel_member::Kind;
use rpc::{proto::channel_member::Kind, ErrorCode, ErrorCodeExt};
use sea_orm::TryGetableMany;
impl Database {
@@ -19,11 +19,7 @@ impl Database {
#[cfg(test)]
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
Ok(self
.create_channel(name, None, creator_id)
.await?
.channel
.id)
Ok(self.create_channel(name, None, creator_id).await?.id)
}
#[cfg(test)]
@@ -36,7 +32,6 @@ impl Database {
Ok(self
.create_channel(name, Some(parent), creator_id)
.await?
.channel
.id)
}
@@ -46,7 +41,7 @@ impl Database {
name: &str,
parent_channel_id: Option<ChannelId>,
admin_id: UserId,
) -> Result<CreateChannelResult> {
) -> Result<Channel> {
let name = Self::sanitize_channel_name(name)?;
self.transaction(move |tx| async move {
let mut parent = None;
@@ -72,14 +67,7 @@ impl Database {
.insert(&*tx)
.await?;
let participants_to_update;
if let Some(parent) = &parent {
participants_to_update = self
.participants_to_notify_for_channel_change(parent, &*tx)
.await?;
} else {
participants_to_update = vec![];
if parent.is_none() {
channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel.id),
@@ -89,12 +77,9 @@ impl Database {
}
.insert(&*tx)
.await?;
};
}
Ok(CreateChannelResult {
channel: Channel::from_model(channel, ChannelRole::Admin),
participants_to_update,
})
Ok(Channel::from_model(channel, ChannelRole::Admin))
})
.await
}
@@ -166,7 +151,7 @@ impl Database {
}
if role.is_none() || role == Some(ChannelRole::Banned) {
Err(anyhow!("not allowed"))?
Err(ErrorCode::Forbidden.anyhow())?
}
let role = role.unwrap();
@@ -718,6 +703,19 @@ impl Database {
})
}
pub async fn new_participants_to_notify(
&self,
parent_channel_id: ChannelId,
) -> Result<Vec<(UserId, ChannelsForUser)>> {
self.weak_transaction(|tx| async move {
let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
self.participants_to_notify_for_channel_change(&parent_channel, &*tx)
.await
})
.await
}
// TODO: this is very expensive, and we should rethink
async fn participants_to_notify_for_channel_change(
&self,
new_parent: &channel::Model,
@@ -1201,7 +1199,7 @@ impl Database {
Ok(channel::Entity::find_by_id(channel_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?)
.ok_or_else(|| proto::ErrorCode::NoSuchChannel.anyhow())?)
}
pub(crate) async fn get_or_create_channel_room(
@@ -1219,7 +1217,9 @@ impl Database {
let room_id = if let Some(room) = room {
if let Some(env) = room.environment {
if &env != environment {
Err(anyhow!("must join using the {} release", env))?;
Err(ErrorCode::WrongReleaseChannel
.with_tag("required", &env)
.anyhow())?;
}
}
room.id
@@ -1285,7 +1285,7 @@ impl Database {
let mut model = channel.into_active_model();
model.parent_path = ActiveValue::Set(new_parent_path);
let channel = model.update(&*tx).await?;
model.update(&*tx).await?;
if new_parent_channel.is_none() {
channel_member::ActiveModel {
@@ -1312,34 +1312,9 @@ impl Database {
.all(&*tx)
.await?;
let participants_to_update: HashMap<_, _> = self
.participants_to_notify_for_channel_change(
new_parent_channel.as_ref().unwrap_or(&channel),
&*tx,
)
.await?
.into_iter()
.collect();
let mut moved_channels: HashSet<ChannelId> = HashSet::default();
for id in descendent_ids {
moved_channels.insert(id);
}
moved_channels.insert(channel_id);
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
for participant in previous_participants {
if participant.kind == proto::channel_member::Kind::AncestorMember {
if !participants_to_update.contains_key(&participant.user_id) {
participants_to_remove.insert(participant.user_id);
}
}
}
Ok(Some(MoveChannelResult {
participants_to_remove,
participants_to_update,
moved_channels,
previous_participants,
descendent_ids,
}))
})
.await

View File

@@ -15,22 +15,18 @@ test_both_dbs!(
async fn test_channel_message_retrieval(db: &Arc<Database>) {
let user = new_test_user(db, "user@example.com").await;
let result = db.create_channel("channel", None, user).await.unwrap();
let channel = db.create_channel("channel", None, user).await.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
db.join_channel_chat(
result.channel.id,
rpc::ConnectionId { owner_id, id: 0 },
user,
)
.await
.unwrap();
db.join_channel_chat(channel.id, rpc::ConnectionId { owner_id, id: 0 }, user)
.await
.unwrap();
let mut all_messages = Vec::new();
for i in 0..10 {
all_messages.push(
db.create_channel_message(
result.channel.id,
channel.id,
user,
&i.to_string(),
&[],
@@ -45,7 +41,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
}
let messages = db
.get_channel_messages(result.channel.id, user, 3, None)
.get_channel_messages(channel.id, user, 3, None)
.await
.unwrap()
.into_iter()
@@ -55,7 +51,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
let messages = db
.get_channel_messages(
result.channel.id,
channel.id,
user,
4,
Some(MessageId::from_proto(all_messages[6])),
@@ -366,12 +362,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
let user_b = new_test_user(db, "user_b@example.com").await;
let user_c = new_test_user(db, "user_c@example.com").await;
let channel = db
.create_channel("channel", None, user_a)
.await
.unwrap()
.channel
.id;
let channel = db.create_channel("channel", None, user_a).await.unwrap().id;
db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
.await
.unwrap();

View File

@@ -3,14 +3,13 @@ mod connection_pool;
use crate::{
auth::{self, Impersonator},
db::{
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult,
CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
MoveChannelResult, NotificationId, ProjectId, RemoveChannelMemberResult,
RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, SetChannelVisibilityResult,
User, UserId,
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId,
RemoveChannelMemberResult, RenameChannelResult, RespondToChannelInvite, RoomId, ServerId,
SetChannelVisibilityResult, User, UserId,
},
executor::Executor,
AppState, Result,
AppState, Error, Result,
};
use anyhow::anyhow;
use async_tungstenite::tungstenite::{
@@ -44,7 +43,7 @@ use rpc::{
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
},
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
Connection, ConnectionId, ErrorCode, ErrorCodeExt, ErrorExt, Peer, Receipt, TypedEnvelope,
};
use serde::{Serialize, Serializer};
use std::{
@@ -543,12 +542,11 @@ impl Server {
}
}
Err(error) => {
peer.respond_with_error(
receipt,
proto::Error {
message: error.to_string(),
},
)?;
let proto_err = match &error {
Error::Internal(err) => err.to_proto(),
_ => ErrorCode::Internal.message(format!("{}", error)).to_proto(),
};
peer.respond_with_error(receipt, proto_err)?;
Err(error)
}
}
@@ -2302,10 +2300,7 @@ async fn create_channel(
let db = session.db().await;
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
let CreateChannelResult {
channel,
participants_to_update,
} = db
let channel = db
.create_channel(&request.name, parent_id, session.user_id)
.await?;
@@ -2314,6 +2309,13 @@ async fn create_channel(
parent_id: request.parent_id,
})?;
let participants_to_update;
if let Some(parent) = parent_id {
participants_to_update = db.new_participants_to_notify(parent).await?;
} else {
participants_to_update = vec![];
}
let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update {
let update = build_channels_update(channels, vec![]);
@@ -2573,44 +2575,56 @@ async fn move_channel(
.move_channel(channel_id, to, session.user_id)
.await?;
notify_channel_moved(result, session).await?;
if let Some(result) = result {
let participants_to_update: HashMap<_, _> = session
.db()
.await
.new_participants_to_notify(to.unwrap_or(channel_id))
.await?
.into_iter()
.collect();
let mut moved_channels: HashSet<ChannelId> = HashSet::default();
for id in result.descendent_ids {
moved_channels.insert(id);
}
moved_channels.insert(channel_id);
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
for participant in result.previous_participants {
if participant.kind == proto::channel_member::Kind::AncestorMember {
if !participants_to_update.contains_key(&participant.user_id) {
participants_to_remove.insert(participant.user_id);
}
}
}
let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect();
let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update {
let mut update = build_channels_update(channels, vec![]);
update.delete_channels = moved_channels.clone();
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
for user_id in participants_to_remove {
let update = proto::UpdateChannels {
delete_channels: moved_channels.clone(),
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
}
response.send(Ack {})?;
Ok(())
}
async fn notify_channel_moved(result: Option<MoveChannelResult>, session: Session) -> Result<()> {
let Some(MoveChannelResult {
participants_to_remove,
participants_to_update,
moved_channels,
}) = result
else {
return Ok(());
};
let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect();
let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update {
let mut update = build_channels_update(channels, vec![]);
update.delete_channels = moved_channels.clone();
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
for user_id in participants_to_remove {
let update = proto::UpdateChannels {
delete_channels: moved_channels.clone(),
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
Ok(())
}
/// Get the list of channel members
async fn get_channel_members(
request: proto::GetChannelMembers,

View File

@@ -132,14 +132,6 @@ impl ChatPanel {
{
this.select_channel(channel_id, None, cx)
.detach_and_log_err(cx);
if ActiveCall::global(cx)
.read(cx)
.room()
.is_some_and(|room| room.read(cx).contains_guests())
{
cx.emit(PanelEvent::Activate)
}
}
this.subscriptions.push(cx.subscribe(
@@ -665,6 +657,13 @@ impl Panel for ChatPanel {
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
fn starts_open(&self, cx: &WindowContext) -> bool {
ActiveCall::global(cx)
.read(cx)
.room()
.is_some_and(|room| room.read(cx).contains_guests())
}
}
impl EventEmitter<PanelEvent> for ChatPanel {}

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId;
use collections::HashMap;
use collections::{HashMap, HashSet};
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -30,7 +30,7 @@ lazy_static! {
pub struct MessageEditor {
pub editor: View<Editor>,
channel_store: Model<ChannelStore>,
users: HashMap<String, UserId>,
channel_members: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
channel_id: Option<ChannelId>,
@@ -108,7 +108,7 @@ impl MessageEditor {
Self {
editor,
channel_store,
users: HashMap::default(),
channel_members: HashMap::default(),
channel_id: None,
mentions: Vec::new(),
mentions_task: None,
@@ -147,8 +147,8 @@ impl MessageEditor {
}
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
self.users.clear();
self.users.extend(
self.channel_members.clear();
self.channel_members.extend(
members
.into_iter()
.map(|member| (member.user.github_login.clone(), member.user.id)),
@@ -221,9 +221,18 @@ impl MessageEditor {
let start_offset = end_offset - query.len();
let start_anchor = buffer.read(cx).anchor_before(start_offset);
let candidates = self
.users
.keys()
let mut names = HashSet::default();
for (github_login, _) in self.channel_members.iter() {
names.insert(github_login.clone());
}
if let Some(channel_id) = self.channel_id {
for participant in self.channel_store.read(cx).channel_participants(channel_id) {
names.insert(participant.github_login.clone());
}
}
let candidates = names
.into_iter()
.map(|user| StringMatchCandidate {
id: 0,
string: user.clone(),
@@ -283,7 +292,7 @@ impl MessageEditor {
text.clear();
text.extend(buffer.text_for_range(range.clone()));
if let Some(username) = text.strip_prefix("@") {
if let Some(user_id) = this.users.get(username) {
if let Some(user_id) = this.channel_members.get(username) {
let start = multi_buffer.anchor_after(range.start);
let end = multi_buffer.anchor_after(range.end);

View File

@@ -22,7 +22,10 @@ use gpui::{
};
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use project::{Fs, Project};
use rpc::proto::{self, PeerId};
use rpc::{
proto::{self, PeerId},
ErrorCode, ErrorExt,
};
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use smallvec::SmallVec;
@@ -35,7 +38,7 @@ use ui::{
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::{NotifyResultExt, NotifyTaskExt},
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
Workspace,
};
@@ -879,7 +882,7 @@ impl CollabPanel {
.update(cx, |workspace, cx| {
let app_state = workspace.app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to join project", cx, |_, _| None);
})
.ok();
}))
@@ -1017,7 +1020,12 @@ impl CollabPanel {
)
})
})
.detach_and_notify_err(cx)
.detach_and_prompt_err("Failed to grant write access", cx, |e, _| {
match e.error_code() {
ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
_ => None,
}
})
}),
)
} else if role == proto::ChannelRole::Member {
@@ -1038,7 +1046,7 @@ impl CollabPanel {
)
})
})
.detach_and_notify_err(cx)
.detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
}),
)
} else {
@@ -1258,7 +1266,11 @@ impl CollabPanel {
app_state,
cx,
)
.detach_and_log_err(cx);
.detach_and_prompt_err(
"Failed to join project",
cx,
|_, _| None,
);
}
}
ListEntry::ParticipantScreen { peer_id, .. } => {
@@ -1432,7 +1444,7 @@ impl CollabPanel {
fn leave_call(cx: &mut WindowContext) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to hang up", cx, |_, _| None);
}
fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
@@ -1534,11 +1546,11 @@ impl CollabPanel {
cx: &mut ViewContext<CollabPanel>,
) {
if let Some(clipboard) = self.channel_clipboard.take() {
self.channel_store.update(cx, |channel_store, cx| {
channel_store
.move_channel(clipboard.channel_id, Some(to_channel_id), cx)
.detach_and_log_err(cx)
})
self.channel_store
.update(cx, |channel_store, cx| {
channel_store.move_channel(clipboard.channel_id, Some(to_channel_id), cx)
})
.detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
}
}
@@ -1610,7 +1622,12 @@ impl CollabPanel {
"Are you sure you want to remove the channel \"{}\"?",
channel.name
);
let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
let answer = cx.prompt(
PromptLevel::Warning,
&prompt_message,
None,
&["Remove", "Cancel"],
);
cx.spawn(|this, mut cx| async move {
if answer.await? == 0 {
channel_store
@@ -1631,7 +1648,12 @@ impl CollabPanel {
"Are you sure you want to remove \"{}\" from your contacts?",
github_login
);
let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
let answer = cx.prompt(
PromptLevel::Warning,
&prompt_message,
None,
&["Remove", "Cancel"],
);
cx.spawn(|_, mut cx| async move {
if answer.await? == 0 {
user_store
@@ -1641,7 +1663,7 @@ impl CollabPanel {
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to remove contact", cx, |_, _| None);
}
fn respond_to_contact_request(
@@ -1654,7 +1676,7 @@ impl CollabPanel {
.update(cx, |store, cx| {
store.respond_to_contact_request(user_id, accept, cx)
})
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to respond to contact request", cx, |_, _| None);
}
fn respond_to_channel_invite(
@@ -1675,7 +1697,7 @@ impl CollabPanel {
.update(cx, |call, cx| {
call.invite(recipient_user_id, Some(self.project.clone()), cx)
})
.detach_and_log_err(cx);
.detach_and_prompt_err("Call failed", cx, |_, _| None);
}
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
@@ -1691,7 +1713,7 @@ impl CollabPanel {
Some(handle),
cx,
)
.detach_and_log_err(cx)
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
}
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
@@ -1704,7 +1726,7 @@ impl CollabPanel {
panel.update(cx, |panel, cx| {
panel
.select_channel(channel_id, None, cx)
.detach_and_log_err(cx);
.detach_and_notify_err(cx);
});
}
});
@@ -1981,7 +2003,7 @@ impl CollabPanel {
.update(cx, |channel_store, cx| {
channel_store.move_channel(dragged_channel.id, None, cx)
})
.detach_and_log_err(cx)
.detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
}))
})
}
@@ -2240,6 +2262,7 @@ impl CollabPanel {
let width = self.width.unwrap_or(px(240.));
div()
.h_6()
.id(channel_id as usize)
.group("")
.flex()
@@ -2256,7 +2279,7 @@ impl CollabPanel {
.update(cx, |channel_store, cx| {
channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
})
.detach_and_log_err(cx)
.detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
}))
.child(
ListItem::new(channel_id as usize)

View File

@@ -14,7 +14,7 @@ use rpc::proto::channel_member;
use std::sync::Arc;
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
use util::TryFutureExt;
use workspace::{notifications::NotifyTaskExt, ModalView};
use workspace::{notifications::DetachAndPromptErr, ModalView};
actions!(
channel_modal,
@@ -498,7 +498,7 @@ impl ChannelModalDelegate {
cx.notify();
})
})
.detach_and_notify_err(cx);
.detach_and_prompt_err("Failed to update role", cx, |_, _| None);
Some(())
}
@@ -530,7 +530,7 @@ impl ChannelModalDelegate {
cx.notify();
})
})
.detach_and_notify_err(cx);
.detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
Some(())
}
@@ -556,7 +556,7 @@ impl ChannelModalDelegate {
cx.notify();
})
})
.detach_and_notify_err(cx);
.detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
}
fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {

View File

@@ -59,10 +59,7 @@ pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
// Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
let hex = match hex.len() {
3 => hex
.chars()
.map(|c| c.to_string().repeat(2))
.collect::<String>(),
3 => hex.chars().map(|c| c.to_string().repeat(2)).collect(),
4 => {
let (rgb, alpha) = hex.split_at(3);
let rgb = rgb
@@ -80,14 +77,12 @@ pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
let hex_val =
u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
let r = ((hex_val >> 24) & 0xFF) as f32 / 255.0;
let g = ((hex_val >> 16) & 0xFF) as f32 / 255.0;
let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0;
let a = (hex_val & 0xFF) as f32 / 255.0;
let color = RGBAColor { r, g, b, a };
Ok(color)
Ok(RGBAColor {
r: ((hex_val >> 24) & 0xFF) as f32 / 255.0,
g: ((hex_val >> 16) & 0xFF) as f32 / 255.0,
b: ((hex_val >> 8) & 0xFF) as f32 / 255.0,
a: (hex_val & 0xFF) as f32 / 255.0,
})
}
// These derives implement to and from palette's color types.
@@ -128,8 +123,7 @@ where
Rgb<S, f32>: FromColorUnclamped<Srgb>,
{
fn from_color_unclamped(color: RGBAColor) -> Self {
let srgb = Srgb::new(color.r, color.g, color.b);
Self::from_color_unclamped(srgb)
Self::from_color_unclamped(Srgb::new(color.r, color.g, color.b))
}
}

View File

@@ -32,4 +32,4 @@ smol.workspace = true
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
env_logger.workspace = true
tempdir.workspace = true
tempfile.workspace = true

View File

@@ -196,7 +196,6 @@ mod tests {
use sqlez::domain::Domain;
use sqlez_macros::sql;
use tempdir::TempDir;
use crate::open_db;
@@ -220,7 +219,10 @@ mod tests {
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let tempdir = tempfile::Builder::new()
.prefix("DbTests")
.tempdir()
.unwrap();
let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
}
@@ -253,7 +255,10 @@ mod tests {
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let tempdir = tempfile::Builder::new()
.prefix("DbTests")
.tempdir()
.unwrap();
{
let corrupt_db =
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
@@ -297,7 +302,10 @@ mod tests {
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let tempdir = tempfile::Builder::new()
.prefix("DbTests")
.tempdir()
.unwrap();
{
// Setup the bad database
let corrupt_db =

View File

@@ -13,12 +13,6 @@ pub struct SelectPrevious {
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectAllMatches {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
@@ -81,7 +75,6 @@ impl_actions!(
[
SelectNext,
SelectPrevious,
SelectAllMatches,
SelectToBeginningOfLine,
MovePageUp,
MovePageDown,
@@ -128,6 +121,7 @@ gpui::actions!(
DeleteToNextWordEnd,
DeleteToPreviousSubwordStart,
DeleteToPreviousWordStart,
DisplayCursorNames,
DuplicateLine,
ExpandMacroRecursively,
FindAllReferences,
@@ -185,6 +179,7 @@ gpui::actions!(
ScrollCursorCenter,
ScrollCursorTop,
SelectAll,
SelectAllMatches,
SelectDown,
SelectLargerSyntaxNode,
SelectLeft,
@@ -214,6 +209,5 @@ gpui::actions!(
Undo,
UndoSelection,
UnfoldLines,
DisplayCursorNames
]
);

View File

@@ -126,6 +126,7 @@ const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
#[doc(hidden)]
@@ -369,7 +370,7 @@ pub struct Editor {
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>,
show_cursor_names: bool,
hovered_cursor: Option<HoveredCursor>,
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
pub show_local_selections: bool,
mode: EditorMode,
show_gutter: bool,
@@ -463,6 +464,7 @@ enum SelectionHistoryMode {
Redoing,
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct HoveredCursor {
replica_id: u16,
selection_id: usize,
@@ -1440,7 +1442,7 @@ impl Editor {
gutter_width: Default::default(),
style: None,
show_cursor_names: false,
hovered_cursor: Default::default(),
hovered_cursors: Default::default(),
editor_actions: Default::default(),
show_copilot_suggestions: mode == EditorMode::Full,
_subscriptions: vec![
@@ -3741,7 +3743,7 @@ impl Editor {
self.show_cursor_names = true;
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(3)).await;
cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
this.update(&mut cx, |this, cx| {
this.show_cursor_names = false;
cx.notify()
@@ -6111,6 +6113,7 @@ impl Editor {
|| (!movement::is_inside_word(&display_map, display_range.start)
&& !movement::is_inside_word(&display_map, display_range.end))
{
// TODO: This is n^2, because we might check all the selections
if selections
.iter()
.find(|selection| selection.range().overlaps(&offset_range))
@@ -6220,25 +6223,76 @@ impl Editor {
pub fn select_all_matches(
&mut self,
action: &SelectAllMatches,
_action: &SelectAllMatches,
cx: &mut ViewContext<Self>,
) -> Result<()> {
self.push_to_selection_history();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
loop {
self.select_next_match_internal(&display_map, action.replace_newest, None, cx)?;
self.select_next_match_internal(&display_map, false, None, cx)?;
let Some(select_next_state) = self.select_next_state.as_mut() else {
return Ok(());
};
if select_next_state.done {
return Ok(());
}
if self
.select_next_state
.as_ref()
.map(|selection_state| selection_state.done)
.unwrap_or(true)
let mut new_selections = self.selections.all::<usize>(cx);
let buffer = &display_map.buffer_snapshot;
let query_matches = select_next_state
.query
.stream_find_iter(buffer.bytes_in_range(0..buffer.len()));
for query_match in query_matches {
let query_match = query_match.unwrap(); // can only fail due to I/O
let offset_range = query_match.start()..query_match.end();
let display_range = offset_range.start.to_display_point(&display_map)
..offset_range.end.to_display_point(&display_map);
if !select_next_state.wordwise
|| (!movement::is_inside_word(&display_map, display_range.start)
&& !movement::is_inside_word(&display_map, display_range.end))
{
break;
self.selections.change_with(cx, |selections| {
new_selections.push(Selection {
id: selections.new_selection_id(),
start: offset_range.start,
end: offset_range.end,
reversed: false,
goal: SelectionGoal::None,
});
});
}
}
new_selections.sort_by_key(|selection| selection.start);
let mut ix = 0;
while ix + 1 < new_selections.len() {
let current_selection = &new_selections[ix];
let next_selection = &new_selections[ix + 1];
if current_selection.range().overlaps(&next_selection.range()) {
if current_selection.id < next_selection.id {
new_selections.remove(ix + 1);
} else {
new_selections.remove(ix);
}
} else {
ix += 1;
}
}
select_next_state.done = true;
self.unfold_ranges(
new_selections.iter().map(|selection| selection.range()),
false,
false,
cx,
);
self.change_selections(Some(Autoscroll::fit()), cx, |selections| {
selections.select(new_selections)
});
Ok(())
}

View File

@@ -3820,6 +3820,18 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
}
#[gpui::test]
async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches::default(), cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
}
#[gpui::test]
async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});

View File

@@ -17,8 +17,8 @@ use crate::{
mouse_context_menu,
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
HalfPageDown, HalfPageUp, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase,
Selection, SoftWrap, ToPoint, MAX_LINE_LEN,
HalfPageDown, HalfPageUp, HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use anyhow::Result;
use collections::{BTreeMap, HashMap};
@@ -567,7 +567,7 @@ impl EditorElement {
cx,
);
hover_at(editor, Some(point), cx);
Self::update_visible_cursor(editor, point, cx);
Self::update_visible_cursor(editor, point, position_map, cx);
}
None => {
update_inlay_link_and_hover_points(
@@ -592,9 +592,10 @@ impl EditorElement {
fn update_visible_cursor(
editor: &mut Editor,
point: DisplayPoint,
position_map: &PositionMap,
cx: &mut ViewContext<Editor>,
) {
let snapshot = editor.snapshot(cx);
let snapshot = &position_map.snapshot;
let Some(hub) = editor.collaboration_hub() else {
return;
};
@@ -612,13 +613,24 @@ impl EditorElement {
.anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right);
let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
editor.hovered_cursor.take();
return;
};
editor.hovered_cursor.replace(crate::HoveredCursor {
let key = crate::HoveredCursor {
replica_id: selection.replica_id,
selection_id: selection.selection.id,
});
};
editor.hovered_cursors.insert(
key.clone(),
cx.spawn(|editor, mut cx| async move {
cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
editor
.update(&mut cx, |editor, cx| {
editor.hovered_cursors.remove(&key);
cx.notify();
})
.ok();
}),
);
cx.notify()
}
@@ -1986,7 +1998,9 @@ impl EditorElement {
if Some(selection.peer_id) == editor.leader_peer_id {
continue;
}
let is_shown = editor.show_cursor_names || editor.hovered_cursor.as_ref().is_some_and(|c| c.replica_id == selection.replica_id && c.selection_id == selection.selection.id);
let key = HoveredCursor{replica_id: selection.replica_id, selection_id: selection.selection.id};
let is_shown = editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
remote_selections
.entry(selection.replica_id)

View File

@@ -15,7 +15,7 @@ actions!(
CopySystemSpecsIntoClipboard,
FileBugReport,
RequestFeature,
OpenZedCommunityRepo
OpenZedRepo
]
);
@@ -31,7 +31,8 @@ pub fn init(cx: &mut AppContext) {
let prompt = cx.prompt(
PromptLevel::Info,
&format!("Copied into clipboard:\n\n{specs}"),
"Copied into clipboard",
Some(&specs),
&["OK"],
);
cx.spawn(|_, _cx| async move {
@@ -42,18 +43,18 @@ pub fn init(cx: &mut AppContext) {
cx.write_to_clipboard(item);
})
.register_action(|_, _: &RequestFeature, cx| {
let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
let url = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
cx.open_url(url);
})
.register_action(move |_, _: &FileBugReport, cx| {
let url = format!(
"https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&SystemSpecs::new(&cx).to_string())
);
cx.open_url(&url);
})
.register_action(move |_, _: &OpenZedCommunityRepo, cx| {
let url = "https://github.com/zed-industries/community";
.register_action(move |_, _: &OpenZedRepo, cx| {
let url = "https://github.com/zed-industries/zed";
cx.open_url(&url);
});
})

View File

@@ -19,7 +19,7 @@ use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
use util::ResultExt;
use workspace::{ModalView, Toast, Workspace};
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo};
// For UI testing purposes
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
@@ -97,7 +97,7 @@ impl ModalView for FeedbackModal {
return true;
}
let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]);
let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", None, &["Yes", "No"]);
cx.spawn(move |this, mut cx| async move {
if answer.await.ok() == Some(0) {
@@ -222,6 +222,7 @@ impl FeedbackModal {
let answer = cx.prompt(
PromptLevel::Info,
"Ready to submit your feedback?",
None,
&["Yes, Submit!", "No"],
);
let client = cx.global::<Arc<Client>>().clone();
@@ -255,6 +256,7 @@ impl FeedbackModal {
let prompt = cx.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
None,
&["OK"],
);
cx.spawn(|_, _cx| async move {
@@ -417,8 +419,7 @@ impl Render for FeedbackModal {
"Submit"
};
let open_community_repo =
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
let open_zed_repo = cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedRepo)));
v_flex()
.elevation_3(cx)
@@ -485,12 +486,12 @@ impl Render for FeedbackModal {
.justify_between()
.gap_1()
.child(
Button::new("community_repository", "Community Repository")
Button::new("zed_repository", "Zed Repository")
.style(ButtonStyle::Transparent)
.icon(IconName::ExternalLink)
.icon_position(IconPosition::End)
.icon_size(IconSize::Small)
.on_click(open_community_repo),
.on_click(open_zed_repo),
)
.child(
h_flex()

View File

@@ -3,7 +3,7 @@ use gpui::AppContext;
use human_bytes::human_bytes;
use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{System, SystemExt};
use sysinfo::{RefreshKind, System, SystemExt};
use util::channel::ReleaseChannel;
#[derive(Clone, Debug, Serialize)]
@@ -23,7 +23,7 @@ impl SystemSpecs {
.map(|v| v.to_string());
let release_channel = cx.global::<ReleaseChannel>().display_name();
let os_name = cx.app_metadata().os_name;
let system = System::new_all();
let system = System::new_with_specifics(RefreshKind::new().with_memory());
let memory = system.total_memory();
let architecture = env::consts::ARCH;
let os_version = cx

View File

@@ -11,6 +11,7 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
cmp,
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
@@ -143,16 +144,51 @@ pub struct FileFinderDelegate {
history_items: Vec<FoundPath>,
}
/// Use a custom ordering for file finder: the regular one
/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:
/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`
///
/// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:
/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`
/// as the files are shown in the project panel lists.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ProjectPanelOrdMatch(PathMatch);
impl Ord for ProjectPanelOrdMatch {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.partial_cmp(other).unwrap()
}
}
impl PartialOrd for ProjectPanelOrdMatch {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(
self.0
.score
.partial_cmp(&other.0.score)
.unwrap_or(cmp::Ordering::Equal)
.then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))
.then_with(|| {
other
.0
.distance_to_relative_ancestor
.cmp(&self.0.distance_to_relative_ancestor)
})
.then_with(|| self.0.path.cmp(&other.0.path).reverse()),
)
}
}
#[derive(Debug, Default)]
struct Matches {
history: Vec<(FoundPath, Option<PathMatch>)>,
search: Vec<PathMatch>,
history: Vec<(FoundPath, Option<ProjectPanelOrdMatch>)>,
search: Vec<ProjectPanelOrdMatch>,
}
#[derive(Debug)]
enum Match<'a> {
History(&'a FoundPath, Option<&'a PathMatch>),
Search(&'a PathMatch),
History(&'a FoundPath, Option<&'a ProjectPanelOrdMatch>),
Search(&'a ProjectPanelOrdMatch),
}
impl Matches {
@@ -176,45 +212,44 @@ impl Matches {
&mut self,
history_items: &Vec<FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
mut new_search_matches: Vec<PathMatch>,
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
extend_old_matches: bool,
) {
let matching_history_paths = matching_history_item_paths(history_items, query);
new_search_matches
.retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
let history_items_to_show = history_items
.iter()
.filter_map(|history_item| {
Some((
history_item.clone(),
Some(
matching_history_paths
.get(&history_item.project.path)?
.clone(),
),
))
})
.collect::<Vec<_>>();
self.history = history_items_to_show;
let new_search_matches = new_search_matches
.filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
let history_items_to_show = history_items.iter().filter_map(|history_item| {
Some((
history_item.clone(),
Some(
matching_history_paths
.get(&history_item.project.path)?
.clone(),
),
))
});
self.history.clear();
util::extend_sorted(
&mut self.history,
history_items_to_show,
100,
|(_, a), (_, b)| b.cmp(a),
);
if extend_old_matches {
self.search
.retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
util::extend_sorted(
&mut self.search,
new_search_matches.into_iter(),
100,
|a, b| b.cmp(a),
)
.retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
} else {
self.search = new_search_matches;
self.search.clear();
}
util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
}
}
fn matching_history_item_paths(
history_items: &Vec<FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
) -> HashMap<Arc<Path>, PathMatch> {
) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
let history_items_by_worktrees = history_items
.iter()
.filter_map(|found_path| {
@@ -257,7 +292,12 @@ fn matching_history_item_paths(
max_results,
)
.into_iter()
.map(|path_match| (Arc::clone(&path_match.path), path_match)),
.map(|path_match| {
(
Arc::clone(&path_match.path),
ProjectPanelOrdMatch(path_match),
)
}),
);
}
matching_history_paths
@@ -383,7 +423,9 @@ impl FileFinderDelegate {
&cancel_flag,
cx.background_executor().clone(),
)
.await;
.await
.into_iter()
.map(ProjectPanelOrdMatch);
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
picker
.update(&mut cx, |picker, cx| {
@@ -401,7 +443,7 @@ impl FileFinderDelegate {
search_id: usize,
did_cancel: bool,
query: PathLikeWithPosition<FileSearchQuery>,
matches: Vec<PathMatch>,
matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
cx: &mut ViewContext<Picker<Self>>,
) {
if search_id >= self.latest_search_id {
@@ -412,8 +454,12 @@ impl FileFinderDelegate {
.latest_search_query
.as_ref()
.map(|query| query.path_like.path_query());
self.matches
.push_new_matches(&self.history_items, &query, matches, extend_old_matches);
self.matches.push_new_matches(
&self.history_items,
&query,
matches.into_iter(),
extend_old_matches,
);
self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel;
cx.notify();
@@ -471,12 +517,12 @@ impl FileFinderDelegate {
if let Some(found_path_match) = found_path_match {
path_match
.positions
.extend(found_path_match.positions.iter())
.extend(found_path_match.0.positions.iter())
}
self.labels_for_path_match(&path_match)
}
Match::Search(path_match) => self.labels_for_path_match(path_match),
Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
};
if file_name_positions.is_empty() {
@@ -556,14 +602,14 @@ impl FileFinderDelegate {
if let Some((worktree, relative_path)) =
project.find_local_worktree(query_path, cx)
{
path_matches.push(PathMatch {
score: 0.0,
path_matches.push(ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: worktree.read(cx).id().to_usize(),
path: Arc::from(relative_path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
});
}));
}
})
.log_err();
@@ -724,8 +770,8 @@ impl PickerDelegate for FileFinderDelegate {
Match::Search(m) => split_or_open(
workspace,
ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(),
worktree_id: WorktreeId::from_usize(m.0.worktree_id),
path: m.0.path.clone(),
},
cx,
),
@@ -805,3 +851,101 @@ impl PickerDelegate for FileFinderDelegate {
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_custom_project_search_ordering_in_file_finder() {
let mut file_finder_sorted_output = vec![
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b0.5")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("c1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a0.5")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
];
file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
assert_eq!(
file_finder_sorted_output,
vec![
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("c1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a0.5")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b0.5")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
]
);
}
}

View File

@@ -114,7 +114,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
.await;
picker.update(cx, |picker, _| {
assert_eq!(
collect_search_results(picker),
collect_search_matches(picker).search_only(),
vec![PathBuf::from("a/b/file2.txt")],
"Matching abs path should be the only match"
)
@@ -136,7 +136,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
.await;
picker.update(cx, |picker, _| {
assert_eq!(
collect_search_results(picker),
collect_search_matches(picker).search_only(),
Vec::<PathBuf>::new(),
"Mismatching abs path should produce no matches"
)
@@ -169,7 +169,7 @@ async fn test_complex_path(cx: &mut TestAppContext) {
picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 1);
assert_eq!(
collect_search_results(picker),
collect_search_matches(picker).search_only(),
vec![PathBuf::from("其他/S数据表格/task.xlsx")],
)
});
@@ -486,7 +486,7 @@ async fn test_single_file_worktrees(cx: &mut TestAppContext) {
assert_eq!(matches.len(), 1);
let (file_name, file_name_positions, full_path, full_path_positions) =
delegate.labels_for_path_match(&matches[0]);
delegate.labels_for_path_match(&matches[0].0);
assert_eq!(file_name, "the-file");
assert_eq!(file_name_positions, &[0, 1, 4]);
assert_eq!(full_path, "the-file");
@@ -556,9 +556,9 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
delegate.matches.history.is_empty(),
"Search matches expected"
);
let matches = delegate.matches.search.clone();
assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
let matches = &delegate.matches.search;
assert_eq!(matches[0].0.path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(matches[1].0.path.as_ref(), Path::new("dir1/a.txt"));
});
}
@@ -957,7 +957,7 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
Some(PathBuf::from("/src/test/first.rs"))
));
assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
});
let second_query = "fsdasdsa";
@@ -1002,10 +1002,65 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
Some(PathBuf::from("/src/test/first.rs"))
));
assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
});
}
#[gpui::test]
async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"test": {
"1_qw": "// First file that matches the query",
"2_second": "// Second file",
"3_third": "// Third file",
"4_fourth": "// Fourth file",
"5_qwqwqw": "// A file with 3 more matches than the first one",
"6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
"7_qwqwqw": "// One more, same amount of query matches as above",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
// generate some history to select from
open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
let finder = open_file_picker(&workspace, cx);
let query = "qw";
finder
.update(cx, |finder, cx| {
finder.delegate.update_matches(query.to_string(), cx)
})
.await;
finder.update(cx, |finder, _| {
let search_matches = collect_search_matches(finder);
assert_eq!(
search_matches.history,
vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
);
assert_eq!(
search_matches.search,
vec![
PathBuf::from("test/5_qwqwqw"),
PathBuf::from("test/7_qwqwqw"),
],
);
});
}
#[gpui::test]
async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
@@ -1048,14 +1103,14 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo
.matches
.search
.iter()
.map(|path_match| path_match.path.to_path_buf())
.map(|path_match| path_match.0.path.to_path_buf())
.collect::<Vec<_>>();
assert_eq!(
search_entries,
vec![
PathBuf::from("collab_ui/collab_ui.rs"),
PathBuf::from("collab_ui/third.rs"),
PathBuf::from("collab_ui/first.rs"),
PathBuf::from("collab_ui/third.rs"),
PathBuf::from("collab_ui/second.rs"),
],
"Despite all search results having the same directory name, the most matching one should be on top"
@@ -1097,7 +1152,7 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext)
.matches
.history
.iter()
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").0.path.to_path_buf())
.collect::<Vec<_>>();
assert_eq!(
history_entries,
@@ -1124,7 +1179,8 @@ async fn open_close_queried_buffer(
assert_eq!(
finder.delegate.matches.len(),
expected_matches,
"Unexpected number of matches found for query {input}"
"Unexpected number of matches found for query `{input}`, matches: {:?}",
finder.delegate.matches
);
finder.delegate.history_items.clone()
});
@@ -1137,7 +1193,7 @@ async fn open_close_queried_buffer(
let active_editor_title = active_editor.read(cx).title(cx);
assert_eq!(
expected_editor_title, active_editor_title,
"Unexpected editor title for query {input}"
"Unexpected editor title for query `{input}`"
);
});
@@ -1210,18 +1266,49 @@ fn active_file_picker(
})
}
fn collect_search_results(picker: &Picker<FileFinderDelegate>) -> Vec<PathBuf> {
let matches = &picker.delegate.matches;
assert!(
matches.history.is_empty(),
"Should have no history matches, but got: {:?}",
matches.history
);
let mut results = matches
.search
.iter()
.map(|path_match| Path::new(path_match.path_prefix.as_ref()).join(&path_match.path))
.collect::<Vec<_>>();
results.sort();
results
#[derive(Debug)]
struct SearchEntries {
history: Vec<PathBuf>,
search: Vec<PathBuf>,
}
impl SearchEntries {
#[track_caller]
fn search_only(self) -> Vec<PathBuf> {
assert!(
self.history.is_empty(),
"Should have no history matches, but got: {:?}",
self.history
);
self.search
}
}
fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
let matches = &picker.delegate.matches;
SearchEntries {
history: matches
.history
.iter()
.map(|(history_path, path_match)| {
path_match
.as_ref()
.map(|path_match| {
Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
})
.unwrap_or_else(|| {
history_path
.absolute
.as_deref()
.unwrap_or_else(|| &history_path.project.path)
.to_path_buf()
})
})
.collect(),
search: matches
.search
.iter()
.map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path))
.collect(),
}
}

View File

@@ -16,7 +16,7 @@ fsevent-sys = "3.0.2"
parking_lot.workspace = true
[dev-dependencies]
tempdir.workspace = true
tempfile.workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-apple-darwin"]

View File

@@ -370,12 +370,14 @@ extern "C" {
mod tests {
use super::*;
use std::{fs, sync::mpsc, thread, time::Duration};
use tempdir::TempDir;
#[test]
fn test_event_stream_simple() {
for _ in 0..3 {
let dir = TempDir::new("test-event-stream").unwrap();
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
@@ -404,7 +406,10 @@ mod tests {
#[test]
fn test_event_stream_delayed_start() {
for _ in 0..3 {
let dir = TempDir::new("test-event-stream").unwrap();
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
@@ -438,7 +443,10 @@ mod tests {
#[test]
fn test_event_stream_shutdown_by_dropping_handle() {
let dir = TempDir::new("test-event-stream").unwrap();
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
flush_historical_events();
@@ -465,7 +473,10 @@ mod tests {
#[test]
fn test_event_stream_shutdown_before_run() {
let dir = TempDir::new("test-event-stream").unwrap();
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));

View File

@@ -76,7 +76,7 @@ cbindgen = "0.26.0"
media = { path = "../media" }
anyhow.workspace = true
block = "0.1"
cocoa = "0.24"
cocoa = "0.25"
core-foundation = { version = "0.9.3", features = ["with-uuid"] }
core-graphics = "0.22.3"
core-text = "19.2"

View File

@@ -15,7 +15,7 @@ Everything in GPUI starts with an `App`. You can create one with `App::new()`, a
## The Big Picture
GPUI offers three different registers(https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
- State management and communication with Models. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's models. Models are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::model_context` module for more information.

View File

@@ -0,0 +1,29 @@
use gpui::*;
struct HelloWorld {
text: SharedString,
}
impl Render for HelloWorld {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.bg(rgb(0x2e7d32))
.size_full()
.justify_center()
.items_center()
.text_xl()
.text_color(rgb(0xffffff))
.child(format!("Hello, {}!", &self.text))
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|_cx| HelloWorld {
text: "World".into(),
})
});
});
}

View File

@@ -150,7 +150,13 @@ pub(crate) trait PlatformWindow {
fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
fn prompt(
&self,
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize>;
fn activate(&self);
fn set_title(&mut self, title: &str);
fn set_edited(&mut self, edited: bool);

View File

@@ -534,67 +534,77 @@ impl Platform for MacPlatform {
&self,
options: PathPromptOptions,
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
unsafe {
let panel = NSOpenPanel::openPanel(nil);
panel.setCanChooseDirectories_(options.directories.to_objc());
panel.setCanChooseFiles_(options.files.to_objc());
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
panel.setResolvesAliases_(false.to_objc());
let (done_tx, done_rx) = oneshot::channel();
let done_tx = Cell::new(Some(done_tx));
let block = ConcreteBlock::new(move |response: NSModalResponse| {
let result = if response == NSModalResponse::NSModalResponseOk {
let mut result = Vec::new();
let urls = panel.URLs();
for i in 0..urls.count() {
let url = urls.objectAtIndex(i);
if url.isFileURL() == YES {
if let Ok(path) = ns_url_to_path(url) {
result.push(path)
let (done_tx, done_rx) = oneshot::channel();
self.foreground_executor()
.spawn(async move {
unsafe {
let panel = NSOpenPanel::openPanel(nil);
panel.setCanChooseDirectories_(options.directories.to_objc());
panel.setCanChooseFiles_(options.files.to_objc());
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
panel.setResolvesAliases_(false.to_objc());
let done_tx = Cell::new(Some(done_tx));
let block = ConcreteBlock::new(move |response: NSModalResponse| {
let result = if response == NSModalResponse::NSModalResponseOk {
let mut result = Vec::new();
let urls = panel.URLs();
for i in 0..urls.count() {
let url = urls.objectAtIndex(i);
if url.isFileURL() == YES {
if let Ok(path) = ns_url_to_path(url) {
result.push(path)
}
}
}
}
}
Some(result)
} else {
None
};
Some(result)
} else {
None
};
if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(result);
if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(result);
}
});
let block = block.copy();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
}
});
let block = block.copy();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
done_rx
}
})
.detach();
done_rx
}
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
unsafe {
let panel = NSSavePanel::savePanel(nil);
let path = ns_string(directory.to_string_lossy().as_ref());
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
panel.setDirectoryURL(url);
let directory = directory.to_owned();
let (done_tx, done_rx) = oneshot::channel();
self.foreground_executor()
.spawn(async move {
unsafe {
let panel = NSSavePanel::savePanel(nil);
let path = ns_string(directory.to_string_lossy().as_ref());
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
panel.setDirectoryURL(url);
let (done_tx, done_rx) = oneshot::channel();
let done_tx = Cell::new(Some(done_tx));
let block = ConcreteBlock::new(move |response: NSModalResponse| {
let mut result = None;
if response == NSModalResponse::NSModalResponseOk {
let url = panel.URL();
if url.isFileURL() == YES {
result = ns_url_to_path(panel.URL()).ok()
}
}
let done_tx = Cell::new(Some(done_tx));
let block = ConcreteBlock::new(move |response: NSModalResponse| {
let mut result = None;
if response == NSModalResponse::NSModalResponseOk {
let url = panel.URL();
if url.isFileURL() == YES {
result = ns_url_to_path(panel.URL()).ok()
}
}
if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(result);
if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(result);
}
});
let block = block.copy();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
}
});
let block = block.copy();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
done_rx
}
})
.detach();
done_rx
}
fn reveal_path(&self, path: &Path) {

View File

@@ -772,7 +772,13 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().input_handler.take()
}
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize> {
fn prompt(
&self,
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize> {
// macOs applies overrides to modal window buttons after they are added.
// Two most important for this logic are:
// * Buttons with "Cancel" title will be displayed as the last buttons in the modal
@@ -808,6 +814,9 @@ impl PlatformWindow for MacWindow {
};
let _: () = msg_send![alert, setAlertStyle: alert_style];
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
if let Some(detail) = detail {
let _: () = msg_send![alert, setInformativeText: ns_string(detail)];
}
for (ix, answer) in answers
.iter()
@@ -1009,7 +1018,7 @@ fn get_scale_factor(native_window: id) -> f32 {
};
// We are not certain what triggers this, but it seems that sometimes
// this method would return 0 (https://github.com/zed-industries/community/issues/2422)
// this method would return 0 (https://github.com/zed-industries/zed/issues/6412)
// It seems most likely that this would happen if the window has no screen
// (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before
// it was rendered for real.

View File

@@ -185,6 +185,7 @@ impl PlatformWindow for TestWindow {
&self,
_level: crate::PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[&str],
) -> futures::channel::oneshot::Receiver<usize> {
self.0

View File

@@ -1478,9 +1478,12 @@ impl<'a> WindowContext<'a> {
&self,
level: PromptLevel,
message: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize> {
self.window.platform_window.prompt(level, message, answers)
self.window
.platform_window
.prompt(level, message, detail, answers)
}
/// Returns all available actions for the focused element.

View File

@@ -139,12 +139,11 @@ pub struct CachedLspAdapter {
impl CachedLspAdapter {
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
let name = adapter.name().await;
let name = adapter.name();
let short_name = adapter.short_name();
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
let disk_based_diagnostics_progress_token =
adapter.disk_based_diagnostics_progress_token().await;
let language_ids = adapter.language_ids().await;
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources();
let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token();
let language_ids = adapter.language_ids();
Arc::new(CachedLspAdapter {
name,
@@ -261,7 +260,7 @@ pub trait LspAdapterDelegate: Send + Sync {
#[async_trait]
pub trait LspAdapter: 'static + Send + Sync {
async fn name(&self) -> LanguageServerName;
fn name(&self) -> LanguageServerName;
fn short_name(&self) -> &'static str;
@@ -337,7 +336,7 @@ pub trait LspAdapter: 'static + Send + Sync {
}
/// Returns initialization options that are going to be sent to a LSP server as a part of [`lsp_types::InitializeParams`]
async fn initialization_options(&self) -> Option<Value> {
fn initialization_options(&self) -> Option<Value> {
None
}
@@ -356,15 +355,15 @@ pub trait LspAdapter: 'static + Send + Sync {
])
}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
fn disk_based_diagnostic_sources(&self) -> Vec<String> {
Default::default()
}
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
None
}
async fn language_ids(&self) -> HashMap<String, String> {
fn language_ids(&self) -> HashMap<String, String> {
Default::default()
}
@@ -1881,7 +1880,7 @@ impl Default for FakeLspAdapter {
#[cfg(any(test, feature = "test-support"))]
#[async_trait]
impl LspAdapter for Arc<FakeLspAdapter> {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName(self.name.into())
}
@@ -1919,15 +1918,15 @@ impl LspAdapter for Arc<FakeLspAdapter> {
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
fn disk_based_diagnostic_sources(&self) -> Vec<String> {
self.disk_based_diagnostics_sources.clone()
}
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
self.disk_based_diagnostics_progress_token.clone()
}
async fn initialization_options(&self) -> Option<Value> {
fn initialization_options(&self) -> Option<Value> {
self.initialization_options.clone()
}

View File

@@ -53,7 +53,7 @@ async-trait.workspace = true
block = "0.1"
bytes = "1.2"
byteorder = "1.4"
cocoa = "0.24"
cocoa = "0.25"
core-foundation = "0.9.3"
core-graphics = "0.22.3"
foreign-types = "0.3"

View File

@@ -83,5 +83,5 @@ prettier = { path = "../prettier", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
git2.workspace = true
tempdir.workspace = true
tempfile.workspace = true
unindent.workspace = true

View File

@@ -974,8 +974,7 @@ impl Project {
// Start all the newly-enabled language servers.
for (worktree, language) in language_servers_to_start {
let worktree_path = worktree.read(cx).abs_path();
self.start_language_servers(&worktree, worktree_path, language, cx);
self.start_language_servers(&worktree, language, cx);
}
// Restart all language servers with changed initialization options.
@@ -2774,8 +2773,8 @@ impl Project {
};
if let Some(file) = buffer_file {
let worktree = file.worktree.clone();
if let Some(tree) = worktree.read(cx).as_local() {
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
if worktree.read(cx).is_local() {
self.start_language_servers(&worktree, new_language, cx);
}
}
}
@@ -2783,7 +2782,6 @@ impl Project {
fn start_language_servers(
&mut self,
worktree: &Model<Worktree>,
worktree_path: Arc<Path>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
@@ -2793,22 +2791,14 @@ impl Project {
return;
}
let worktree_id = worktree.read(cx).id();
for adapter in language.lsp_adapters() {
self.start_language_server(
worktree_id,
worktree_path.clone(),
adapter.clone(),
language.clone(),
cx,
);
self.start_language_server(worktree, adapter.clone(), language.clone(), cx);
}
}
fn start_language_server(
&mut self,
worktree_id: WorktreeId,
worktree_path: Arc<Path>,
worktree: &Model<Worktree>,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
@@ -2817,6 +2807,9 @@ impl Project {
return;
}
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
let worktree_path = worktree.abs_path();
let key = (worktree_id, adapter.name.clone());
if self.language_server_ids.contains_key(&key) {
return;
@@ -2949,20 +2942,14 @@ impl Project {
this.update(&mut cx, |this, cx| {
let worktrees = this.worktrees.clone();
for worktree in worktrees {
let worktree = match worktree.upgrade() {
Some(worktree) => worktree.read(cx),
None => continue,
};
let worktree_id = worktree.id();
let root_path = worktree.abs_path();
this.start_language_server(
worktree_id,
root_path,
adapter.clone(),
language.clone(),
cx,
);
if let Some(worktree) = worktree.upgrade() {
this.start_language_server(
&worktree,
adapter.clone(),
language.clone(),
cx,
);
}
}
})
.ok();
@@ -3176,7 +3163,7 @@ impl Project {
}
})
.detach();
let mut initialization_options = adapter.adapter.initialization_options().await;
let mut initialization_options = adapter.adapter.initialization_options();
match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => {
merge_json_value_into(override_options, initialization_options);
@@ -3332,7 +3319,7 @@ impl Project {
worktree_id: WorktreeId,
adapter_name: LanguageServerName,
cx: &mut ModelContext<Self>,
) -> Task<(Option<PathBuf>, Vec<WorktreeId>)> {
) -> Task<Vec<WorktreeId>> {
let key = (worktree_id, adapter_name);
if let Some(server_id) = self.language_server_ids.remove(&key) {
log::info!("stopping language server {}", key.1 .0);
@@ -3370,8 +3357,6 @@ impl Project {
let server_state = self.language_servers.remove(&server_id);
cx.emit(Event::LanguageServerRemoved(server_id));
cx.spawn(move |this, mut cx| async move {
let mut root_path = None;
let server = match server_state {
Some(LanguageServerState::Starting(task)) => task.await,
Some(LanguageServerState::Running { server, .. }) => Some(server),
@@ -3379,7 +3364,6 @@ impl Project {
};
if let Some(server) = server {
root_path = Some(server.root_path().clone());
if let Some(shutdown) = server.shutdown() {
shutdown.await;
}
@@ -3393,10 +3377,10 @@ impl Project {
.ok();
}
(root_path, orphaned_worktrees)
orphaned_worktrees
})
} else {
Task::ready((None, Vec::new()))
Task::ready(Vec::new())
}
}
@@ -3426,7 +3410,6 @@ impl Project {
None
}
// TODO This will break in the case where the adapter's root paths and worktrees are not equal
fn restart_language_servers(
&mut self,
worktree: Model<Worktree>,
@@ -3434,50 +3417,42 @@ impl Project {
cx: &mut ModelContext<Self>,
) {
let worktree_id = worktree.read(cx).id();
let fallback_path = worktree.read(cx).abs_path();
let mut stops = Vec::new();
for adapter in language.lsp_adapters() {
stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx));
}
if stops.is_empty() {
let stop_tasks = language
.lsp_adapters()
.iter()
.map(|adapter| {
let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx);
(stop_task, adapter.name.clone())
})
.collect::<Vec<_>>();
if stop_tasks.is_empty() {
return;
}
let mut stops = stops.into_iter();
cx.spawn(move |this, mut cx| async move {
let (original_root_path, mut orphaned_worktrees) = stops.next().unwrap().await;
for stop in stops {
let (_, worktrees) = stop.await;
orphaned_worktrees.extend_from_slice(&worktrees);
// For each stopped language server, record all of the worktrees with which
// it was associated.
let mut affected_worktrees = Vec::new();
for (stop_task, language_server_name) in stop_tasks {
for affected_worktree_id in stop_task.await {
affected_worktrees.push((affected_worktree_id, language_server_name.clone()));
}
}
let this = match this.upgrade() {
Some(this) => this,
None => return,
};
this.update(&mut cx, |this, cx| {
// Attempt to restart using original server path. Fallback to passed in
// path if we could not retrieve the root path
let root_path = original_root_path
.map(|path_buf| Arc::from(path_buf.as_path()))
.unwrap_or(fallback_path);
this.start_language_servers(&worktree, root_path, language.clone(), cx);
// Restart the language server for the given worktree.
this.start_language_servers(&worktree, language.clone(), cx);
// Lookup new server ids and set them for each of the orphaned worktrees
for adapter in language.lsp_adapters() {
for (affected_worktree_id, language_server_name) in affected_worktrees {
if let Some(new_server_id) = this
.language_server_ids
.get(&(worktree_id, adapter.name.clone()))
.get(&(worktree_id, language_server_name.clone()))
.cloned()
{
for &orphaned_worktree in &orphaned_worktrees {
this.language_server_ids
.insert((orphaned_worktree, adapter.name.clone()), new_server_id);
}
this.language_server_ids
.insert((affected_worktree_id, language_server_name), new_server_id);
}
}
})

View File

@@ -194,12 +194,14 @@ impl AsRef<Path> for RepositoryWorkDirectory {
pub struct WorkDirectoryEntry(ProjectEntryId);
impl WorkDirectoryEntry {
pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option<RepoPath> {
worktree.entry_for_id(self.0).and_then(|entry| {
path.strip_prefix(&entry.path)
.ok()
.map(move |path| path.into())
})
pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Result<RepoPath> {
let entry = worktree
.entry_for_id(self.0)
.ok_or_else(|| anyhow!("entry not found"))?;
let path = path
.strip_prefix(&entry.path)
.map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, entry.path))?;
Ok(path.into())
}
}
@@ -970,13 +972,15 @@ impl LocalWorktree {
let mut index_task = None;
let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?;
if let Some(repo) = snapshot.repository_for_path(&path) {
let repo_path = repo.work_directory.relativize(&snapshot, &path).unwrap();
if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) {
let repo = repo.repo_ptr.clone();
index_task = Some(
cx.background_executor()
.spawn(async move { repo.lock().load_index_text(&repo_path) }),
);
if let Some(repo_path) = repo.work_directory.relativize(&snapshot, &path).log_err()
{
if let Some(git_repo) = snapshot.git_repositories.get(&*repo.work_directory) {
let git_repo = git_repo.repo_ptr.clone();
index_task = Some(
cx.background_executor()
.spawn(async move { git_repo.lock().load_index_text(&repo_path) }),
);
}
}
}

View File

@@ -58,7 +58,6 @@ pub struct ProjectPanel {
workspace: WeakView<Workspace>,
width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
was_deserialized: bool,
}
#[derive(Copy, Clone, Debug)]
@@ -244,7 +243,6 @@ impl ProjectPanel {
workspace: workspace.weak_handle(),
width: None,
pending_serialization: Task::ready(None),
was_deserialized: false,
};
this.update_visible_entries(None, cx);
@@ -324,7 +322,6 @@ impl ProjectPanel {
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
panel.was_deserialized = true;
cx.notify();
});
}
@@ -781,6 +778,7 @@ impl ProjectPanel {
let answer = cx.prompt(
PromptLevel::Info,
&format!("Delete {file_name:?}?"),
None,
&["Delete", "Cancel"],
);
@@ -1460,9 +1458,6 @@ impl ProjectPanel {
cx.notify();
}
}
pub fn was_deserialized(&self) -> bool {
self.was_deserialized
}
}
impl Render for ProjectPanel {
@@ -1637,6 +1632,14 @@ impl Panel for ProjectPanel {
fn persistent_name() -> &'static str {
"Project Panel"
}
fn starts_open(&self, cx: &WindowContext) -> bool {
self.project.read(cx).visible_worktrees(cx).any(|tree| {
tree.read(cx)
.root_entry()
.map_or(false, |entry| entry.is_dir())
})
}
}
impl FocusableView for ProjectPanel {
@@ -1937,7 +1940,6 @@ mod tests {
.update(cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
workspace.add_panel(panel.clone(), cx);
workspace.toggle_dock(panel.read(cx).position(cx), cx);
panel
})
.unwrap();
@@ -2295,7 +2297,6 @@ mod tests {
.update(cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
workspace.add_panel(panel.clone(), cx);
workspace.toggle_dock(panel.read(cx).position(cx), cx);
panel
})
.unwrap();
@@ -2571,7 +2572,6 @@ mod tests {
.update(cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
workspace.add_panel(panel.clone(), cx);
workspace.toggle_dock(panel.read(cx).position(cx), cx);
panel
})
.unwrap();

View File

@@ -43,6 +43,6 @@ prost-build = "0.9"
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
smol.workspace = true
tempdir.workspace = true
tempfile.workspace = true
ctor.workspace = true
env_logger.workspace = true

View File

@@ -197,6 +197,19 @@ message Ack {}
message Error {
string message = 1;
ErrorCode code = 2;
repeated string tags = 3;
}
enum ErrorCode {
Internal = 0;
NoSuchChannel = 1;
Disconnected = 2;
SignedOut = 3;
UpgradeRequired = 4;
Forbidden = 5;
WrongReleaseChannel = 6;
NeedsCla = 7;
}
message Test {

223
crates/rpc/src/error.rs Normal file
View File

@@ -0,0 +1,223 @@
/// Some helpers for structured error handling.
///
/// The helpers defined here allow you to pass type-safe error codes from
/// the collab server to the client; and provide a mechanism for additional
/// structured data alongside the message.
///
/// When returning an error, it can be as simple as:
///
/// `return Err(Error::Forbidden.into())`
///
/// If you'd like to log more context, you can set a message. These messages
/// show up in our logs, but are not shown visibly to users.
///
/// `return Err(Error::Forbidden.message("not an admin").into())`
///
/// If you'd like to provide enough context that the UI can render a good error
/// message (or would be helpful to see in a structured format in the logs), you
/// can use .with_tag():
///
/// `return Err(Error::WrongReleaseChannel.with_tag("required", "stable").into())`
///
/// When handling an error you can use .error_code() to match which error it was
/// and .error_tag() to read any tags.
///
/// ```
/// match err.error_code() {
/// ErrorCode::Forbidden => alert("I'm sorry I can't do that.")
/// ErrorCode::WrongReleaseChannel =>
/// alert(format!("You need to be on the {} release channel.", err.error_tag("required").unwrap()))
/// ErrorCode::Internal => alert("Sorry, something went wrong")
/// }
/// ```
///
use crate::proto;
pub use proto::ErrorCode;
/// ErrorCodeExt provides some helpers for structured error handling.
///
/// The primary implementation is on the proto::ErrorCode to easily convert
/// that into an anyhow::Error, which we use pervasively.
///
/// The RpcError struct provides support for further metadata if needed.
pub trait ErrorCodeExt {
/// Return an anyhow::Error containing this.
/// (useful in places where .into() doesn't have enough type information)
fn anyhow(self) -> anyhow::Error;
/// Add a message to the error (by default the error code is used)
fn message(self, msg: String) -> RpcError;
/// Add a tag to the error. Tags are key value pairs that can be used
/// to send semi-structured data along with the error.
fn with_tag(self, k: &str, v: &str) -> RpcError;
}
impl ErrorCodeExt for proto::ErrorCode {
fn anyhow(self) -> anyhow::Error {
self.into()
}
fn message(self, msg: String) -> RpcError {
let err: RpcError = self.into();
err.message(msg)
}
fn with_tag(self, k: &str, v: &str) -> RpcError {
let err: RpcError = self.into();
err.with_tag(k, v)
}
}
/// ErrorExt provides helpers for structured error handling.
///
/// The primary implementation is on the anyhow::Error, which is
/// what we use throughout our codebase. Though under the hood this
pub trait ErrorExt {
/// error_code() returns the ErrorCode (or ErrorCode::Internal if there is none)
fn error_code(&self) -> proto::ErrorCode;
/// error_tag() returns the value of the tag with the given key, if any.
fn error_tag(&self, k: &str) -> Option<&str>;
/// to_proto() converts the error into a proto::Error
fn to_proto(&self) -> proto::Error;
}
impl ErrorExt for anyhow::Error {
fn error_code(&self) -> proto::ErrorCode {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.code
} else {
proto::ErrorCode::Internal
}
}
fn error_tag(&self, k: &str) -> Option<&str> {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.error_tag(k)
} else {
None
}
}
fn to_proto(&self) -> proto::Error {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.to_proto()
} else {
ErrorCode::Internal.message(format!("{}", self)).to_proto()
}
}
}
impl From<proto::ErrorCode> for anyhow::Error {
fn from(value: proto::ErrorCode) -> Self {
RpcError {
request: None,
code: value,
msg: format!("{:?}", value).to_string(),
tags: Default::default(),
}
.into()
}
}
#[derive(Clone, Debug)]
pub struct RpcError {
request: Option<String>,
msg: String,
code: proto::ErrorCode,
tags: Vec<String>,
}
/// RpcError is a structured error type that is returned by the collab server.
/// In addition to a message, it lets you set a specific ErrorCode, and attach
/// small amounts of metadata to help the client handle the error appropriately.
///
/// This struct is not typically used directly, as we pass anyhow::Error around
/// in the app; however it is useful for chaining .message() and .with_tag() on
/// ErrorCode.
impl RpcError {
/// from_proto converts a proto::Error into an anyhow::Error containing
/// an RpcError.
pub fn from_proto(error: &proto::Error, request: &str) -> anyhow::Error {
RpcError {
request: Some(request.to_string()),
code: error.code(),
msg: error.message.clone(),
tags: error.tags.clone(),
}
.into()
}
}
impl ErrorCodeExt for RpcError {
fn message(mut self, msg: String) -> RpcError {
self.msg = msg;
self
}
fn with_tag(mut self, k: &str, v: &str) -> RpcError {
self.tags.push(format!("{}={}", k, v));
self
}
fn anyhow(self) -> anyhow::Error {
self.into()
}
}
impl ErrorExt for RpcError {
fn error_tag(&self, k: &str) -> Option<&str> {
for tag in &self.tags {
let mut parts = tag.split('=');
if let Some(key) = parts.next() {
if key == k {
return parts.next();
}
}
}
None
}
fn error_code(&self) -> proto::ErrorCode {
self.code
}
fn to_proto(&self) -> proto::Error {
proto::Error {
code: self.code as i32,
message: self.msg.clone(),
tags: self.tags.clone(),
}
}
}
impl std::error::Error for RpcError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl std::fmt::Display for RpcError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(request) = &self.request {
write!(f, "RPC request {} failed: {}", request, self.msg)?
} else {
write!(f, "{}", self.msg)?
}
for tag in &self.tags {
write!(f, " {}", tag)?
}
Ok(())
}
}
impl From<proto::ErrorCode> for RpcError {
fn from(code: proto::ErrorCode) -> Self {
RpcError {
request: None,
code,
msg: format!("{:?}", code).to_string(),
tags: Default::default(),
}
}
}

View File

@@ -1,3 +1,5 @@
use crate::{ErrorCode, ErrorCodeExt, ErrorExt, RpcError};
use super::{
proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, PeerId, RequestMessage},
Connection,
@@ -423,11 +425,7 @@ impl Peer {
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
Err(anyhow!(
"RPC request {} failed - {}",
T::NAME,
error.message
))
Err(RpcError::from_proto(&error, T::NAME))
} else {
Ok(TypedEnvelope {
message_id: response.id,
@@ -516,9 +514,12 @@ impl Peer {
envelope: Box<dyn AnyTypedEnvelope>,
) -> Result<()> {
let connection = self.connection_state(envelope.sender_id())?;
let response = proto::Error {
message: format!("message {} was not handled", envelope.payload_type_name()),
};
let response = ErrorCode::Internal
.message(format!(
"message {} was not handled",
envelope.payload_type_name()
))
.to_proto();
let message_id = connection
.next_message_id
.fetch_add(1, atomic::Ordering::SeqCst);
@@ -692,17 +693,17 @@ mod tests {
server
.send(
server_to_client_conn_id,
proto::Error {
message: "message 1".to_string(),
},
ErrorCode::Internal
.message("message 1".to_string())
.to_proto(),
)
.unwrap();
server
.send(
server_to_client_conn_id,
proto::Error {
message: "message 2".to_string(),
},
ErrorCode::Internal
.message("message 2".to_string())
.to_proto(),
)
.unwrap();
server.respond(request.receipt(), proto::Ack {}).unwrap();
@@ -797,17 +798,17 @@ mod tests {
server
.send(
server_to_client_conn_id,
proto::Error {
message: "message 1".to_string(),
},
ErrorCode::Internal
.message("message 1".to_string())
.to_proto(),
)
.unwrap();
server
.send(
server_to_client_conn_id,
proto::Error {
message: "message 2".to_string(),
},
ErrorCode::Internal
.message("message 2".to_string())
.to_proto(),
)
.unwrap();
server.respond(request1.receipt(), proto::Ack {}).unwrap();

View File

@@ -1,10 +1,12 @@
pub mod auth;
mod conn;
mod error;
mod notification;
mod peer;
pub mod proto;
pub use conn::Connection;
pub use error::*;
pub use notification::*;
pub use peer::*;
mod macros;

View File

@@ -1,3 +1,5 @@
mod registrar;
use crate::{
history::SearchHistory,
mode::{next_mode, SearchMode},
@@ -29,6 +31,9 @@ use workspace::{
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
pub use registrar::DivRegistrar;
use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
#[derive(PartialEq, Clone, Deserialize)]
pub struct Deploy {
pub focus: bool,
@@ -422,230 +427,59 @@ impl ToolbarItemView for BufferSearchBar {
}
}
/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are.
pub trait SearchActionsRegistrar {
fn register_handler<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
);
fn register_handler_for_dismissed_search<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
);
}
type GetSearchBar<T> =
for<'a, 'b> fn(&'a T, &'a mut ViewContext<'b, T>) -> Option<View<BufferSearchBar>>;
/// Registers search actions on a div that can be taken out.
pub struct DivRegistrar<'a, 'b, T: 'static> {
div: Option<Div>,
cx: &'a mut ViewContext<'b, T>,
search_getter: GetSearchBar<T>,
}
impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
pub fn new(search_getter: GetSearchBar<T>, cx: &'a mut ViewContext<'b, T>) -> Self {
Self {
div: Some(div()),
cx,
search_getter,
}
}
pub fn into_div(self) -> Div {
// This option is always Some; it's an option in the first place because we want to call methods
// on div that require ownership.
self.div.unwrap()
}
}
impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
fn register_handler<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
let getter = self.search_getter;
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, cx| {
let should_notify = (getter)(this, cx)
.clone()
.map(|search_bar| {
search_bar.update(cx, |search_bar, cx| {
if search_bar.is_dismissed()
|| search_bar.active_searchable_item.is_none()
{
false
} else {
callback(search_bar, action, cx);
true
}
})
})
.unwrap_or(false);
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}))
});
}
fn register_handler_for_dismissed_search<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
let getter = self.search_getter;
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, cx| {
let should_notify = (getter)(this, cx)
.clone()
.map(|search_bar| {
search_bar.update(cx, |search_bar, cx| {
if search_bar.is_dismissed() {
callback(search_bar, action, cx);
true
} else {
false
}
})
})
.unwrap_or(false);
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}))
});
}
}
/// Register actions for an active pane.
impl SearchActionsRegistrar for Workspace {
fn register_handler<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
self.register_action(move |workspace, action: &A, cx| {
if workspace.has_active_modal(cx) {
cx.propagate();
return;
}
let pane = workspace.active_pane();
pane.update(cx, move |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
let should_notify = search_bar.update(cx, move |search_bar, cx| {
if search_bar.is_dismissed()
|| search_bar.active_searchable_item.is_none()
{
false
} else {
callback(search_bar, action, cx);
true
}
});
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}
})
});
});
}
fn register_handler_for_dismissed_search<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
self.register_action(move |workspace, action: &A, cx| {
if workspace.has_active_modal(cx) {
cx.propagate();
return;
}
let pane = workspace.active_pane();
pane.update(cx, move |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
let should_notify = search_bar.update(cx, move |search_bar, cx| {
if search_bar.is_dismissed() {
callback(search_bar, action, cx);
true
} else {
false
}
});
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}
})
});
});
}
}
impl BufferSearchBar {
pub fn register(registrar: &mut impl SearchActionsRegistrar) {
registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| {
registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
if this.supported_options().case {
this.toggle_case_sensitive(action, cx);
}
});
registrar.register_handler(|this, action: &ToggleWholeWord, cx| {
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| {
if this.supported_options().word {
this.toggle_whole_word(action, cx);
}
});
registrar.register_handler(|this, action: &ToggleReplace, cx| {
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
if this.supported_options().replacement {
this.toggle_replace(action, cx);
}
});
registrar.register_handler(|this, _: &ActivateRegexMode, cx| {
}));
registrar.register_handler(ForDeployed(|this, _: &ActivateRegexMode, cx| {
if this.supported_options().regex {
this.activate_search_mode(SearchMode::Regex, cx);
}
});
registrar.register_handler(|this, _: &ActivateTextMode, cx| {
}));
registrar.register_handler(ForDeployed(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
});
registrar.register_handler(|this, action: &CycleMode, cx| {
}));
registrar.register_handler(ForDeployed(|this, action: &CycleMode, cx| {
if this.supported_options().regex {
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
// cycling.
this.cycle_mode(action, cx)
}
});
registrar.register_handler(|this, action: &SelectNextMatch, cx| {
}));
registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
this.select_next_match(action, cx);
});
registrar.register_handler(|this, action: &SelectPrevMatch, cx| {
}));
registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| {
this.select_prev_match(action, cx);
});
registrar.register_handler(|this, action: &SelectAllMatches, cx| {
}));
registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| {
this.select_all_matches(action, cx);
});
registrar.register_handler(|this, _: &editor::actions::Cancel, cx| {
}));
registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| {
this.dismiss(&Dismiss, cx);
});
}));
// register deploy buffer search for both search bar states, since we want to focus into the search bar
// when the deploy action is triggered in the buffer.
registrar.register_handler(|this, deploy, cx| {
registrar.register_handler(ForDeployed(|this, deploy, cx| {
this.deploy(deploy, cx);
});
registrar.register_handler_for_dismissed_search(|this, deploy, cx| {
}));
registrar.register_handler(ForDismissed(|this, deploy, cx| {
this.deploy(deploy, cx);
})
}))
}
pub fn new(cx: &mut ViewContext<Self>) -> Self {
@@ -930,7 +764,7 @@ impl BufferSearchBar {
event: &editor::EditorEvent,
cx: &mut ViewContext<Self>,
) {
if let editor::EditorEvent::Edited { .. } = event {
if let editor::EditorEvent::Edited = event {
self.query_contains_error = false;
self.clear_matches(cx);
let search = self.update_matches(cx);

View File

@@ -0,0 +1,172 @@
use gpui::{div, Action, Div, InteractiveElement, View, ViewContext};
use workspace::Workspace;
use crate::BufferSearchBar;
/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are.
pub trait SearchActionsRegistrar {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>);
}
type SearchBarActionCallback<A> = fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>);
type GetSearchBar<T> =
for<'a, 'b> fn(&'a T, &'a mut ViewContext<'b, T>) -> Option<View<BufferSearchBar>>;
/// Registers search actions on a div that can be taken out.
pub struct DivRegistrar<'a, 'b, T: 'static> {
div: Option<Div>,
cx: &'a mut ViewContext<'b, T>,
search_getter: GetSearchBar<T>,
}
impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
pub fn new(search_getter: GetSearchBar<T>, cx: &'a mut ViewContext<'b, T>) -> Self {
Self {
div: Some(div()),
cx,
search_getter,
}
}
pub fn into_div(self) -> Div {
// This option is always Some; it's an option in the first place because we want to call methods
// on div that require ownership.
self.div.unwrap()
}
}
impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {
let getter = self.search_getter;
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, cx| {
let should_notify = (getter)(this, cx)
.clone()
.map(|search_bar| {
search_bar.update(cx, |search_bar, cx| {
callback.execute(search_bar, action, cx)
})
})
.unwrap_or(false);
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}))
});
}
}
/// Register actions for an active pane.
impl SearchActionsRegistrar for Workspace {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {
self.register_action(move |workspace, action: &A, cx| {
if workspace.has_active_modal(cx) {
cx.propagate();
return;
}
let pane = workspace.active_pane();
let callback = callback.clone();
pane.update(cx, |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
let should_notify = search_bar.update(cx, move |search_bar, cx| {
callback.execute(search_bar, action, cx)
});
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}
})
});
});
}
}
type DidHandleAction = bool;
/// Potentially executes the underlying action if some preconditions are met (e.g. buffer search bar is visible)
pub trait ActionExecutor<A: Action>: 'static + Clone {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
cx: &mut ViewContext<BufferSearchBar>,
) -> DidHandleAction;
}
/// Run an action when the search bar has been dismissed from the panel.
pub struct ForDismissed<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for ForDismissed<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for ForDismissed<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
cx: &mut ViewContext<BufferSearchBar>,
) -> DidHandleAction {
if search_bar.is_dismissed() {
self.0(search_bar, action, cx);
true
} else {
false
}
}
}
/// Run an action when the search bar is deployed.
pub struct ForDeployed<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for ForDeployed<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for ForDeployed<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
cx: &mut ViewContext<BufferSearchBar>,
) -> DidHandleAction {
if search_bar.is_dismissed() || search_bar.active_searchable_item.is_none() {
false
} else {
self.0(search_bar, action, cx);
true
}
}
}
/// Run an action when the search bar has any matches, regardless of whether it
/// is visible or not.
pub struct WithResults<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for WithResults<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for WithResults<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
cx: &mut ViewContext<BufferSearchBar>,
) -> DidHandleAction {
if search_bar.active_match_index.is_some() {
self.0(search_bar, action, cx);
true
} else {
false
}
}
}

View File

@@ -88,6 +88,12 @@ pub fn init(cx: &mut AppContext) {
register_workspace_action(workspace, move |search_bar, action: &CycleMode, cx| {
search_bar.cycle_mode(action, cx)
});
register_workspace_action(
workspace,
move |search_bar, action: &SelectPrevMatch, cx| {
search_bar.select_prev_match(action, cx)
},
);
register_workspace_action(
workspace,
move |search_bar, action: &SelectNextMatch, cx| {
@@ -746,6 +752,7 @@ impl ProjectSearchView {
cx.prompt(
PromptLevel::Info,
prompt_text.as_str(),
None,
&["Continue", "Cancel"],
)
})?;
@@ -1549,7 +1556,7 @@ impl ProjectSearchBar {
}
}
pub fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
if let Some(search) = self.active_project_search.as_ref() {
search.update(cx, |this, cx| {
this.select_match(Direction::Next, cx);

View File

@@ -56,7 +56,7 @@ node_runtime = { path = "../node_runtime"}
pretty_assertions.workspace = true
rand.workspace = true
unindent.workspace = true
tempdir.workspace = true
tempfile.workspace = true
ctor.workspace = true
env_logger.workspace = true

View File

@@ -63,7 +63,10 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
languages.add(rust_language);
languages.add(toml_language);
let db_dir = tempdir::TempDir::new("vector-store").unwrap();
let db_dir = tempfile::Builder::new()
.prefix("vector-store")
.tempdir()
.unwrap();
let db_path = db_dir.path().join("db.sqlite");
let embedding_provider = Arc::new(FakeEmbeddingProvider::default());

View File

@@ -2634,7 +2634,7 @@ impl Default for LineEnding {
return Self::Unix;
#[cfg(not(unix))]
return Self::CRLF;
return Self::Windows;
}
}

View File

@@ -11,7 +11,7 @@ path = "src/util.rs"
doctest = true
[features]
test-support = ["tempdir", "git2"]
test-support = ["tempfile", "git2"]
[dependencies]
anyhow.workspace = true
@@ -25,7 +25,7 @@ smol.workspace = true
url = "2.2"
rand.workspace = true
rust-embed.workspace = true
tempdir = { workspace = true, optional = true }
tempfile = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
git2 = { workspace = true, optional = true }
@@ -33,5 +33,5 @@ dirs = "3.0"
take-until = "0.2.0"
[dev-dependencies]
tempdir.workspace = true
tempfile.workspace = true
git2.workspace = true

View File

@@ -6,13 +6,13 @@ use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use tempdir::TempDir;
use tempfile::TempDir;
pub use assertions::*;
pub use marked_text::*;
pub fn temp_tree(tree: serde_json::Value) -> TempDir {
let dir = TempDir::new("").unwrap();
let dir = TempDir::new().unwrap();
write_tree(dir.path(), tree);
dir
}

View File

@@ -146,7 +146,7 @@ async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) {
cx.simulate_keystrokes(["shift-g"]);
cx.assert_editor_state("aa\nbb\ncˇc");
// can go to line 1 (https://github.com/zed-industries/community/issues/710)
// can go to line 1 (https://github.com/zed-industries/zed/issues/5812)
cx.simulate_keystrokes(["1", "shift-g"]);
cx.assert_editor_state("aˇa\nbb\ncc");
}

View File

@@ -899,7 +899,7 @@ mod test {
})
.await;
//https://github.com/zed-industries/community/issues/1950
// https://github.com/zed-industries/zed/issues/6274
cx.set_shared_state(indoc! {
"Theˇ quick brown

View File

@@ -38,6 +38,9 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
false
}
fn starts_open(&self, _cx: &WindowContext) -> bool {
false
}
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
}
@@ -414,7 +417,7 @@ impl Dock {
let name = panel.persistent_name().to_string();
self.panel_entries.push(PanelEntry {
panel: Arc::new(panel),
panel: Arc::new(panel.clone()),
_subscriptions: subscriptions,
});
if let Some(serialized) = self.serialized_dock.clone() {
@@ -429,6 +432,9 @@ impl Dock {
};
}
}
} else if panel.read(cx).starts_open(cx) {
self.activate_panel(self.panel_entries.len() - 1, cx);
self.set_open(true, cx);
}
cx.notify()

View File

@@ -1,8 +1,8 @@
use crate::{Toast, Workspace};
use collections::HashMap;
use gpui::{
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
Task, View, ViewContext, VisualContext, WindowContext,
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter,
PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
};
use std::{any::TypeId, ops::DerefMut};
@@ -299,7 +299,7 @@ pub trait NotifyTaskExt {
impl<R, E> NotifyTaskExt for Task<Result<R, E>>
where
E: std::fmt::Debug + 'static,
E: std::fmt::Debug + Sized + 'static,
R: 'static,
{
fn detach_and_notify_err(self, cx: &mut WindowContext) {
@@ -307,3 +307,39 @@ where
.detach();
}
}
pub trait DetachAndPromptErr {
fn detach_and_prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
);
}
impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
where
R: 'static,
{
fn detach_and_prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
) {
let msg = msg.to_owned();
cx.spawn(|mut cx| async move {
if let Err(err) = self.await {
log::error!("{err:?}");
if let Ok(prompt) = cx.update(|cx| {
let detail = f(&err, cx)
.unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
}) {
prompt.await.ok();
}
}
})
.detach();
}
}

View File

@@ -870,7 +870,7 @@ impl Pane {
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
all_dirty_items: usize,
cx: &AppContext,
) -> String {
) -> (String, String) {
/// Quantity of item paths displayed in prompt prior to cutoff..
const FILE_NAMES_CUTOFF_POINT: usize = 10;
let mut file_names: Vec<_> = items
@@ -894,10 +894,12 @@ impl Pane {
file_names.push(format!(".. {} files not shown", not_shown_files).into());
}
}
let file_names = file_names.join("\n");
format!(
"Do you want to save changes to the following {} files?\n{file_names}",
all_dirty_items
(
format!(
"Do you want to save changes to the following {} files?",
all_dirty_items
),
file_names.join("\n"),
)
}
@@ -929,11 +931,12 @@ impl Pane {
cx.spawn(|pane, mut cx| async move {
if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
let answer = pane.update(&mut cx, |_, cx| {
let prompt =
let (prompt, detail) =
Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
cx.prompt(
PromptLevel::Warning,
&prompt,
Some(&detail),
&["Save all", "Discard all", "Cancel"],
)
})?;
@@ -1131,6 +1134,7 @@ impl Pane {
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
None,
&["Overwrite", "Discard", "Cancel"],
)
})?;
@@ -1154,6 +1158,7 @@ impl Pane {
cx.prompt(
PromptLevel::Warning,
&prompt,
None,
&["Save", "Don't Save", "Cancel"],
)
})?;

View File

@@ -14,8 +14,8 @@ mod workspace_settings;
use anyhow::{anyhow, Context as _, Result};
use call::ActiveCall;
use client::{
proto::{self, PeerId},
Client, Status, TypedEnvelope, UserStore,
proto::{self, ErrorCode, PeerId},
Client, ErrorExt, Status, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
@@ -30,8 +30,8 @@ use gpui::{
DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
WindowBounds, WindowContext, WindowHandle, WindowOptions,
Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@@ -1159,6 +1159,7 @@ impl Workspace {
cx.prompt(
PromptLevel::Warning,
"Do you want to leave the current call?",
None,
&["Close window and hang up", "Cancel"],
)
})?;
@@ -1214,7 +1215,7 @@ impl Workspace {
// Override save mode and display "Save all files" prompt
if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
let answer = workspace.update(&mut cx, |_, cx| {
let prompt = Pane::file_names_for_prompt(
let (prompt, detail) = Pane::file_names_for_prompt(
&mut dirty_items.iter().map(|(_, handle)| handle),
dirty_items.len(),
cx,
@@ -1222,6 +1223,7 @@ impl Workspace {
cx.prompt(
PromptLevel::Warning,
&prompt,
Some(&detail),
&["Save all", "Discard all", "Cancel"],
)
})?;
@@ -3450,7 +3452,7 @@ fn open_items(
}
fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
workspace
.update(cx, |workspace, cx| {
@@ -3887,13 +3889,16 @@ async fn join_channel_internal(
if should_prompt {
if let Some(workspace) = requesting_window {
let answer = workspace.update(cx, |_, cx| {
cx.prompt(
PromptLevel::Warning,
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
&["Yes, Join Channel", "Cancel"],
)
})?.await;
let answer = workspace
.update(cx, |_, cx| {
cx.prompt(
PromptLevel::Warning,
"Do you want to switch channels?",
Some("Leaving this call will unshare your current project."),
&["Yes, Join Channel", "Cancel"],
)
})?
.await;
if answer == Ok(1) {
return Ok(false);
@@ -3919,10 +3924,10 @@ async fn join_channel_internal(
| Status::Reconnecting
| Status::Reauthenticating => continue,
Status::Connected { .. } => break 'outer,
Status::SignedOut => return Err(anyhow!("not signed in")),
Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
return Err(anyhow!("zed is offline"))
return Err(ErrorCode::Disconnected.into())
}
}
}
@@ -3995,9 +4000,27 @@ pub fn join_channel(
if let Some(active_window) = active_window {
active_window
.update(&mut cx, |_, cx| {
let detail: SharedString = match err.error_code() {
ErrorCode::SignedOut => {
"Please sign in to continue.".into()
},
ErrorCode::UpgradeRequired => {
"Your are running an unsupported version of Zed. Please update to continue.".into()
},
ErrorCode::NoSuchChannel => {
"No matching channel was found. Please check the link and try again.".into()
},
ErrorCode::Forbidden => {
"This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
},
ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
ErrorCode::WrongReleaseChannel => format!("Others in the channel are using the {} release of Zed. Please switch to join this call.", err.error_tag("required").unwrap_or("other")).into(),
_ => format!("{}\n\nPlease try again.", err).into(),
};
cx.prompt(
PromptLevel::Critical,
&format!("Failed to join channel: {}", err),
"Failed to join channel",
Some(&detail),
&["Ok"],
)
})?
@@ -4224,6 +4247,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
cx.prompt(
PromptLevel::Info,
"Are you sure you want to restart?",
None,
&["Restart", "Cancel"],
)
})

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.119.18"
version = "0.121.0"
publish = false
license = "GPL-3.0-only"
@@ -31,7 +31,6 @@ command_palette = { path = "../command_palette" }
# component_test = { path = "../component_test" }
client = { path = "../client" }
# clock = { path = "../clock" }
color = { path = "../color" }
copilot = { path = "../copilot" }
copilot_ui = { path = "../copilot_ui" }
diagnostics = { path = "../diagnostics" }
@@ -109,7 +108,7 @@ schemars.workspace = true
simplelog = "0.9"
smallvec.workspace = true
smol.workspace = true
tempdir.workspace = true
tempfile.workspace = true
thiserror.workspace = true
tiny_http = "0.8"
toml.workspace = true

View File

@@ -1 +1 @@
stable
dev

View File

@@ -15,7 +15,7 @@ pub struct CLspAdapter;
#[async_trait]
impl super::LspAdapter for CLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("clangd".into())
}

View File

@@ -33,7 +33,7 @@ impl CssLspAdapter {
#[async_trait]
impl LspAdapter for CssLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-css-language-server".into())
}
@@ -91,7 +91,7 @@ impl LspAdapter for CssLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true
}))

View File

@@ -67,7 +67,7 @@ pub struct ElixirLspAdapter;
#[async_trait]
impl LspAdapter for ElixirLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("elixir-ls".into())
}
@@ -301,7 +301,7 @@ pub struct NextLspAdapter;
#[async_trait]
impl LspAdapter for NextLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("next-ls".into())
}
@@ -452,7 +452,7 @@ pub struct LocalLspAdapter {
#[async_trait]
impl LspAdapter for LocalLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("local-ls".into())
}

View File

@@ -33,7 +33,7 @@ lazy_static! {
#[async_trait]
impl super::LspAdapter for GoLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("gopls".into())
}

View File

@@ -33,7 +33,7 @@ impl HtmlLspAdapter {
#[async_trait]
impl LspAdapter for HtmlLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-html-language-server".into())
}
@@ -91,7 +91,7 @@ impl LspAdapter for HtmlLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true
}))

View File

@@ -38,7 +38,7 @@ impl JsonLspAdapter {
#[async_trait]
impl LspAdapter for JsonLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("json-language-server".into())
}
@@ -96,7 +96,7 @@ impl LspAdapter for JsonLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true
}))
@@ -140,7 +140,7 @@ impl LspAdapter for JsonLspAdapter {
})
}
async fn language_ids(&self) -> HashMap<String, String> {
fn language_ids(&self) -> HashMap<String, String> {
[("JSON".into(), "jsonc".into())].into_iter().collect()
}
}

View File

@@ -18,7 +18,7 @@ pub struct LuaLspAdapter;
#[async_trait]
impl super::LspAdapter for LuaLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("lua-language-server".into())
}

View File

@@ -8,7 +8,7 @@ pub struct NuLanguageServer;
#[async_trait]
impl LspAdapter for NuLanguageServer {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("nu".into())
}

View File

@@ -36,7 +36,7 @@ impl IntelephenseLspAdapter {
#[async_trait]
impl LspAdapter for IntelephenseLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("intelephense".into())
}
@@ -96,10 +96,10 @@ impl LspAdapter for IntelephenseLspAdapter {
None
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
fn initialization_options(&self) -> Option<serde_json::Value> {
None
}
async fn language_ids(&self) -> HashMap<String, String> {
fn language_ids(&self) -> HashMap<String, String> {
HashMap::from_iter([("PHP".into(), "php".into())])
}
}

View File

@@ -30,7 +30,7 @@ impl PythonLspAdapter {
#[async_trait]
impl LspAdapter for PythonLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("pyright".into())
}

View File

@@ -8,7 +8,7 @@ pub struct RubyLanguageServer;
#[async_trait]
impl LspAdapter for RubyLanguageServer {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("solargraph".into())
}

View File

@@ -18,7 +18,7 @@ pub struct RustLspAdapter;
#[async_trait]
impl LspAdapter for RustLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("rust-analyzer".into())
}
@@ -98,11 +98,11 @@ impl LspAdapter for RustLspAdapter {
})
}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
fn disk_based_diagnostic_sources(&self) -> Vec<String> {
vec!["rustc".into()]
}
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
Some("rust-analyzer/flycheck".into())
}

View File

@@ -32,7 +32,7 @@ impl SvelteLspAdapter {
#[async_trait]
impl LspAdapter for SvelteLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("svelte-language-server".into())
}
@@ -90,7 +90,7 @@ impl LspAdapter for SvelteLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true
}))

View File

@@ -1,6 +1,6 @@
name = "Svelte"
path_suffixes = ["svelte"]
line_comment = "// "
block_comment = ["<!-- ", " -->"]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },

View File

@@ -34,7 +34,7 @@ impl TailwindLspAdapter {
#[async_trait]
impl LspAdapter for TailwindLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("tailwindcss-language-server".into())
}
@@ -92,7 +92,7 @@ impl LspAdapter for TailwindLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true,
"userLanguages": {
@@ -112,7 +112,7 @@ impl LspAdapter for TailwindLspAdapter {
})
}
async fn language_ids(&self) -> HashMap<String, String> {
fn language_ids(&self) -> HashMap<String, String> {
HashMap::from_iter([
("HTML".to_string(), "html".to_string()),
("CSS".to_string(), "css".to_string()),

View File

@@ -46,7 +46,7 @@ struct TypeScriptVersions {
#[async_trait]
impl LspAdapter for TypeScriptLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("typescript-language-server".into())
}
@@ -150,7 +150,7 @@ impl LspAdapter for TypeScriptLspAdapter {
})
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true,
"tsserver": {
@@ -159,7 +159,7 @@ impl LspAdapter for TypeScriptLspAdapter {
}))
}
async fn language_ids(&self) -> HashMap<String, String> {
fn language_ids(&self) -> HashMap<String, String> {
HashMap::from_iter([
("TypeScript".into(), "typescript".into()),
("JavaScript".into(), "javascript".into()),
@@ -227,7 +227,7 @@ impl LspAdapter for EsLintLspAdapter {
})
}
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("eslint".into())
}
@@ -315,7 +315,7 @@ impl LspAdapter for EsLintLspAdapter {
None
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
fn initialization_options(&self) -> Option<serde_json::Value> {
None
}
}

View File

@@ -8,7 +8,7 @@ pub struct UiuaLanguageServer;
#[async_trait]
impl LspAdapter for UiuaLanguageServer {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("uiua".into())
}

View File

@@ -40,7 +40,7 @@ impl VueLspAdapter {
}
#[async_trait]
impl super::LspAdapter for VueLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("vue-language-server".into())
}
@@ -60,7 +60,7 @@ impl super::LspAdapter for VueLspAdapter {
ts_version: self.node.npm_package_latest_version("typescript").await?,
}) as Box<_>)
}
async fn initialization_options(&self) -> Option<Value> {
fn initialization_options(&self) -> Option<Value> {
let typescript_sdk_path = self.typescript_install_path.lock();
let typescript_sdk_path = typescript_sdk_path
.as_ref()

View File

@@ -35,7 +35,7 @@ impl YamlLspAdapter {
#[async_trait]
impl LspAdapter for YamlLspAdapter {
async fn name(&self) -> LanguageServerName {
fn name(&self) -> LanguageServerName {
LanguageServerName("yaml-language-server".into())
}

View File

@@ -179,27 +179,12 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
)?;
workspace_handle.update(&mut cx, |workspace, cx| {
let was_deserialized = project_panel.read(cx).was_deserialized();
workspace.add_panel(project_panel, cx);
workspace.add_panel(terminal_panel, cx);
workspace.add_panel(assistant_panel, cx);
workspace.add_panel(channels_panel, cx);
workspace.add_panel(chat_panel, cx);
workspace.add_panel(notification_panel, cx);
if !was_deserialized
&& workspace
.project()
.read(cx)
.visible_worktrees(cx)
.any(|tree| {
tree.read(cx)
.root_entry()
.map_or(false, |entry| entry.is_dir())
})
{
workspace.open_panel::<ProjectPanel>(cx);
}
cx.focus_self();
})
})
@@ -385,16 +370,12 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewCo
}
fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
use std::fmt::Write as _;
let app_name = cx.global::<ReleaseChannel>().display_name();
let version = env!("CARGO_PKG_VERSION");
let mut message = format!("{app_name} {version}");
if let Some(sha) = cx.try_global::<AppCommitSha>() {
write!(&mut message, "\n\n{}", sha.0).unwrap();
}
let message = format!("{app_name} {version}");
let detail = cx.try_global::<AppCommitSha>().map(|sha| sha.0.as_ref());
let prompt = cx.prompt(PromptLevel::Info, &message, &["OK"]);
let prompt = cx.prompt(PromptLevel::Info, &message, detail, &["OK"]);
cx.foreground_executor()
.spawn(async {
prompt.await.ok();
@@ -425,6 +406,7 @@ fn quit(_: &Quit, cx: &mut AppContext) {
cx.prompt(
PromptLevel::Info,
"Are you sure you want to quit?",
None,
&["Quit", "Cancel"],
)
})
@@ -752,7 +734,7 @@ fn open_settings_file(
mod tests {
use super::*;
use assets::Assets;
use editor::{scroll::Autoscroll, DisplayPoint, Editor, EditorEvent};
use editor::{scroll::Autoscroll, DisplayPoint, Editor};
use gpui::{
actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext,
VisualTestContext, WindowHandle,
@@ -1528,10 +1510,10 @@ mod tests {
.as_fake()
.insert_file("/root/a.txt", "changed".to_string())
.await;
editor
.condition::<EditorEvent>(cx, |editor, cx| editor.has_conflict(cx))
.await;
cx.run_until_parked();
cx.read(|cx| assert!(editor.is_dirty(cx)));
cx.read(|cx| assert!(editor.has_conflict(cx)));
let save_task = window
.update(cx, |workspace, cx| {

View File

@@ -1,52 +0,0 @@
[⬅ Back to Index](./index.md)
# Developing Zed's Backend
Zed's backend consists of the following components:
- The Zed.dev web site
- implemented in the [`zed.dev`](https://github.com/zed-industries/zed.dev) repository
- hosted on [Vercel](https://vercel.com/zed-industries/zed-dev).
- The Zed Collaboration server
- implemented in the [`crates/collab`](https://github.com/zed-industries/zed/tree/main/crates/collab) directory of the main `zed` repository
- hosted on [DigitalOcean](https://cloud.digitalocean.com/projects/6c680a82-9d3b-4f1a-91e5-63a6ca4a8611), using Kubernetes
- The Zed Postgres database
- defined via migrations in the [`crates/collab/migrations`](https://github.com/zed-industries/zed/tree/main/crates/collab/migrations) directory
- hosted on DigitalOcean
---
## Local Development
Here's some things you need to develop backend code locally.
### Dependencies
- **Postgres** - download [Postgres.app](https://postgresapp.com).
### Setup
1. Check out the `zed` and `zed.dev` repositories into a common parent directory
2. Set the `GITHUB_TOKEN` environment variable to one of your GitHub personal access tokens (PATs).
- You can create a PAT [here](https://github.com/settings/tokens).
- You may want to add something like this to your `~/.zshrc`:
```
export GITHUB_TOKEN=<the personal access token>
```
3. In the `zed.dev` directory, run `npm install` to install dependencies.
4. In the `zed directory`, run `script/bootstrap` to set up the database
5. In the `zed directory`, run `foreman start` to start both servers
---
## Production Debugging
### Datadog
Zed uses Datadog to collect metrics and logs from backend services. The Zed organization lives within Datadog's _US5_ [site](https://docs.datadoghq.com/getting_started/site/), so it can be accessed at [us5.datadoghq.com](https://us5.datadoghq.com). Useful things to look at in Datadog:
- The [Logs](https://us5.datadoghq.com/logs) page shows logs from Zed.dev and the Collab server, and the internals of Zed's Kubernetes cluster.
- The [collab metrics dashboard](https://us5.datadoghq.com/dashboard/y2d-gxz-h4h/collab?from_ts=1660517946462&to_ts=1660604346462&live=true) shows metrics about the running collab server

View File

@@ -1,104 +0,0 @@
[⬅ Back to Index](./index.md)
# Building Zed
How to build Zed from source for the first time.
## Prerequisites
- Be added to the GitHub organization
- Be added to the Vercel team
## Process
Expect this to take 30min to an hour! Some of these steps will take quite a while based on your connection speed, and how long your first build will be.
1. Install the [GitHub CLI](https://cli.github.com/):
- `brew install gh`
1. Clone the `zed` repo
- `gh repo clone zed-industries/zed`
1. Install Xcode from the macOS App Store
1. Install Xcode command line tools
- `xcode-select --install`
- If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.`
1. Install [Postgres](https://postgresapp.com)
1. Install rust/rustup
- `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
1. Install the wasm toolchain
- `rustup target add wasm32-wasi`
1. Install Livekit & Foreman
- `brew install livekit`
- `brew install foreman`
1. Generate an GitHub API Key
- Go to https://github.com/settings/tokens and Generate new token
- GitHub currently provides two kinds of tokens:
- Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected
Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories
- (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos
- Keep the token in the browser tab/editor for the next two steps
1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken`
1. Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies:
```
cd ..
git clone https://github.com/zed-industries/zed.dev
cd zed.dev && npm install
npm install -g vercel
```
1. Link your zed.dev project to Vercel
- `vercel link`
- Select the `zed-industries` team. If you don't have this get someone on the team to add you to it.
- Select the `zed.dev` project
1. Run `vercel pull` to pull down the environment variables and project info from Vercel
1. Open Postgres.app
1. From `./path/to/zed/`:
- Run:
- `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap`
- Replace `{yourGithubAPIToken}` with the API token you generated above.
- You don't need to include the GITHUB_TOKEN if you exported it above.
- Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault).
- If you get:
- ```bash
Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)!
Please create a new installation in /opt/homebrew using one of the
"Alternative Installs" from:
https://docs.brew.sh/Installation
```
- In that case try:
- `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
- If Homebrew is not in your PATH:
- Replace `{username}` with your home folder name (usually your login name)
- `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile`
- `eval "$(/opt/homebrew/bin/brew shellenv)"`
1. To run the Zed app:
- If you are working on zed:
- `cargo run`
- If you are just using the latest version, but not working on zed:
- `cargo run --release`
- If you need to run the collaboration server locally:
- `script/zed-local`
## Troubleshooting
### `error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`
- Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer`
### `xcrun: error: unable to find utility "metal", not a developer tool or in PATH`
### Seeding errors during `script/bootstrap` runs
```
seeding database...
thread 'main' panicked at 'failed to deserialize github user from 'https://api.github.com/orgs/zed-industries/teams/staff/members': reqwest::Error { kind: Decode, source: Error("invalid type: map, expected a sequence", line: 1, column: 0) }', crates/collab/src/bin/seed.rs:111:10
```
Wrong permissions for `GITHUB_TOKEN` token used, the token needs to be able to read from private repos.
For Classic GitHub Tokens, that required OAuth scope `repo` (seacrh the scope name above for more details)
Same command
`sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer`
### If you experience errors that mention some dependency is using unstable features
Try `cargo clean` and `cargo build`

View File

@@ -1,34 +0,0 @@
[⬅ Back to Index](./index.md)
# Company & Vision
## Vision
Our goal is to make Zed the primary tool software teams use to collaborate.
To do this, Zed will...
* Make collaboration a first-class feature of the code authoring environment.
* Enable text-based conversations about any piece of text, independent of whether/when it was committed to version control.
* Make it smooth to edit and discuss code with teammates in real time.
* Make it easy to recall past conversations any area of the code.
We believe the best way to make collaboration amazing is to build it into a new editor rather than retrofitting an existing editor. This means that in order for a team to adopt Zed for collaboration, each team member will need to adopt it as their editor as well.
For this reason, we need to deliver a clearly superior experience as a single-user code editor in addition to being an excellent collaboration tool. This will take time, but we believe the dominance of VS Code demonstrates that it's possible for a single tool to capture substantial market share. We can proceed incrementally, capturing one team at a time and gradually transitioning conversations away from GitHub.
## Zed Values
Everyone wants to work quickly and have a lot of users. What are we unwilling to sacrifice in pursuit of those goals?
- **Performance.** Speed is core to our brand and value proposition. It's important that we consistently deliver a response in less than 8ms on modern hardware for fine-grained actions. Coarse-grained actions should render feedback within 50ms. We consider the performance goals of the product at all times, and take the time to ensure our code meets them with reasonable usage. Once we have met our goals, we assess the impact vs effort of further performance investment and know when to say when. We measure our performance in the field and make an effort to maintain or improve real-world performance and promptly address regressions.
- **Craftsmanship.** Zed is a premium product, and we put care into design and user experience. We can always cut scope, but what we do ship should be quality. Incomplete is okay, so long as we're executing on a coherent subset well. Half-baked, unintuitive, or broken is not okay.
- **Shipping.** Knowledge matters only in as much as it drives results. We're here to build a real product in the real world. We care a lot about the experience of developing Zed, but we care about the user's experience more.
- **Code quality.** This enables craftsmanship. Nobody is creative in a trash heap, and we're willing to dedicate time to keep our codebase clean. If we're spending no time refactoring, we are likely underinvesting. When we realize a design flaw, we assess its centrality to the rest of the system and consider budgeting time to address it. If we're spending all of our time refactoring, we are likely either overinvesting or paying off debt from past underinvestment. It's up to each engineer to allocate a reasonable refactoring budget. We shouldn't be navel gazing, but we also shouldn't be afraid to invest.
- **Pairing.** Zed depends on regular pair programming to promote cohesion on our remote team. We believe pairing is a powerful substitute for beuracratic management, excessive documentation, and tedious code review. Nobody has to pair all day, every day, but everyone is responsible for pairing at least 2 hours a week with a variety of other engineers. If anyone wants to pair all day every day, that is explicitly endorsed and credited. If pairing temporarily reduces our throughput due to working on one thing instead of two, we trust that it will pay for itself in the long term by increasing our velocity and allowing us to more effectively grow our team.
- **Long-term thinking.** The Zed vision began several years ago, and we expect Zed to be around many years from today. We must always be mindful to avoid overengineering for the future, but we should also keep the long-term in mind. Are we building a system our future selves would want to work on in 5 years?

View File

@@ -1,74 +0,0 @@
[⬅ Back to Index](./index.md)
# Design Tools & Links
Generally useful tools and resources for design.
## General
[Names of Signs & Symbols](https://www.prepressure.com/fonts/basics/character-names#curlybrackets)
[The Noun Project](https://thenounproject.com/) - Icons for everything, attempts to describe all of human language visually.
[SVG Repo](https://www.svgrepo.com/) - Open-licensed SVG Vector and Icons
[Font Awsesome](https://fontawesome.com/) - High quality icons, has been around for many years.
---
## Color
[Opacity/Transparency Hex Values](https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4)
[Color Ramp Generator](https://lyft-colorbox.herokuapp.com)
[Designing a Comprehensive Color System
](https://www.rethinkhq.com/videos/designing-a-comprehensive-color-system-for-lyft) - [Linda Dong](https://twitter.com/lindadong)
---
## Figma & Plugins
[Figma Plugins for Designers](https://www.uiprep.com/blog/21-best-figma-plugins-for-designers-in-2021)
[Icon Resizer](https://www.figma.com/community/plugin/739117729229117975/Icon-Resizer)
[Code Syntax Highlighter](https://www.figma.com/community/plugin/938793197191698232/Code-Syntax-Highlighter)
[Proportional Scale](https://www.figma.com/community/plugin/756895186298946525/Proportional-Scale)
[LilGrid](https://www.figma.com/community/plugin/795397421598343178/LilGrid)
Organize your selection into a grid.
[Automator](https://www.figma.com/community/plugin/1005114571859948695/Automator)
Build photoshop-style batch actions to automate things.
[Figma Tokens](https://www.figma.com/community/plugin/843461159747178978/Figma-Tokens)
Use tokens in Figma and generate JSON from them.
---
## Design Systems
### Naming
[Naming Design Tokens](https://uxdesign.cc/naming-design-tokens-9454818ed7cb)
### Storybook
[Collaboration with design tokens and storybook](https://zure.com/blog/collaboration-with-design-tokens-and-storybook/)
### Example DS Documentation
[Tailwind CSS Documentation](https://tailwindcss.com/docs/container)
[Material Design Docs](https://material.io/design/color/the-color-system.html#color-usage-and-palettes)
[Carbon Design System Docs](https://www.carbondesignsystem.com)
[Adobe Spectrum](https://spectrum.adobe.com/)
- Great documentation, like [Color System](https://spectrum.adobe.com/page/color-system/) and [Design Tokens](https://spectrum.adobe.com/page/design-tokens/).
- A good place to start if thinking about building a design system.

View File

@@ -1,14 +0,0 @@
[⬅ Back to Index](./index.md)
# Welcome to Zed
Welcome! These internal docs are a work in progress. You can contribute to them by submitting a PR directly!
## Contents
- [The Company](./company-and-vision.md)
- [Tools We Use](./tools.md)
- [Building Zed](./building-zed.md)
- [Release Process](./release-process.md)
- [Backend Development](./backend-development.md)
- [Design Tools & Links](./design-tools.md)

View File

@@ -1,22 +0,0 @@
# Local Collaboration
## Setting up the local collaboration server
### Setting up for the first time?
1. Make sure you have livekit installed (`brew install livekit`)
1. Install [Postgres](https://postgresapp.com) and run it.
1. Then, from the root of the repo, run `script/bootstrap`.
### Have a db that is out of date? / Need to migrate?
1. Make sure you have livekit installed (`brew install livekit`)
1. Try `cd crates/collab && cargo run -- migrate` from the root of the repo.
1. Run `script/seed-db`
## Testing collab locally
1. Run `foreman start` from the root of the repo.
1. In another terminal run `script/zed-local`.
1. Two copies of Zed will open. Add yourself as a contact in the one that is not you.
1. Start a collaboration session as normal with any open project.

View File

@@ -1,100 +0,0 @@
[⬅ Back to Index](./index.md)
# Zed's Release Process
The process to create and ship a Zed release
## Overview
### Release Channels
Users of Zed can choose between two _release channels_ - 'Stable' and 'Preview'. Most people use Stable, but Preview exists so that the Zed team and other early-adopters can test new features before they are released to our general user-base.
### Weekly (Minor) Releases
We normally publish new releases of Zed on Wednesdays, for both the Stable and Preview channels. For each of these releases, we bump Zed's _minor_ version number.
For the Preview channel, we build the new release based on what's on the `main` branch. For the Stable channel, we build the new release based on the last Preview release.
### Hotfix (Patch) Releases
When we find a _regression_ in Zed (a bug that wasn't present in an earlier version), or find a significant bug in a newly-released feature, we typically publish a hotfix release. For these releases, we bump Zed's _patch_ version number.
### Server Deployments
Often, changes in the Zed app require corresponding changes in the `collab` server. At the currente stage of our copmany, we don't attempt to keep our server backwards-compatible with older versions of the app. Instead, when making a change, we simply bump Zed's _protocol version_ number (in the `rpc` crate), which causes the server to recognize that it isn't compatible with earlier versions of the Zed app.
This means that when releasing a new version of Zed that has changes to the RPC protocol, we need to deploy a new version of the `collab` server at the same time.
## Instructions
### Publishing a Minor Release
1. Announce your intent to publish a new version in Discord. This gives other people a chance to raise concerns or postpone the release if they want to get something merged before publishing a new version.
1. Open your terminal and `cd` into your local copy of Zed. Checkout `main` and perform a `git pull` to ensure you have the latest version.
1. Run the following command, which will update two git branches and two git tags (one for each release channel):
```
script/bump-zed-minor-versions
```
1. The script will make local changes only, and print out a shell command that you can use to push all of these branches and tags.
1. Pushing the two new tags will trigger two CI builds that, when finished, will create two draft releases (Stable and Preview) containing `Zed.dmg` files.
1. Now you need to write the release notes for the Stable and Preview releases. For the Stable release, you can just copy the release notes from the last week's Preview release, plus any hotfixes that were published on the Preview channel since then. Some of the hotfixes may not be relevant for the Stable release notes, if they were fixing bugs that were only present in Preview.
1. For the Preview release, you can retrieve the list of changes by running this command (make sure you have at least `Node 18` installed):
```
GITHUB_ACCESS_TOKEN=your_access_token script/get-preview-channel-changes
```
1. The script will list all the merged pull requests and you can use it as a reference to write the release notes. If there were protocol changes, it will also emit a warning.
1. Once CI creates the draft releases, add each release's notes and save the drafts.
1. If there have been server-side changes since the last release, you'll need to re-deploy the `collab` server. See below.
1. Before publishing, download the Zed.dmg and smoke test it to ensure everything looks good.
### Publishing a Patch Release
1. Announce your intent to publish a new patch version in Discord.
1. Open your terminal and `cd` into your local copy of Zed. Check out the branch corresponding to the release channel where the fix is needed. For example, if the fix is for a bug in Stable, and the current stable version is `0.63.0`, then checkout the branch `v0.63.x`. Run `git pull` to ensure your branch is up-to-date.
1. Find the merge commit where your bug-fix landed on `main`. You can browse the merged pull requests on main by running `git log main --grep Merge`.
1. Cherry-pick those commits onto the current release branch:
```
git cherry-pick -m1 <THE-COMMIT-SHA>
```
1. Run the following command, which will bump the version of Zed and create a new tag:
```
script/bump-zed-patch-version
```
1. The script will make local changes only, and print out a shell command that you can use to push all the branch and tag.
1. Pushing the new tag will trigger a CI build that, when finished, will create a draft release containing a `Zed.dmg` file.
1. Once the draft release is created, fill in the release notes based on the bugfixes that you cherry-picked.
1. If any of the bug-fixes require server-side changes, you'll need to re-deploy the `collab` server. See below.
1. Before publishing, download the Zed.dmg and smoke test it to ensure everything looks good.
1. Clicking publish on the release will cause old Zed instances to auto-update and the Zed.dev releases page to re-build and display the new release.
### Deploying the Server
1. Deploying the server is a two-step process that begins with pushing a tag. 1. Check out the commit you'd like to deploy. Often it will be the head of `main`, but could be on any branch.
1. Run the following script, which will bump the version of the `collab` crate and create a new tag. The script takes an argument `minor` or `patch`, to indicate how to increment the version. If you're releasing new features, use `minor`. If it's just a bugfix, use `patch`
```
script/bump-collab-version patch
```
1. This script will make local changes only, and print out a shell command that you can use to push the branch and tag.
1. Pushing the new tag will trigger a CI build that, when finished will upload a new versioned docker image to the DigitalOcean docker registry.
1. If needing a migration:
- First check that the migration is valid. The database serves both preview and stable simultaneously, so new columns need to have defaults and old tables or columns can't be dropped.
- Then use `script/deploy-migration` <release channel> <version number> (production, staging, preview, nightly). ex: `script/deploy-migration preview 0.19.0`
- If there is an 'Error: container is waiting to start', you can review logs manually with: `kubectl --namespace <environment> logs <pod name>` to make sure the mgiration ran successfully.
1. Once that CI job completes, you will be able to run the following command to deploy that docker image. The script takes two arguments: an environment (`production`, `preview`, or `staging`), and a version number (e.g. `0.10.1`):
```
script/deploy preview 0.10.1
```
1. This command should complete quickly, updating the given environment to use the given version number of the `collab` server.

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