Compare commits
77 Commits
v0.119.18
...
collab-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd25902aeb | ||
|
|
fc2f5d86c7 | ||
|
|
fbdca993ff | ||
|
|
adb6f3e9f7 | ||
|
|
ca27ac21c2 | ||
|
|
7068161bd7 | ||
|
|
2b844f5cb5 | ||
|
|
50c3ad963e | ||
|
|
e13fb31287 | ||
|
|
0a78c67647 | ||
|
|
062288dea5 | ||
|
|
dd3ec15acc | ||
|
|
d17d37ff61 | ||
|
|
710e47977d | ||
|
|
c6e7cf1cbc | ||
|
|
dcf05812c2 | ||
|
|
0c4679f892 | ||
|
|
dd07d2f8a2 | ||
|
|
1c2859d72b | ||
|
|
01424a62ea | ||
|
|
5fcc75be1f | ||
|
|
cf3b2ba146 | ||
|
|
e77db87bad | ||
|
|
f9170cb239 | ||
|
|
bf7489269e | ||
|
|
ba97661e1c | ||
|
|
320088f1fa | ||
|
|
df420c3767 | ||
|
|
4bcd3494b7 | ||
|
|
865369882e | ||
|
|
a181dc8d58 | ||
|
|
249a6da54a | ||
|
|
f185aca25a | ||
|
|
6ed7cc7833 | ||
|
|
90c1d8f734 | ||
|
|
416696a686 | ||
|
|
10437794e4 | ||
|
|
569bb687be | ||
|
|
9bc968eabb | ||
|
|
4ac3095a15 | ||
|
|
5907bb5b55 | ||
|
|
28b2c89254 | ||
|
|
cf3b4b0ba7 | ||
|
|
71ec781215 | ||
|
|
e1b7b5eaa6 | ||
|
|
40dbe15b2a | ||
|
|
6c555fe13c | ||
|
|
334dc620ea | ||
|
|
1a11da916b | ||
|
|
fc01eeebbc | ||
|
|
fde4c09906 | ||
|
|
482c01aaf3 | ||
|
|
db33eafdb1 | ||
|
|
855e0f6f36 | ||
|
|
c56debc705 | ||
|
|
e7db5d0638 | ||
|
|
a860ca6a3c | ||
|
|
4427e7968b | ||
|
|
2a11c22760 | ||
|
|
6285decfa2 | ||
|
|
4599fa840d | ||
|
|
da01c1a83b | ||
|
|
a7368904f3 | ||
|
|
ad537f638c | ||
|
|
e072c96003 | ||
|
|
9693e14809 | ||
|
|
291f353085 | ||
|
|
08d2ba72d6 | ||
|
|
c5ad1728f9 | ||
|
|
85f5e7d0bb | ||
|
|
fd6f71d287 | ||
|
|
c1df166700 | ||
|
|
c81d318098 | ||
|
|
f8604e88ef | ||
|
|
489ef23b76 | ||
|
|
a4897e00b4 | ||
|
|
4e085b2052 |
24
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
Normal 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
|
||||
47
.github/ISSUE_TEMPLATE/1_language_support.yml
vendored
Normal file
47
.github/ISSUE_TEMPLATE/1_language_support.yml
vendored
Normal 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
38
.github/ISSUE_TEMPLATE/2_bug_report.yml
vendored
Normal 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
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -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>)).
|
||||
|
||||
17
.github/workflows/update_top_ranking_issues.yml
vendored
Normal file
17
.github/workflows/update_top_ranking_issues.yml
vendored
Normal 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
2
.gitignore
vendored
@@ -25,3 +25,5 @@ DerivedData/
|
||||
.netrc
|
||||
.swiftpm
|
||||
**/*.db
|
||||
.pytest_cache
|
||||
.venv
|
||||
|
||||
5
.mailmap
5
.mailmap
@@ -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>
|
||||
|
||||
@@ -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
241
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2959,6 +2959,7 @@ impl InlineAssistant {
|
||||
cx.prompt(
|
||||
PromptLevel::Info,
|
||||
prompt_text.as_str(),
|
||||
None,
|
||||
&["Continue", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -29,4 +29,4 @@ serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tempdir.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>>) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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, |_| {});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
29
crates/gpui/examples/hello_world.rs
Normal file
29
crates/gpui/examples/hello_world.rs
Normal 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(),
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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) }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
223
crates/rpc/src/error.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
172
crates/search/src/buffer_search/registrar.rs
Normal file
172
crates/search/src/buffer_search/registrar.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -2634,7 +2634,7 @@ impl Default for LineEnding {
|
||||
return Self::Unix;
|
||||
|
||||
#[cfg(not(unix))]
|
||||
return Self::CRLF;
|
||||
return Self::Windows;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
stable
|
||||
dev
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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())])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl PythonLspAdapter {
|
||||
|
||||
#[async_trait]
|
||||
impl LspAdapter for PythonLspAdapter {
|
||||
async fn name(&self) -> LanguageServerName {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("pyright".into())
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "Svelte"
|
||||
path_suffixes = ["svelte"]
|
||||
line_comment = "// "
|
||||
block_comment = ["<!-- ", " -->"]
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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?
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
Reference in New Issue
Block a user