Compare commits
129 Commits
v0.119.20
...
collab-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5db7e8f89e | ||
|
|
1761e60362 | ||
|
|
0a124a9908 | ||
|
|
f5f7be1500 | ||
|
|
c9c9a6b11b | ||
|
|
8bc105ca1d | ||
|
|
ac05853f2c | ||
|
|
b5c64a128e | ||
|
|
22034d2083 | ||
|
|
08de0d88b1 | ||
|
|
c6d33d4bb9 | ||
|
|
3c3cdecdff | ||
|
|
f537be4704 | ||
|
|
8da5e57d6d | ||
|
|
47860a5dc8 | ||
|
|
14322c8365 | ||
|
|
3847762d8c | ||
|
|
802405f6bc | ||
|
|
252e7e9467 | ||
|
|
8d2a401f09 | ||
|
|
698108ac8b | ||
|
|
0aa1b1f070 | ||
|
|
c4cf5f2b2c | ||
|
|
f7a2118b15 | ||
|
|
abdf302367 | ||
|
|
4b672621d3 | ||
|
|
1734403aa3 | ||
|
|
742329ee8f | ||
|
|
ebbfff5ce8 | ||
|
|
204af431e0 | ||
|
|
d519fc6b02 | ||
|
|
e5b71cc6ac | ||
|
|
37647a67a7 | ||
|
|
9820895284 | ||
|
|
81aac492bd | ||
|
|
50b9e5d8d2 | ||
|
|
716221cd38 | ||
|
|
20c90f07e1 | ||
|
|
039ef1ad5a | ||
|
|
b72c037199 | ||
|
|
9fd0938eb1 | ||
|
|
b5fc91e455 | ||
|
|
a3d431d86f | ||
|
|
43060b2d13 | ||
|
|
0457ad3f6e | ||
|
|
006e003698 | ||
|
|
b5fa5beee4 | ||
|
|
6103f67875 | ||
|
|
3d898c562e | ||
|
|
e9edad1d51 | ||
|
|
dd25902aeb | ||
|
|
fc2f5d86c7 | ||
|
|
fbdca993ff | ||
|
|
adb6f3e9f7 | ||
|
|
cff2e8bbe0 | ||
|
|
ca27ac21c2 | ||
|
|
7068161bd7 | ||
|
|
2b844f5cb5 | ||
|
|
50c3ad963e | ||
|
|
e13fb31287 | ||
|
|
0a78c67647 | ||
|
|
062288dea5 | ||
|
|
dd3ec15acc | ||
|
|
d17d37ff61 | ||
|
|
710e47977d | ||
|
|
c6e7cf1cbc | ||
|
|
dcf05812c2 | ||
|
|
0c4679f892 | ||
|
|
dd07d2f8a2 | ||
|
|
5c1de4ce26 | ||
|
|
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
|
||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -1,5 +1,5 @@
|
||||
[[PR Description]]
|
||||
|
||||
|
||||
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>)).
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
2
.github/workflows/publish_collab_image.yml
vendored
2
.github/workflows/publish_collab_image.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
run: docker system prune
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
2
.github/workflows/randomized_tests.yml
vendored
2
.github/workflows/randomized_tests.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
6
.github/workflows/release_nightly.yml
vendored
6
.github/workflows/release_nightly.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
needs: style
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
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@v4
|
||||
- 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.
|
||||
|
||||
@@ -37,8 +37,9 @@ We plan to set aside time each week to pair program with contributors on promisi
|
||||
- Add test coverage and documentation
|
||||
- Choose tasks that align with our roadmap
|
||||
- Pair with us and watch us code to learn the codebase
|
||||
- Low effort PRs, such as those that just re-arrange syntax, won't be merged without a compelling justification
|
||||
|
||||
## Bird-eye's view of Zed
|
||||
## Bird's-eye view of Zed
|
||||
|
||||
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
|
||||
|
||||
|
||||
509
Cargo.lock
generated
509
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -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" }
|
||||
@@ -140,6 +140,7 @@ tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir"
|
||||
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40"}
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
|
||||
tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
|
||||
@@ -160,18 +161,12 @@ tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", re
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa"}
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"}
|
||||
tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
|
||||
tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
|
||||
|
||||
[patch.crates-io]
|
||||
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"
|
||||
|
||||
@@ -447,6 +447,10 @@
|
||||
//
|
||||
"lsp": "elixir_ls"
|
||||
},
|
||||
// Settings specific to our deno integration
|
||||
"deno": {
|
||||
"enable": false
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,11 +61,12 @@ impl ChannelBuffer {
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let buffer = cx.new_model(|_| {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let capability = channel_store.read(cx).channel_capability(channel.id);
|
||||
language::Buffer::remote(
|
||||
response.buffer_id,
|
||||
response.replica_id as u16,
|
||||
channel.channel_buffer_capability(),
|
||||
capability,
|
||||
base_text,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -13,11 +13,11 @@ use gpui::{
|
||||
};
|
||||
use language::Capability;
|
||||
use rpc::{
|
||||
proto::{self, ChannelVisibility},
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
TypedEnvelope,
|
||||
};
|
||||
use std::{mem, sync::Arc, time::Duration};
|
||||
use util::{async_maybe, ResultExt};
|
||||
use util::{async_maybe, maybe, ResultExt};
|
||||
|
||||
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
let channel_store =
|
||||
@@ -29,33 +29,47 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub type ChannelId = u64;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct NotesVersion {
|
||||
epoch: u64,
|
||||
version: clock::Global,
|
||||
}
|
||||
|
||||
pub struct ChannelStore {
|
||||
pub channel_index: ChannelIndex,
|
||||
channel_invitations: Vec<Arc<Channel>>,
|
||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||
channel_states: HashMap<ChannelId, ChannelState>,
|
||||
|
||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
||||
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
_rpc_subscription: Subscription,
|
||||
_rpc_subscriptions: [Subscription; 2],
|
||||
_watch_connection_status: Task<Option<()>>,
|
||||
disconnect_channel_buffers_task: Option<Task<()>>,
|
||||
_update_channels: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: SharedString,
|
||||
pub visibility: proto::ChannelVisibility,
|
||||
pub role: proto::ChannelRole,
|
||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||
pub unseen_message_id: Option<u64>,
|
||||
pub parent_path: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChannelState {
|
||||
latest_chat_message: Option<u64>,
|
||||
latest_notes_versions: Option<NotesVersion>,
|
||||
observed_chat_message: Option<u64>,
|
||||
observed_notes_versions: Option<NotesVersion>,
|
||||
role: Option<ChannelRole>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn link(&self) -> String {
|
||||
RELEASE_CHANNEL.link_prefix().to_owned()
|
||||
@@ -65,6 +79,17 @@ impl Channel {
|
||||
+ &self.id.to_string()
|
||||
}
|
||||
|
||||
pub fn is_root_channel(&self) -> bool {
|
||||
self.parent_path.is_empty()
|
||||
}
|
||||
|
||||
pub fn root_id(&self) -> ChannelId {
|
||||
self.parent_path
|
||||
.first()
|
||||
.map(|id| *id as ChannelId)
|
||||
.unwrap_or(self.id)
|
||||
}
|
||||
|
||||
pub fn slug(&self) -> String {
|
||||
let slug: String = self
|
||||
.name
|
||||
@@ -74,14 +99,6 @@ impl Channel {
|
||||
|
||||
slug.trim_matches(|c| c == '-').to_string()
|
||||
}
|
||||
|
||||
pub fn channel_buffer_capability(&self) -> Capability {
|
||||
if self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin {
|
||||
Capability::ReadWrite
|
||||
} else {
|
||||
Capability::ReadOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChannelMembership {
|
||||
@@ -100,8 +117,7 @@ impl ChannelMembership {
|
||||
},
|
||||
kind_order: match self.kind {
|
||||
proto::channel_member::Kind::Member => 0,
|
||||
proto::channel_member::Kind::AncestorMember => 1,
|
||||
proto::channel_member::Kind::Invitee => 2,
|
||||
proto::channel_member::Kind::Invitee => 1,
|
||||
},
|
||||
username_order: self.user.github_login.as_str(),
|
||||
}
|
||||
@@ -137,8 +153,10 @@ impl ChannelStore {
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let rpc_subscription =
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_channels);
|
||||
let rpc_subscriptions = [
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_channels),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels),
|
||||
];
|
||||
|
||||
let mut connection_status = client.status();
|
||||
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
|
||||
@@ -175,7 +193,7 @@ impl ChannelStore {
|
||||
update_channels_tx,
|
||||
client,
|
||||
user_store,
|
||||
_rpc_subscription: rpc_subscription,
|
||||
_rpc_subscriptions: rpc_subscriptions,
|
||||
_watch_connection_status: watch_connection_status,
|
||||
disconnect_channel_buffers_task: None,
|
||||
_update_channels: cx.spawn(|this, mut cx| async move {
|
||||
@@ -195,6 +213,7 @@ impl ChannelStore {
|
||||
.await
|
||||
.log_err();
|
||||
}),
|
||||
channel_states: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,39 +325,16 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_states
|
||||
.get(&channel_id)
|
||||
.map(|channel| channel.unseen_note_version.is_some())
|
||||
.is_some_and(|state| state.has_channel_buffer_changed())
|
||||
}
|
||||
|
||||
pub fn has_new_messages(&self, channel_id: ChannelId) -> Option<bool> {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
pub fn has_new_messages(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_states
|
||||
.get(&channel_id)
|
||||
.map(|channel| channel.unseen_message_id.is_some())
|
||||
}
|
||||
|
||||
pub fn notes_changed(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index.note_changed(channel_id, epoch, version);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn new_message(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index.new_message(channel_id, message_id);
|
||||
cx.notify();
|
||||
.is_some_and(|state| state.has_new_messages())
|
||||
}
|
||||
|
||||
pub fn acknowledge_message_id(
|
||||
@@ -347,8 +343,23 @@ impl ChannelStore {
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index
|
||||
.acknowledge_message_id(channel_id, message_id);
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_insert_with(|| Default::default())
|
||||
.acknowledge_message_id(message_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn update_latest_message_id(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_insert_with(|| Default::default())
|
||||
.update_latest_message_id(message_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -359,9 +370,25 @@ impl ChannelStore {
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index
|
||||
.acknowledge_note_version(channel_id, epoch, version);
|
||||
cx.notify();
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_insert_with(|| Default::default())
|
||||
.acknowledge_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn update_latest_notes_version(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_insert_with(|| Default::default())
|
||||
.update_latest_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn open_channel_chat(
|
||||
@@ -454,10 +481,42 @@ impl ChannelStore {
|
||||
}
|
||||
|
||||
pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
|
||||
let Some(channel) = self.channel_for_id(channel_id) else {
|
||||
return false;
|
||||
};
|
||||
channel.role == proto::ChannelRole::Admin
|
||||
self.channel_role(channel_id) == proto::ChannelRole::Admin
|
||||
}
|
||||
|
||||
pub fn is_root_channel(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
.get(&channel_id)
|
||||
.map_or(false, |channel| channel.is_root_channel())
|
||||
}
|
||||
|
||||
pub fn is_public_channel(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
.get(&channel_id)
|
||||
.map_or(false, |channel| {
|
||||
channel.visibility == ChannelVisibility::Public
|
||||
})
|
||||
}
|
||||
|
||||
pub fn channel_capability(&self, channel_id: ChannelId) -> Capability {
|
||||
match self.channel_role(channel_id) {
|
||||
ChannelRole::Admin | ChannelRole::Member => Capability::ReadWrite,
|
||||
_ => Capability::ReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_role(&self, channel_id: ChannelId) -> proto::ChannelRole {
|
||||
maybe!({
|
||||
let mut channel = self.channel_for_id(channel_id)?;
|
||||
if !channel.is_root_channel() {
|
||||
channel = self.channel_for_id(channel.root_id())?;
|
||||
}
|
||||
let root_channel_state = self.channel_states.get(&channel.id);
|
||||
root_channel_state?.role
|
||||
})
|
||||
.unwrap_or(proto::ChannelRole::Guest)
|
||||
}
|
||||
|
||||
pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
|
||||
@@ -508,7 +567,7 @@ impl ChannelStore {
|
||||
pub fn move_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
to: Option<ChannelId>,
|
||||
to: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
@@ -747,6 +806,36 @@ impl ChannelStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_update_user_channels(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::UpdateUserChannels>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for buffer_version in message.payload.observed_channel_buffer_version {
|
||||
let version = language::proto::deserialize_version(&buffer_version.version);
|
||||
this.acknowledge_notes_version(
|
||||
buffer_version.channel_id,
|
||||
buffer_version.epoch,
|
||||
&version,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
for message_id in message.payload.observed_channel_message_id {
|
||||
this.acknowledge_message_id(message_id.channel_id, message_id.message_id, cx);
|
||||
}
|
||||
for membership in message.payload.channel_memberships {
|
||||
if let Some(role) = ChannelRole::from_i32(membership.role) {
|
||||
this.channel_states
|
||||
.entry(membership.channel_id)
|
||||
.or_insert_with(|| ChannelState::default())
|
||||
.set_role(role)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
self.channel_index.clear();
|
||||
self.channel_invitations.clear();
|
||||
@@ -909,10 +998,7 @@ impl ChannelStore {
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility(),
|
||||
role: channel.role(),
|
||||
name: channel.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel.parent_path,
|
||||
}),
|
||||
),
|
||||
@@ -921,8 +1007,8 @@ impl ChannelStore {
|
||||
|
||||
let channels_changed = !payload.channels.is_empty()
|
||||
|| !payload.delete_channels.is_empty()
|
||||
|| !payload.unseen_channel_messages.is_empty()
|
||||
|| !payload.unseen_channel_buffer_changes.is_empty();
|
||||
|| !payload.latest_channel_message_ids.is_empty()
|
||||
|| !payload.latest_channel_buffer_versions.is_empty();
|
||||
|
||||
if channels_changed {
|
||||
if !payload.delete_channels.is_empty() {
|
||||
@@ -963,20 +1049,19 @@ impl ChannelStore {
|
||||
}
|
||||
}
|
||||
|
||||
for unseen_buffer_change in payload.unseen_channel_buffer_changes {
|
||||
let version = language::proto::deserialize_version(&unseen_buffer_change.version);
|
||||
index.note_changed(
|
||||
unseen_buffer_change.channel_id,
|
||||
unseen_buffer_change.epoch,
|
||||
&version,
|
||||
);
|
||||
for latest_buffer_version in payload.latest_channel_buffer_versions {
|
||||
let version = language::proto::deserialize_version(&latest_buffer_version.version);
|
||||
self.channel_states
|
||||
.entry(latest_buffer_version.channel_id)
|
||||
.or_default()
|
||||
.update_latest_notes_version(latest_buffer_version.epoch, &version)
|
||||
}
|
||||
|
||||
for unseen_channel_message in payload.unseen_channel_messages {
|
||||
index.new_messages(
|
||||
unseen_channel_message.channel_id,
|
||||
unseen_channel_message.message_id,
|
||||
);
|
||||
for latest_channel_message in payload.latest_channel_message_ids {
|
||||
self.channel_states
|
||||
.entry(latest_channel_message.channel_id)
|
||||
.or_default()
|
||||
.update_latest_message_id(latest_channel_message.message_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,3 +1110,69 @@ impl ChannelStore {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelState {
|
||||
fn set_role(&mut self, role: ChannelRole) {
|
||||
self.role = Some(role);
|
||||
}
|
||||
|
||||
fn has_channel_buffer_changed(&self) -> bool {
|
||||
if let Some(latest_version) = &self.latest_notes_versions {
|
||||
if let Some(observed_version) = &self.observed_notes_versions {
|
||||
latest_version.epoch > observed_version.epoch
|
||||
|| latest_version
|
||||
.version
|
||||
.changed_since(&observed_version.version)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn has_new_messages(&self) -> bool {
|
||||
let latest_message_id = self.latest_chat_message;
|
||||
let observed_message_id = self.observed_chat_message;
|
||||
|
||||
latest_message_id.is_some_and(|latest_message_id| {
|
||||
latest_message_id > observed_message_id.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
fn acknowledge_message_id(&mut self, message_id: u64) {
|
||||
let observed = self.observed_chat_message.get_or_insert(message_id);
|
||||
*observed = (*observed).max(message_id);
|
||||
}
|
||||
|
||||
fn update_latest_message_id(&mut self, message_id: u64) {
|
||||
self.latest_chat_message =
|
||||
Some(message_id.max(self.latest_chat_message.unwrap_or_default()));
|
||||
}
|
||||
|
||||
fn acknowledge_notes_version(&mut self, epoch: u64, version: &clock::Global) {
|
||||
if let Some(existing) = &mut self.observed_notes_versions {
|
||||
if existing.epoch == epoch {
|
||||
existing.version.join(version);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.observed_notes_versions = Some(NotesVersion {
|
||||
epoch,
|
||||
version: version.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn update_latest_notes_version(&mut self, epoch: u64, version: &clock::Global) {
|
||||
if let Some(existing) = &mut self.latest_notes_versions {
|
||||
if existing.epoch == epoch {
|
||||
existing.version.join(version);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.latest_notes_versions = Some(NotesVersion {
|
||||
epoch,
|
||||
version: version.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,43 +37,6 @@ impl ChannelIndex {
|
||||
channels_by_id: &mut self.channels_by_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_note_version(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
) {
|
||||
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
|
||||
let channel = Arc::make_mut(channel);
|
||||
if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version {
|
||||
if epoch > *unseen_epoch
|
||||
|| epoch == *unseen_epoch && version.observed_all(unseen_version)
|
||||
{
|
||||
channel.unseen_note_version = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
|
||||
let channel = Arc::make_mut(channel);
|
||||
if let Some(unseen_message_id) = channel.unseen_message_id {
|
||||
if message_id >= unseen_message_id {
|
||||
channel.unseen_message_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
|
||||
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version);
|
||||
}
|
||||
|
||||
pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A guard for ensuring that the paths index maintains its sort and uniqueness
|
||||
@@ -85,36 +48,25 @@ pub struct ChannelPathsInsertGuard<'a> {
|
||||
}
|
||||
|
||||
impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
|
||||
insert_note_changed(self.channels_by_id, channel_id, epoch, version);
|
||||
}
|
||||
|
||||
pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
insert_new_message(self.channels_by_id, channel_id, message_id)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
|
||||
let mut ret = false;
|
||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||
let existing_channel = Arc::make_mut(existing_channel);
|
||||
|
||||
ret = existing_channel.visibility != channel_proto.visibility()
|
||||
|| existing_channel.role != channel_proto.role()
|
||||
|| existing_channel.name != channel_proto.name;
|
||||
|| existing_channel.name != channel_proto.name
|
||||
|| existing_channel.parent_path != channel_proto.parent_path;
|
||||
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
existing_channel.role = channel_proto.role();
|
||||
existing_channel.name = channel_proto.name.into();
|
||||
existing_channel.parent_path = channel_proto.parent_path.into();
|
||||
} else {
|
||||
self.channels_by_id.insert(
|
||||
channel_proto.id,
|
||||
Arc::new(Channel {
|
||||
id: channel_proto.id,
|
||||
visibility: channel_proto.visibility(),
|
||||
role: channel_proto.role(),
|
||||
name: channel_proto.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel_proto.parent_path,
|
||||
}),
|
||||
);
|
||||
@@ -153,32 +105,3 @@ fn channel_path_sorting_key<'a>(
|
||||
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
|
||||
.chain(name)
|
||||
}
|
||||
|
||||
fn insert_note_changed(
|
||||
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
|
||||
channel_id: u64,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
) {
|
||||
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
|
||||
let unseen_version = Arc::make_mut(channel)
|
||||
.unseen_note_version
|
||||
.get_or_insert((0, clock::Global::new()));
|
||||
if epoch > unseen_version.0 {
|
||||
*unseen_version = (epoch, version.clone());
|
||||
} else {
|
||||
unseen_version.1.join(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_new_message(
|
||||
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
|
||||
channel_id: u64,
|
||||
message_id: u64,
|
||||
) {
|
||||
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
|
||||
let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0);
|
||||
*unseen_message_id = message_id.max(*unseen_message_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,12 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: Vec::new(),
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
parent_path: Vec::new(),
|
||||
},
|
||||
],
|
||||
@@ -38,8 +36,8 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string(), proto::ChannelRole::Member),
|
||||
(0, "b".to_string(), proto::ChannelRole::Admin),
|
||||
(0, "a".to_string()),
|
||||
(0, "b".to_string()),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -52,14 +50,12 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
id: 3,
|
||||
name: "x".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: vec![1],
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
name: "y".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
parent_path: vec![2],
|
||||
},
|
||||
],
|
||||
@@ -70,10 +66,10 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
(0, "a".to_string(), proto::ChannelRole::Member),
|
||||
(1, "y".to_string(), proto::ChannelRole::Member),
|
||||
(0, "b".to_string(), proto::ChannelRole::Admin),
|
||||
(1, "x".to_string(), proto::ChannelRole::Admin),
|
||||
(0, "a".to_string()),
|
||||
(1, "y".to_string()),
|
||||
(0, "b".to_string()),
|
||||
(1, "x".to_string()),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -91,21 +87,18 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
id: 0,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: vec![],
|
||||
},
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: vec![0],
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "c".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: vec![0, 1],
|
||||
},
|
||||
],
|
||||
@@ -118,9 +111,9 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string(), proto::ChannelRole::Admin),
|
||||
(1, "b".to_string(), proto::ChannelRole::Admin),
|
||||
(2, "c".to_string(), proto::ChannelRole::Admin),
|
||||
(0, "a".to_string()),
|
||||
(1, "b".to_string()),
|
||||
(2, "c".to_string()),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -135,11 +128,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Make sure that the 1/2/3 path is gone
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[(0, "a".to_string(), proto::ChannelRole::Admin)],
|
||||
cx,
|
||||
);
|
||||
assert_channels(&channel_store, &[(0, "a".to_string())], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -156,18 +145,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
parent_path: vec![],
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.update(|cx| {
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[(0, "the-channel".to_string(), proto::ChannelRole::Member)],
|
||||
cx,
|
||||
);
|
||||
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
|
||||
});
|
||||
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
@@ -368,13 +352,13 @@ fn update_channels(
|
||||
#[track_caller]
|
||||
fn assert_channels(
|
||||
channel_store: &Model<ChannelStore>,
|
||||
expected_channels: &[(usize, String, proto::ChannelRole)],
|
||||
expected_channels: &[(usize, String)],
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let actual = channel_store.update(cx, |store, _| {
|
||||
store
|
||||
.ordered_channels()
|
||||
.map(|(depth, channel)| (depth, channel.name.to_string(), channel.role))
|
||||
.map(|(depth, channel)| (depth, channel.name.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(actual, expected_channels);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ impl UserStore {
|
||||
load_users.await?;
|
||||
|
||||
// Users are fetched in parallel above and cached in call to get_users
|
||||
// No need to paralellize here
|
||||
// No need to parallelize here
|
||||
let mut updated_contacts = Vec::new();
|
||||
let this = this
|
||||
.upgrade()
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.40.1"
|
||||
version = "0.42.0"
|
||||
publish = false
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tables::*;
|
||||
pub use tables::*;
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
|
||||
pub use ids::*;
|
||||
@@ -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.",
|
||||
@@ -453,36 +502,6 @@ pub struct NewUserResult {
|
||||
pub signup_device_id: Option<String>,
|
||||
}
|
||||
|
||||
/// The result of moving a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct MoveChannelResult {
|
||||
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
|
||||
pub participants_to_remove: HashSet<UserId>,
|
||||
pub moved_channels: HashSet<ChannelId>,
|
||||
}
|
||||
|
||||
/// The result of renaming a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct RenameChannelResult {
|
||||
pub channel: Channel,
|
||||
pub participants_to_update: HashMap<UserId, Channel>,
|
||||
}
|
||||
|
||||
/// The result of creating a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct CreateChannelResult {
|
||||
pub channel: Channel,
|
||||
pub participants_to_update: Vec<(UserId, ChannelsForUser)>,
|
||||
}
|
||||
|
||||
/// The result of setting a channel's visibility.
|
||||
#[derive(Debug)]
|
||||
pub struct SetChannelVisibilityResult {
|
||||
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
|
||||
pub participants_to_remove: HashSet<UserId>,
|
||||
pub channels_to_remove: Vec<ChannelId>,
|
||||
}
|
||||
|
||||
/// The result of updating a channel membership.
|
||||
#[derive(Debug)]
|
||||
pub struct MembershipUpdated {
|
||||
@@ -522,18 +541,16 @@ pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
pub role: ChannelRole,
|
||||
/// parent_path is the channel ids from the root to this one (not including this one)
|
||||
pub parent_path: Vec<ChannelId>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn from_model(value: channel::Model, role: ChannelRole) -> Self {
|
||||
fn from_model(value: channel::Model) -> Self {
|
||||
Channel {
|
||||
id: value.id,
|
||||
visibility: value.visibility,
|
||||
name: value.clone().name,
|
||||
role,
|
||||
parent_path: value.ancestors().collect(),
|
||||
}
|
||||
}
|
||||
@@ -543,7 +560,6 @@ impl Channel {
|
||||
id: self.id.to_proto(),
|
||||
name: self.name.clone(),
|
||||
visibility: self.visibility.into(),
|
||||
role: self.role.into(),
|
||||
parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
|
||||
}
|
||||
}
|
||||
@@ -569,9 +585,10 @@ impl ChannelMember {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ChannelsForUser {
|
||||
pub channels: Vec<Channel>,
|
||||
pub channel_memberships: Vec<channel_member::Model>,
|
||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||
pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
|
||||
pub channel_messages: Vec<proto::UnseenChannelMessage>,
|
||||
pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
||||
pub latest_channel_messages: Vec<proto::ChannelMessageId>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -129,6 +129,15 @@ impl ChannelRole {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_see_channel(&self, visibility: ChannelVisibility) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest => visibility == ChannelVisibility::Public,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the role allows access to all descendant channels
|
||||
pub fn can_see_all_descendants(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
|
||||
@@ -748,18 +748,11 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unseen_channel_buffer_changes(
|
||||
pub async fn latest_channel_buffer_changes(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
channel_ids: &[ChannelId],
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::UnseenChannelBufferChange>> {
|
||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||
enum QueryIds {
|
||||
ChannelId,
|
||||
Id,
|
||||
}
|
||||
|
||||
) -> Result<Vec<proto::ChannelBufferVersion>> {
|
||||
let mut channel_ids_by_buffer_id = HashMap::default();
|
||||
let mut rows = buffer::Entity::find()
|
||||
.filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied()))
|
||||
@@ -771,51 +764,23 @@ impl Database {
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let mut observed_edits_by_buffer_id = HashMap::default();
|
||||
let mut rows = observed_buffer_edits::Entity::find()
|
||||
.filter(observed_buffer_edits::Column::UserId.eq(user_id))
|
||||
.filter(
|
||||
observed_buffer_edits::Column::BufferId
|
||||
.is_in(channel_ids_by_buffer_id.keys().copied()),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
observed_edits_by_buffer_id.insert(row.buffer_id, row);
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let latest_operations = self
|
||||
.get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx)
|
||||
.await?;
|
||||
|
||||
let mut changes = Vec::default();
|
||||
for latest in latest_operations {
|
||||
if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) {
|
||||
if (
|
||||
observed.epoch,
|
||||
observed.lamport_timestamp,
|
||||
observed.replica_id,
|
||||
) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) {
|
||||
changes.push(proto::UnseenChannelBufferChange {
|
||||
channel_id: channel_id.to_proto(),
|
||||
epoch: latest.epoch as u64,
|
||||
Ok(latest_operations
|
||||
.iter()
|
||||
.flat_map(|op| {
|
||||
Some(proto::ChannelBufferVersion {
|
||||
channel_id: channel_ids_by_buffer_id.get(&op.buffer_id)?.to_proto(),
|
||||
epoch: op.epoch as u64,
|
||||
version: vec![proto::VectorClockEntry {
|
||||
replica_id: latest.replica_id as u32,
|
||||
timestamp: latest.lamport_timestamp as u32,
|
||||
replica_id: op.replica_id as u32,
|
||||
timestamp: op.lamport_timestamp as u32,
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns the latest operations for the buffers with the specified IDs.
|
||||
|
||||
@@ -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?.0.id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -36,7 +32,7 @@ impl Database {
|
||||
Ok(self
|
||||
.create_channel(name, Some(parent), creator_id)
|
||||
.await?
|
||||
.channel
|
||||
.0
|
||||
.id)
|
||||
}
|
||||
|
||||
@@ -46,10 +42,15 @@ impl Database {
|
||||
name: &str,
|
||||
parent_channel_id: Option<ChannelId>,
|
||||
admin_id: UserId,
|
||||
) -> Result<CreateChannelResult> {
|
||||
) -> Result<(
|
||||
Channel,
|
||||
Option<channel_member::Model>,
|
||||
Vec<channel_member::Model>,
|
||||
)> {
|
||||
let name = Self::sanitize_channel_name(name)?;
|
||||
self.transaction(move |tx| async move {
|
||||
let mut parent = None;
|
||||
let mut membership = None;
|
||||
|
||||
if let Some(parent_channel_id) = parent_channel_id {
|
||||
let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
|
||||
@@ -72,29 +73,26 @@ 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() {
|
||||
membership = Some(
|
||||
channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel.id),
|
||||
user_id: ActiveValue::Set(admin_id),
|
||||
accepted: ActiveValue::Set(true),
|
||||
role: ActiveValue::Set(ChannelRole::Admin),
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel.id),
|
||||
user_id: ActiveValue::Set(admin_id),
|
||||
accepted: ActiveValue::Set(true),
|
||||
role: ActiveValue::Set(ChannelRole::Admin),
|
||||
}
|
||||
.insert(&*tx)
|
||||
let channel_members = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
};
|
||||
|
||||
Ok(CreateChannelResult {
|
||||
channel: Channel::from_model(channel, ChannelRole::Admin),
|
||||
participants_to_update,
|
||||
})
|
||||
Ok((Channel::from_model(channel), membership, channel_members))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -137,16 +135,9 @@ impl Database {
|
||||
);
|
||||
} else if channel.visibility == ChannelVisibility::Public {
|
||||
role = Some(ChannelRole::Guest);
|
||||
let channel_to_join = self
|
||||
.public_ancestors_including_self(&channel, &*tx)
|
||||
.await?
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or(channel.clone());
|
||||
|
||||
channel_member::Entity::insert(channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel_to_join.id),
|
||||
channel_id: ActiveValue::Set(channel.root_id()),
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
accepted: ActiveValue::Set(true),
|
||||
role: ActiveValue::Set(ChannelRole::Guest),
|
||||
@@ -155,7 +146,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
accept_invite_result = Some(
|
||||
self.calculate_membership_updated(&channel_to_join, user_id, &*tx)
|
||||
self.calculate_membership_updated(&channel, user_id, &*tx)
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -166,7 +157,7 @@ impl Database {
|
||||
}
|
||||
|
||||
if role.is_none() || role == Some(ChannelRole::Banned) {
|
||||
Err(anyhow!("not allowed"))?
|
||||
Err(ErrorCode::Forbidden.anyhow())?
|
||||
}
|
||||
let role = role.unwrap();
|
||||
|
||||
@@ -188,76 +179,47 @@ impl Database {
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
admin_id: UserId,
|
||||
) -> Result<SetChannelVisibilityResult> {
|
||||
) -> Result<(Channel, Vec<channel_member::Model>)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
let previous_members = self
|
||||
.get_channel_participant_details_internal(&channel, &*tx)
|
||||
.await?;
|
||||
if visibility == ChannelVisibility::Public {
|
||||
if let Some(parent_id) = channel.parent_id() {
|
||||
let parent = self.get_channel_internal(parent_id, &*tx).await?;
|
||||
|
||||
if parent.visibility != ChannelVisibility::Public {
|
||||
Err(ErrorCode::BadPublicNesting
|
||||
.with_tag("direction", "parent")
|
||||
.anyhow())?;
|
||||
}
|
||||
}
|
||||
} else if visibility == ChannelVisibility::Members {
|
||||
if self
|
||||
.get_channel_descendants_including_self(vec![channel_id], &*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.any(|channel| {
|
||||
channel.id != channel_id && channel.visibility == ChannelVisibility::Public
|
||||
})
|
||||
{
|
||||
Err(ErrorCode::BadPublicNesting
|
||||
.with_tag("direction", "children")
|
||||
.anyhow())?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut model = channel.into_active_model();
|
||||
model.visibility = ActiveValue::Set(visibility);
|
||||
let channel = model.update(&*tx).await?;
|
||||
|
||||
let mut participants_to_update: HashMap<UserId, ChannelsForUser> = self
|
||||
.participants_to_notify_for_channel_change(&channel, &*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
let channel_members = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut channels_to_remove: Vec<ChannelId> = vec![];
|
||||
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
|
||||
match visibility {
|
||||
ChannelVisibility::Members => {
|
||||
let all_descendents: Vec<ChannelId> = self
|
||||
.get_channel_descendants_including_self(vec![channel_id], &*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect();
|
||||
|
||||
channels_to_remove = channel::Entity::find()
|
||||
.filter(
|
||||
channel::Column::Id
|
||||
.is_in(all_descendents)
|
||||
.and(channel::Column::Visibility.eq(ChannelVisibility::Public)),
|
||||
)
|
||||
.all(&*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect();
|
||||
|
||||
channels_to_remove.push(channel_id);
|
||||
|
||||
for member in previous_members {
|
||||
if member.role.can_only_see_public_descendants() {
|
||||
participants_to_remove.insert(member.user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
ChannelVisibility::Public => {
|
||||
if let Some(public_parent) = self.public_parent_channel(&channel, &*tx).await? {
|
||||
let parent_updates = self
|
||||
.participants_to_notify_for_channel_change(&public_parent, &*tx)
|
||||
.await?;
|
||||
|
||||
for (user_id, channels) in parent_updates {
|
||||
participants_to_update.insert(user_id, channels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SetChannelVisibilityResult {
|
||||
participants_to_update,
|
||||
participants_to_remove,
|
||||
channels_to_remove,
|
||||
})
|
||||
Ok((Channel::from_model(channel), channel_members))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -290,7 +252,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let members_to_notify: Vec<UserId> = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.select_only()
|
||||
.column(channel_member::Column::UserId)
|
||||
.distinct()
|
||||
@@ -327,6 +289,9 @@ impl Database {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, inviter_id, &*tx)
|
||||
.await?;
|
||||
if !channel.is_root() {
|
||||
Err(ErrorCode::NotARootChannel.anyhow())?
|
||||
}
|
||||
|
||||
channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
@@ -338,7 +303,7 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let channel = Channel::from_model(channel, role);
|
||||
let channel = Channel::from_model(channel);
|
||||
|
||||
let notifications = self
|
||||
.create_notification(
|
||||
@@ -377,35 +342,24 @@ impl Database {
|
||||
channel_id: ChannelId,
|
||||
admin_id: UserId,
|
||||
new_name: &str,
|
||||
) -> Result<RenameChannelResult> {
|
||||
) -> Result<(Channel, Vec<channel_member::Model>)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let new_name = Self::sanitize_channel_name(new_name)?.to_string();
|
||||
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let role = self
|
||||
.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
let mut model = channel.into_active_model();
|
||||
model.name = ActiveValue::Set(new_name.clone());
|
||||
let channel = model.update(&*tx).await?;
|
||||
|
||||
let participants = self
|
||||
.get_channel_participant_details_internal(&channel, &*tx)
|
||||
let channel_members = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(RenameChannelResult {
|
||||
channel: Channel::from_model(channel.clone(), role),
|
||||
participants_to_update: participants
|
||||
.iter()
|
||||
.map(|participant| {
|
||||
(
|
||||
participant.user_id,
|
||||
Channel::from_model(channel.clone(), participant.role),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
Ok((Channel::from_model(channel), channel_members))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -580,10 +534,7 @@ impl Database {
|
||||
|
||||
let channels = channels
|
||||
.into_iter()
|
||||
.filter_map(|channel| {
|
||||
let role = *role_for_channel.get(&channel.id)?;
|
||||
Some(Channel::from_model(channel, role))
|
||||
})
|
||||
.filter_map(|channel| Some(Channel::from_model(channel)))
|
||||
.collect();
|
||||
|
||||
Ok(channels)
|
||||
@@ -591,6 +542,26 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_channel_memberships(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<(Vec<channel_member::Model>, Vec<channel::Model>)> {
|
||||
self.transaction(|tx| async move {
|
||||
let memberships = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::UserId.eq(user_id))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
let channels = self
|
||||
.get_channel_descendants_including_self(
|
||||
memberships.iter().map(|m| m.channel_id),
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
Ok((memberships, channels))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.transaction(|tx| async move {
|
||||
@@ -609,12 +580,16 @@ impl Database {
|
||||
ancestor_channel: Option<&channel::Model>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<ChannelsForUser> {
|
||||
let mut filter = channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(true));
|
||||
|
||||
if let Some(ancestor) = ancestor_channel {
|
||||
filter = filter.and(channel_member::Column::ChannelId.eq(ancestor.root_id()));
|
||||
}
|
||||
|
||||
let channel_memberships = channel_member::Entity::find()
|
||||
.filter(
|
||||
channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(true)),
|
||||
)
|
||||
.filter(filter)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
@@ -625,56 +600,20 @@ impl Database {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut roles_by_channel_id: HashMap<ChannelId, ChannelRole> = HashMap::default();
|
||||
for membership in channel_memberships.iter() {
|
||||
roles_by_channel_id.insert(membership.channel_id, membership.role);
|
||||
}
|
||||
|
||||
let mut visible_channel_ids: HashSet<ChannelId> = HashSet::default();
|
||||
let roles_by_channel_id = channel_memberships
|
||||
.iter()
|
||||
.map(|membership| (membership.channel_id, membership.role))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let channels: Vec<Channel> = descendants
|
||||
.into_iter()
|
||||
.filter_map(|channel| {
|
||||
let parent_role = channel
|
||||
.parent_id()
|
||||
.and_then(|parent_id| roles_by_channel_id.get(&parent_id));
|
||||
|
||||
let role = if let Some(parent_role) = parent_role {
|
||||
let role = if let Some(existing_role) = roles_by_channel_id.get(&channel.id) {
|
||||
existing_role.max(*parent_role)
|
||||
} else {
|
||||
*parent_role
|
||||
};
|
||||
roles_by_channel_id.insert(channel.id, role);
|
||||
role
|
||||
let parent_role = roles_by_channel_id.get(&channel.root_id())?;
|
||||
if parent_role.can_see_channel(channel.visibility) {
|
||||
Some(Channel::from_model(channel))
|
||||
} else {
|
||||
*roles_by_channel_id.get(&channel.id)?
|
||||
};
|
||||
|
||||
let can_see_parent_paths = role.can_see_all_descendants()
|
||||
|| role.can_only_see_public_descendants()
|
||||
&& channel.visibility == ChannelVisibility::Public;
|
||||
if !can_see_parent_paths {
|
||||
return None;
|
||||
None
|
||||
}
|
||||
|
||||
visible_channel_ids.insert(channel.id);
|
||||
|
||||
if let Some(ancestor) = ancestor_channel {
|
||||
if !channel
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == ancestor.id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut channel = Channel::from_model(channel, role);
|
||||
channel
|
||||
.parent_path
|
||||
.retain(|id| visible_channel_ids.contains(&id));
|
||||
|
||||
Some(channel)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -702,76 +641,21 @@ impl Database {
|
||||
}
|
||||
|
||||
let channel_ids = channels.iter().map(|c| c.id).collect::<Vec<_>>();
|
||||
let channel_buffer_changes = self
|
||||
.unseen_channel_buffer_changes(user_id, &channel_ids, &*tx)
|
||||
let latest_buffer_versions = self
|
||||
.latest_channel_buffer_changes(&channel_ids, &*tx)
|
||||
.await?;
|
||||
|
||||
let unseen_messages = self
|
||||
.unseen_channel_messages(user_id, &channel_ids, &*tx)
|
||||
.await?;
|
||||
let latest_messages = self.latest_channel_messages(&channel_ids, &*tx).await?;
|
||||
|
||||
Ok(ChannelsForUser {
|
||||
channel_memberships,
|
||||
channels,
|
||||
channel_participants,
|
||||
unseen_buffer_changes: channel_buffer_changes,
|
||||
channel_messages: unseen_messages,
|
||||
latest_buffer_versions,
|
||||
latest_channel_messages: latest_messages,
|
||||
})
|
||||
}
|
||||
|
||||
async fn participants_to_notify_for_channel_change(
|
||||
&self,
|
||||
new_parent: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<(UserId, ChannelsForUser)>> {
|
||||
let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new();
|
||||
|
||||
let members = self
|
||||
.get_channel_participant_details_internal(new_parent, &*tx)
|
||||
.await?;
|
||||
|
||||
for member in members.iter() {
|
||||
if !member.role.can_see_all_descendants() {
|
||||
continue;
|
||||
}
|
||||
results.push((
|
||||
member.user_id,
|
||||
self.get_user_channels(member.user_id, Some(new_parent), &*tx)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
let public_parents = self
|
||||
.public_ancestors_including_self(new_parent, &*tx)
|
||||
.await?;
|
||||
let public_parent = public_parents.last();
|
||||
|
||||
let Some(public_parent) = public_parent else {
|
||||
return Ok(results);
|
||||
};
|
||||
|
||||
// could save some time in the common case by skipping this if the
|
||||
// new channel is not public and has no public descendants.
|
||||
let public_members = if public_parent == new_parent {
|
||||
members
|
||||
} else {
|
||||
self.get_channel_participant_details_internal(public_parent, &*tx)
|
||||
.await?
|
||||
};
|
||||
|
||||
for member in public_members {
|
||||
if !member.role.can_only_see_public_descendants() {
|
||||
continue;
|
||||
};
|
||||
results.push((
|
||||
member.user_id,
|
||||
self.get_user_channels(member.user_id, Some(public_parent), &*tx)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Sets the role for the specified channel member.
|
||||
pub async fn set_channel_member_role(
|
||||
&self,
|
||||
@@ -809,7 +693,7 @@ impl Database {
|
||||
))
|
||||
} else {
|
||||
Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model(
|
||||
channel, role,
|
||||
channel,
|
||||
)))
|
||||
}
|
||||
})
|
||||
@@ -839,22 +723,30 @@ impl Database {
|
||||
if role == ChannelRole::Admin {
|
||||
Ok(members
|
||||
.into_iter()
|
||||
.map(|channel_member| channel_member.to_proto())
|
||||
.map(|channel_member| proto::ChannelMember {
|
||||
role: channel_member.role.into(),
|
||||
user_id: channel_member.user_id.to_proto(),
|
||||
kind: if channel_member.accepted {
|
||||
Kind::Member
|
||||
} else {
|
||||
Kind::Invitee
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
.collect())
|
||||
} else {
|
||||
return Ok(members
|
||||
.into_iter()
|
||||
.filter_map(|member| {
|
||||
if member.kind == proto::channel_member::Kind::Invitee {
|
||||
if !member.accepted {
|
||||
return None;
|
||||
}
|
||||
Some(ChannelMember {
|
||||
role: member.role,
|
||||
user_id: member.user_id,
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
Some(proto::ChannelMember {
|
||||
role: member.role.into(),
|
||||
user_id: member.user_id.to_proto(),
|
||||
kind: Kind::Member.into(),
|
||||
})
|
||||
})
|
||||
.map(|channel_member| channel_member.to_proto())
|
||||
.collect());
|
||||
}
|
||||
}
|
||||
@@ -863,83 +755,11 @@ impl Database {
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<ChannelMember>> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryMemberDetails {
|
||||
UserId,
|
||||
Role,
|
||||
IsDirectMember,
|
||||
Accepted,
|
||||
Visibility,
|
||||
}
|
||||
|
||||
let mut stream = channel_member::Entity::find()
|
||||
.left_join(channel::Entity)
|
||||
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
|
||||
.select_only()
|
||||
.column(channel_member::Column::UserId)
|
||||
.column(channel_member::Column::Role)
|
||||
.column_as(
|
||||
channel_member::Column::ChannelId.eq(channel.id),
|
||||
QueryMemberDetails::IsDirectMember,
|
||||
)
|
||||
.column(channel_member::Column::Accepted)
|
||||
.column(channel::Column::Visibility)
|
||||
.into_values::<_, QueryMemberDetails>()
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut user_details: HashMap<UserId, ChannelMember> = HashMap::default();
|
||||
|
||||
while let Some(user_membership) = stream.next().await {
|
||||
let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): (
|
||||
UserId,
|
||||
ChannelRole,
|
||||
bool,
|
||||
bool,
|
||||
ChannelVisibility,
|
||||
) = user_membership?;
|
||||
let kind = match (is_direct_member, is_invite_accepted) {
|
||||
(true, true) => proto::channel_member::Kind::Member,
|
||||
(true, false) => proto::channel_member::Kind::Invitee,
|
||||
(false, true) => proto::channel_member::Kind::AncestorMember,
|
||||
(false, false) => continue,
|
||||
};
|
||||
|
||||
if channel_role == ChannelRole::Guest
|
||||
&& visibility != ChannelVisibility::Public
|
||||
&& channel.visibility != ChannelVisibility::Public
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(details_mut) = user_details.get_mut(&user_id) {
|
||||
if channel_role.should_override(details_mut.role) {
|
||||
details_mut.role = channel_role;
|
||||
}
|
||||
if kind == Kind::Member {
|
||||
details_mut.kind = kind;
|
||||
// the UI is going to be a bit confusing if you already have permissions
|
||||
// that are greater than or equal to the ones you're being invited to.
|
||||
} else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember {
|
||||
details_mut.kind = kind;
|
||||
}
|
||||
} else {
|
||||
user_details.insert(
|
||||
user_id,
|
||||
ChannelMember {
|
||||
user_id,
|
||||
kind,
|
||||
role: channel_role,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(user_details
|
||||
.into_iter()
|
||||
.map(|(_, details)| details)
|
||||
.collect())
|
||||
) -> Result<Vec<channel_member::Model>> {
|
||||
Ok(channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(tx)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Returns the participants in the given channel.
|
||||
@@ -1018,7 +838,7 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<channel_member::Model>> {
|
||||
let row = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.filter(channel_member::Column::UserId.eq(user_id))
|
||||
.filter(channel_member::Column::Accepted.eq(false))
|
||||
.one(&*tx)
|
||||
@@ -1027,33 +847,6 @@ impl Database {
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
async fn public_parent_channel(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<channel::Model>> {
|
||||
let mut path = self.public_ancestors_including_self(channel, &*tx).await?;
|
||||
if path.last().unwrap().id == channel.id {
|
||||
path.pop();
|
||||
}
|
||||
Ok(path.pop())
|
||||
}
|
||||
|
||||
pub(crate) async fn public_ancestors_including_self(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<channel::Model>> {
|
||||
let visible_channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(channel.ancestors_including_self()))
|
||||
.filter(channel::Column::Visibility.eq(ChannelVisibility::Public))
|
||||
.order_by_asc(channel::Column::ParentPath)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(visible_channels)
|
||||
}
|
||||
|
||||
/// Returns the role for a user in the given channel.
|
||||
pub async fn channel_role_for_user(
|
||||
&self,
|
||||
@@ -1061,77 +854,25 @@ impl Database {
|
||||
user_id: UserId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<ChannelRole>> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryChannelMembership {
|
||||
ChannelId,
|
||||
Role,
|
||||
Visibility,
|
||||
}
|
||||
|
||||
let mut rows = channel_member::Entity::find()
|
||||
.left_join(channel::Entity)
|
||||
let membership = channel_member::Entity::find()
|
||||
.filter(
|
||||
channel_member::Column::ChannelId
|
||||
.is_in(channel.ancestors_including_self())
|
||||
.eq(channel.root_id())
|
||||
.and(channel_member::Column::UserId.eq(user_id))
|
||||
.and(channel_member::Column::Accepted.eq(true)),
|
||||
)
|
||||
.select_only()
|
||||
.column(channel_member::Column::ChannelId)
|
||||
.column(channel_member::Column::Role)
|
||||
.column(channel::Column::Visibility)
|
||||
.into_values::<_, QueryChannelMembership>()
|
||||
.stream(&*tx)
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut user_role: Option<ChannelRole> = None;
|
||||
let Some(membership) = membership else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut is_participant = false;
|
||||
let mut current_channel_visibility = None;
|
||||
|
||||
// note these channels are not iterated in any particular order,
|
||||
// our current logic takes the highest permission available.
|
||||
while let Some(row) = rows.next().await {
|
||||
let (membership_channel, role, visibility): (
|
||||
ChannelId,
|
||||
ChannelRole,
|
||||
ChannelVisibility,
|
||||
) = row?;
|
||||
|
||||
match role {
|
||||
ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => {
|
||||
if let Some(users_role) = user_role {
|
||||
user_role = Some(users_role.max(role));
|
||||
} else {
|
||||
user_role = Some(role)
|
||||
}
|
||||
}
|
||||
ChannelRole::Guest if visibility == ChannelVisibility::Public => {
|
||||
is_participant = true
|
||||
}
|
||||
ChannelRole::Guest => {}
|
||||
}
|
||||
if channel.id == membership_channel {
|
||||
current_channel_visibility = Some(visibility);
|
||||
}
|
||||
}
|
||||
// free up database connection
|
||||
drop(rows);
|
||||
|
||||
if is_participant && user_role.is_none() {
|
||||
if current_channel_visibility.is_none() {
|
||||
current_channel_visibility = channel::Entity::find()
|
||||
.filter(channel::Column::Id.eq(channel.id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.map(|channel| channel.visibility);
|
||||
}
|
||||
if current_channel_visibility == Some(ChannelVisibility::Public) {
|
||||
user_role = Some(ChannelRole::Guest);
|
||||
}
|
||||
if !membership.role.can_see_channel(channel.visibility) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(user_role)
|
||||
Ok(Some(membership.role))
|
||||
}
|
||||
|
||||
// Get the descendants of the given set if channels, ordered by their
|
||||
@@ -1184,11 +925,10 @@ impl Database {
|
||||
pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let role = self
|
||||
.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
self.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
.await?;
|
||||
|
||||
Ok(Channel::from_model(channel, role))
|
||||
Ok(Channel::from_model(channel))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1201,7 +941,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 +959,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
|
||||
@@ -1243,62 +985,40 @@ impl Database {
|
||||
pub async fn move_channel(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
new_parent_id: Option<ChannelId>,
|
||||
new_parent_id: ChannelId,
|
||||
admin_id: UserId,
|
||||
) -> Result<Option<MoveChannelResult>> {
|
||||
) -> Result<(Vec<Channel>, Vec<channel_member::Model>)> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
.await?;
|
||||
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
|
||||
|
||||
let new_parent_path;
|
||||
let new_parent_channel;
|
||||
if let Some(new_parent_id) = new_parent_id {
|
||||
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||
}
|
||||
|
||||
new_parent_path = new_parent.path();
|
||||
new_parent_channel = Some(new_parent);
|
||||
} else {
|
||||
new_parent_path = String::new();
|
||||
new_parent_channel = None;
|
||||
};
|
||||
|
||||
let previous_participants = self
|
||||
.get_channel_participant_details_internal(&channel, &*tx)
|
||||
.await?;
|
||||
|
||||
let old_path = format!("{}{}/", channel.parent_path, channel.id);
|
||||
let new_path = format!("{}{}/", new_parent_path, channel.id);
|
||||
|
||||
if old_path == new_path {
|
||||
return Ok(None);
|
||||
if new_parent.root_id() != channel.root_id() {
|
||||
Err(anyhow!(ErrorCode::WrongMoveTarget))?;
|
||||
}
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!(ErrorCode::CircularNesting))?;
|
||||
}
|
||||
|
||||
if channel.visibility == ChannelVisibility::Public
|
||||
&& new_parent.visibility != ChannelVisibility::Public
|
||||
{
|
||||
Err(anyhow!(ErrorCode::BadPublicNesting))?;
|
||||
}
|
||||
|
||||
let root_id = channel.root_id();
|
||||
let old_path = format!("{}{}/", channel.parent_path, channel.id);
|
||||
let new_path = format!("{}{}/", new_parent.path(), channel.id);
|
||||
|
||||
let mut model = channel.into_active_model();
|
||||
model.parent_path = ActiveValue::Set(new_parent_path);
|
||||
model.parent_path = ActiveValue::Set(new_parent.path());
|
||||
let channel = model.update(&*tx).await?;
|
||||
|
||||
if new_parent_channel.is_none() {
|
||||
channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
user_id: ActiveValue::Set(admin_id),
|
||||
accepted: ActiveValue::Set(true),
|
||||
role: ActiveValue::Set(ChannelRole::Admin),
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let descendent_ids =
|
||||
ChannelId::find_by_statement::<QueryIds>(Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
@@ -1312,35 +1032,22 @@ 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,
|
||||
)
|
||||
let all_moved_ids = Some(channel.id).into_iter().chain(descendent_ids);
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(all_moved_ids))
|
||||
.all(&*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
.map(|c| Channel::from_model(c))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut moved_channels: HashSet<ChannelId> = HashSet::default();
|
||||
for id in descendent_ids {
|
||||
moved_channels.insert(id);
|
||||
}
|
||||
moved_channels.insert(channel_id);
|
||||
let channel_members = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(root_id))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
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,
|
||||
}))
|
||||
Ok((channels, channel_members))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -385,25 +385,11 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the unseen messages for the given user in the specified channels.
|
||||
pub async fn unseen_channel_messages(
|
||||
pub async fn latest_channel_messages(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
channel_ids: &[ChannelId],
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::UnseenChannelMessage>> {
|
||||
let mut observed_messages_by_channel_id = HashMap::default();
|
||||
let mut rows = observed_channel_messages::Entity::find()
|
||||
.filter(observed_channel_messages::Column::UserId.eq(user_id))
|
||||
.filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied()))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
observed_messages_by_channel_id.insert(row.channel_id, row);
|
||||
}
|
||||
drop(rows);
|
||||
) -> Result<Vec<proto::ChannelMessageId>> {
|
||||
let mut values = String::new();
|
||||
for id in channel_ids {
|
||||
if !values.is_empty() {
|
||||
@@ -413,7 +399,7 @@ impl Database {
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
return Ok(Default::default());
|
||||
return Ok(Vec::default());
|
||||
}
|
||||
|
||||
let sql = format!(
|
||||
@@ -437,26 +423,20 @@ impl Database {
|
||||
);
|
||||
|
||||
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
|
||||
let last_messages = channel_message::Model::find_by_statement(stmt)
|
||||
.all(&*tx)
|
||||
let mut last_messages = channel_message::Model::find_by_statement(stmt)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut changes = Vec::new();
|
||||
for last_message in last_messages {
|
||||
if let Some(observed_message) =
|
||||
observed_messages_by_channel_id.get(&last_message.channel_id)
|
||||
{
|
||||
if observed_message.channel_message_id == last_message.id {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
changes.push(proto::UnseenChannelMessage {
|
||||
channel_id: last_message.channel_id.to_proto(),
|
||||
message_id: last_message.id.to_proto(),
|
||||
let mut results = Vec::new();
|
||||
while let Some(result) = last_messages.next().await {
|
||||
let message = result?;
|
||||
results.push(proto::ChannelMessageId {
|
||||
channel_id: message.channel_id.to_proto(),
|
||||
message_id: message.id.to_proto(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Removes the channel message with the given ID.
|
||||
|
||||
@@ -17,6 +17,14 @@ impl Model {
|
||||
self.ancestors().last()
|
||||
}
|
||||
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.parent_path.is_empty()
|
||||
}
|
||||
|
||||
pub fn root_id(&self) -> ChannelId {
|
||||
self.ancestors().next().unwrap_or(self.id)
|
||||
}
|
||||
|
||||
pub fn ancestors(&self) -> impl Iterator<Item = ChannelId> + '_ {
|
||||
self.parent_path
|
||||
.trim_end_matches('/')
|
||||
|
||||
@@ -150,14 +150,13 @@ impl Drop for TestDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec<Channel> {
|
||||
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
|
||||
channels
|
||||
.iter()
|
||||
.map(|(id, parent_path, name, role)| Channel {
|
||||
.map(|(id, parent_path, name)| Channel {
|
||||
id: *id,
|
||||
name: name.to_string(),
|
||||
visibility: ChannelVisibility::Members,
|
||||
role: *role,
|
||||
parent_path: parent_path.to_vec(),
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -330,8 +330,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
db.latest_channel_buffer_changes(
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
@@ -348,12 +347,12 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
rpc::proto::ChannelBufferVersion {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
rpc::proto::ChannelBufferVersion {
|
||||
channel_id: buffers[1].channel_id.to_proto(),
|
||||
epoch: 1,
|
||||
version: serialize_version(&text_buffers[1].version())
|
||||
@@ -362,99 +361,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
== buffer_changes[1].version.first().unwrap().replica_id)
|
||||
.collect::<Vec<_>>(),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.observe_buffer_version(
|
||||
buffers[1].id,
|
||||
observer_id,
|
||||
1,
|
||||
serialize_version(&text_buffers[1].version()).as_slice(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_changes = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
buffers[2].channel_id,
|
||||
],
|
||||
&*tx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe an earlier version of the buffer.
|
||||
db.observe_buffer_version(
|
||||
buffers[1].id,
|
||||
observer_id,
|
||||
1,
|
||||
&[rpc::proto::VectorClockEntry {
|
||||
replica_id: 0,
|
||||
timestamp: 0,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_changes = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
buffers[2].channel_id,
|
||||
],
|
||||
&*tx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
rpc::proto::ChannelBufferVersion {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
|
||||
@@ -62,23 +62,13 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed", ChannelRole::Admin),
|
||||
(crdb_id, &[zed_id], "crdb", ChannelRole::Admin),
|
||||
(
|
||||
livestreaming_id,
|
||||
&[zed_id],
|
||||
"livestreaming",
|
||||
ChannelRole::Admin
|
||||
),
|
||||
(replace_id, &[zed_id], "replace", ChannelRole::Admin),
|
||||
(rust_id, &[], "rust", ChannelRole::Admin),
|
||||
(cargo_id, &[rust_id], "cargo", ChannelRole::Admin),
|
||||
(
|
||||
cargo_ra_id,
|
||||
&[rust_id, cargo_id],
|
||||
"cargo-ra",
|
||||
ChannelRole::Admin
|
||||
)
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(replace_id, &[zed_id], "replace"),
|
||||
(rust_id, &[], "rust"),
|
||||
(cargo_id, &[rust_id], "cargo"),
|
||||
(cargo_ra_id, &[rust_id, cargo_id], "cargo-ra",)
|
||||
],)
|
||||
);
|
||||
|
||||
@@ -86,15 +76,10 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed", ChannelRole::Member),
|
||||
(crdb_id, &[zed_id], "crdb", ChannelRole::Member),
|
||||
(
|
||||
livestreaming_id,
|
||||
&[zed_id],
|
||||
"livestreaming",
|
||||
ChannelRole::Member
|
||||
),
|
||||
(replace_id, &[zed_id], "replace", ChannelRole::Member)
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(replace_id, &[zed_id], "replace")
|
||||
],)
|
||||
);
|
||||
|
||||
@@ -112,15 +97,10 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed", ChannelRole::Admin),
|
||||
(crdb_id, &[zed_id], "crdb", ChannelRole::Admin),
|
||||
(
|
||||
livestreaming_id,
|
||||
&[zed_id],
|
||||
"livestreaming",
|
||||
ChannelRole::Admin
|
||||
),
|
||||
(replace_id, &[zed_id], "replace", ChannelRole::Admin)
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(replace_id, &[zed_id], "replace")
|
||||
],)
|
||||
);
|
||||
|
||||
@@ -271,14 +251,19 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_3.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -420,13 +405,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Move to same parent should be a no-op
|
||||
assert!(db
|
||||
.move_channel(projects_id, Some(zed_id), user_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
@@ -437,20 +415,8 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
],
|
||||
);
|
||||
|
||||
// Move the project channel to the root
|
||||
db.move_channel(projects_id, None, user_id).await.unwrap();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Can't move a channel into its ancestor
|
||||
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||
db.move_channel(projects_id, livestreaming_id, user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
@@ -458,8 +424,8 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
(projects_id, &[zed_id]),
|
||||
(livestreaming_id, &[zed_id, projects_id]),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -476,32 +442,39 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
let guest = new_test_user(db, "guest@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
let active_channel_id = db
|
||||
let internal_channel_id = db
|
||||
.create_sub_channel("active", zed_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
let vim_channel_id = db
|
||||
.create_sub_channel("vim", active_channel_id, admin)
|
||||
let public_channel_id = db
|
||||
.create_sub_channel("vim", zed_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel_id, crate::db::ChannelVisibility::Public, admin)
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(active_channel_id, member, admin, ChannelRole::Member)
|
||||
db.set_channel_visibility(
|
||||
public_channel_id,
|
||||
crate::db::ChannelVisibility::Public,
|
||||
admin,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(zed_channel, member, admin, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(vim_channel_id, guest, admin, ChannelRole::Guest)
|
||||
db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.respond_to_channel_invite(active_channel_id, member, true)
|
||||
db.respond_to_channel_invite(zed_channel, member, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
admin,
|
||||
&*tx,
|
||||
)
|
||||
@@ -511,7 +484,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
member,
|
||||
&*tx,
|
||||
)
|
||||
@@ -521,7 +494,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel_id, admin)
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -532,12 +505,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
@@ -548,13 +521,13 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(vim_channel_id, guest, true)
|
||||
db.respond_to_channel_invite(zed_channel, guest, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
guest,
|
||||
&*tx,
|
||||
)
|
||||
@@ -564,23 +537,29 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_channel_tree(channels, &[(vim_channel_id, &[])]);
|
||||
assert_channel_tree(
|
||||
channels,
|
||||
&[(zed_channel, &[]), (public_channel_id, &[zed_channel])],
|
||||
);
|
||||
let channels = db.get_channels_for_user(member).await.unwrap().channels;
|
||||
assert_channel_tree(
|
||||
channels,
|
||||
&[
|
||||
(active_channel_id, &[]),
|
||||
(vim_channel_id, &[active_channel_id]),
|
||||
(zed_channel, &[]),
|
||||
(internal_channel_id, &[zed_channel]),
|
||||
(public_channel_id, &[zed_channel]),
|
||||
],
|
||||
);
|
||||
|
||||
db.set_channel_member_role(vim_channel_id, admin, guest, ChannelRole::Banned)
|
||||
db.set_channel_member_role(zed_channel, admin, guest, ChannelRole::Banned)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(),
|
||||
&db.get_channel_internal(public_channel_id, &*tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
&*tx,
|
||||
)
|
||||
@@ -590,7 +569,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.is_err());
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel_id, admin)
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -601,12 +580,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
@@ -617,11 +596,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
]
|
||||
);
|
||||
|
||||
db.remove_channel_member(vim_channel_id, guest, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
db.remove_channel_member(zed_channel, guest, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -631,7 +606,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
|
||||
// currently people invited to parent channels are not shown here
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel_id, admin)
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -642,14 +617,19 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -670,7 +650,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(active_channel_id, &*tx)
|
||||
&db.get_channel_internal(internal_channel_id, &*tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
@@ -683,7 +663,9 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(),
|
||||
&db.get_channel_internal(public_channel_id, &*tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
&*tx,
|
||||
)
|
||||
@@ -693,7 +675,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel_id, admin)
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -704,17 +686,17 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
@@ -723,67 +705,10 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_channel_tree(
|
||||
channels,
|
||||
&[(zed_channel, &[]), (vim_channel_id, &[zed_channel])],
|
||||
&[(zed_channel, &[]), (public_channel_id, &[zed_channel])],
|
||||
)
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_user_joins_correct_channel,
|
||||
test_user_joins_correct_channel_postgres,
|
||||
test_user_joins_correct_channel_sqlite
|
||||
);
|
||||
|
||||
async fn test_user_joins_correct_channel(db: &Arc<Database>) {
|
||||
let admin = new_test_user(db, "admin@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
|
||||
let active_channel = db
|
||||
.create_sub_channel("active", zed_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim_channel = db
|
||||
.create_sub_channel("vim", active_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim2_channel = db
|
||||
.create_sub_channel("vim2", vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let most_public = db
|
||||
.transaction(|tx| async move {
|
||||
Ok(db
|
||||
.public_ancestors_including_self(
|
||||
&db.get_channel_internal(vim_channel, &*tx).await.unwrap(),
|
||||
&tx,
|
||||
)
|
||||
.await?
|
||||
.first()
|
||||
.cloned())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.id;
|
||||
|
||||
assert_eq!(most_public, zed_channel)
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_guest_access,
|
||||
test_guest_access_postgres,
|
||||
|
||||
@@ -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().0;
|
||||
|
||||
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])),
|
||||
@@ -100,7 +96,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// As user A, create messages that re-use the same nonces. The requests
|
||||
// As user A, create messages that reuse the same nonces. The requests
|
||||
// succeed, but return the same ids.
|
||||
let id1 = db
|
||||
.create_channel_message(
|
||||
@@ -239,11 +235,10 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let second_message = db
|
||||
let _ = db
|
||||
.create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2)
|
||||
.await
|
||||
.unwrap()
|
||||
.message_id;
|
||||
.unwrap();
|
||||
|
||||
let third_message = db
|
||||
.create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3)
|
||||
@@ -262,97 +257,27 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
|
||||
.message_id;
|
||||
|
||||
// Check that observer has new messages
|
||||
let unseen_messages = db
|
||||
let latest_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
db.latest_channel_messages(&[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
latest_messages,
|
||||
[
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
rpc::proto::ChannelMessageId {
|
||||
channel_id: channel_1.to_proto(),
|
||||
message_id: third_message.to_proto(),
|
||||
},
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
rpc::proto::ChannelMessageId {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe the second message
|
||||
db.observe_channel_message(channel_1, observer, second_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer still has a new message
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_1.to_proto(),
|
||||
message_id: third_message.to_proto(),
|
||||
},
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe the third message,
|
||||
db.observe_channel_message(channel_1, observer, third_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer does not have a new method
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
}]
|
||||
);
|
||||
|
||||
// Observe the second message again, should not regress our observed state
|
||||
db.observe_channel_message(channel_1, observer, second_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer does not have a new message
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
@@ -370,7 +295,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
|
||||
.create_channel("channel", None, user_a)
|
||||
.await
|
||||
.unwrap()
|
||||
.channel
|
||||
.0
|
||||
.id;
|
||||
db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
|
||||
.await
|
||||
|
||||
@@ -3,14 +3,12 @@ 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, RespondToChannelInvite, RoomId, ServerId, User, UserId,
|
||||
},
|
||||
executor::Executor,
|
||||
AppState, Result,
|
||||
AppState, Error, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use async_tungstenite::tungstenite::{
|
||||
@@ -44,7 +42,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 +541,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)
|
||||
}
|
||||
}
|
||||
@@ -604,6 +601,7 @@ impl Server {
|
||||
let mut pool = this.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user_id, user.admin);
|
||||
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
||||
this.peer.send(connection_id, build_update_user_channels(&channels_for_user.channel_memberships))?;
|
||||
this.peer.send(connection_id, build_channels_update(
|
||||
channels_for_user,
|
||||
channel_invites
|
||||
@@ -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, owner, channel_members) = db
|
||||
.create_channel(&request.name, parent_id, session.user_id)
|
||||
.await?;
|
||||
|
||||
@@ -2315,12 +2310,29 @@ async fn create_channel(
|
||||
})?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for (user_id, channels) in participants_to_update {
|
||||
let update = build_channels_update(channels, vec![]);
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
if user_id == session.user_id {
|
||||
continue;
|
||||
}
|
||||
if let Some(owner) = owner {
|
||||
let update = proto::UpdateUserChannels {
|
||||
channel_memberships: vec![proto::ChannelMembership {
|
||||
channel_id: owner.channel_id.to_proto(),
|
||||
role: owner.role.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(owner.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
for channel_member in channel_members {
|
||||
if !channel_member.role.can_see_channel(channel.visibility) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let update = proto::UpdateChannels {
|
||||
channels: vec![channel.to_proto()],
|
||||
..Default::default()
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(channel_member.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
@@ -2437,7 +2449,9 @@ async fn remove_channel_member(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Toggle the channel between public and private
|
||||
/// Toggle the channel between public and private.
|
||||
/// Care is taken to maintain the invariant that public channels only descend from public channels,
|
||||
/// (though members-only channels can appear at any point in the hierarchy).
|
||||
async fn set_channel_visibility(
|
||||
request: proto::SetChannelVisibility,
|
||||
response: Response<proto::SetChannelVisibility>,
|
||||
@@ -2447,27 +2461,25 @@ async fn set_channel_visibility(
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let visibility = request.visibility().into();
|
||||
|
||||
let SetChannelVisibilityResult {
|
||||
participants_to_update,
|
||||
participants_to_remove,
|
||||
channels_to_remove,
|
||||
} = db
|
||||
let (channel, channel_members) = db
|
||||
.set_channel_visibility(channel_id, visibility, session.user_id)
|
||||
.await?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for (user_id, channels) in participants_to_update {
|
||||
let update = build_channels_update(channels, vec![]);
|
||||
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: channels_to_remove.iter().map(|id| id.to_proto()).collect(),
|
||||
..Default::default()
|
||||
for member in channel_members {
|
||||
let update = if member.role.can_see_channel(channel.visibility) {
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel.to_proto()],
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
proto::UpdateChannels {
|
||||
delete_channels: vec![channel.id.to_proto()],
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
|
||||
for connection_id in connection_pool.user_connection_ids(member.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
@@ -2476,7 +2488,7 @@ async fn set_channel_visibility(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Alter the role for a user in the channel
|
||||
/// Alter the role for a user in the channel.
|
||||
async fn set_channel_member_role(
|
||||
request: proto::SetChannelMemberRole,
|
||||
response: Response<proto::SetChannelMemberRole>,
|
||||
@@ -2532,10 +2544,7 @@ async fn rename_channel(
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let RenameChannelResult {
|
||||
channel,
|
||||
participants_to_update,
|
||||
} = db
|
||||
let (channel, channel_members) = db
|
||||
.rename_channel(channel_id, session.user_id, &request.name)
|
||||
.await?;
|
||||
|
||||
@@ -2544,13 +2553,15 @@ async fn rename_channel(
|
||||
})?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for (user_id, channel) in participants_to_update {
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
let update = proto::UpdateChannels {
|
||||
channels: vec![channel.to_proto()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for channel_member in channel_members {
|
||||
if !channel_member.role.can_see_channel(channel.visibility) {
|
||||
continue;
|
||||
}
|
||||
let update = proto::UpdateChannels {
|
||||
channels: vec![channel.to_proto()],
|
||||
..Default::default()
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(channel_member.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
@@ -2565,49 +2576,41 @@ async fn move_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let to = request.to.map(ChannelId::from_proto);
|
||||
let to = ChannelId::from_proto(request.to);
|
||||
|
||||
let result = session
|
||||
let (channels, channel_members) = session
|
||||
.db()
|
||||
.await
|
||||
.move_channel(channel_id, to, session.user_id)
|
||||
.await?;
|
||||
|
||||
notify_channel_moved(result, session).await?;
|
||||
|
||||
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 member in channel_members {
|
||||
let channels = channels
|
||||
.iter()
|
||||
.filter_map(|channel| {
|
||||
if member.role.can_see_channel(channel.visibility) {
|
||||
Some(channel.to_proto())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if channels.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for user_id in participants_to_remove {
|
||||
let update = proto::UpdateChannels {
|
||||
delete_channels: moved_channels.clone(),
|
||||
channels,
|
||||
..Default::default()
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
|
||||
for connection_id in connection_pool.user_connection_ids(member.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
response.send(Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2837,7 +2840,7 @@ async fn update_channel_buffer(
|
||||
session.peer.send(
|
||||
peer_id.into(),
|
||||
proto::UpdateChannels {
|
||||
unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange {
|
||||
latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
|
||||
channel_id: channel_id.to_proto(),
|
||||
epoch: epoch as u64,
|
||||
version: version.clone(),
|
||||
@@ -3023,7 +3026,7 @@ async fn send_channel_message(
|
||||
session.peer.send(
|
||||
peer_id.into(),
|
||||
proto::UpdateChannels {
|
||||
unseen_channel_messages: vec![proto::UnseenChannelMessage {
|
||||
latest_channel_message_ids: vec![proto::ChannelMessageId {
|
||||
channel_id: channel_id.to_proto(),
|
||||
message_id: message_id.to_proto(),
|
||||
}],
|
||||
@@ -3278,6 +3281,18 @@ fn notify_membership_updated(
|
||||
user_id: UserId,
|
||||
peer: &Peer,
|
||||
) {
|
||||
let user_channels_update = proto::UpdateUserChannels {
|
||||
channel_memberships: result
|
||||
.new_channels
|
||||
.channel_memberships
|
||||
.iter()
|
||||
.map(|cm| proto::ChannelMembership {
|
||||
channel_id: cm.channel_id.to_proto(),
|
||||
role: cm.role.into(),
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut update = build_channels_update(result.new_channels, vec![]);
|
||||
update.delete_channels = result
|
||||
.removed_channels
|
||||
@@ -3287,10 +3302,27 @@ fn notify_membership_updated(
|
||||
update.remove_channel_invitations = vec![result.channel_id.to_proto()];
|
||||
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
peer.send(connection_id, user_channels_update.clone())
|
||||
.trace_err();
|
||||
peer.send(connection_id, update.clone()).trace_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn build_update_user_channels(
|
||||
memberships: &Vec<db::channel_member::Model>,
|
||||
) -> proto::UpdateUserChannels {
|
||||
proto::UpdateUserChannels {
|
||||
channel_memberships: memberships
|
||||
.iter()
|
||||
.map(|m| proto::ChannelMembership {
|
||||
channel_id: m.channel_id.to_proto(),
|
||||
role: m.role.into(),
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_channels_update(
|
||||
channels: ChannelsForUser,
|
||||
channel_invites: Vec<db::Channel>,
|
||||
@@ -3301,8 +3333,8 @@ fn build_channels_update(
|
||||
update.channels.push(channel.to_proto());
|
||||
}
|
||||
|
||||
update.unseen_channel_buffer_changes = channels.unseen_buffer_changes;
|
||||
update.unseen_channel_messages = channels.channel_messages;
|
||||
update.latest_channel_buffer_versions = channels.latest_buffer_versions;
|
||||
update.latest_channel_message_ids = channels.latest_channel_messages;
|
||||
|
||||
for (channel_id, participants) in channels.channel_participants {
|
||||
update
|
||||
|
||||
@@ -637,7 +637,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(has_buffer_changed);
|
||||
|
||||
@@ -655,7 +654,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
@@ -672,7 +670,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
@@ -687,7 +684,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
@@ -714,7 +710,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(has_buffer_changed);
|
||||
}
|
||||
|
||||
@@ -195,6 +195,13 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.set_channel_visibility(parent_channel_id, proto::ChannelVisibility::Public, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
|
||||
@@ -313,7 +313,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
@@ -341,7 +340,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(!b_has_messages);
|
||||
@@ -359,7 +357,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(!b_has_messages);
|
||||
@@ -382,7 +379,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
@@ -402,7 +398,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
|
||||
@@ -48,13 +48,11 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -94,7 +92,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -141,13 +138,11 @@ async fn test_core_channels(
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
],
|
||||
@@ -169,19 +164,16 @@ async fn test_core_channels(
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 2,
|
||||
},
|
||||
],
|
||||
@@ -213,19 +205,16 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".into(),
|
||||
depth: 2,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -247,7 +236,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
assert_channels(
|
||||
@@ -257,7 +245,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -280,7 +267,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -311,7 +297,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a-renamed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
}
|
||||
@@ -420,7 +405,6 @@ async fn test_channel_room(
|
||||
id: zed_id,
|
||||
name: "zed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
cx_b.read(|cx| {
|
||||
@@ -681,7 +665,6 @@ async fn test_permissions_update_while_invited(
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
assert_channels(client_b.channel_store(), cx_b, &[]);
|
||||
@@ -709,7 +692,6 @@ async fn test_permissions_update_while_invited(
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
assert_channels(client_b.channel_store(), cx_b, &[]);
|
||||
@@ -748,7 +730,6 @@ async fn test_channel_rename(
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -760,7 +741,6 @@ async fn test_channel_rename(
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
}
|
||||
@@ -889,7 +869,6 @@ async fn test_lost_channel_creation(
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -913,13 +892,11 @@ async fn test_lost_channel_creation(
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -944,13 +921,11 @@ async fn test_lost_channel_creation(
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -1035,7 +1010,7 @@ async fn test_channel_link_notifications(
|
||||
let vim_channel = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("vim", None, cx)
|
||||
channel_store.create_channel("vim", Some(zed_channel), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1048,26 +1023,16 @@ async fn test_channel_link_notifications(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.move_channel(vim_channel, Some(active_channel), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// the new channel shows for b and c
|
||||
assert_channels_list_shape(
|
||||
client_a.channel_store(),
|
||||
cx_a,
|
||||
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
|
||||
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
|
||||
);
|
||||
assert_channels_list_shape(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
|
||||
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
|
||||
);
|
||||
assert_channels_list_shape(
|
||||
client_c.channel_store(),
|
||||
@@ -1078,7 +1043,7 @@ async fn test_channel_link_notifications(
|
||||
let helix_channel = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("helix", None, cx)
|
||||
channel_store.create_channel("helix", Some(zed_channel), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1086,7 +1051,7 @@ async fn test_channel_link_notifications(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.move_channel(helix_channel, Some(vim_channel), cx)
|
||||
channel_store.move_channel(helix_channel, vim_channel, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1102,6 +1067,7 @@ async fn test_channel_link_notifications(
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// the new channel shows for b and c
|
||||
assert_channels_list_shape(
|
||||
@@ -1110,8 +1076,8 @@ async fn test_channel_link_notifications(
|
||||
&[
|
||||
(zed_channel, 0),
|
||||
(active_channel, 1),
|
||||
(vim_channel, 2),
|
||||
(helix_channel, 3),
|
||||
(vim_channel, 1),
|
||||
(helix_channel, 2),
|
||||
],
|
||||
);
|
||||
assert_channels_list_shape(
|
||||
@@ -1119,41 +1085,6 @@ async fn test_channel_link_notifications(
|
||||
cx_c,
|
||||
&[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)],
|
||||
);
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Members, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// the members-only channel is still shown for c, but hidden for b
|
||||
assert_channels_list_shape(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[
|
||||
(zed_channel, 0),
|
||||
(active_channel, 1),
|
||||
(vim_channel, 2),
|
||||
(helix_channel, 3),
|
||||
],
|
||||
);
|
||||
cx_b.read(|cx| {
|
||||
client_b.channel_store().read_with(cx, |channel_store, _| {
|
||||
assert_eq!(
|
||||
channel_store
|
||||
.channel_for_id(vim_channel)
|
||||
.unwrap()
|
||||
.visibility,
|
||||
proto::ChannelVisibility::Members
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1170,24 +1101,20 @@ async fn test_channel_membership_notifications(
|
||||
|
||||
let channels = server
|
||||
.make_channel_tree(
|
||||
&[
|
||||
("zed", None),
|
||||
("active", Some("zed")),
|
||||
("vim", Some("active")),
|
||||
],
|
||||
&[("zed", None), ("vim", Some("zed")), ("opensource", None)],
|
||||
(&client_a, cx_a),
|
||||
)
|
||||
.await;
|
||||
let zed_channel = channels[0];
|
||||
let _active_channel = channels[1];
|
||||
let vim_channel = channels[2];
|
||||
let vim_channel = channels[1];
|
||||
let opensource_channel = channels[2];
|
||||
|
||||
try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
|
||||
[
|
||||
channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
|
||||
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx),
|
||||
channel_store.invite_member(vim_channel, user_b, proto::ChannelRole::Member, cx),
|
||||
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx),
|
||||
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Admin, cx),
|
||||
channel_store.invite_member(opensource_channel, user_b, proto::ChannelRole::Member, cx),
|
||||
]
|
||||
}))
|
||||
.await
|
||||
@@ -1203,14 +1130,6 @@ async fn test_channel_membership_notifications(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |channel_store, cx| {
|
||||
channel_store.respond_to_channel_invite(vim_channel, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// we have an admin (a), and a guest (b) with access to all of zed, and membership in vim.
|
||||
@@ -1222,45 +1141,42 @@ async fn test_channel_membership_notifications(
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
client_a
|
||||
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||
channel_store.is_channel_admin(zed_channel)
|
||||
});
|
||||
|
||||
client_b
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.remove_member(vim_channel, user_b, cx)
|
||||
.update(cx_b, |channel_store, cx| {
|
||||
channel_store.respond_to_channel_invite(opensource_channel, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
],
|
||||
)
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_member_role(opensource_channel, user_b, ChannelRole::Admin, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.run_until_parked();
|
||||
|
||||
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||
channel_store.is_channel_admin(opensource_channel)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1329,25 +1245,6 @@ async fn test_guest_access(
|
||||
assert_eq!(participants.len(), 1);
|
||||
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||
});
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Members, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1451,7 +1348,7 @@ async fn test_channel_moving(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.move_channel(channel_d_id, Some(channel_b_id), cx)
|
||||
channel_store.move_channel(channel_d_id, channel_b_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1476,7 +1373,6 @@ struct ExpectedChannel {
|
||||
depth: usize,
|
||||
id: ChannelId,
|
||||
name: SharedString,
|
||||
role: ChannelRole,
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -1494,7 +1390,6 @@ fn assert_channel_invitations(
|
||||
depth: 0,
|
||||
name: channel.name.clone(),
|
||||
id: channel.id,
|
||||
role: channel.role,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
@@ -1516,7 +1411,6 @@ fn assert_channels(
|
||||
depth,
|
||||
name: channel.name.clone().into(),
|
||||
id: channel.id,
|
||||
role: channel.role,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
|
||||
@@ -3884,6 +3884,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
// Join project as client C and observe the diagnostics.
|
||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
executor.run_until_parked();
|
||||
let project_c_diagnostic_summaries =
|
||||
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
|
||||
|
||||
@@ -125,6 +125,7 @@ impl TestServer {
|
||||
let channel_id = server
|
||||
.make_channel("a", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||
.await;
|
||||
cx_a.run_until_parked();
|
||||
|
||||
(client_a, client_b, channel_id)
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ impl ChannelView {
|
||||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.notes_changed(
|
||||
store.update_latest_notes_version(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
|
||||
@@ -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(
|
||||
@@ -274,7 +266,7 @@ impl ChatPanel {
|
||||
} => {
|
||||
if !self.active {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store.new_message(*channel_id, *message_id, cx)
|
||||
store.update_latest_message_id(*channel_id, *message_id, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -351,9 +343,11 @@ impl ChatPanel {
|
||||
this.pt_3().child(
|
||||
h_flex()
|
||||
.text_ui_sm()
|
||||
.child(div().absolute().child(
|
||||
Avatar::new(message.sender.avatar_uri.clone()).size(cx.rem_size()),
|
||||
))
|
||||
.child(
|
||||
div().absolute().child(
|
||||
Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pl(cx.rem_size() + px(6.0))
|
||||
@@ -665,6 +659,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, ChannelVisibility, 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 {
|
||||
@@ -1126,13 +1134,6 @@ impl CollabPanel {
|
||||
"Rename",
|
||||
Some(Box::new(SecondaryConfirm)),
|
||||
cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
"Move this channel",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.start_move_channel(channel_id, cx)
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(channel_name) = clipboard_channel_name {
|
||||
@@ -1145,23 +1146,52 @@ impl CollabPanel {
|
||||
);
|
||||
}
|
||||
|
||||
context_menu = context_menu
|
||||
.separator()
|
||||
.entry(
|
||||
"Invite Members",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
if self.channel_store.read(cx).is_root_channel(channel_id) {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Manage Members",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
"Delete",
|
||||
} else {
|
||||
context_menu = context_menu.entry(
|
||||
"Move this channel",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.start_move_channel(channel_id, cx)
|
||||
}),
|
||||
);
|
||||
if self.channel_store.read(cx).is_public_channel(channel_id) {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Make Channel Private",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Make Channel Public",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context_menu = context_menu.entry(
|
||||
"Delete",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
|
||||
);
|
||||
}
|
||||
|
||||
context_menu
|
||||
@@ -1258,7 +1288,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 +1466,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>) {
|
||||
@@ -1478,10 +1512,6 @@ impl CollabPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
|
||||
}
|
||||
|
||||
fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
||||
}
|
||||
@@ -1518,6 +1548,27 @@ impl CollabPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_channel_visibility(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_id, visibility, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to set channel visibility", cx, |e, _| match e.error_code() {
|
||||
ErrorCode::BadPublicNesting =>
|
||||
if e.error_tag("direction") == Some("parent") {
|
||||
Some("To make a channel public, its parent channel must be public.".to_string())
|
||||
} else {
|
||||
Some("To make a channel private, all of its subchannels must be private.".to_string())
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
}
|
||||
|
||||
fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
|
||||
self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
|
||||
}
|
||||
@@ -1534,14 +1585,27 @@ 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.move_channel(clipboard.channel_id, to_channel_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn move_channel(&self, channel_id: ChannelId, to: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(channel_id, to, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to move channel", cx, |e, _| match e.error_code() {
|
||||
ErrorCode::BadPublicNesting => {
|
||||
Some("Public channels must have public parents".into())
|
||||
}
|
||||
ErrorCode::CircularNesting => Some("You cannot move a channel into itself".into()),
|
||||
ErrorCode::WrongMoveTarget => {
|
||||
Some("You cannot move a channel into a different root channel".into())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
ChannelView::open(channel_id, workspace, cx).detach();
|
||||
@@ -1610,7 +1674,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 +1700,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 +1715,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 +1728,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 +1749,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 +1765,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 +1778,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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1958,32 +2032,19 @@ impl CollabPanel {
|
||||
| Section::Offline => true,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group("section-header")
|
||||
.child(
|
||||
ListHeader::new(text)
|
||||
.when(can_collapse, |header| {
|
||||
header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
|
||||
move |this, _, cx| {
|
||||
this.toggle_section_expanded(section, cx);
|
||||
},
|
||||
))
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
)
|
||||
.when(section == Section::Channels, |el| {
|
||||
el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(dragged_channel.id, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}))
|
||||
})
|
||||
h_flex().w_full().group("section-header").child(
|
||||
ListHeader::new(text)
|
||||
.when(can_collapse, |header| {
|
||||
header
|
||||
.toggle(Some(!is_collapsed))
|
||||
.on_toggle(cx.listener(move |this, _, cx| {
|
||||
this.toggle_section_expanded(section, cx);
|
||||
}))
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_contact(
|
||||
@@ -2197,17 +2258,16 @@ impl CollabPanel {
|
||||
Some(call_channel == channel_id)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let is_public = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let is_public = channel_store
|
||||
.channel_for_id(channel_id)
|
||||
.map(|channel| channel.visibility)
|
||||
== Some(proto::ChannelVisibility::Public);
|
||||
let disclosed =
|
||||
has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
|
||||
|
||||
let has_messages_notification = channel.unseen_message_id.is_some();
|
||||
let has_notes_notification = channel.unseen_note_version.is_some();
|
||||
let has_messages_notification = channel_store.has_new_messages(channel_id);
|
||||
let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
|
||||
|
||||
const FACEPILE_LIMIT: usize = 3;
|
||||
let participants = self.channel_store.read(cx).channel_participants(channel_id);
|
||||
@@ -2238,25 +2298,36 @@ impl CollabPanel {
|
||||
};
|
||||
|
||||
let width = self.width.unwrap_or(px(240.));
|
||||
let root_id = channel.root_id();
|
||||
|
||||
div()
|
||||
.h_6()
|
||||
.id(channel_id as usize)
|
||||
.group("")
|
||||
.flex()
|
||||
.w_full()
|
||||
.on_drag(channel.clone(), move |channel, cx| {
|
||||
cx.new_view(|_| DraggedChannelView {
|
||||
channel: channel.clone(),
|
||||
width,
|
||||
.when(!channel.is_root_channel(), |el| {
|
||||
el.on_drag(channel.clone(), move |channel, cx| {
|
||||
cx.new_view(|_| DraggedChannelView {
|
||||
channel: channel.clone(),
|
||||
width,
|
||||
})
|
||||
})
|
||||
})
|
||||
.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.drag_over::<Channel>({
|
||||
move |style, dragged_channel: &Channel, cx| {
|
||||
if dragged_channel.root_id() == root_id {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
} else {
|
||||
style
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
if dragged_channel.root_id() != root_id {
|
||||
return;
|
||||
}
|
||||
this.move_channel(dragged_channel.id, channel_id, cx);
|
||||
}))
|
||||
.child(
|
||||
ListItem::new(channel_id as usize)
|
||||
|
||||
@@ -10,11 +10,10 @@ use gpui::{
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
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,
|
||||
@@ -359,10 +358,8 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_member(selected_user.id, cx);
|
||||
}
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||
self.invite_member(selected_user, cx)
|
||||
}
|
||||
Some(proto::channel_member::Kind::Member) => {}
|
||||
None => self.invite_member(selected_user, cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -402,10 +399,6 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
.children(
|
||||
if request_status == Some(proto::channel_member::Kind::Invitee) {
|
||||
Some(Label::new("Invited"))
|
||||
} else if membership.map(|m| m.kind)
|
||||
== Some(channel_member::Kind::AncestorMember)
|
||||
{
|
||||
Some(Label::new("Parent"))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -498,7 +491,7 @@ impl ChannelModalDelegate {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx);
|
||||
.detach_and_prompt_err("Failed to update role", cx, |_, _| None);
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -530,7 +523,7 @@ impl ChannelModalDelegate {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx);
|
||||
.detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -556,23 +549,16 @@ 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>>) {
|
||||
let Some(membership) = self.member_at_index(ix) else {
|
||||
return;
|
||||
};
|
||||
if membership.kind == proto::channel_member::Kind::AncestorMember {
|
||||
return;
|
||||
}
|
||||
let user_id = membership.user.id;
|
||||
let picker = cx.view().clone();
|
||||
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
|
||||
if membership.kind == channel_member::Kind::AncestorMember {
|
||||
return menu.entry("Inherited membership", None, |_| {});
|
||||
};
|
||||
|
||||
let role = membership.role;
|
||||
|
||||
if role == ChannelRole::Admin || role == ChannelRole::Member {
|
||||
|
||||
@@ -542,7 +542,9 @@ impl CollabTitlebarItem {
|
||||
})?
|
||||
.clone();
|
||||
|
||||
Some(Avatar::new(follower.avatar_uri.clone()))
|
||||
Some(div().mt(-px(4.)).child(
|
||||
Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)),
|
||||
))
|
||||
},
|
||||
))
|
||||
.children(if extra_count > 0 {
|
||||
|
||||
@@ -5,7 +5,6 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
stories = ["dep:itertools", "dep:story"]
|
||||
@@ -15,20 +14,6 @@ path = "src/color.rs"
|
||||
doctest = true
|
||||
|
||||
[dependencies]
|
||||
# TODO: Clean up dependencies
|
||||
anyhow.workspace = true
|
||||
fs = { path = "../fs" }
|
||||
indexmap = "1.6.2"
|
||||
parking_lot.workspace = true
|
||||
refineable.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings = { path = "../settings" }
|
||||
story = { path = "../story", optional = true }
|
||||
toml.workspace = true
|
||||
uuid.workspace = true
|
||||
util = { path = "../util" }
|
||||
itertools = { version = "0.11.0", optional = true }
|
||||
palette = "0.7.3"
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,15 +88,15 @@ impl CopilotCodeVerification {
|
||||
let connect_button_label = if connect_clicked {
|
||||
"Waiting for connection..."
|
||||
} else {
|
||||
"Connect to Github"
|
||||
"Connect to GitHub"
|
||||
};
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(Headline::new("Use Github Copilot in Zed.").size(HeadlineSize::Large))
|
||||
.child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large))
|
||||
.child(
|
||||
Label::new("Using Copilot requires an active subscription on Github.")
|
||||
Label::new("Using Copilot requires an active subscription on GitHub.")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Self::render_device_code(data, cx))
|
||||
@@ -139,7 +139,7 @@ impl CopilotCodeVerification {
|
||||
"You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
|
||||
).color(Color::Warning))
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Subscribe on Github")
|
||||
Button::new("copilot-subscribe-button", "Subscribe on GitHub")
|
||||
.full_width()
|
||||
.on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
|
||||
)
|
||||
|
||||
@@ -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![
|
||||
@@ -2500,34 +2502,43 @@ impl Editor {
|
||||
)
|
||||
});
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = language.line_comment_prefix().filter(|_| {
|
||||
let comment_delimiter = language.line_comment_prefixes().filter(|_| {
|
||||
let is_comment_extension_enabled =
|
||||
multi_buffer.settings_at(0, cx).extend_comment_on_newline;
|
||||
is_cursor && is_comment_extension_enabled
|
||||
});
|
||||
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
|
||||
buffer
|
||||
.buffer_line_for_row(start_point.row)
|
||||
.is_some_and(|(snapshot, range)| {
|
||||
let mut index_of_first_non_whitespace = 0;
|
||||
let line_starts_with_comment = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip_while(|c| {
|
||||
let should_skip = c.is_whitespace();
|
||||
if should_skip {
|
||||
index_of_first_non_whitespace += 1;
|
||||
}
|
||||
should_skip
|
||||
})
|
||||
.take(delimiter.len())
|
||||
.eq(delimiter.chars());
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
index_of_first_non_whitespace + delimiter.len()
|
||||
<= start_point.column as usize;
|
||||
line_starts_with_comment
|
||||
&& cursor_is_placed_after_comment_marker
|
||||
let get_comment_delimiter = |delimiters: &[Arc<str>]| {
|
||||
let max_len_of_delimiter =
|
||||
delimiters.iter().map(|delimiter| delimiter.len()).max()?;
|
||||
let (snapshot, range) =
|
||||
buffer.buffer_line_for_row(start_point.row)?;
|
||||
|
||||
let mut index_of_first_non_whitespace = 0;
|
||||
let comment_candidate = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip_while(|c| {
|
||||
let should_skip = c.is_whitespace();
|
||||
if should_skip {
|
||||
index_of_first_non_whitespace += 1;
|
||||
}
|
||||
should_skip
|
||||
})
|
||||
.then(|| delimiter.clone())
|
||||
.take(max_len_of_delimiter)
|
||||
.collect::<String>();
|
||||
let comment_prefix = delimiters.iter().find(|comment_prefix| {
|
||||
comment_candidate.starts_with(comment_prefix.as_ref())
|
||||
})?;
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
index_of_first_non_whitespace + comment_prefix.len()
|
||||
<= start_point.column as usize;
|
||||
if cursor_is_placed_after_comment_marker {
|
||||
Some(comment_prefix.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let comment_delimiter = if let Some(delimiters) = comment_delimiter {
|
||||
get_comment_delimiter(delimiters)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -3741,7 +3752,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 +6122,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 +6232,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(())
|
||||
}
|
||||
|
||||
@@ -6507,7 +6570,10 @@ impl Editor {
|
||||
}
|
||||
|
||||
// If the language has line comments, toggle those.
|
||||
if let Some(full_comment_prefix) = language.line_comment_prefix() {
|
||||
if let Some(full_comment_prefix) = language
|
||||
.line_comment_prefixes()
|
||||
.and_then(|prefixes| prefixes.first())
|
||||
{
|
||||
// Split the comment prefix's trailing whitespace into a separate string,
|
||||
// as that portion won't be used for detecting if a line is a comment.
|
||||
let comment_prefix = full_comment_prefix.trim_end_matches(' ');
|
||||
@@ -6515,7 +6581,7 @@ impl Editor {
|
||||
let mut all_selection_lines_are_comments = true;
|
||||
|
||||
for row in start_row..=end_row {
|
||||
if snapshot.is_line_blank(row) && start_row < end_row {
|
||||
if start_row < end_row && snapshot.is_line_blank(row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1942,7 +1942,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comment: Some("//".into()),
|
||||
line_comments: vec!["//".into()],
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
@@ -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, |_| {});
|
||||
@@ -5724,7 +5736,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comment: Some("// ".into()),
|
||||
line_comments: vec!["// ".into()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
@@ -5826,7 +5838,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comment: Some("// ".into()),
|
||||
line_comments: vec!["// ".into()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
@@ -5981,7 +5993,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
|
||||
let javascript_language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
line_comment: Some("// ".into()),
|
||||
line_comments: vec!["// ".into()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_typescript::language_tsx()),
|
||||
|
||||
@@ -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)
|
||||
@@ -3568,7 +3582,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// multi-buffer support
|
||||
// in DisplayPoint co-ordinates, this is what we're dealing with:
|
||||
// in DisplayPoint coordinates, this is what we're dealing with:
|
||||
// 0: [[file
|
||||
// 1: header]]
|
||||
// 2: aaaaaa
|
||||
|
||||
@@ -1971,7 +1971,7 @@ pub mod tests {
|
||||
assert_eq!(
|
||||
lsp_request_count.load(Ordering::Relaxed),
|
||||
3,
|
||||
"Should query for new hints when they got reenabled"
|
||||
"Should query for new hints when they got re-enabled"
|
||||
);
|
||||
assert_eq!(
|
||||
vec![
|
||||
@@ -1980,7 +1980,7 @@ pub mod tests {
|
||||
"type hint".to_string(),
|
||||
],
|
||||
cached_hint_labels(editor),
|
||||
"Should get its cached hints fully repopulated after the hints got reenabled"
|
||||
"Should get its cached hints fully repopulated after the hints got re-enabled"
|
||||
);
|
||||
assert_eq!(
|
||||
vec!["parameter hint".to_string()],
|
||||
@@ -1990,11 +1990,11 @@ pub mod tests {
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
|
||||
"Cache should update editor settings when hints got reenabled"
|
||||
"Cache should update editor settings when hints got re-enabled"
|
||||
);
|
||||
assert_eq!(
|
||||
inlay_cache.version, edits_made,
|
||||
"Cache should update its version after hints got reenabled"
|
||||
"Cache should update its version after hints got re-enabled"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2736,7 +2736,7 @@ pub mod tests {
|
||||
assert_eq!(expected_hints, cached_hint_labels(editor),
|
||||
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
|
||||
assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scrolled buffer");
|
||||
});
|
||||
|
||||
editor_edited.store(true, Ordering::Release);
|
||||
@@ -2762,7 +2762,7 @@ pub mod tests {
|
||||
assert_eq!(
|
||||
expected_hints,
|
||||
cached_hint_labels(editor),
|
||||
"After multibuffer edit, editor gets scolled back to the last selection; \
|
||||
"After multibuffer edit, editor gets scrolled back to the last selection; \
|
||||
all hints should be invalidated and required for all of its visible excerpts"
|
||||
);
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
|
||||
@@ -15,47 +15,53 @@ actions!(
|
||||
CopySystemSpecsIntoClipboard,
|
||||
FileBugReport,
|
||||
RequestFeature,
|
||||
OpenZedCommunityRepo
|
||||
OpenZedRepo
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
// TODO: a way to combine these two into one?
|
||||
cx.observe_new_views(feedback_modal::FeedbackModal::register)
|
||||
.detach();
|
||||
const fn zed_repo_url() -> &'static str {
|
||||
"https://github.com/zed-industries/zed"
|
||||
}
|
||||
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||
const fn request_feature_url() -> &'static str {
|
||||
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml"
|
||||
}
|
||||
|
||||
fn file_bug_report_url(specs: &SystemSpecs) -> String {
|
||||
format!(
|
||||
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
|
||||
urlencoding::encode(&specs.to_string())
|
||||
)
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, cx| {
|
||||
feedback_modal::FeedbackModal::register(workspace, cx);
|
||||
workspace
|
||||
.register_action(|_, _: &CopySystemSpecsIntoClipboard, cx| {
|
||||
let specs = SystemSpecs::new(&cx).to_string();
|
||||
let specs = SystemSpecs::new(&cx).to_string();
|
||||
|
||||
let prompt = cx.prompt(
|
||||
PromptLevel::Info,
|
||||
&format!("Copied into clipboard:\n\n{specs}"),
|
||||
&["OK"],
|
||||
);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
let item = ClipboardItem::new(specs.clone());
|
||||
cx.write_to_clipboard(item);
|
||||
let prompt = cx.prompt(
|
||||
PromptLevel::Info,
|
||||
"Copied into clipboard",
|
||||
Some(&specs),
|
||||
&["OK"],
|
||||
);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
cx.write_to_clipboard(ClipboardItem::new(specs.clone()));
|
||||
})
|
||||
.register_action(|_, _: &RequestFeature, cx| {
|
||||
let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
|
||||
cx.open_url(url);
|
||||
cx.open_url(request_feature_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={}",
|
||||
urlencoding::encode(&SystemSpecs::new(&cx).to_string())
|
||||
);
|
||||
cx.open_url(&url);
|
||||
cx.open_url(&file_bug_report_url(&SystemSpecs::new(&cx)));
|
||||
})
|
||||
.register_action(move |_, _: &OpenZedCommunityRepo, cx| {
|
||||
let url = "https://github.com/zed-industries/community";
|
||||
cx.open_url(&url);
|
||||
});
|
||||
.register_action(move |_, _: &OpenZedRepo, cx| {
|
||||
cx.open_url(zed_repo_url());
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -574,7 +574,7 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Displays a platform modal for selecting a new path where a file can be saved.
|
||||
/// The provided directory will be used to set the iniital location.
|
||||
/// The provided directory will be used to set the initial location.
|
||||
/// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
|
||||
/// If cancelled, a `None` will be relayed instead.
|
||||
pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
|
||||
@@ -136,25 +136,6 @@ impl Render for () {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {}
|
||||
}
|
||||
|
||||
/// A quick way to create a [`Render`]able view without having to define a new type.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct TestView(Box<dyn FnMut(&mut ViewContext<TestView>) -> AnyElement>);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl TestView {
|
||||
/// Construct a TestView from a render closure.
|
||||
pub fn new<F: FnMut(&mut ViewContext<TestView>) -> AnyElement + 'static>(f: F) -> Self {
|
||||
Self(Box::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Render for TestView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
(self.0)(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// You can derive [`IntoElement`] on any type that implements this trait.
|
||||
/// It is used to construct reusable `components` out of plain data. Think of
|
||||
/// components as a recipe for a certain pattern of elements. RenderOnce allows
|
||||
|
||||
@@ -782,10 +782,20 @@ pub trait InteractiveElement: Sized {
|
||||
}
|
||||
|
||||
/// Apply the given style when the given data type is dragged over this element
|
||||
fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
|
||||
self.interactivity()
|
||||
.drag_over_styles
|
||||
.push((TypeId::of::<S>(), f(StyleRefinement::default())));
|
||||
fn drag_over<S: 'static>(
|
||||
mut self,
|
||||
f: impl 'static + Fn(StyleRefinement, &S, &WindowContext) -> StyleRefinement,
|
||||
) -> Self {
|
||||
self.interactivity().drag_over_styles.push((
|
||||
TypeId::of::<S>(),
|
||||
Box::new(move |currently_dragged: &dyn Any, cx| {
|
||||
f(
|
||||
StyleRefinement::default(),
|
||||
currently_dragged.downcast_ref::<S>().unwrap(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1174,7 +1184,10 @@ pub struct Interactivity {
|
||||
pub(crate) group_hover_style: Option<GroupStyle>,
|
||||
pub(crate) active_style: Option<Box<StyleRefinement>>,
|
||||
pub(crate) group_active_style: Option<GroupStyle>,
|
||||
pub(crate) drag_over_styles: Vec<(TypeId, StyleRefinement)>,
|
||||
pub(crate) drag_over_styles: Vec<(
|
||||
TypeId,
|
||||
Box<dyn Fn(&dyn Any, &mut WindowContext) -> StyleRefinement>,
|
||||
)>,
|
||||
pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>,
|
||||
pub(crate) mouse_down_listeners: Vec<MouseDownListener>,
|
||||
pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
|
||||
@@ -1980,7 +1993,7 @@ impl Interactivity {
|
||||
}
|
||||
}
|
||||
|
||||
for (state_type, drag_over_style) in &self.drag_over_styles {
|
||||
for (state_type, build_drag_over_style) in &self.drag_over_styles {
|
||||
if *state_type == drag.value.as_ref().type_id()
|
||||
&& bounds
|
||||
.intersect(&cx.content_mask().bounds)
|
||||
@@ -1990,7 +2003,7 @@ impl Interactivity {
|
||||
cx.stacking_order(),
|
||||
)
|
||||
{
|
||||
style.refine(drag_over_style);
|
||||
style.refine(&build_drag_over_style(drag.value.as_ref(), cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl Overlay {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the position in window co-ordinates
|
||||
/// Sets the position in window coordinates
|
||||
/// (otherwise the location the overlay is rendered is used)
|
||||
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
|
||||
self.anchor_position = Some(anchor);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -94,7 +94,6 @@ type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
|
||||
|
||||
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FocusEvent {
|
||||
previous_focus_path: SmallVec<[FocusId; 8]>,
|
||||
current_focus_path: SmallVec<[FocusId; 8]>,
|
||||
@@ -1479,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.
|
||||
@@ -2022,12 +2024,11 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
}
|
||||
}
|
||||
|
||||
// Always emit a notify effect, so that handlers fire correctly
|
||||
self.window_cx.app.push_effect(Effect::Notify {
|
||||
emitter: self.view.model.entity_id,
|
||||
});
|
||||
if !self.window.drawing {
|
||||
self.window_cx.window.dirty = true;
|
||||
self.window_cx.app.push_effect(Effect::Notify {
|
||||
emitter: self.view.model.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2759,59 +2760,3 @@ pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>)
|
||||
border_color: border_color.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
self as gpui, div, FocusHandle, InteractiveElement, IntoElement, Render, TestAppContext,
|
||||
ViewContext, VisualContext,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_notify_on_focus(cx: &mut TestAppContext) {
|
||||
struct TestFocusView {
|
||||
handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Render for TestFocusView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().id("test").track_focus(&self.handle)
|
||||
}
|
||||
}
|
||||
|
||||
let notify_counter = Rc::new(RefCell::new(0));
|
||||
|
||||
let (notify_producer, cx) = cx.add_window_view(|cx| {
|
||||
cx.activate_window();
|
||||
let handle = cx.focus_handle();
|
||||
|
||||
cx.on_focus(&handle, |_, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
TestFocusView { handle }
|
||||
});
|
||||
|
||||
let focus_handle = cx.update(|cx| notify_producer.read(cx).handle.clone());
|
||||
|
||||
let _notify_consumer = cx.new_view({
|
||||
|cx| {
|
||||
let notify_counter = notify_counter.clone();
|
||||
cx.observe(¬ify_producer, move |_, _, _| {
|
||||
*notify_counter.borrow_mut() += 1;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.focus(&focus_handle);
|
||||
});
|
||||
|
||||
assert_eq!(*notify_counter.borrow(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ pub struct Diagnostic {
|
||||
/// Whether this diagnostic is considered to originate from an analysis of
|
||||
/// files on disk, as opposed to any unsaved buffer contents. This is a
|
||||
/// property of a given diagnostic source, and is configured for a given
|
||||
/// language server via the [LspAdapter::disk_based_diagnostic_sources] method
|
||||
/// language server via the [`LspAdapter::disk_based_diagnostic_sources`](crate::LspAdapter::disk_based_diagnostic_sources) method
|
||||
/// for the language server.
|
||||
pub is_disk_based: bool,
|
||||
/// Whether this diagnostic marks unnecessary code.
|
||||
@@ -236,7 +236,7 @@ pub async fn prepare_completion_documentation(
|
||||
}
|
||||
}
|
||||
|
||||
/// Documentation associated with a [Completion].
|
||||
/// Documentation associated with a [`Completion`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Documentation {
|
||||
/// There is no documentation for this completion.
|
||||
@@ -301,7 +301,7 @@ pub enum Operation {
|
||||
lamport_timestamp: clock::Lamport,
|
||||
/// Whether the selections are in 'line mode'.
|
||||
line_mode: bool,
|
||||
/// The [CursorShape] associated with these selections.
|
||||
/// The [`CursorShape`] associated with these selections.
|
||||
cursor_shape: CursorShape,
|
||||
},
|
||||
|
||||
@@ -347,7 +347,7 @@ pub enum Event {
|
||||
|
||||
/// The file associated with a buffer.
|
||||
pub trait File: Send + Sync {
|
||||
/// Returns the [LocalFile] associated with this file, if the
|
||||
/// Returns the [`LocalFile`] associated with this file, if the
|
||||
/// file is local.
|
||||
fn as_local(&self) -> Option<&dyn LocalFile>;
|
||||
|
||||
@@ -378,7 +378,7 @@ pub trait File: Send + Sync {
|
||||
/// Returns whether the file has been deleted.
|
||||
fn is_deleted(&self) -> bool;
|
||||
|
||||
/// Converts this file into an [Any] trait object.
|
||||
/// Converts this file into an [`Any`] trait object.
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
/// Converts this file into a protobuf message.
|
||||
@@ -1538,8 +1538,6 @@ impl Buffer {
|
||||
/// Starts a transaction, providing the current time. Subsequent transactions
|
||||
/// that occur within a short period of time will be grouped together. This
|
||||
/// is controlled by the buffer's undo grouping duration.
|
||||
///
|
||||
/// See [`Buffer::set_group_interval`].
|
||||
pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> {
|
||||
self.transaction_depth += 1;
|
||||
if self.was_dirty_before_starting_transaction.is_none() {
|
||||
@@ -1556,8 +1554,6 @@ impl Buffer {
|
||||
/// Terminates the current transaction, providing the current time. Subsequent transactions
|
||||
/// that occur within a short period of time will be grouped together. This
|
||||
/// is controlled by the buffer's undo grouping duration.
|
||||
///
|
||||
/// See [`Buffer::set_group_interval`].
|
||||
pub fn end_transaction_at(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
@@ -2420,7 +2416,7 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
/// Iterates over chunks of text in the given range of the buffer. Text is chunked
|
||||
/// in an arbitrary way due to being stored in a [`rope::Rope`]. The text is also
|
||||
/// in an arbitrary way due to being stored in a [`Rope`](text::Rope). The text is also
|
||||
/// returned in chunks where each chunk has a single syntax highlighting style and
|
||||
/// diagnostic status.
|
||||
pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> BufferChunks {
|
||||
|
||||
@@ -1657,7 +1657,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
line_comment: Some("// ".into()),
|
||||
line_comments: vec!["// ".into()],
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
@@ -1681,7 +1681,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
||||
overrides: [(
|
||||
"element".into(),
|
||||
LanguageConfigOverride {
|
||||
line_comment: Override::Remove { remove: true },
|
||||
line_comments: Override::Remove { remove: true },
|
||||
block_comment: Override::Set(("{/*".into(), "*/}".into())),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1718,7 +1718,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
||||
let snapshot = buffer.snapshot();
|
||||
|
||||
let config = snapshot.language_scope_at(0).unwrap();
|
||||
assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
|
||||
assert_eq!(config.line_comment_prefixes().unwrap(), &[Arc::from("// ")]);
|
||||
// Both bracket pairs are enabled
|
||||
assert_eq!(
|
||||
config.brackets().map(|e| e.1).collect::<Vec<_>>(),
|
||||
@@ -1728,7 +1728,10 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
||||
let string_config = snapshot
|
||||
.language_scope_at(text.find("b\"").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// ");
|
||||
assert_eq!(
|
||||
string_config.line_comment_prefixes().unwrap(),
|
||||
&[Arc::from("// ")]
|
||||
);
|
||||
// Second bracket pair is disabled
|
||||
assert_eq!(
|
||||
string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
|
||||
@@ -1739,7 +1742,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
||||
let element_config = snapshot
|
||||
.language_scope_at(text.find("<F>").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(element_config.line_comment_prefix(), None);
|
||||
assert_eq!(element_config.line_comment_prefixes(), None);
|
||||
assert_eq!(
|
||||
element_config.block_comment_delimiters(),
|
||||
Some((&"{/*".into(), &"*/}".into()))
|
||||
@@ -1753,7 +1756,10 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
||||
let tag_config = snapshot
|
||||
.language_scope_at(text.find(" d=").unwrap() + 1)
|
||||
.unwrap();
|
||||
assert_eq!(tag_config.line_comment_prefix().unwrap().as_ref(), "// ");
|
||||
assert_eq!(
|
||||
tag_config.line_comment_prefixes().unwrap(),
|
||||
&[Arc::from("// ")]
|
||||
);
|
||||
assert_eq!(
|
||||
tag_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
|
||||
&[true, true]
|
||||
@@ -1765,10 +1771,9 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
expression_in_element_config
|
||||
.line_comment_prefix()
|
||||
.unwrap()
|
||||
.as_ref(),
|
||||
"// "
|
||||
.line_comment_prefixes()
|
||||
.unwrap(),
|
||||
&[Arc::from("// ")]
|
||||
);
|
||||
assert_eq!(
|
||||
expression_in_element_config
|
||||
@@ -1884,14 +1889,17 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
|
||||
|
||||
let snapshot = buffer.snapshot();
|
||||
let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
|
||||
assert_eq!(html_config.line_comment_prefix(), None);
|
||||
assert_eq!(html_config.line_comment_prefixes(), Some(&vec![]));
|
||||
assert_eq!(
|
||||
html_config.block_comment_delimiters(),
|
||||
Some((&"<!--".into(), &"-->".into()))
|
||||
);
|
||||
|
||||
let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap();
|
||||
assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# ");
|
||||
assert_eq!(
|
||||
ruby_config.line_comment_prefixes().unwrap(),
|
||||
&[Arc::from("# ")]
|
||||
);
|
||||
assert_eq!(ruby_config.block_comment_delimiters(), None);
|
||||
|
||||
buffer
|
||||
@@ -2293,7 +2301,7 @@ fn ruby_lang() -> Language {
|
||||
LanguageConfig {
|
||||
name: "Ruby".into(),
|
||||
path_suffixes: vec!["rb".to_string()],
|
||||
line_comment: Some("# ".into()),
|
||||
line_comments: vec!["# ".into()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_ruby::language()),
|
||||
|
||||
@@ -12,7 +12,7 @@ use text::{Anchor, FromAnchor, PointUtf16, ToOffset};
|
||||
/// A set of diagnostics associated with a given buffer, provided
|
||||
/// by a single language server.
|
||||
///
|
||||
/// The diagnostics are stored in a [SumTree], which allows this struct
|
||||
/// The diagnostics are stored in a [`SumTree`], which allows this struct
|
||||
/// to be cheaply copied, and allows for efficient retrieval of the
|
||||
/// diagnostics that intersect a given range of the buffer.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -21,9 +21,9 @@ pub struct DiagnosticSet {
|
||||
}
|
||||
|
||||
/// A single diagnostic in a set. Generic over its range type, because
|
||||
/// the diagnostics are stored internally as [Anchor]s, but can be
|
||||
/// resolved to different coordinates types like [usize] byte offsets or
|
||||
/// [Point]s.
|
||||
/// the diagnostics are stored internally as [`Anchor`]s, but can be
|
||||
/// resolved to different coordinates types like [`usize`] byte offsets or
|
||||
/// [`Point`](gpui::Point)s.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DiagnosticEntry<T> {
|
||||
/// The range of the buffer where the diagnostic applies.
|
||||
@@ -52,7 +52,7 @@ pub struct Summary {
|
||||
}
|
||||
|
||||
impl<T> DiagnosticEntry<T> {
|
||||
/// Returns a raw LSP diagnostic ssed to provide diagnostic context to lsp
|
||||
/// Returns a raw LSP diagnostic ssed to provide diagnostic context to LSP
|
||||
/// codeAction request
|
||||
pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic {
|
||||
let code = self
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -299,10 +298,12 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary>;
|
||||
|
||||
/// Returns true if a language server can be reinstalled.
|
||||
/// If language server initialization fails, a reinstallation will be attempted unless the value returned from this method is false.
|
||||
/// Returns `true` if a language server can be reinstalled.
|
||||
///
|
||||
/// If language server initialization fails, a reinstallation will be attempted unless the value returned from this method is `false`.
|
||||
///
|
||||
/// Implementations that rely on software already installed on user's system
|
||||
/// should have [`can_be_reinstalled`] return false.
|
||||
/// should have [`can_be_reinstalled`](Self::can_be_reinstalled) return `false`.
|
||||
fn can_be_reinstalled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -314,7 +315,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
|
||||
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
/// A callback called for each [`lsp_types::CompletionItem`] obtained from LSP server.
|
||||
/// A callback called for each [`lsp::CompletionItem`] obtained from LSP server.
|
||||
/// Some LspAdapter implementations might want to modify the obtained item to
|
||||
/// change how it's displayed.
|
||||
async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
|
||||
@@ -336,12 +337,12 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
None
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
/// Returns initialization options that are going to be sent to a LSP server as a part of [`lsp::InitializeParams`]
|
||||
fn initialization_options(&self) -> Option<Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> Value {
|
||||
fn workspace_configuration(&self, _workspace_root: &Path, _cx: &mut AppContext) -> Value {
|
||||
serde_json::json!({})
|
||||
}
|
||||
|
||||
@@ -356,15 +357,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()
|
||||
}
|
||||
|
||||
@@ -417,8 +418,10 @@ pub struct LanguageConfig {
|
||||
#[serde(default)]
|
||||
pub collapsed_placeholder: String,
|
||||
/// A line comment string that is inserted in e.g. `toggle comments` action.
|
||||
/// A language can have multiple flavours of line comments. All of the provided line comments are
|
||||
/// used for comment continuations on the next line, but only the first one is used for Editor::ToggleComments.
|
||||
#[serde(default)]
|
||||
pub line_comment: Option<Arc<str>>,
|
||||
pub line_comments: Vec<Arc<str>>,
|
||||
/// Starting and closing characters of a block comment.
|
||||
#[serde(default)]
|
||||
pub block_comment: Option<(Arc<str>, Arc<str>)>,
|
||||
@@ -461,7 +464,7 @@ pub struct LanguageScope {
|
||||
#[derive(Clone, Deserialize, Default, Debug)]
|
||||
pub struct LanguageConfigOverride {
|
||||
#[serde(default)]
|
||||
pub line_comment: Override<Arc<str>>,
|
||||
pub line_comments: Override<Vec<Arc<str>>>,
|
||||
#[serde(default)]
|
||||
pub block_comment: Override<(Arc<str>, Arc<str>)>,
|
||||
#[serde(skip_deserializing)]
|
||||
@@ -507,7 +510,7 @@ impl Default for LanguageConfig {
|
||||
increase_indent_pattern: Default::default(),
|
||||
decrease_indent_pattern: Default::default(),
|
||||
autoclose_before: Default::default(),
|
||||
line_comment: Default::default(),
|
||||
line_comments: Default::default(),
|
||||
block_comment: Default::default(),
|
||||
scope_opt_in_language_servers: Default::default(),
|
||||
overrides: Default::default(),
|
||||
@@ -585,7 +588,7 @@ impl<'de> Deserialize<'de> for BracketPairConfig {
|
||||
}
|
||||
|
||||
/// Describes a single bracket pair and how an editor should react to e.g. inserting
|
||||
/// an opening bracket or to a newline character insertion inbetween `start` and `end` characters.
|
||||
/// an opening bracket or to a newline character insertion in between `start` and `end` characters.
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
pub struct BracketPair {
|
||||
/// Starting substring for a bracket.
|
||||
@@ -1711,10 +1714,10 @@ impl LanguageScope {
|
||||
|
||||
/// Returns line prefix that is inserted in e.g. line continuations or
|
||||
/// in `toggle comments` action.
|
||||
pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
|
||||
pub fn line_comment_prefixes(&self) -> Option<&Vec<Arc<str>>> {
|
||||
Override::as_option(
|
||||
self.config_override().map(|o| &o.line_comment),
|
||||
self.language.config.line_comment.as_ref(),
|
||||
self.config_override().map(|o| &o.line_comments),
|
||||
Some(&self.language.config.line_comments),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1881,7 +1884,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 +1922,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes an [`operation::EditOperation`] to be sent over RPC.
|
||||
/// Serializes an [`EditOperation`] to be sent over RPC.
|
||||
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
|
||||
proto::operation::Edit {
|
||||
replica_id: operation.timestamp.replica_id as u32,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -590,6 +590,7 @@ impl Project {
|
||||
client.add_model_request_handler(Self::handle_delete_project_entry);
|
||||
client.add_model_request_handler(Self::handle_expand_project_entry);
|
||||
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
|
||||
client.add_model_request_handler(Self::handle_resolve_completion_documentation);
|
||||
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||
client.add_model_request_handler(Self::handle_on_type_formatting);
|
||||
client.add_model_request_handler(Self::handle_inlay_hints);
|
||||
@@ -974,8 +975,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.
|
||||
@@ -2714,13 +2714,12 @@ impl Project {
|
||||
})?;
|
||||
|
||||
for (adapter, server) in servers {
|
||||
let workspace_config =
|
||||
let settings =
|
||||
cx.update(|cx| adapter.workspace_configuration(server.root_path(), cx))?;
|
||||
|
||||
server
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
lsp::DidChangeConfigurationParams {
|
||||
settings: workspace_config.clone(),
|
||||
},
|
||||
lsp::DidChangeConfigurationParams { settings },
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -7751,6 +7726,40 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_resolve_completion_documentation(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::ResolveCompletionDocumentation>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ResolveCompletionDocumentationResponse> {
|
||||
let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?;
|
||||
|
||||
let completion = this
|
||||
.read_with(&mut cx, |this, _| {
|
||||
let id = LanguageServerId(envelope.payload.language_server_id as usize);
|
||||
let Some(server) = this.language_server_for_id(id) else {
|
||||
return Err(anyhow!("No language server {id}"));
|
||||
};
|
||||
|
||||
Ok(server.request::<lsp::request::ResolveCompletionItem>(lsp_completion))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let mut is_markdown = false;
|
||||
let text = match completion.documentation {
|
||||
Some(lsp::Documentation::String(text)) => text,
|
||||
|
||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => {
|
||||
is_markdown = kind == lsp::MarkupKind::Markdown;
|
||||
value
|
||||
}
|
||||
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown })
|
||||
}
|
||||
|
||||
async fn handle_apply_code_action(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::ApplyCodeAction>,
|
||||
|
||||
@@ -60,9 +60,11 @@ impl Project {
|
||||
.detach();
|
||||
|
||||
if let Some(python_settings) = &python_settings.as_option() {
|
||||
let activate_command = Project::get_activate_command(python_settings);
|
||||
let activate_script_path =
|
||||
self.find_activate_script_path(python_settings, working_directory);
|
||||
self.activate_python_virtual_environment(
|
||||
activate_command,
|
||||
activate_script_path,
|
||||
&terminal_handle,
|
||||
cx,
|
||||
@@ -104,15 +106,24 @@ impl Project {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_activate_command(settings: &VenvSettingsContent) -> &'static str {
|
||||
match settings.activate_script {
|
||||
terminal_settings::ActivateScript::Nushell => "overlay use",
|
||||
_ => "source",
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_python_virtual_environment(
|
||||
&mut self,
|
||||
activate_command: &'static str,
|
||||
activate_script: Option<PathBuf>,
|
||||
terminal_handle: &Model<Terminal>,
|
||||
cx: &mut ModelContext<Project>,
|
||||
) {
|
||||
if let Some(activate_script) = activate_script {
|
||||
// Paths are not strings so we need to jump through some hoops to format the command without `format!`
|
||||
let mut command = Vec::from("source ".as_bytes());
|
||||
let mut command = Vec::from(activate_command.as_bytes());
|
||||
command.push(b' ');
|
||||
command.extend_from_slice(activate_script.as_os_str().as_bytes());
|
||||
command.push(b'\n');
|
||||
|
||||
|
||||
@@ -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"],
|
||||
);
|
||||
|
||||
@@ -1370,7 +1368,7 @@ impl ProjectPanel {
|
||||
entry_id: *entry_id,
|
||||
})
|
||||
})
|
||||
.drag_over::<ProjectEntryId>(|style| {
|
||||
.drag_over::<ProjectEntryId>(|style, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
|
||||
@@ -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
|
||||
|
||||
@@ -181,7 +181,9 @@ message Envelope {
|
||||
MarkNotificationRead mark_notification_read = 153;
|
||||
LspExtExpandMacro lsp_ext_expand_macro = 154;
|
||||
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
|
||||
SetRoomParticipantRole set_room_participant_role = 156; // Current max
|
||||
SetRoomParticipantRole set_room_participant_role = 156;
|
||||
|
||||
UpdateUserChannels update_user_channels = 157; // current max
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +199,23 @@ 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;
|
||||
NotARootChannel = 8;
|
||||
BadPublicNesting = 9;
|
||||
CircularNesting = 10;
|
||||
WrongMoveTarget = 11;
|
||||
}
|
||||
|
||||
message Test {
|
||||
@@ -979,21 +998,26 @@ message UpdateChannels {
|
||||
repeated Channel channel_invitations = 5;
|
||||
repeated uint64 remove_channel_invitations = 6;
|
||||
repeated ChannelParticipants channel_participants = 7;
|
||||
repeated UnseenChannelMessage unseen_channel_messages = 9;
|
||||
repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10;
|
||||
repeated ChannelMessageId latest_channel_message_ids = 8;
|
||||
repeated ChannelBufferVersion latest_channel_buffer_versions = 9;
|
||||
}
|
||||
|
||||
message UnseenChannelMessage {
|
||||
message UpdateUserChannels {
|
||||
repeated ChannelMessageId observed_channel_message_id = 1;
|
||||
repeated ChannelBufferVersion observed_channel_buffer_version = 2;
|
||||
repeated ChannelMembership channel_memberships = 3;
|
||||
}
|
||||
|
||||
message ChannelMembership {
|
||||
uint64 channel_id = 1;
|
||||
ChannelRole role = 2;
|
||||
}
|
||||
|
||||
message ChannelMessageId {
|
||||
uint64 channel_id = 1;
|
||||
uint64 message_id = 2;
|
||||
}
|
||||
|
||||
message UnseenChannelBufferChange {
|
||||
uint64 channel_id = 1;
|
||||
uint64 epoch = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
}
|
||||
|
||||
message ChannelPermission {
|
||||
uint64 channel_id = 1;
|
||||
ChannelRole role = 3;
|
||||
@@ -1028,7 +1052,6 @@ message ChannelMember {
|
||||
enum Kind {
|
||||
Member = 0;
|
||||
Invitee = 1;
|
||||
AncestorMember = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1135,7 +1158,7 @@ message GetChannelMessagesById {
|
||||
|
||||
message MoveChannel {
|
||||
uint64 channel_id = 1;
|
||||
optional uint64 to = 2;
|
||||
uint64 to = 2;
|
||||
}
|
||||
|
||||
message JoinChannelBuffer {
|
||||
@@ -1573,7 +1596,6 @@ message Channel {
|
||||
uint64 id = 1;
|
||||
string name = 2;
|
||||
ChannelVisibility visibility = 3;
|
||||
ChannelRole role = 4;
|
||||
repeated uint64 parent_path = 5;
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
|
||||
@@ -269,6 +269,7 @@ messages!(
|
||||
(UpdateChannelBuffer, Foreground),
|
||||
(UpdateChannelBufferCollaborators, Foreground),
|
||||
(UpdateChannels, Foreground),
|
||||
(UpdateUserChannels, Foreground),
|
||||
(UpdateContacts, Foreground),
|
||||
(UpdateDiagnosticSummary, Foreground),
|
||||
(UpdateDiffBase, Foreground),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
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;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 67;
|
||||
pub const PROTOCOL_VERSION: u32 = 68;
|
||||
|
||||
@@ -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());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user